[automerger skipped] Merge "Import translations. DO NOT MERGE ANYWHERE" into sc-qpr1-dev am: 74b9848555 -s ours
am skip reason: subject contains skip directive
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/services/Car/+/16156298
Change-Id: Ieb7437a6499c6b09a7ab1c14fedf8eec34e00cc5
diff --git a/Android.mk b/Android.mk
index 8b1efbf..0d59692 100644
--- a/Android.mk
+++ b/Android.mk
@@ -15,5 +15,8 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
+# Include car_ui_portrait
+include $(LOCAL_PATH)/car_product/car_ui_portrait/Android.mk
+
# Include the sub-makefiles
include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/car-admin-ui-lib/Android.bp b/car-admin-ui-lib/Android.bp
index 35a8fda..96615fa 100644
--- a/car-admin-ui-lib/Android.bp
+++ b/car-admin-ui-lib/Android.bp
@@ -28,7 +28,8 @@
"src/**/*.java",
],
libs: [
- "SettingsLib"
+ "SettingsLib",
+ "modules-utils-preconditions",
],
resource_dirs: [
"src/main/res"
diff --git a/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/ManagedDeviceTextView.java b/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/ManagedDeviceTextView.java
index 9d2fc49..46b2a2c 100644
--- a/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/ManagedDeviceTextView.java
+++ b/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/ManagedDeviceTextView.java
@@ -70,8 +70,7 @@
if (DEBUG) {
Log.d(TAG, "organization name not set, using device owner app name instead");
}
- return res.getString(R.string.car_admin_ui_managed_device_message_by_app,
- dpm.getDeviceOwnerNameOnAnyUser());
+ return res.getString(R.string.car_admin_ui_managed_device_message_generic);
} catch (Exception e) {
Log.w(TAG, "error getting name of device owner organization", e);
return res.getString(R.string.car_admin_ui_managed_device_message_generic);
diff --git a/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/UserAvatarView.java b/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/UserAvatarView.java
index e4d2a15..3b92265 100644
--- a/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/UserAvatarView.java
+++ b/car-admin-ui-lib/src/main/java/com/android/car/admin/ui/UserAvatarView.java
@@ -24,6 +24,7 @@
import android.util.AttributeSet;
import android.view.View;
+import com.android.internal.util.Preconditions;
import com.android.settingslib.drawable.UserIconDrawable;
// TODO(b/176262528): copied from com.android.systemui, ideally it should be provided by a common
@@ -121,21 +122,26 @@
}
public void setDrawable(Drawable d) {
- if (d instanceof UserIconDrawable) {
- throw new RuntimeException("Recursively adding UserIconDrawable");
- }
+ Preconditions.checkArgument(!(d instanceof UserIconDrawable),
+ "Recursively adding UserIconDrawable: %s", d);
mDrawable.setIconDrawable(d);
mDrawable.setBadge(null);
}
public void setDrawableWithBadge(Drawable d, int userId) {
- if (d instanceof UserIconDrawable) {
- throw new RuntimeException("Recursively adding UserIconDrawable");
- }
+ Preconditions.checkArgument(!(d instanceof UserIconDrawable),
+ "Recursively adding UserIconDrawable: %s", d);
mDrawable.setIconDrawable(d);
mDrawable.setBadgeIfManagedUser(getContext(), userId);
}
+ public void setDrawableWithBadge(Drawable d) {
+ Preconditions.checkArgument(!(d instanceof UserIconDrawable),
+ "Recursively adding UserIconDrawable: %s", d);
+ mDrawable.setIconDrawable(d);
+ mDrawable.setBadgeIfManagedDevice(getContext());
+ }
+
public UserIconDrawable getUserIconDrawable() {
return mDrawable;
}
diff --git a/car-admin-ui-lib/src/main/res/values/strings.xml b/car-admin-ui-lib/src/main/res/values/strings.xml
index f03a066..a591e5d 100644
--- a/car-admin-ui-lib/src/main/res/values/strings.xml
+++ b/car-admin-ui-lib/src/main/res/values/strings.xml
@@ -13,11 +13,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<resources>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <!-- TODO(b/175214581): STOPSHIP use proper sentence, remove translatable=false, and document what they mean -->
- <string name="car_admin_ui_managed_device_message_generic" translatable="false">This device is owned and managed by an enteprise</string>
- <string name="car_admin_ui_managed_device_message_by_org" translatable="false">This device is owned by and managed by %1$s</string>
- <string name="car_admin_ui_managed_device_message_by_app" translatable="false">This device is owned by an enterprise and managed by app %1$s</string>
+ <!-- Message displayed in multiple places indicating the device is managed by an organization, but the name of the organization is not known [CHAR LIMIT=NONE]-->
+ <string name="car_admin_ui_managed_device_message_generic">Vehicle managed by an organization</string>
+ <!-- Message displayed in multiple places indicating the device is managed by an organization [CHAR LIMIT=NONE]-->
+ <string name="car_admin_ui_managed_device_message_by_org">Vehicle managed by <xliff:g id="organization name" example="Acme Corporation">%1$s</xliff:g></string>
</resources>
diff --git a/car-internal-lib/src/com/android/car/internal/ICarServiceHelper.aidl b/car-internal-lib/src/com/android/car/internal/ICarServiceHelper.aidl
index 22148eb..cabbc07 100644
--- a/car-internal-lib/src/com/android/car/internal/ICarServiceHelper.aidl
+++ b/car-internal-lib/src/com/android/car/internal/ICarServiceHelper.aidl
@@ -55,4 +55,10 @@
* Creates the given user, even when it's disallowed by DevicePolicyManager.
*/
UserInfo createUserEvenWhenDisallowed(String name, String userType, int flags);
+
+ /**
+ * Designates the given {@code activity} to be launched in {@code TaskDisplayArea} of
+ * {@code featureId} in the display of {@code displayId}.
+ */
+ int setPersistentActivity(in ComponentName activity, int displayId, int featureId);
}
diff --git a/car-lib/src/android/car/Car.java b/car-lib/src/android/car/Car.java
index aa98215..a298f7f 100644
--- a/car-lib/src/android/car/Car.java
+++ b/car-lib/src/android/car/Car.java
@@ -32,6 +32,7 @@
import android.car.admin.CarDevicePolicyManager;
import android.car.annotation.MandatoryFeature;
import android.car.annotation.OptionalFeature;
+import android.car.app.CarActivityManager;
import android.car.cluster.CarInstrumentClusterManager;
import android.car.cluster.ClusterActivityState;
import android.car.cluster.ClusterHomeManager;
@@ -373,6 +374,14 @@
@OptionalFeature
public static final String CAR_TELEMETRY_SERVICE = "car_telemetry_service";
+ /**
+ * Service name for {@link android.car.app.CarActivityManager}
+ *
+ * @hide
+ */
+ @MandatoryFeature
+ public static final String CAR_ACTIVITY_SERVICE = "car_activity_service";
+
/** Permission necessary to access car's mileage information.
* @hide
*/
@@ -483,6 +492,15 @@
"android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL";
/**
+ * Permission necessary to listen for the instrument cluster's navigation state changes.
+ *
+ * @hide
+ */
+ public static final String PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE =
+ "android.car.permission.CAR_MONITOR_CLUSTER_NAVIGATION_STATE";
+
+
+ /**
* Application must have this permission in order to be launched in the instrument cluster
* display.
*
@@ -868,6 +886,14 @@
public static final String PERMISSION_COLLECT_CAR_WATCHDOG_METRICS =
"android.car.permission.COLLECT_CAR_WATCHDOG_METRICS";
+ /**
+ * Permission necessary to control launching applications in Car.
+ *
+ * @hide
+ */
+ public static final String PERMISSION_CONTROL_CAR_APP_LAUNCH =
+ "android.car.permission.CONTROL_CAR_APP_LAUNCH";
+
/** @hide */
@IntDef({CONNECTION_TYPE_EMBEDDED})
@Retention(RetentionPolicy.SOURCE)
@@ -1900,6 +1926,9 @@
case CAR_TELEMETRY_SERVICE:
manager = new CarTelemetryManager(this, binder);
break;
+ case CAR_ACTIVITY_SERVICE:
+ manager = new CarActivityManager(this, binder);
+ break;
default:
// Experimental or non-existing
String className = null;
diff --git a/car-lib/src/android/car/CarAppFocusManager.java b/car-lib/src/android/car/CarAppFocusManager.java
index cc0d10b..7a6d1f3 100644
--- a/car-lib/src/android/car/CarAppFocusManager.java
+++ b/car-lib/src/android/car/CarAppFocusManager.java
@@ -17,6 +17,7 @@
package android.car;
import android.annotation.IntDef;
+import android.annotation.Nullable;
import android.annotation.TestApi;
import android.os.IBinder;
import android.os.RemoteException;
@@ -28,6 +29,7 @@
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -221,6 +223,22 @@
}
/**
+ * Returns the package names of the current owner of a given application type, or {@code null}
+ * if there is no owner. This method might return more than one package name if the current
+ * owner uses the "android:sharedUserId" feature.
+ *
+ * @hide
+ */
+ @Nullable
+ public List<String> getAppTypeOwner(@AppFocusType int appType) {
+ try {
+ return mService.getAppTypeOwner(appType);
+ } catch (RemoteException e) {
+ return handleRemoteExceptionFromCarService(e, null);
+ }
+ }
+
+ /**
* Checks if listener is associated with active a focus
* @param callback
* @param appType
diff --git a/car-lib/src/android/car/IAppFocus.aidl b/car-lib/src/android/car/IAppFocus.aidl
index f3d6d1f..118ad3b 100644
--- a/car-lib/src/android/car/IAppFocus.aidl
+++ b/car-lib/src/android/car/IAppFocus.aidl
@@ -30,4 +30,5 @@
int requestAppFocus(IAppFocusOwnershipCallback callback, int appType) = 4;
/** callback used as a token */
void abandonAppFocus(IAppFocusOwnershipCallback callback, int appType) = 5;
+ List<String> getAppTypeOwner(int appType) = 6;
}
diff --git a/car-lib/src/android/car/ICarUserService.aidl b/car-lib/src/android/car/ICarUserService.aidl
index 267044d..bee3bba 100644
--- a/car-lib/src/android/car/ICarUserService.aidl
+++ b/car-lib/src/android/car/ICarUserService.aidl
@@ -40,8 +40,8 @@
List<UserInfo> getPassengers(int driverId);
boolean startPassenger(int passengerId, int zoneId);
boolean stopPassenger(int passengerId);
- void setLifecycleListenerForUid(in IResultReceiver listener);
- void resetLifecycleListenerForUid();
+ void setLifecycleListenerForApp(String pkgName, in IResultReceiver listener);
+ void resetLifecycleListenerForApp(in IResultReceiver listener);
UserIdentificationAssociationResponse getUserIdentificationAssociation(in int[] types);
void setUserIdentificationAssociation(int timeoutMs, in int[] types, in int[] values,
in AndroidFuture<UserIdentificationAssociationResponse> result);
diff --git a/car-lib/src/android/car/app/CarActivityManager.java b/car-lib/src/android/car/app/CarActivityManager.java
new file mode 100644
index 0000000..dd521f9
--- /dev/null
+++ b/car-lib/src/android/car/app/CarActivityManager.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2021 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 android.car.app;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.app.Activity;
+import android.car.Car;
+import android.car.CarManagerBase;
+import android.car.user.CarUserManager;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * API to manage {@link android.app.Activity} in Car.
+ *
+ * @hide
+ */
+public final class CarActivityManager extends CarManagerBase {
+ private static final String TAG = CarUserManager.class.getSimpleName();
+
+ /** Indicates that the operation was successful. */
+ public static final int RESULT_SUCCESS = 0;
+ /** Indicates that the operation was failed with the unknown reason. */
+ public static final int RESULT_FAILURE = -1;
+ /**
+ * Indicates that the operation was failed because the requester isn't the current user or
+ * the system user
+ */
+ public static final int RESULT_INVALID_USER = -2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "RESULT_", value = {
+ RESULT_SUCCESS,
+ RESULT_FAILURE,
+ RESULT_INVALID_USER,
+ })
+ @Target({ElementType.TYPE_USE})
+ public @interface ResultTypeEnum {}
+
+ /**
+ * Internal error code for throwing {@link ActivityNotFoundException} from service.
+ * @hide
+ */
+ public static final int ERROR_CODE_ACTIVITY_NOT_FOUND = -101;
+
+ private final ICarActivityService mService;
+
+ /**
+ * @hide
+ */
+ public CarActivityManager(@NonNull Car car, @NonNull IBinder service) {
+ this(car, ICarActivityService.Stub.asInterface(service));
+ }
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public CarActivityManager(@NonNull Car car, @NonNull ICarActivityService service) {
+ super(car);
+
+ mService = service;
+ }
+
+ /**
+ * Designates the given {@code activity} to be launched in {@code TaskDisplayArea} of
+ * {@code featureId} in the display of {@code displayId}.
+ * <p>Note: this will not affect the existing {@link Activity}.
+ * Note: You can map assign {@code Activity} to one {@code TaskDisplayArea} only. If
+ * you assign it to the multiple {@code TaskDisplayArea}s, then the last one wins.
+ * Note: The requester should be the current user or the system user, if not, the operation will
+ * be failed with {@code RESULT_INVALID_USER}.
+ *
+ * @param activity {@link Activity} to designate
+ * @param displayId {@code Display} where {@code TaskDisplayArea} is located in
+ * @param featureId {@code TaskDisplayArea} where {@link Activity} is launched in, if it is
+ * {@code DisplayAreaOrganizer.FEATURE_UNDEFINED}, then it'll remove the existing one.
+ * @return {@code ResultTypeEnum}. {@code RESULT_SUCCESS} if the operation is successful,
+ * otherwise, {@code RESULT_XXX} depending on the type of the error.
+ * @throws {@link IllegalArgumentException} if {@code displayId} or {@code featureId} is
+ * invalid. {@link ActivityNotFoundException} if {@code activity} is not found
+ * when it tries to remove.
+ */
+ @RequiresPermission(Car.PERMISSION_CONTROL_CAR_APP_LAUNCH)
+ @ResultTypeEnum
+ public int setPersistentActivity(
+ @NonNull ComponentName activity, int displayId, int featureId) {
+ try {
+ return mService.setPersistentActivity(activity, displayId, featureId);
+ } catch (IllegalArgumentException | IllegalStateException | SecurityException e) {
+ throw e;
+ } catch (ServiceSpecificException e) {
+ return handleServiceSpecificFromCarService(e);
+ } catch (RemoteException | RuntimeException e) {
+ return handleExceptionFromCarService(e, RESULT_FAILURE);
+ }
+ }
+
+ /** @hide */
+ @Override
+ protected void onCarDisconnected() {
+ // nothing to do
+ }
+
+ private int handleServiceSpecificFromCarService(ServiceSpecificException e)
+ throws ActivityNotFoundException {
+ if (e.errorCode == ERROR_CODE_ACTIVITY_NOT_FOUND) {
+ throw new ActivityNotFoundException(e.getMessage());
+ }
+ // don't know what this is
+ throw new IllegalStateException(e);
+ }
+}
diff --git a/car-lib/src/android/car/app/ICarActivityService.aidl b/car-lib/src/android/car/app/ICarActivityService.aidl
new file mode 100644
index 0000000..9f3e764
--- /dev/null
+++ b/car-lib/src/android/car/app/ICarActivityService.aidl
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 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 android.car.app;
+
+import android.content.ComponentName;
+
+/** @hide */
+interface ICarActivityService {
+ /**
+ * Designates the given {@code activity} to be launched in {@code TaskDisplayArea} of
+ * {@code featureId} in the display of {@code displayId}.
+ */
+ int setPersistentActivity(in ComponentName activity, int displayId, int featureId) = 0;
+}
+
diff --git a/car-lib/src/android/car/cluster/ClusterHomeManager.java b/car-lib/src/android/car/cluster/ClusterHomeManager.java
index 9b0cce4..3be2148 100644
--- a/car-lib/src/android/car/cluster/ClusterHomeManager.java
+++ b/car-lib/src/android/car/cluster/ClusterHomeManager.java
@@ -64,9 +64,9 @@
public static final int CONFIG_DISPLAY_ID = 0x10;
/**
- * Callback for ClusterHome to get notifications
+ * Callback for ClusterHome to get notifications when cluster state changes.
*/
- public interface ClusterHomeCallback {
+ public interface ClusterStateListener {
/**
* Called when ClusterOS changes the cluster display state, the geometry of cluster display,
* or the uiType.
@@ -74,61 +74,121 @@
* @param changes the flag indicates which fields are updated
*/
void onClusterStateChanged(ClusterState state, @Config int changes);
+ }
+ /**
+ * Callback for ClusterHome to get notifications when cluster navigation state changes.
+ */
+ public interface ClusterNavigationStateListener {
/** Called when the App who owns the navigation focus casts the new navigation state. */
void onNavigationState(byte[] navigationState);
}
- private static class CallbackRecord {
+ private static class ClusterStateListenerRecord {
final Executor mExecutor;
- final ClusterHomeCallback mCallback;
- CallbackRecord(Executor executor, ClusterHomeCallback callback) {
+ final ClusterStateListener mListener;
+ ClusterStateListenerRecord(Executor executor, ClusterStateListener listener) {
mExecutor = executor;
- mCallback = callback;
+ mListener = listener;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
- if (!(obj instanceof CallbackRecord)) {
+ if (!(obj instanceof ClusterStateListenerRecord)) {
return false;
}
- return mCallback == ((CallbackRecord) obj).mCallback;
+ return mListener == ((ClusterStateListenerRecord) obj).mListener;
}
@Override
public int hashCode() {
- return mCallback.hashCode();
+ return mListener.hashCode();
+ }
+ }
+
+ private static class ClusterNavigationStateListenerRecord {
+ final Executor mExecutor;
+ final ClusterNavigationStateListener mListener;
+
+ ClusterNavigationStateListenerRecord(Executor executor,
+ ClusterNavigationStateListener listener) {
+ mExecutor = executor;
+ mListener = listener;
+ }
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof ClusterNavigationStateListenerRecord)) {
+ return false;
+ }
+ return mListener == ((ClusterNavigationStateListenerRecord) obj).mListener;
+ }
+ @Override
+ public int hashCode() {
+ return mListener.hashCode();
}
}
private final IClusterHomeService mService;
- private final IClusterHomeCallbackImpl mBinderCallback;
- private final CopyOnWriteArrayList<CallbackRecord> mCallbacks = new CopyOnWriteArrayList<>();
+ private final IClusterStateListenerImpl mClusterStateListenerBinderCallback;
+ private final IClusterNavigationStateListenerImpl mClusterNavigationStateListenerBinderCallback;
+ private final CopyOnWriteArrayList<ClusterStateListenerRecord> mStateListeners =
+ new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<ClusterNavigationStateListenerRecord>
+ mNavigationStateListeners = new CopyOnWriteArrayList<>();
/** @hide */
@VisibleForTesting
public ClusterHomeManager(Car car, IBinder service) {
super(car);
mService = IClusterHomeService.Stub.asInterface(service);
- mBinderCallback = new IClusterHomeCallbackImpl(this);
+ mClusterStateListenerBinderCallback = new IClusterStateListenerImpl(this);
+ mClusterNavigationStateListenerBinderCallback =
+ new IClusterNavigationStateListenerImpl(this);
}
/**
* Registers the callback for ClusterHome.
*/
@RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
- public void registerClusterHomeCallback(
- @NonNull Executor executor, @NonNull ClusterHomeCallback callback) {
+ public void registerClusterStateListener(
+ @NonNull Executor executor, @NonNull ClusterStateListener callback) {
Objects.requireNonNull(executor, "executor cannot be null");
Objects.requireNonNull(callback, "callback cannot be null");
- CallbackRecord callbackRecord = new CallbackRecord(executor, callback);
- if (!mCallbacks.addIfAbsent(callbackRecord)) {
+ ClusterStateListenerRecord clusterStateListenerRecord =
+ new ClusterStateListenerRecord(executor, callback);
+ if (!mStateListeners.addIfAbsent(clusterStateListenerRecord)) {
return;
}
- if (mCallbacks.size() == 1) {
+ if (mStateListeners.size() == 1) {
try {
- mService.registerCallback(mBinderCallback);
+ mService.registerClusterStateListener(mClusterStateListenerBinderCallback);
+ } catch (RemoteException e) {
+ handleRemoteExceptionFromCarService(e);
+ }
+ }
+ }
+
+ /**
+ * Registers the callback for ClusterHome.
+ */
+ @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE)
+ public void registerClusterNavigationStateListener(
+ @NonNull Executor executor, @NonNull ClusterNavigationStateListener callback) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ ClusterNavigationStateListenerRecord clusterStateListenerRecord =
+ new ClusterNavigationStateListenerRecord(executor, callback);
+ if (!mNavigationStateListeners.addIfAbsent(clusterStateListenerRecord)) {
+ return;
+ }
+ if (mNavigationStateListeners.size() == 1) {
+ try {
+ mService.registerClusterNavigationStateListener(
+ mClusterNavigationStateListenerBinderCallback);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
@@ -139,24 +199,46 @@
* Unregisters the callback.
*/
@RequiresPermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL)
- public void unregisterClusterHomeCallback(@NonNull ClusterHomeCallback callback) {
+ public void unregisterClusterStateListener(@NonNull ClusterStateListener callback) {
Objects.requireNonNull(callback, "callback cannot be null");
- if (!mCallbacks.remove(new CallbackRecord(/* executor= */ null, callback))) {
+ if (!mStateListeners
+ .remove(new ClusterStateListenerRecord(/* executor= */ null, callback))) {
return;
}
- if (mCallbacks.isEmpty()) {
+ if (mStateListeners.isEmpty()) {
try {
- mService.unregisterCallback(mBinderCallback);
+ mService.unregisterClusterStateListener(mClusterStateListenerBinderCallback);
} catch (RemoteException ignored) {
// ignore for unregistering
}
}
}
- private static class IClusterHomeCallbackImpl extends IClusterHomeCallback.Stub {
+ /**
+ * Unregisters the callback.
+ */
+ @RequiresPermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE)
+ public void unregisterClusterNavigationStateListener(
+ @NonNull ClusterNavigationStateListener callback) {
+ Objects.requireNonNull(callback, "callback cannot be null");
+ if (!mNavigationStateListeners.remove(new ClusterNavigationStateListenerRecord(
+ /* executor= */ null, callback))) {
+ return;
+ }
+ if (mNavigationStateListeners.isEmpty()) {
+ try {
+ mService.unregisterClusterNavigationStateListener(
+ mClusterNavigationStateListenerBinderCallback);
+ } catch (RemoteException ignored) {
+ // ignore for unregistering
+ }
+ }
+ }
+
+ private static class IClusterStateListenerImpl extends IClusterStateListener.Stub {
private final WeakReference<ClusterHomeManager> mManager;
- private IClusterHomeCallbackImpl(ClusterHomeManager manager) {
+ private IClusterStateListenerImpl(ClusterHomeManager manager) {
mManager = new WeakReference<>(manager);
}
@@ -164,19 +246,28 @@
public void onClusterStateChanged(@NonNull ClusterState state, @Config int changes) {
ClusterHomeManager manager = mManager.get();
if (manager != null) {
- for (CallbackRecord cb : manager.mCallbacks) {
+ for (ClusterStateListenerRecord cb : manager.mStateListeners) {
cb.mExecutor.execute(
- () -> cb.mCallback.onClusterStateChanged(state, changes));
+ () -> cb.mListener.onClusterStateChanged(state, changes));
}
}
}
+ }
+
+ private static class IClusterNavigationStateListenerImpl extends
+ IClusterNavigationStateListener.Stub {
+ private final WeakReference<ClusterHomeManager> mManager;
+
+ private IClusterNavigationStateListenerImpl(ClusterHomeManager manager) {
+ mManager = new WeakReference<>(manager);
+ }
@Override
public void onNavigationStateChanged(@NonNull byte[] navigationState) {
ClusterHomeManager manager = mManager.get();
if (manager != null) {
- for (CallbackRecord cb : manager.mCallbacks) {
- cb.mExecutor.execute(() -> cb.mCallback.onNavigationState(navigationState));
+ for (ClusterNavigationStateListenerRecord lr : manager.mNavigationStateListeners) {
+ lr.mExecutor.execute(() -> lr.mListener.onNavigationState(navigationState));
}
}
}
@@ -267,6 +358,7 @@
@Override
protected void onCarDisconnected() {
- mCallbacks.clear();
+ mStateListeners.clear();
+ mNavigationStateListeners.clear();
}
}
diff --git a/car-lib/src/android/car/cluster/IClusterHomeCallback.aidl b/car-lib/src/android/car/cluster/IClusterHomeCallback.aidl
deleted file mode 100644
index 08c967b..0000000
--- a/car-lib/src/android/car/cluster/IClusterHomeCallback.aidl
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2021 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 android.car.cluster;
-
-import android.car.cluster.ClusterState;
-
-/** @hide */
-oneway interface IClusterHomeCallback {
- /**
- * Called when ClusterOS changes the cluster display state, the geometry of cluster display,
- * or the uiType.
- */
- void onClusterStateChanged(in ClusterState state, int changes) = 1;
- /** Called when the App who owns the navigation focus casts the new navigation state. */
- void onNavigationStateChanged(in byte[] navigationState) = 2;
-}
diff --git a/car-lib/src/android/car/cluster/IClusterHomeService.aidl b/car-lib/src/android/car/cluster/IClusterHomeService.aidl
index 7fa42af..c5c589b 100644
--- a/car-lib/src/android/car/cluster/IClusterHomeService.aidl
+++ b/car-lib/src/android/car/cluster/IClusterHomeService.aidl
@@ -17,7 +17,8 @@
package android.car.cluster;
import android.car.cluster.ClusterState;
-import android.car.cluster.IClusterHomeCallback;
+import android.car.cluster.IClusterStateListener;
+import android.car.cluster.IClusterNavigationStateListener;
import android.content.Intent;
import android.os.Bundle;
@@ -48,10 +49,16 @@
* does not stop the activity but it will not be re-launched any more.
*/
void stopFixedActivityMode() = 3;
- /** Registers a callback */
- void registerCallback(in IClusterHomeCallback callback) = 4;
- /** Unregisters a callback */
- void unregisterCallback(in IClusterHomeCallback callback) = 5;
+ // 4 removed. Do not use - void registerCallback(in IClusterHomeCallback callback) = 4;
+ // 5 removed. Do not use - void unregisterCallback(in IClusterHomeCallback callback) = 5;
/** Returns the current {@code ClusterDisplayState}. */
ClusterState getClusterState() = 6;
+ /** Registers a listener to listen for cluster state changes. */
+ void registerClusterStateListener(in IClusterStateListener listener) = 7;
+ /** Unregisters a cluster state listener. */
+ void unregisterClusterStateListener(in IClusterStateListener listener) = 8;
+ /** Registers a listener to lsiten for clustser navigation state changes. */
+ void registerClusterNavigationStateListener(in IClusterNavigationStateListener listener) = 9;
+ /** Unregisters a cluster navigation state listener. */
+ void unregisterClusterNavigationStateListener(in IClusterNavigationStateListener listener) = 10;
}
diff --git a/car-lib/src/android/car/cluster/IClusterNavigationStateListener.aidl b/car-lib/src/android/car/cluster/IClusterNavigationStateListener.aidl
new file mode 100644
index 0000000..470b891
--- /dev/null
+++ b/car-lib/src/android/car/cluster/IClusterNavigationStateListener.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 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 android.car.cluster;
+
+import android.car.cluster.ClusterState;
+
+/** @hide */
+oneway interface IClusterNavigationStateListener {
+ // 1 removed. Do not use - void onClusterStateChanged(in ClusterState state, int changes) = 1;
+
+ /** Called when the App who owns the navigation focus casts the new navigation state. */
+ void onNavigationStateChanged(in byte[] navigationState) = 2;
+}
diff --git a/car-lib/src/android/car/cluster/IClusterStateListener.aidl b/car-lib/src/android/car/cluster/IClusterStateListener.aidl
new file mode 100644
index 0000000..4fac990
--- /dev/null
+++ b/car-lib/src/android/car/cluster/IClusterStateListener.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2021 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 android.car.cluster;
+
+import android.car.cluster.ClusterState;
+
+/** @hide */
+oneway interface IClusterStateListener {
+ /**
+ * Called when ClusterOS changes the cluster display state, the geometry of cluster display,
+ * or the uiType.
+ */
+ void onClusterStateChanged(in ClusterState state, int changes) = 1;
+
+ // 2 removed. Do not use - void onNavigationStateChanged(in byte[] navigationState) = 2;
+}
diff --git a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
index fe697b1..008fc4b 100644
--- a/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
+++ b/car-lib/src/android/car/cluster/renderer/InstrumentClusterRenderingService.java
@@ -23,6 +23,7 @@
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.UserIdInt;
+import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.Service;
import android.car.Car;
@@ -152,7 +153,7 @@
String packageName) {
try {
ProviderInfo[] providers = packageManager.getPackageInfo(packageName,
- PackageManager.GET_PROVIDERS).providers;
+ PackageManager.GET_PROVIDERS | PackageManager.MATCH_ANY_USER).providers;
if (providers == null) {
return Collections.emptyList();
}
@@ -370,8 +371,16 @@
}
}
+ /**
+ * Returns the cluster activity from the application given by its package name.
+ *
+ * @return the {@link ComponentName} of the cluster activity, or null if the given application
+ * doesn't have a cluster activity.
+ *
+ * @hide
+ */
@Nullable
- private ComponentName getComponentFromPackage(@NonNull String packageName) {
+ public ComponentName getComponentFromPackage(@NonNull String packageName) {
PackageManager packageManager = getPackageManager();
// Check package permission.
@@ -385,8 +394,8 @@
Intent intent = new Intent(Intent.ACTION_MAIN)
.addCategory(Car.CAR_CATEGORY_NAVIGATION)
.setPackage(packageName);
- List<ResolveInfo> resolveList = packageManager.queryIntentActivities(intent,
- PackageManager.GET_RESOLVED_FILTER);
+ List<ResolveInfo> resolveList = packageManager.queryIntentActivitiesAsUser(intent,
+ PackageManager.GET_RESOLVED_FILTER, ActivityManager.getCurrentUser());
if (resolveList == null || resolveList.isEmpty()
|| resolveList.get(0).getComponentInfo() == null) {
Log.i(TAG, "Failed to resolve an intent: " + intent);
diff --git a/car-lib/src/android/car/input/CarInputManager.java b/car-lib/src/android/car/input/CarInputManager.java
index 8199bd0..7eddcf8 100644
--- a/car-lib/src/android/car/input/CarInputManager.java
+++ b/car-lib/src/android/car/input/CarInputManager.java
@@ -264,8 +264,10 @@
* same {@link CarInputManager} instance, then only the last registered callback will receive
* events, even if they were registered for different input event types.
*
- * @throws SecurityException is caller doesn't have android.car.permission.CAR_MONITOR_INPUT
- * permission granted
+ * @throws SecurityException if caller doesn't have
+ * {@code android.car.permission.CAR_MONITOR_INPUT} permission
+ * granted. Currently this method also accept
+ * {@code android.permission.MONITOR_INPUT}
* @throws IllegalArgumentException if targetDisplayType parameter correspond to a non supported
* display type
* @throws IllegalArgumentException if inputTypes parameter contains invalid or non supported
@@ -292,6 +294,10 @@
* CarInputCaptureCallback)} except that callbacks are invoked using
* the executor passed as parameter.
*
+ * @throws SecurityException if caller doesn't have
+ * {@code android.permission.MONITOR_INPUT} permission
+ * granted. Currently this method also accept
+ * {@code android.car.permission.CAR_MONITOR_INPUT}
* @param targetDisplayType the display type to register callback against
* @param inputTypes the input type to register callback against
* @param requestFlags the capture request flag
diff --git a/car-lib/src/android/car/telemetry/CarTelemetryManager.java b/car-lib/src/android/car/telemetry/CarTelemetryManager.java
index 3d3cf50..097ad98 100644
--- a/car-lib/src/android/car/telemetry/CarTelemetryManager.java
+++ b/car-lib/src/android/car/telemetry/CarTelemetryManager.java
@@ -45,7 +45,7 @@
private static final boolean DEBUG = false;
private static final String TAG = CarTelemetryManager.class.getSimpleName();
- private static final int MANIFEST_MAX_SIZE_BYTES = 10 * 1024; // 10 kb
+ private static final int METRICS_CONFIG_MAX_SIZE_BYTES = 10 * 1024; // 10 kb
private final CarTelemetryServiceListener mCarTelemetryServiceListener =
new CarTelemetryServiceListener(this);
@@ -58,49 +58,51 @@
private Executor mExecutor;
/**
- * Status to indicate that manifest was added successfully.
+ * Status to indicate that MetricsConfig was added successfully.
*/
- public static final int ERROR_NONE = 0;
+ public static final int ERROR_METRICS_CONFIG_NONE = 0;
/**
- * Status to indicate that add manifest failed because the same manifest based on the
+ * Status to indicate that add MetricsConfig failed because the same MetricsConfig based on the
* ManifestKey already exists.
*/
- public static final int ERROR_SAME_MANIFEST_EXISTS = 1;
+ public static final int ERROR_METRICS_CONFIG_ALREADY_EXISTS = 1;
/**
- * Status to indicate that add manifest failed because a newer version of the manifest exists.
+ * Status to indicate that add MetricsConfig failed because a newer version of the MetricsConfig
+ * exists.
*/
- public static final int ERROR_NEWER_MANIFEST_EXISTS = 2;
+ public static final int ERROR_METRICS_CONFIG_VERSION_TOO_OLD = 2;
/**
- * Status to indicate that add manifest failed because CarTelemetryService is unable to parse
- * the given byte array into a Manifest.
+ * Status to indicate that add MetricsConfig failed because CarTelemetryService is unable to
+ * parse the given byte array into a MetricsConfig.
*/
- public static final int ERROR_PARSE_MANIFEST_FAILED = 3;
+ public static final int ERROR_METRICS_CONFIG_PARSE_FAILED = 3;
/**
- * Status to indicate that add manifest failed because of failure to verify the signature of
- * the manifest.
+ * Status to indicate that add MetricsConfig failed because of failure to verify the signature
+ * of the MetricsConfig.
*/
- public static final int ERROR_SIGNATURE_VERIFICATION_FAILED = 4;
+ public static final int ERROR_METRICS_CONFIG_SIGNATURE_VERIFICATION_FAILED = 4;
/**
- * Status to indicate that add manifest failed because of a general error in cars.
+ * Status to indicate that add MetricsConfig failed because of a general error in cars.
*/
- public static final int ERROR_UNKNOWN = 5;
+ public static final int ERROR_METRICS_CONFIG_UNKNOWN = 5;
/** @hide */
- @IntDef(prefix = {"ERROR_"}, value = {
- ERROR_NONE,
- ERROR_SAME_MANIFEST_EXISTS,
- ERROR_NEWER_MANIFEST_EXISTS,
- ERROR_PARSE_MANIFEST_FAILED,
- ERROR_SIGNATURE_VERIFICATION_FAILED,
- ERROR_UNKNOWN
+ @IntDef(prefix = {"ERROR_METRICS_CONFIG_"}, value = {
+ ERROR_METRICS_CONFIG_NONE,
+ ERROR_METRICS_CONFIG_ALREADY_EXISTS,
+ ERROR_METRICS_CONFIG_VERSION_TOO_OLD,
+ ERROR_METRICS_CONFIG_PARSE_FAILED,
+ ERROR_METRICS_CONFIG_SIGNATURE_VERIFICATION_FAILED,
+ ERROR_METRICS_CONFIG_UNKNOWN
})
@Retention(RetentionPolicy.SOURCE)
- public @interface AddManifestError {}
+ public @interface MetricsConfigError {
+ }
/**
* Application registers {@link CarTelemetryResultsListener} object to receive data from
@@ -110,22 +112,31 @@
*/
public interface CarTelemetryResultsListener {
/**
- * Called by {@link com.android.car.telemetry.CarTelemetryService} to send script result to
- * the client.
+ * Sends script results to the client. Called by {@link CarTelemetryServiceListener}.
+ *
* TODO(b/184964661): Publish the documentation for the format of the results.
*
- * @param key the {@link ManifestKey} that the result is associated with.
- * @param result the serialized car telemetry result.
+ * @param key the {@link MetricsConfigKey} that the result is associated with.
+ * @param result the car telemetry result as serialized bytes.
*/
- void onResult(@NonNull ManifestKey key, @NonNull byte[] result);
+ void onResult(@NonNull MetricsConfigKey key, @NonNull byte[] result);
/**
- * Called by {@link com.android.car.telemetry.CarTelemetryService} to send error message to
- * the client.
+ * Sends script execution errors to the client.
*
+ * @param key the {@link MetricsConfigKey} that the error is associated with
* @param error the serialized car telemetry error.
*/
- void onError(@NonNull byte[] error);
+ void onError(@NonNull MetricsConfigKey key, @NonNull byte[] error);
+
+ /**
+ * Sends the {@link #addMetricsConfig(MetricsConfigKey, byte[])} status to the client.
+ *
+ * @param key the {@link MetricsConfigKey} that the status is associated with
+ * @param statusCode See {@link MetricsConfigError}.
+ */
+ void onAddMetricsConfigStatus(@NonNull MetricsConfigKey key,
+ @MetricsConfigError int statusCode);
}
/**
@@ -141,7 +152,7 @@
}
@Override
- public void onResult(@NonNull ManifestKey key, @NonNull byte[] result) {
+ public void onResult(@NonNull MetricsConfigKey key, @NonNull byte[] result) {
CarTelemetryManager manager = mManager.get();
if (manager == null) {
return;
@@ -150,28 +161,67 @@
}
@Override
- public void onError(@NonNull byte[] error) {
+ public void onError(@NonNull MetricsConfigKey key, @NonNull byte[] error) {
CarTelemetryManager manager = mManager.get();
if (manager == null) {
return;
}
- manager.onError(error);
+ manager.onError(key, error);
+ }
+
+ @Override
+ public void onAddMetricsConfigStatus(@NonNull MetricsConfigKey key,
+ @MetricsConfigError int statusCode) {
+ CarTelemetryManager manager = mManager.get();
+ if (manager == null) {
+ return;
+ }
+ manager.onAddMetricsConfigStatus(key, statusCode);
}
}
- private void onResult(ManifestKey key, byte[] result) {
+ private void onResult(MetricsConfigKey key, byte[] result) {
long token = Binder.clearCallingIdentity();
- synchronized (mLock) {
- mExecutor.execute(() -> mResultsListener.onResult(key, result));
+ Executor executor = getExecutor();
+ if (executor == null) {
+ return;
}
+ executor.execute(() -> {
+ CarTelemetryResultsListener listener = getResultsListener();
+ if (listener != null) {
+ listener.onResult(key, result);
+ }
+ });
Binder.restoreCallingIdentity(token);
}
- private void onError(byte[] error) {
+ private void onError(MetricsConfigKey key, byte[] error) {
long token = Binder.clearCallingIdentity();
- synchronized (mLock) {
- mExecutor.execute(() -> mResultsListener.onError(error));
+ Executor executor = getExecutor();
+ if (executor == null) {
+ return;
}
+ executor.execute(() -> {
+ CarTelemetryResultsListener listener = getResultsListener();
+ if (listener != null) {
+ listener.onError(key, error);
+ }
+ });
+ Binder.restoreCallingIdentity(token);
+ }
+
+ private void onAddMetricsConfigStatus(MetricsConfigKey key, int statusCode) {
+ long token = Binder.clearCallingIdentity();
+ Executor executor = getExecutor();
+ if (executor == null) {
+ return;
+ }
+ executor.execute(() -> {
+ CarTelemetryResultsListener listener = getResultsListener();
+ if (listener != null) {
+ listener.onAddMetricsConfigStatus(key, statusCode);
+ }
+ });
Binder.restoreCallingIdentity(token);
}
@@ -205,11 +255,11 @@
/**
* Registers a listener with {@link com.android.car.telemetry.CarTelemetryService} for client
- * to receive script execution results.
+ * to receive script execution results. The listener must be set before invoking other APIs in
+ * this class.
*
* @param listener to received data from {@link com.android.car.telemetry.CarTelemetryService}.
* @throws IllegalStateException if the listener is already set.
- *
* @hide
*/
@RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
@@ -249,77 +299,92 @@
}
/**
- * Called by client to send telemetry manifest. The size of the manifest cannot exceed a
- * predefined size. Otherwise an exception is thrown.
- * The {@link ManifestKey} is used to uniquely identify a manifest. If a manifest of the same
- * name already exists in {@link com.android.car.telemetry.CarTelemetryService}, then the
- * version will be compared. If the version is strictly higher, the existing manifest will be
- * replaced by the new one. All cache and intermediate results will be cleared if replaced.
- * TODO(b/185420981): Update javadoc after CarTelemetryService has concrete implementation.
+ * Sends a telemetry MetricsConfig to CarTelemetryService. The size of the MetricsConfig cannot
+ * exceed a predefined size, otherwise an exception is thrown.
+ * The {@link MetricsConfigKey} is used to uniquely identify a MetricsConfig. If a MetricsConfig
+ * of the same name already exists in {@link com.android.car.telemetry.CarTelemetryService},
+ * the config version will be compared. If the version is strictly higher, the existing
+ * MetricsConfig will be replaced by the new one. All legacy data will be cleared if replaced.
+ * Client should use {@link #sendFinishedReports(MetricsConfigKey)} to get the result before
+ * replacing a MetricsConfig.
+ * The status of this API is sent back asynchronously via {@link CarTelemetryResultsListener}.
*
- * @param key the unique key to identify the manifest.
- * @param manifest the serialized bytes of a Manifest object.
- * @return {@link #AddManifestError} to tell the result of the request.
- * @throws IllegalArgumentException if the manifest size exceeds limit.
- *
+ * @param key the unique key to identify the MetricsConfig.
+ * @param metricsConfig the serialized bytes of a MetricsConfig object.
+ * @throws IllegalArgumentException if the MetricsConfig size exceeds limit.
+ * @throws IllegalStateException if the listener is not set.
* @hide
*/
@RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
- public @AddManifestError int addManifest(@NonNull ManifestKey key, @NonNull byte[] manifest) {
- if (manifest.length > MANIFEST_MAX_SIZE_BYTES) {
- throw new IllegalArgumentException("Manifest size exceeds limit.");
+ public void addMetricsConfig(@NonNull MetricsConfigKey key, @NonNull byte[] metricsConfig) {
+ if (getResultsListener() == null) {
+ throw new IllegalStateException("Listener must be set.");
+ }
+ if (metricsConfig.length > METRICS_CONFIG_MAX_SIZE_BYTES) {
+ throw new IllegalArgumentException("MetricsConfig size exceeds limit.");
}
try {
- return mService.addManifest(key, manifest);
- } catch (RemoteException e) {
- handleRemoteExceptionFromCarService(e);
- }
- return ERROR_UNKNOWN;
- }
-
- /**
- * Removes a manifest from {@link com.android.car.telemetry.CarTelemetryService}. If the
- * manifest does not exist, nothing will be removed but the status will be indicated in the
- * return value.
- *
- * @param key the unique key to identify the manifest. Name and version must be exact.
- * @return true for success, false otherwise.
- * @hide
- */
- @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
- public boolean removeManifest(@NonNull ManifestKey key) {
- try {
- return mService.removeManifest(key);
- } catch (RemoteException e) {
- handleRemoteExceptionFromCarService(e);
- }
- return false;
- }
-
- /**
- * Removes all manifests from {@link com.android.car.telemetry.CarTelemetryService}.
- *
- * @hide
- */
- @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
- public void removeAllManifests() {
- try {
- mService.removeAllManifests();
+ mService.addMetricsConfig(key, metricsConfig);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
}
/**
- * An asynchronous API for the client to get script execution results of a specific manifest
- * from the {@link com.android.car.telemetry.CarTelemetryService} through the listener.
+ * Removes a MetricsConfig from {@link com.android.car.telemetry.CarTelemetryService}. This
+ * will also remove outputs produced by the MetricsConfig. If the MetricsConfig does not exist,
+ * nothing will be removed.
+ *
+ * @param key the unique key to identify the MetricsConfig. Name and version must be exact.
+ * @throws IllegalStateException if the listener is not set.
+ * @hide
+ */
+ @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
+ public void removeMetricsConfig(@NonNull MetricsConfigKey key) {
+ if (getResultsListener() == null) {
+ throw new IllegalStateException("Listener must be set.");
+ }
+ try {
+ mService.removeMetricsConfig(key);
+ } catch (RemoteException e) {
+ handleRemoteExceptionFromCarService(e);
+ }
+ }
+
+ /**
+ * Removes all MetricsConfigs from {@link com.android.car.telemetry.CarTelemetryService}. This
+ * will also remove all MetricsConfig outputs.
+ *
+ * @throws IllegalStateException if the listener is not set.
+ * @hide
+ */
+ @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
+ public void removeAllMetricsConfigs() {
+ if (getResultsListener() == null) {
+ throw new IllegalStateException("Listener must be set.");
+ }
+ try {
+ mService.removeAllMetricsConfigs();
+ } catch (RemoteException e) {
+ handleRemoteExceptionFromCarService(e);
+ }
+ }
+
+ /**
+ * Gets script execution results of a MetricsConfig as from the
+ * {@link com.android.car.telemetry.CarTelemetryService}. This API is asynchronous and the
+ * result is sent back asynchronously via the {@link CarTelemetryResultsListener}.
* This call is destructive. The returned results will be deleted from CarTelemetryService.
*
- * @param key the unique key to identify the manifest.
+ * @param key the unique key to identify the MetricsConfig.
+ * @throws IllegalStateException if the listener is not set.
* @hide
*/
@RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
- public void sendFinishedReports(@NonNull ManifestKey key) {
+ public void sendFinishedReports(@NonNull MetricsConfigKey key) {
+ if (getResultsListener() == null) {
+ throw new IllegalStateException("Listener must be set.");
+ }
try {
mService.sendFinishedReports(key);
} catch (RemoteException e) {
@@ -328,14 +393,18 @@
}
/**
- * An asynchronous API for the client to get all script execution results
- * from the {@link com.android.car.telemetry.CarTelemetryService} through the listener.
+ * Gets all script execution results from {@link com.android.car.telemetry.CarTelemetryService}
+ * asynchronously via the {@link CarTelemetryResultsListener}.
* This call is destructive. The returned results will be deleted from CarTelemetryService.
*
+ * @throws IllegalStateException if the listener is not set.
* @hide
*/
@RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
public void sendAllFinishedReports() {
+ if (getResultsListener() == null) {
+ throw new IllegalStateException("Listener must be set.");
+ }
try {
mService.sendAllFinishedReports();
} catch (RemoteException e) {
@@ -343,19 +412,15 @@
}
}
- /**
- * An asynchronous API for the client to get all script execution errors
- * from the {@link com.android.car.telemetry.CarTelemetryService} through the listener.
- * This call is destructive. The returned results will be deleted from CarTelemetryService.
- *
- * @hide
- */
- @RequiresPermission(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE)
- public void sendScriptExecutionErrors() {
- try {
- mService.sendScriptExecutionErrors();
- } catch (RemoteException e) {
- handleRemoteExceptionFromCarService(e);
+ private CarTelemetryResultsListener getResultsListener() {
+ synchronized (mLock) {
+ return mResultsListener;
+ }
+ }
+
+ private Executor getExecutor() {
+ synchronized (mLock) {
+ return mExecutor;
}
}
}
diff --git a/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl b/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl
index 09743d8..8343c34 100644
--- a/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl
+++ b/car-lib/src/android/car/telemetry/ICarTelemetryService.aidl
@@ -1,7 +1,7 @@
package android.car.telemetry;
import android.car.telemetry.ICarTelemetryServiceListener;
-import android.car.telemetry.ManifestKey;
+import android.car.telemetry.MetricsConfigKey;
/**
* Internal binder interface for {@code CarTelemetryService}, used by {@code CarTelemetryManager}.
@@ -21,33 +21,29 @@
void clearListener();
/**
- * Sends telemetry manifests to CarTelemetryService.
+ * Sends telemetry MetricsConfigs to CarTelemetryService.
*/
- int addManifest(in ManifestKey key, in byte[] manifest);
+ void addMetricsConfig(in MetricsConfigKey key, in byte[] metricsConfig);
/**
- * Removes a manifest based on the key.
+ * Removes a MetricsConfig based on the key. This will also remove outputs produced by the
+ * MetricsConfig.
*/
- boolean removeManifest(in ManifestKey key);
+ void removeMetricsConfig(in MetricsConfigKey key);
/**
- * Removes all manifests.
+ * Removes all MetricsConfigs. This will also remove all MetricsConfig outputs.
*/
- void removeAllManifests();
+ void removeAllMetricsConfigs();
/**
- * Sends script results associated with the given key using the
+ * Sends script results or errors associated with the given key using the
* {@code ICarTelemetryServiceListener}.
*/
- void sendFinishedReports(in ManifestKey key);
+ void sendFinishedReports(in MetricsConfigKey key);
/**
- * Sends all script results associated using the {@code ICarTelemetryServiceListener}.
+ * Sends all script results or errors using the {@code ICarTelemetryServiceListener}.
*/
void sendAllFinishedReports();
-
- /**
- * Sends all errors using the {@code ICarTelemetryServiceListener}.
- */
- void sendScriptExecutionErrors();
}
\ No newline at end of file
diff --git a/car-lib/src/android/car/telemetry/ICarTelemetryServiceListener.aidl b/car-lib/src/android/car/telemetry/ICarTelemetryServiceListener.aidl
index ba4ca2d..f77558c 100644
--- a/car-lib/src/android/car/telemetry/ICarTelemetryServiceListener.aidl
+++ b/car-lib/src/android/car/telemetry/ICarTelemetryServiceListener.aidl
@@ -16,7 +16,7 @@
package android.car.telemetry;
-import android.car.telemetry.ManifestKey;
+import android.car.telemetry.MetricsConfigKey;
import java.util.List;
/**
@@ -34,13 +34,21 @@
* @param key the key that the result is associated with.
* @param result the serialized bytes of the script execution result message.
*/
- void onResult(in ManifestKey key, in byte[] result);
+ void onResult(in MetricsConfigKey key, in byte[] result);
/**
- * Called by {@code CarTelemetryService} to provide telemetry errors. This call is destrutive.
+ * Called by {@code CarTelemetryService} to provide telemetry errors. This call is destructive.
* The parameter will no longer be stored in {@code CarTelemetryService}.
*
* @param error the serialized bytes of an error message.
*/
- void onError(in byte[] error);
+ void onError(in MetricsConfigKey key, in byte[] error);
+
+ /**
+ * Sends the {@link #addMetricsConfig(MetricsConfigKey, byte[])} status to the client.
+ *
+ * @param key the {@link MetricsConfigKey} that the status is associated with.
+ * @param statusCode indicating add status.
+ */
+ void onAddMetricsConfigStatus(in MetricsConfigKey key, in int statusCode);
}
\ No newline at end of file
diff --git a/car-lib/src/android/car/telemetry/IScriptExecutor.aidl b/car-lib/src/android/car/telemetry/IScriptExecutor.aidl
deleted file mode 100644
index 83ea3f0..0000000
--- a/car-lib/src/android/car/telemetry/IScriptExecutor.aidl
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (c) 2021, 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 android.car.telemetry;
-
-import android.car.telemetry.IScriptExecutorListener;
-import android.os.Bundle;
-
-/**
- * An internal API provided by isolated Script Executor process
- * for executing Lua scripts in a sandboxed environment
- *
- * @hide
- */
-interface IScriptExecutor {
- /**
- * Executes a specified function in provided Lua script with given input arguments.
- *
- * @param scriptBody complete body of Lua script that also contains the function to be invoked
- * @param functionName the name of the function to execute
- * @param publishedData input data provided by the source which the function handles
- * @param savedState key-value pairs preserved from the previous invocation of the function
- * @param listener callback for the sandboxed environent to report back script execution results, errors, and logs
- */
- void invokeScript(String scriptBody,
- String functionName,
- in Bundle publishedData,
- in @nullable Bundle savedState,
- in IScriptExecutorListener listener);
-}
diff --git a/car-lib/src/android/car/telemetry/IScriptExecutorListener.aidl b/car-lib/src/android/car/telemetry/IScriptExecutorListener.aidl
deleted file mode 100644
index d751a61..0000000
--- a/car-lib/src/android/car/telemetry/IScriptExecutorListener.aidl
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (c) 2021, 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 android.car.telemetry;
-
-import android.os.Bundle;
-
-/**
- * Listener for {@code IScriptExecutor#invokeScript}.
- *
- * An invocation of a script by Script Executor will result in a call of only one
- * of the three methods below. If a script fully completes its objective, onScriptFinished
- * is called. If a script's invocation completes normally, onSuccess is called.
- * onError is called if any error happens before or during script execution and we
- * should abandon this run of the script.
- */
-interface IScriptExecutorListener {
- /**
- * Called by ScriptExecutor when the script declares itself as "finished".
- *
- * @param result final results of the script that will be uploaded.
- */
- void onScriptFinished(in byte[] result);
-
- /**
- * Called by ScriptExecutor when a function completes successfully and also provides
- * optional state that the script wants CarTelemetryService to persist.
- *
- * @param stateToPersist key-value pairs to persist
- */
- void onSuccess(in @nullable Bundle stateToPersist);
-
- /**
- * Default error type.
- */
- const int ERROR_TYPE_UNSPECIFIED = 0;
-
- /**
- * Used when an error occurs in the ScriptExecutor code.
- */
- const int ERROR_TYPE_SCRIPT_EXECUTOR_ERROR = 1;
-
- /**
- * Used when an error occurs while executing the Lua script (such as
- * errors returned by lua_pcall)
- */
- const int ERROR_TYPE_LUA_RUNTIME_ERROR = 2;
-
-
- /**
- * Called by ScriptExecutor to report errors that prevented the script
- * from running or completing execution successfully.
- *
- * @param errorType type of the error message as defined in this aidl file.
- * @param messsage the human-readable message containing information helpful for analysis or debugging.
- * @param stackTrace the stack trace of the error if available.
- */
- void onError(int errorType, String message, @nullable String stackTrace);
-}
-
diff --git a/car-lib/src/android/car/telemetry/ManifestKey.aidl b/car-lib/src/android/car/telemetry/ManifestKey.aidl
deleted file mode 100644
index 25097df..0000000
--- a/car-lib/src/android/car/telemetry/ManifestKey.aidl
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2021 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 android.car.telemetry;
-
-/**
- * @hide
- */
-parcelable ManifestKey;
\ No newline at end of file
diff --git a/car-lib/src/android/car/telemetry/ManifestKey.java b/car-lib/src/android/car/telemetry/ManifestKey.java
deleted file mode 100644
index b0a69c2..0000000
--- a/car-lib/src/android/car/telemetry/ManifestKey.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2021 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 android.car.telemetry;
-
-import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-/**
- * A parcelable that wraps around the Manifest name and version.
- *
- * @hide
- */
-public final class ManifestKey implements Parcelable {
-
- @NonNull
- private String mName;
- private int mVersion;
-
- @NonNull
- public String getName() {
- return mName;
- }
-
- public int getVersion() {
- return mVersion;
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel out, int flags) {
- out.writeString(mName);
- out.writeInt(mVersion);
- }
-
- private ManifestKey(Parcel in) {
- mName = in.readString();
- mVersion = in.readInt();
- }
-
- public ManifestKey(@NonNull String name, int version) {
- mName = name;
- mVersion = version;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- public static final @NonNull Parcelable.Creator<ManifestKey> CREATOR =
- new Parcelable.Creator<ManifestKey>() {
- @Override
- public ManifestKey createFromParcel(Parcel in) {
- return new ManifestKey(in);
- }
-
- @Override
- public ManifestKey[] newArray(int size) {
- return new ManifestKey[size];
- }
- };
-}
diff --git a/car-lib/src/android/car/telemetry/MetricsConfigKey.aidl b/car-lib/src/android/car/telemetry/MetricsConfigKey.aidl
new file mode 100644
index 0000000..2c00127
--- /dev/null
+++ b/car-lib/src/android/car/telemetry/MetricsConfigKey.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2021 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 android.car.telemetry;
+
+/**
+ * @hide
+ */
+parcelable MetricsConfigKey;
\ No newline at end of file
diff --git a/car-lib/src/android/car/telemetry/MetricsConfigKey.java b/car-lib/src/android/car/telemetry/MetricsConfigKey.java
new file mode 100644
index 0000000..ddc9718
--- /dev/null
+++ b/car-lib/src/android/car/telemetry/MetricsConfigKey.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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 android.car.telemetry;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A parcelable that wraps around the Manifest name and version.
+ *
+ * @hide
+ */
+public final class MetricsConfigKey implements Parcelable {
+
+ @NonNull
+ private String mName;
+ private int mVersion;
+
+ @NonNull
+ public String getName() {
+ return mName;
+ }
+
+ public int getVersion() {
+ return mVersion;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeString(mName);
+ out.writeInt(mVersion);
+ }
+
+ private MetricsConfigKey(Parcel in) {
+ mName = in.readString();
+ mVersion = in.readInt();
+ }
+
+ public MetricsConfigKey(@NonNull String name, int version) {
+ mName = name;
+ mVersion = version;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof MetricsConfigKey)) {
+ return false;
+ }
+ MetricsConfigKey other = (MetricsConfigKey) o;
+ return mName.equals(other.getName()) && mVersion == other.getVersion();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mName, mVersion);
+ }
+
+ public static final @NonNull Parcelable.Creator<MetricsConfigKey> CREATOR =
+ new Parcelable.Creator<MetricsConfigKey>() {
+ @Override
+ public MetricsConfigKey createFromParcel(Parcel in) {
+ return new MetricsConfigKey(in);
+ }
+
+ @Override
+ public MetricsConfigKey[] newArray(int size) {
+ return new MetricsConfigKey[size];
+ }
+ };
+}
diff --git a/car-lib/src/android/car/user/CarUserManager.java b/car-lib/src/android/car/user/CarUserManager.java
index dace5a8..f42da45 100644
--- a/car-lib/src/android/car/user/CarUserManager.java
+++ b/car-lib/src/android/car/user/CarUserManager.java
@@ -307,13 +307,20 @@
public @interface UserIdentificationAssociationValue{}
private final Object mLock = new Object();
+
private final ICarUserService mService;
private final UserManager mUserManager;
+ /**
+ * Map of listeners registers by the app.
+ */
@Nullable
@GuardedBy("mLock")
private ArrayMap<UserLifecycleListener, Executor> mListeners;
+ /**
+ * Receiver used to receive user-lifecycle callbacks from the service.
+ */
@Nullable
@GuardedBy("mLock")
private LifecycleResultReceiver mReceiver;
@@ -332,6 +339,7 @@
public CarUserManager(@NonNull Car car, @NonNull ICarUserService service,
@NonNull UserManager userManager) {
super(car);
+
mService = service;
mUserManager = userManager;
}
@@ -524,20 +532,31 @@
Objects.requireNonNull(listener, "listener cannot be null");
int uid = myUid();
+ String packageName = getContext().getPackageName();
+ if (DBG) {
+ Log.d(TAG, "addListener(): uid=" + uid + ", pkg=" + packageName
+ + ", listener=" + listener);
+ }
synchronized (mLock) {
Preconditions.checkState(mListeners == null || !mListeners.containsKey(listener),
"already called for this listener");
if (mReceiver == null) {
mReceiver = new LifecycleResultReceiver();
try {
- EventLog.writeEvent(EventLogTags.CAR_USER_MGR_ADD_LISTENER, uid);
- if (DBG) Log.d(TAG, "Setting lifecycle receiver for uid " + uid);
- mService.setLifecycleListenerForUid(mReceiver);
+ EventLog.writeEvent(EventLogTags.CAR_USER_MGR_ADD_LISTENER, uid, packageName);
+ if (DBG) {
+ Log.d(TAG, "Setting lifecycle receiver for uid " + uid + " and package "
+ + packageName);
+ }
+ mService.setLifecycleListenerForApp(packageName, mReceiver);
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
}
} else {
- if (DBG) Log.d(TAG, "Already set receiver for uid " + uid);
+ if (DBG) {
+ Log.d(TAG, "Already set receiver for uid " + uid + " and package "
+ + packageName);
+ }
}
if (mListeners == null) {
@@ -547,7 +566,7 @@
+ " already has " + mListeners.size() + " listeners: "
+ mListeners.keySet().stream()
.map((l) -> getLambdaName(l))
- .collect(Collectors.toList()), new Exception());
+ .collect(Collectors.toList()), new Exception("caller's stack"));
}
if (DBG) Log.d(TAG, "Adding listener: " + listener);
mListeners.put(listener, executor);
@@ -568,6 +587,11 @@
Objects.requireNonNull(listener, "listener cannot be null");
int uid = myUid();
+ String packageName = getContext().getPackageName();
+ if (DBG) {
+ Log.d(TAG, "removeListener(): uid=" + uid + ", pkg=" + packageName
+ + ", listener=" + listener);
+ }
synchronized (mLock) {
Preconditions.checkState(mListeners != null && mListeners.containsKey(listener),
"not called for this listener yet");
@@ -584,10 +608,13 @@
return;
}
- EventLog.writeEvent(EventLogTags.CAR_USER_MGR_REMOVE_LISTENER, uid);
- if (DBG) Log.d(TAG, "Removing lifecycle receiver for uid=" + uid);
+ EventLog.writeEvent(EventLogTags.CAR_USER_MGR_REMOVE_LISTENER, uid, packageName);
+ if (DBG) {
+ Log.d(TAG, "Removing lifecycle receiver for uid=" + uid + " and package "
+ + packageName);
+ }
try {
- mService.resetLifecycleListenerForUid();
+ mService.resetLifecycleListenerForApp(mReceiver);
mReceiver = null;
} catch (RemoteException e) {
handleRemoteExceptionFromCarService(e);
diff --git a/car-lib/src/android/car/watchdog/CarWatchdogManager.java b/car-lib/src/android/car/watchdog/CarWatchdogManager.java
index aa930a0..007bb22 100644
--- a/car-lib/src/android/car/watchdog/CarWatchdogManager.java
+++ b/car-lib/src/android/car/watchdog/CarWatchdogManager.java
@@ -410,8 +410,8 @@
/**
* Returns resource overuse stats for a specific user package.
*
- * @param packageName Name of the package whose stats should to be returned.
- * @param userId ID of the user whose stats should be returned.
+ * @param packageName Name of the package whose stats should be returned.
+ * @param userHandle Handle of the user whose stats should be returned.
* @param resourceOveruseFlag Flag to indicate the types of resource overuse stats to return.
* @param maxStatsPeriod Maximum period to aggregate the resource overuse stats.
*
@@ -635,7 +635,10 @@
* exception. This API may be used by CarSettings application or UI notification.
*
* @param packageName Name of the package whose setting should to be updated.
- * @param userHandle User whose setting should to be updated.
+ * Note: All packages under shared UID share the killable state as well. Thus
+ * setting the killable state for one package will set the killable state for
+ * all other packages that share a UID.
+ * @param userHandle User whose setting should be updated.
* @param isKillable Whether or not the package for the specified user is killable on resource
* overuse.
*
@@ -657,7 +660,7 @@
*
* <p>This API may be used by CarSettings application or UI notification.
*
- * @param userHandle User whose killable states for all packages should to be returned.
+ * @param userHandle User whose killable states for all packages should be returned.
*
* @hide
*/
diff --git a/car-lib/src/android/car/watchdog/IoOveruseConfiguration.java b/car-lib/src/android/car/watchdog/IoOveruseConfiguration.java
index 608c3c3..2e23d46 100644
--- a/car-lib/src/android/car/watchdog/IoOveruseConfiguration.java
+++ b/car-lib/src/android/car/watchdog/IoOveruseConfiguration.java
@@ -48,6 +48,10 @@
/**
* Package specific thresholds only for system and vendor packages.
*
+ * NOTE: For packages that share a UID, the package name should be the shared package name
+ * because the I/O usage is aggregated for all packages under the shared UID. The shared
+ * package names should have the prefix 'shared:'.
+ *
* <p>System component must provide package specific thresholds only for system packages.
* <p>Vendor component must provide package specific thresholds only for vendor packages.
*/
diff --git a/car-lib/src/android/car/watchdog/ResourceOveruseConfiguration.java b/car-lib/src/android/car/watchdog/ResourceOveruseConfiguration.java
index 03a972a..a411a0d 100644
--- a/car-lib/src/android/car/watchdog/ResourceOveruseConfiguration.java
+++ b/car-lib/src/android/car/watchdog/ResourceOveruseConfiguration.java
@@ -76,8 +76,10 @@
/**
* List of system or vendor packages that are safe to be killed on resource overuse.
*
- * <p>System component must provide only safe-to-kill system packages in this list.
- * <p>Vendor component must provide only safe-to-kill vendor packages in this list.
+ * <p>When specifying shared package names, the package names should contain the prefix
+ * 'shared:'.
+ * <p>System components must provide only safe-to-kill system packages in this list.
+ * <p>Vendor components must provide only safe-to-kill vendor packages in this list.
*/
private @NonNull List<String> mSafeToKillPackages;
@@ -87,6 +89,9 @@
* <p>Any pre-installed package name starting with one of the prefixes or any package from the
* vendor partition is identified as a vendor package and vendor provided thresholds are applied
* to these packages. This list must be provided only by the vendor component.
+ *
+ * <p>When specifying shared package name prefixes, the prefix should contain 'shared:' at
+ * the beginning.
*/
private @NonNull List<String> mVendorPackagePrefixes;
@@ -97,6 +102,10 @@
* <p>This mapping must contain only packages that can be mapped to one of the
* {@link ApplicationCategoryType} types. This mapping must be defined only by the system and
* vendor components.
+ *
+ * <p>For packages under a shared UID, the application category type must be specified
+ * for the shared package name and not for individual packages under the shared UID. When
+ * specifying shared package names, the package names should contain the prefix 'shared:'.
*/
private @NonNull Map<String, String> mPackagesToAppCategoryTypes;
diff --git a/car-lib/src/android/car/watchdog/ResourceOveruseStats.java b/car-lib/src/android/car/watchdog/ResourceOveruseStats.java
index 992cff2..d9b639f 100644
--- a/car-lib/src/android/car/watchdog/ResourceOveruseStats.java
+++ b/car-lib/src/android/car/watchdog/ResourceOveruseStats.java
@@ -33,6 +33,9 @@
public final class ResourceOveruseStats implements Parcelable {
/**
* Name of the package, whose stats are recorded in the below fields.
+ *
+ * NOTE: For packages that share a UID, the package name will be the shared package name because
+ * the stats are aggregated for all packages under the shared UID.
*/
private @NonNull String mPackageName;
diff --git a/car-lib/src/com/android/car/internal/common/EventLogTags.logtags b/car-lib/src/com/android/car/internal/common/EventLogTags.logtags
index 65a3fb1..45fdfe9 100644
--- a/car-lib/src/com/android/car/internal/common/EventLogTags.logtags
+++ b/car-lib/src/com/android/car/internal/common/EventLogTags.logtags
@@ -69,8 +69,8 @@
150100 car_user_svc_initial_user_info_req (request_type|1),(timeout|1)
150101 car_user_svc_initial_user_info_resp (status|1),(action|1),(user_id|1),(flags|1),(safe_name|3),(user_locales|3)
150103 car_user_svc_set_initial_user (user_id|1)
-150104 car_user_svc_set_lifecycle_listener (uid|1)
-150105 car_user_svc_reset_lifecycle_listener (uid|1)
+150104 car_user_svc_set_lifecycle_listener (uid|1),(package_name|3)
+150105 car_user_svc_reset_lifecycle_listener (uid|1),(package_name|3)
150106 car_user_svc_switch_user_req (user_id|1),(timeout|1)
150107 car_user_svc_switch_user_resp (hal_callback_status|1),(user_switch_status|1),(error_message|3)
150108 car_user_svc_post_switch_user_req (target_user_id|1),(current_user_id|1)
@@ -86,7 +86,7 @@
150118 car_user_svc_create_user_user_removed (user_id|1),(reason|3)
150119 car_user_svc_remove_user_req (user_id|1),(hasCallerRestrictions|1)
150120 car_user_svc_remove_user_resp (user_id|1),(result|1)
-150121 car_user_svc_notify_app_lifecycle_listener (uid|1),(event_type|1),(from_user_id|1),(to_user_id|1)
+150121 car_user_svc_notify_app_lifecycle_listener (uid|1),(package_name|3),(event_type|1),(from_user_id|1),(to_user_id|1)
150122 car_user_svc_notify_internal_lifecycle_listener (listener_name|3),(event_type|1),(from_user_id|1),(to_user_id|1)
150123 car_user_svc_pre_creation_requested (number_users|1),(number_guests|1)
150124 car_user_svc_pre_creation_status (number_existing_users|1),(number_users_to_add|1),(number_users_to_remove|1),(number_existing_guests|1),(number_guests_to_add|1),(number_guests_to_remove|1),(number_invalid_users_to_remove|1)
@@ -111,8 +111,8 @@
150152 car_user_hal_create_user_resp (request_id|1),(status|1),(result|1),(error_message|3)
150153 car_user_hal_remove_user_req (target_user_id|1),(current_user_id|1)
-150171 car_user_mgr_add_listener (uid|1)
-150172 car_user_mgr_remove_listener (uid|1)
+150171 car_user_mgr_add_listener (uid|1),(package_name|3)
+150172 car_user_mgr_remove_listener (uid|1),(package_name|3)
150173 car_user_mgr_disconnected (uid|1)
150174 car_user_mgr_switch_user_req (uid|1),(user_id|1)
150175 car_user_mgr_switch_user_resp (uid|1),(status|1),(error_message|3)
diff --git a/car-test-lib/Android.bp b/car-test-lib/Android.bp
index bb72590..bad376e 100644
--- a/car-test-lib/Android.bp
+++ b/car-test-lib/Android.bp
@@ -48,5 +48,6 @@
"android.hardware.automotive.vehicle-V2.0-java",
"mockito-target-extended",
"compatibility-device-util-axt",
+ "android.test.mock",
],
}
diff --git a/car-test-lib/src/android/car/test/mocks/AndroidMockitoHelper.java b/car-test-lib/src/android/car/test/mocks/AndroidMockitoHelper.java
index deb3946..29b1471 100644
--- a/car-test-lib/src/android/car/test/mocks/AndroidMockitoHelper.java
+++ b/car-test-lib/src/android/car/test/mocks/AndroidMockitoHelper.java
@@ -50,6 +50,7 @@
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
/**
* Provides common Mockito calls for core Android classes.
@@ -133,6 +134,16 @@
}
/**
+ * Mocks {@code UserManager#getUserHandles()} to return the simple users with the given ids.
+ */
+ public static void mockUmGetUserHandles(@NonNull UserManager um, boolean excludeDying,
+ @NonNull @UserIdInt int... userIds) {
+ List<UserHandle> result = UserTestingHelper.newUsers(userIds).stream().map(
+ UserInfo::getUserHandle).collect(Collectors.toList());
+ when(um.getUserHandles(excludeDying)).thenReturn(result);
+ }
+
+ /**
* Mocks {@code UserManager#getUsers(excludePartial, excludeDying, excludeDying)} to return the
* given users.
*/
diff --git a/car-test-lib/src/android/car/test/util/BroadcastingFakeContext.java b/car-test-lib/src/android/car/test/util/BroadcastingFakeContext.java
new file mode 100644
index 0000000..014e283
--- /dev/null
+++ b/car-test-lib/src/android/car/test/util/BroadcastingFakeContext.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2021 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 android.car.test.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.test.mock.MockContext;
+
+import com.google.common.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A fake implementation for {@link android.content.Context}, that helps broadcast {@link Intent}s
+ * to registered {@link BroadcastReceiver} instances.
+ */
+// TODO(b/202420937): Add unit tests for this class.
+public final class BroadcastingFakeContext extends MockContext {
+ private BroadcastReceiver mReceiver;
+ private IntentFilter mIntentFilter;
+ private Handler mHandler;
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+ mReceiver = receiver;
+ mIntentFilter = filter;
+
+ return null;
+ }
+
+ @Override
+ public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+ String broadcastPermission, Handler scheduler) {
+ mReceiver = receiver;
+ mIntentFilter = filter;
+ mHandler = scheduler;
+
+ return null;
+ }
+
+ @Override
+ public void sendBroadcast(Intent intent) {
+ if (mHandler == null) {
+ mReceiver.onReceive(this, intent);
+ return;
+ }
+
+ CountDownLatch latch = new CountDownLatch(1);
+ mHandler.getLooper().getQueue().addIdleHandler(() -> {
+ latch.countDown();
+ return false;
+ });
+
+ mHandler.post(() -> mReceiver.onReceive(this, intent));
+
+ // wait until the queue is idle
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(
+ "Interrupted while waiting for Broadcast Intent to be received");
+ }
+ }
+
+ @Override
+ public void unregisterReceiver(BroadcastReceiver receiver) {
+ if (receiver == mReceiver) {
+ mReceiver = null;
+ mIntentFilter = null;
+ mHandler = null;
+ }
+ }
+
+ public void verifyReceiverNotRegistered() {
+ assertThat(mIntentFilter).isNull();
+ assertThat(mReceiver).isNull();
+ assertThat(mHandler).isNull();
+ }
+
+ public void verifyReceiverRegistered(String expectedAction) {
+ assertThat(mIntentFilter.actionsIterator()).isNotNull();
+ ArrayList<String> actions = Lists.newArrayList(mIntentFilter.actionsIterator());
+ assertWithMessage("IntentFilter actions").that(actions).contains(expectedAction);
+ assertWithMessage("Registered BroadcastReceiver").that(mReceiver).isNotNull();
+ }
+}
diff --git a/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java b/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java
index 627aa86..3693ebc 100644
--- a/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java
+++ b/car-test-lib/src/android/car/testapi/BlockingUserLifecycleListener.java
@@ -54,6 +54,8 @@
private static final long DEFAULT_TIMEOUT_MS = 2_000;
+ private static int sNextId;
+
private final Object mLock = new Object();
private final CountDownLatch mLatch = new CountDownLatch(1);
@@ -79,6 +81,8 @@
private final long mTimeoutMs;
+ private final int mId = ++sNextId;
+
private BlockingUserLifecycleListener(Builder builder) {
mExpectedEventTypes = Collections
.unmodifiableList(new ArrayList<>(builder.mExpectedEventTypes));
@@ -276,7 +280,7 @@
@NonNull
private String stateToString() {
synchronized (mLock) {
- return "timeout=" + mTimeoutMs + "ms"
+ return "id=" + mId + ",timeout=" + mTimeoutMs + "ms"
+ ",expectedEventTypes=" + toString(mExpectedEventTypes)
+ ",expectedEventTypesLeft=" + toString(mExpectedEventTypesLeft)
+ (expectingSpecificUser() ? ",forUser=" + mForUserId : "")
diff --git a/car-test-lib/src/android/car/testapi/CarTelemetryController.java b/car-test-lib/src/android/car/testapi/CarTelemetryController.java
deleted file mode 100644
index e4df3f3..0000000
--- a/car-test-lib/src/android/car/testapi/CarTelemetryController.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2021 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 android.car.testapi;
-
-import android.car.telemetry.ManifestKey;
-
-/**
- * Controller to manipulate and verify {@link android.car.telemetry.CarTelemetryManager} in
- * unit tests.
- */
-public interface CarTelemetryController {
- /**
- * Returns {@code true} if a {@link
- * android.car.telemetry.CarTelemetryManager.CarTelemetryResultsListener} is
- * registered with the manager, otherwise returns {@code false}.
- */
- boolean isListenerSet();
-
- /**
- * Returns the number of valid manifests registered with the manager.
- */
- int getValidManifestsCount();
-
- /**
- * Associate a blob of data with the given key, used for testing the flush reports APIs.
- */
- void addDataForKey(ManifestKey key, byte[] data);
-
- /**
- * Configure the blob of data to be flushed with the
- * {@code FakeCarTelemetryService#flushScriptExecutionErrors()} API.
- */
- void setErrorData(byte[] error);
-}
diff --git a/car-test-lib/src/android/car/testapi/FakeCar.java b/car-test-lib/src/android/car/testapi/FakeCar.java
index 0508ed0..0356774 100644
--- a/car-test-lib/src/android/car/testapi/FakeCar.java
+++ b/car-test-lib/src/android/car/testapi/FakeCar.java
@@ -127,14 +127,6 @@
return mService.mCarUxRestrictionService;
}
- /**
- * Returns a test controller that can modify and query the underlying service for the {@link
- * android.car.telemetry.CarTelemetryManager}.
- */
- public CarTelemetryController getCarTelemetryController() {
- return mService.mCarTelemetry;
- }
-
private static class FakeCarService extends ICar.Stub {
@Mock ICarPackageManager.Stub mCarPackageManager;
@Mock ICarDiagnostic.Stub mCarDiagnostic;
@@ -150,7 +142,6 @@
private final FakeCarProjectionService mCarProjection;
private final FakeInstrumentClusterNavigation mInstrumentClusterNavigation;
private final FakeCarUxRestrictionsService mCarUxRestrictionService;
- private final FakeCarTelemetryService mCarTelemetry;
FakeCarService(Context context) {
MockitoAnnotations.initMocks(this);
@@ -160,7 +151,6 @@
mCarProjection = new FakeCarProjectionService(context);
mInstrumentClusterNavigation = new FakeInstrumentClusterNavigation();
mCarUxRestrictionService = new FakeCarUxRestrictionsService();
- mCarTelemetry = new FakeCarTelemetryService();
}
@Override
@@ -203,8 +193,6 @@
return mCarDrivingState;
case Car.CAR_UX_RESTRICTION_SERVICE:
return mCarUxRestrictionService;
- case Car.CAR_TELEMETRY_SERVICE:
- return mCarTelemetry;
default:
Log.w(TAG, "getCarService for unknown service:" + serviceName);
return null;
diff --git a/car-test-lib/src/android/car/testapi/FakeCarTelemetryService.java b/car-test-lib/src/android/car/testapi/FakeCarTelemetryService.java
deleted file mode 100644
index 4c6c912..0000000
--- a/car-test-lib/src/android/car/testapi/FakeCarTelemetryService.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2021 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 android.car.testapi;
-
-import static android.car.telemetry.CarTelemetryManager.ERROR_NEWER_MANIFEST_EXISTS;
-import static android.car.telemetry.CarTelemetryManager.ERROR_NONE;
-import static android.car.telemetry.CarTelemetryManager.ERROR_SAME_MANIFEST_EXISTS;
-
-import android.car.telemetry.CarTelemetryManager.AddManifestError;
-import android.car.telemetry.ICarTelemetryService;
-import android.car.telemetry.ICarTelemetryServiceListener;
-import android.car.telemetry.ManifestKey;
-import android.os.RemoteException;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * A fake implementation of {@link ICarTelemetryService.Stub} to facilitate the use of
- * {@link android.car.telemetry.CarTelemetryManager} in external unit tests.
- *
- * @hide
- */
-public class FakeCarTelemetryService extends ICarTelemetryService.Stub implements
- CarTelemetryController {
-
- private byte[] mErrorBytes;
- private ICarTelemetryServiceListener mListener;
-
- private final Map<String, Integer> mNameVersionMap = new HashMap<>();
- private final Map<ManifestKey, byte[]> mManifestMap = new HashMap<>();
- private final Map<ManifestKey, byte[]> mScriptResultMap = new HashMap<>();
-
- @Override
- public void setListener(ICarTelemetryServiceListener listener) {
- mListener = listener;
- }
-
- @Override
- public void clearListener() {
- mListener = null;
- }
-
- @Override
- public @AddManifestError int addManifest(ManifestKey key, byte[] manifest) {
- if (mNameVersionMap.getOrDefault(key.getName(), 0) > key.getVersion()) {
- return ERROR_NEWER_MANIFEST_EXISTS;
- } else if (mNameVersionMap.getOrDefault(key.getName(), 0) == key.getVersion()) {
- return ERROR_SAME_MANIFEST_EXISTS;
- }
- mNameVersionMap.put(key.getName(), key.getVersion());
- mManifestMap.put(key, manifest);
- return ERROR_NONE;
- }
-
- @Override
- public boolean removeManifest(ManifestKey key) {
- if (!mManifestMap.containsKey(key)) {
- return false;
- }
- mNameVersionMap.remove(key.getName());
- mManifestMap.remove(key);
- return true;
- }
-
- @Override
- public void removeAllManifests() {
- mNameVersionMap.clear();
- mManifestMap.clear();
- }
-
- @Override
- public void sendFinishedReports(ManifestKey key) throws RemoteException {
- if (!mScriptResultMap.containsKey(key)) {
- return;
- }
- mListener.onResult(key, mScriptResultMap.get(key));
- mScriptResultMap.remove(key);
- }
-
- @Override
- public void sendAllFinishedReports() throws RemoteException {
- for (Map.Entry<ManifestKey, byte[]> entry : mScriptResultMap.entrySet()) {
- mListener.onResult(entry.getKey(), entry.getValue());
- }
- mScriptResultMap.clear();
- }
-
- @Override
- public void sendScriptExecutionErrors() throws RemoteException {
- mListener.onError(mErrorBytes);
- }
-
- /**************************** CarTelemetryController impl ********************************/
- @Override
- public boolean isListenerSet() {
- return mListener != null;
- }
-
- @Override
- public int getValidManifestsCount() {
- return mManifestMap.size();
- }
-
- @Override
- public void addDataForKey(ManifestKey key, byte[] data) {
- mScriptResultMap.put(key, data);
- }
-
- @Override
- public void setErrorData(byte[] error) {
- mErrorBytes = error;
- }
-}
diff --git a/car_product/build/car.mk b/car_product/build/car.mk
index df09aae..7700cca 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -24,6 +24,7 @@
CarActivityResolver \
CarDeveloperOptions \
CarSettingsIntelligence \
+ CarManagedProvisioning \
OneTimeInitializer \
CarProvision \
StatementService \
@@ -45,6 +46,7 @@
BugReportApp \
NetworkPreferenceApp \
SampleCustomInputService \
+ AdasLocationTestApp \
# SEPolicy for test apps / services
BOARD_SEPOLICY_DIRS += packages/services/Car/car_product/sepolicy/test
@@ -128,6 +130,7 @@
car-frameworks-service \
com.android.car.procfsinspector \
libcar-framework-service-jni \
+ ScriptExecutor \
# RROs
PRODUCT_PACKAGES += \
diff --git a/car_product/build/car_base.mk b/car_product/build/car_base.mk
index ecd503b..cbd0b3c 100644
--- a/car_product/build/car_base.mk
+++ b/car_product/build/car_base.mk
@@ -42,7 +42,6 @@
Keyguard \
LatinIME \
Launcher2 \
- ManagedProvisioning \
PacProcessor \
PrintSpooler \
ProxyHandler \
@@ -61,6 +60,7 @@
A2dpSinkService \
PackageInstaller \
carbugreportd \
+ vehicle_binding_util \
# ENABLE_CAMERA_SERVICE must be set as true from the product's makefile if it wants to support
# Android Camera service.
@@ -74,7 +74,8 @@
ifeq ($(ENABLE_EVS_SAMPLE), true)
# ENABLE_EVS_SAMPLE should set be true or their vendor specific equivalents should be included in
# the device.mk with the corresponding selinux policies
-PRODUCT_PRODUCT_PROPERTIES += persist.automotive.evs.mode=0
+LOCAL_EVS_PROPERTIES ?= persist.automotive.evs.mode=0
+PRODUCT_PRODUCT_PROPERTIES += $(LOCAL_EVS_PROPERTIES)
PRODUCT_PACKAGES += evs_app \
android.hardware.automotive.evs@1.1-sample \
android.frameworks.automotive.display@1.0-service
@@ -82,8 +83,7 @@
include packages/services/Car/cpp/evs/sampleDriver/sepolicy/evsdriver.mk
endif
ifeq ($(ENABLE_CAREVSSERVICE_SAMPLE), true)
-PRODUCT_PACKAGES += CarEvsCameraPreviewApp \
- CarSystemUIEvsRRO
+PRODUCT_PACKAGES += CarEvsCameraPreviewApp
endif
ifeq ($(ENABLE_REAR_VIEW_CAMERA_SAMPLE), true)
PRODUCT_PACKAGES += SampleRearViewCamera
@@ -109,6 +109,10 @@
packages/services/Car/car_product/init/init.bootstat.rc:system/etc/init/init.bootstat.car.rc \
packages/services/Car/car_product/init/init.car.rc:system/etc/init/init.car.rc
+# Device policy management support
+PRODUCT_COPY_FILES += \
+ frameworks/native/data/etc/android.software.device_admin.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.software.device_admin.xml
+
# Enable car watchdog
include packages/services/Car/cpp/watchdog/product/carwatchdog.mk
diff --git a/car_product/build/preinstalled-packages-product-car-base.xml b/car_product/build/preinstalled-packages-product-car-base.xml
index f8ffbc1..b241c89 100644
--- a/car_product/build/preinstalled-packages-product-car-base.xml
+++ b/car_product/build/preinstalled-packages-product-car-base.xml
@@ -271,6 +271,11 @@
<install-in-user-type package="com.android.bluetoothmidiservice">
<install-in user-type="FULL" />
</install-in-user-type>
+ <!-- ManagedProvisioning app is used for provisioning the device. It
+ requires UX for the provisioning flow. -->
+ <install-in-user-type package="com.android.managedprovisioning">
+ <install-in user-type="FULL" />
+ </install-in-user-type>
<install-in-user-type package="com.android.statementservice">
<install-in user-type="FULL" />
</install-in-user-type>
diff --git a/car_product/car_ui_portrait/Android.mk b/car_product/car_ui_portrait/Android.mk
new file mode 100644
index 0000000..53ad94b
--- /dev/null
+++ b/car_product/car_ui_portrait/Android.mk
@@ -0,0 +1,21 @@
+# Copyright (C) 2021 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.
+#
+
+car_ui_portrait_modules := \
+ rro/car-ui-customizations \
+ rro/car-ui-toolbar-customizations \
+ apps/HideApps
+
+include $(call all-named-subdir-makefiles,$(car_ui_portrait_modules))
diff --git a/car_product/car_ui_portrait/OWNERS b/car_product/car_ui_portrait/OWNERS
new file mode 100644
index 0000000..f539bfb
--- /dev/null
+++ b/car_product/car_ui_portrait/OWNERS
@@ -0,0 +1,6 @@
+# Car UI Portrait Reference OWNERS
+hseog@google.com
+priyanksingh@google.com
+juliakawano@google.com
+stenning@google.com
+igorr@google.com
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/Android.bp b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/Android.bp
new file mode 100644
index 0000000..3c5fb85
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/Android.bp
@@ -0,0 +1,53 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "CarUiPortraitSettings",
+ overrides: ["CarSettings"],
+ platform_apis: true,
+
+ manifest: "AndroidManifest.xml",
+
+ resource_dirs: ["res"],
+
+ static_libs: [
+ "CarSettings-core",
+ ],
+
+ certificate: "platform",
+
+ optimize: {
+ enabled: false,
+ },
+
+ privileged: true,
+
+ dex_preopt: {
+ enabled: false,
+ },
+
+ required: ["allowed_privapp_com.android.car.settings"],
+
+ dxflags: ["--multi-dex"],
+
+ product_variables: {
+ pdk: {
+ enabled: false,
+ },
+ },
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/AndroidManifest.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/AndroidManifest.xml
new file mode 100644
index 0000000..82fcdf5
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.settings"
+ android:sharedUserId="android.uid.system"
+ coreApp="true">
+</manifest>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/settings_recyclerview_default.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/settings_recyclerview_default.xml
new file mode 100644
index 0000000..4616fdf
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/settings_recyclerview_default.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <com.android.car.ui.FocusArea
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/settings_car_ui_focus_area"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/settings_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tag="carUiPreferenceRecyclerView"
+ app:carUiSize="small"
+ app:enableDivider="true" />
+ </com.android.car.ui.FocusArea>
+</merge>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/top_level_recyclerview.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/top_level_recyclerview.xml
new file mode 100644
index 0000000..4dbe9be
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/layout/top_level_recyclerview.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <com.android.car.ui.FocusArea
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/settings_car_ui_focus_area"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/top_level_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tag="carUiPreferenceRecyclerView"
+ app:carUiSize="small"
+ app:enableDivider="true" />
+ </com.android.car.ui.FocusArea>
+</merge>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/config.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/config.xml
new file mode 100644
index 0000000..eaf603f
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <bool name="config_global_force_single_pane">false</bool>
+ <string name="config_homepage_fragment_class" translatable="false">com.android.car.settings.bluetooth.BluetoothSettingsFragment</string>
+ <bool name="config_top_level_enable_chevrons">false</bool>
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/dimens.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/dimens.xml
new file mode 100644
index 0000000..f2a58a4
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSettings/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2018 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.
+-->
+
+<resources>
+ <!-- Top-level menu -->
+ <dimen name="top_level_menu_width">400dp</dimen>
+ <dimen name="top_level_recyclerview_margin_right">@*android:dimen/car_padding_2</dimen>
+ <dimen name="top_level_foreground_icon_inset">8dp</dimen>
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/Android.bp b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/Android.bp
new file mode 100644
index 0000000..01717c7
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/Android.bp
@@ -0,0 +1,77 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "CarUiPortraitSystemUI",
+
+ srcs: ["src/**/*.java"],
+
+ resource_dirs: ["res"],
+
+ static_libs: [
+ "CarSystemUI-core",
+ ],
+
+ libs: [
+ "android.car",
+ ],
+
+ manifest: "AndroidManifest.xml",
+
+ overrides: [
+ "CarSystemUI",
+ ],
+
+ platform_apis: true,
+ system_ext_specific: true,
+ certificate: "platform",
+ privileged: true,
+
+ optimize: {
+ proguard_flags_files: [
+ "proguard.flags",
+ ],
+ },
+ dxflags: ["--multi-dex"],
+
+ plugins: ["dagger2-compiler"],
+
+ required: ["privapp_whitelist_com.android.systemui", "allowed_privapp_com.android.carsystemui"],
+}
+
+//####################################################################################
+// Build a static library to help mocking in testing. This is meant to be used
+// for internal unit tests.
+//####################################################################################
+android_library {
+ name: "CarUiPortraitSystemUI-tests",
+
+ srcs: ["src/**/*.java"],
+
+ resource_dirs: ["res"],
+
+ libs: [
+ "android.car",
+ ],
+
+ static_libs: [
+ "CarSystemUI-tests",
+ ],
+
+ plugins: ["dagger2-compiler"],
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/AndroidManifest.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/AndroidManifest.xml
new file mode 100644
index 0000000..fc2e241
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ package="com.android.systemui"
+ android:sharedUserId="android.uid.systemui"
+ coreApp="true">
+</manifest>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/proguard.flags b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/proguard.flags
new file mode 100644
index 0000000..3de0064
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/proguard.flags
@@ -0,0 +1,6 @@
+-keep class com.android.systemui.CarUiPortraitSystemUIFactory
+
+-keep class com.android.systemui.DaggerCarUiPortraitGlobalRootComponent { *; }
+-keep class com.android.systemui.DaggerCarUiPortraitGlobalRootComponent$CarUiPortraitSysUIComponentImpl { *; }
+
+-include ../../../../../../../packages/apps/Car/SystemUI/proguard.flags
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_apps.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_apps.xml
new file mode 100644
index 0000000..a98b3a7
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_apps.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <vector android:width="@dimen/system_bar_icon_drawing_size"
+ android:height="@dimen/system_bar_icon_drawing_size"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/car_nav_icon_fill_color"
+ android:pathData="M6,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,20c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM6,20c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM6,14c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,14c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM16,6c0,1.1 0.9,2 2,2s2,-0.9 2,-2 -0.9,-2 -2,-2 -2,0.9 -2,2zM12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,14c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,20c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z"/>
+ </vector>
+ </item>
+</selector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_hvac.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_hvac.xml
new file mode 100644
index 0000000..b42c86c
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_hvac.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <vector android:width="@dimen/system_bar_icon_drawing_size"
+ android:height="@dimen/system_bar_icon_drawing_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/car_nav_icon_fill_color"
+ android:pathData="M16.34,8.36l-2.29,0.82c-0.18,-0.13 -0.38,-0.25 -0.58,-0.34c0.17,-0.83 0.63,-1.58 1.36,-2.06C16.85,5.44 16.18,2 13.39,2C9,2 7.16,5.01 8.36,7.66l0.82,2.29c-0.13,0.18 -0.25,0.38 -0.34,0.58c-0.83,-0.17 -1.58,-0.63 -2.06,-1.36C5.44,7.15 2,7.82 2,10.61c0,4.4 3.01,6.24 5.66,5.03l2.29,-0.82c0.18,0.13 0.38,0.25 0.58,0.34c-0.17,0.83 -0.63,1.58 -1.36,2.06C7.15,18.56 7.82,22 10.61,22c4.4,0 6.24,-3.01 5.03,-5.66l-0.82,-2.29c0.13,-0.18 0.25,-0.38 0.34,-0.58c0.83,0.17 1.58,0.63 2.06,1.36c1.34,2.01 4.77,1.34 4.77,-1.45C22,9 18.99,7.16 16.34,8.36zM12,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5c0.83,0 1.5,0.67 1.5,1.5C13.5,12.83 12.83,13.5 12,13.5zM10.24,5.22C10.74,4.44 11.89,4 13.39,4c0.79,0 0.71,0.86 0.34,1.11c-1.22,0.81 -2,2.06 -2.25,3.44c-0.21,0.03 -0.42,0.08 -0.62,0.15l-0.68,-1.88C10,6.42 9.86,5.81 10.24,5.22zM6.83,13.82c-0.4,0.18 -1.01,0.32 -1.61,-0.06C4.44,13.26 4,12.11 4,10.61c0,-0.79 0.86,-0.71 1.11,-0.34c0.81,1.22 2.06,2 3.44,2.25c0.03,0.21 0.08,0.42 0.15,0.62L6.83,13.82zM13.76,18.78c-0.5,0.77 -1.65,1.22 -3.15,1.22c-0.79,0 -0.71,-0.86 -0.34,-1.11c1.22,-0.81 2,-2.06 2.25,-3.44c0.21,-0.03 0.42,-0.08 0.62,-0.15l0.68,1.88C14,17.58 14.14,18.18 13.76,18.78zM18.89,13.73c-0.81,-1.22 -2.06,-2 -3.44,-2.25c-0.03,-0.21 -0.08,-0.42 -0.15,-0.62l1.88,-0.68c0.4,-0.18 1.01,-0.32 1.61,0.06c0.77,0.5 1.22,1.65 1.22,3.15C20,14.19 19.14,14.11 18.89,13.73z"/>
+ </vector>
+ </item>
+</selector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_mic.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_mic.xml
new file mode 100644
index 0000000..f282b65
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_mic.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <vector android:width="@dimen/system_bar_icon_drawing_size"
+ android:height="@dimen/system_bar_icon_drawing_size"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/car_nav_icon_fill_color"
+ android:pathData="M12,14c1.66,0 3,-1.34 3,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM11,5c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v6c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1L11,5zM17,11c0,2.76 -2.24,5 -5,5s-5,-2.24 -5,-5L5,11c0,3.53 2.61,6.43 6,6.92L11,21h2v-3.08c3.39,-0.49 6,-3.39 6,-6.92h-2z"/>
+ </vector>
+ </item>
+</selector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_notification.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_notification.xml
new file mode 100644
index 0000000..27b69a8
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_notification.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <vector android:width="@dimen/system_bar_icon_drawing_size"
+ android:height="@dimen/system_bar_icon_drawing_size"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="@color/car_nav_icon_fill_color"
+ android:pathData="M18,17v-6c0,-3.07 -1.63,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.64,5.36 6,7.92 6,11v6L4,17v2h16v-2h-2zM16,17L8,17v-6c0,-2.48 1.51,-4.5 4,-4.5s4,2.02 4,4.5v6zM12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2z"/>
+ </vector>
+ </item>
+</selector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_user_icon.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_user_icon.xml
new file mode 100644
index 0000000..45887dc
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/car_ic_user_icon.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/system_bar_user_icon_drawing_size"
+ android:height="@dimen/system_bar_user_icon_drawing_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/system_bar_icon_color"
+ android:pathData="M12,5.9c1.16,0 2.1,0.94 2.1,2.1s-0.94,2.1 -2.1,2.1S9.9,9.16 9.9,8s0.94,-2.1 2.1,-2.1m0,9c2.97,0 6.1,1.46 6.1,2.1v1.1L5.9,18.1L5.9,17c0,-0.64 3.13,-2.1 6.1,-2.1M12,4C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,13c-2.67,0 -8,1.34 -8,4v3h16v-3c0,-2.66 -5.33,-4 -8,-4z"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar.xml
new file mode 100644
index 0000000..7e72373
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:id="@android:id/background">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/hvac_off_background_color" />
+ </shape>
+ </item>
+ <item android:id="@android:id/progress">
+ <clip>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/hvac_on_background_color" />
+ </shape>
+ </clip>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_background.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_background.xml
new file mode 100644
index 0000000..7104440
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_background.xml
@@ -0,0 +1,42 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/hvac_panel_seek_bar_radius"/>
+ <solid android:color="@color/hvac_off_background_color" />
+ </shape>
+ </item>
+ <item
+ android:gravity="left"
+ android:width="@dimen/hvac_panel_button_dimen">
+ <selector>
+ <item android:state_selected="true">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/hvac_panel_seek_bar_radius"/>
+ <solid android:color="@color/hvac_on_background_color" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/hvac_panel_seek_bar_radius"/>
+ <solid android:color="@color/hvac_off_background_color" />
+ </shape>
+ </item>
+ </selector>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb.xml
new file mode 100644
index 0000000..63b731f
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/hvac_on_background_color"/>
+ <size android:height="@dimen/hvac_panel_button_dimen"
+ android:width="@dimen/hvac_panel_button_dimen"/>
+ </shape>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_1.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_1.xml
new file mode 100644
index 0000000..a0befd8
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_1.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/hvac_on_background_color"/>
+ <size android:height="@dimen/hvac_panel_button_dimen"
+ android:width="@dimen/hvac_panel_button_dimen"/>
+ </shape>
+ </item>
+ <item
+ android:drawable="@drawable/ic_mode_fan_1"
+ android:gravity="center"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"/>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_2.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_2.xml
new file mode 100644
index 0000000..c0725c3
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_2.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/hvac_on_background_color"/>
+ <size android:height="@dimen/hvac_panel_button_dimen"
+ android:width="@dimen/hvac_panel_button_dimen"/>
+ </shape>
+ </item>
+ <item
+ android:drawable="@drawable/ic_mode_fan_2"
+ android:gravity="center"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"/>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_3.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_3.xml
new file mode 100644
index 0000000..d11d90b
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_3.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/hvac_on_background_color"/>
+ <size android:height="@dimen/hvac_panel_button_dimen"
+ android:width="@dimen/hvac_panel_button_dimen"/>
+ </shape>
+ </item>
+ <item
+ android:drawable="@drawable/ic_mode_fan_3"
+ android:gravity="center"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"/>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_4.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_4.xml
new file mode 100644
index 0000000..177d9a4
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_4.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/hvac_on_background_color"/>
+ <size android:height="@dimen/hvac_panel_button_dimen"
+ android:width="@dimen/hvac_panel_button_dimen"/>
+ </shape>
+ </item>
+ <item
+ android:drawable="@drawable/ic_mode_fan_4"
+ android:gravity="center"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"/>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_5.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_5.xml
new file mode 100644
index 0000000..c87f92a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_5.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/hvac_on_background_color"/>
+ <size android:height="@dimen/hvac_panel_button_dimen"
+ android:width="@dimen/hvac_panel_button_dimen"/>
+ </shape>
+ </item>
+ <item
+ android:drawable="@drawable/ic_mode_fan_5"
+ android:gravity="center"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"/>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_6.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_6.xml
new file mode 100644
index 0000000..fc8452d
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_6.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/hvac_on_background_color"/>
+ <size android:height="@dimen/hvac_panel_button_dimen"
+ android:width="@dimen/hvac_panel_button_dimen"/>
+ </shape>
+ </item>
+ <item
+ android:drawable="@drawable/ic_mode_fan_6"
+ android:gravity="center"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"/>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_7.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_7.xml
new file mode 100644
index 0000000..4531e65
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_7.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/hvac_on_background_color"/>
+ <size android:height="@dimen/hvac_panel_button_dimen"
+ android:width="@dimen/hvac_panel_button_dimen"/>
+ </shape>
+ </item>
+ <item
+ android:drawable="@drawable/ic_mode_fan_7"
+ android:gravity="center"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"/>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_8.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_8.xml
new file mode 100644
index 0000000..9905a24
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/fan_speed_seek_bar_thumb_8.xml
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/hvac_on_background_color"/>
+ <size android:height="@dimen/hvac_panel_button_dimen"
+ android:width="@dimen/hvac_panel_button_dimen"/>
+ </shape>
+ </item>
+ <item
+ android:drawable="@drawable/ic_mode_fan_8"
+ android:gravity="center"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"/>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_cool_on_bg.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_cool_on_bg.xml
new file mode 100644
index 0000000..711158c
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_cool_on_bg.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/hvac_on_cooling_background_color"/>
+ <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+ </shape>
+ </item>
+ <item>
+ <ripple android:color="@color/car_ui_ripple_color">
+ <item android:id="@android:id/mask">
+ <shape>
+ <solid android:color="?android:colorAccent"/>
+ <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+ </shape>
+ </item>
+ </ripple>
+ </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_heat_on_bg.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_heat_on_bg.xml
new file mode 100644
index 0000000..d069bd9
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_heat_on_bg.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/hvac_on_heating_background_color"/>
+ <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+ </shape>
+ </item>
+ <item>
+ <ripple android:color="@color/car_ui_ripple_color">
+ <item android:id="@android:id/mask">
+ <shape>
+ <solid android:color="?android:colorAccent"/>
+ <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+ </shape>
+ </item>
+ </ripple>
+ </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_off_bg.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_off_bg.xml
new file mode 100644
index 0000000..d40ad01
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_off_bg.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/hvac_off_background_color"/>
+ <corners android:radius="@dimen/hvac_panel_off_button_radius"/>
+ </shape>
+ </item>
+ <item>
+ <ripple android:color="@color/car_ui_ripple_color">
+ <item android:id="@android:id/mask">
+ <shape>
+ <solid android:color="?android:colorAccent"/>
+ <corners android:radius="@dimen/hvac_panel_off_button_radius"/>
+ </shape>
+ </item>
+ </ripple>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_on_bg.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_on_bg.xml
new file mode 100644
index 0000000..a5d66bc
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_button_on_bg.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/hvac_on_background_color"/>
+ <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+ </shape>
+ </item>
+ <item>
+ <ripple android:color="@color/car_ui_ripple_color">
+ <item android:id="@android:id/mask">
+ <shape>
+ <solid android:color="?android:colorAccent"/>
+ <corners android:radius="@dimen/hvac_panel_on_button_radius"/>
+ </shape>
+ </item>
+ </ripple>
+ </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_cool_background.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_cool_background.xml
new file mode 100644
index 0000000..b1f9e79
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_cool_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_selected="true"
+ android:drawable="@drawable/hvac_button_cool_on_bg"/>
+ <item android:drawable="@drawable/hvac_button_off_bg"/>
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_decrease_button.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_decrease_button.xml
new file mode 100644
index 0000000..ea3a853
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_decrease_button.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <item>
+ <aapt:attr name="android:drawable">
+ <vector android:width="@dimen/hvac_temperature_button_size"
+ android:height="@dimen/hvac_temperature_button_size"
+ android:viewportWidth="64"
+ android:viewportHeight="64">
+ <path
+ android:pathData="M32,0L32,0A32,32 0,0 1,64 32L64,32A32,32 0,0 1,32 64L32,64A32,32 0,0 1,0 32L0,32A32,32 0,0 1,32 0z"
+ android:fillColor="@color/hvac_temperature_adjust_button_color"/>
+ </vector>
+ </aapt:attr>
+ </item>
+ <item android:gravity="center">
+ <aapt:attr name="android:drawable">
+ <vector android:width="24dp"
+ android:height="3dp"
+ android:viewportWidth="24"
+ android:viewportHeight="3">
+ <path
+ android:fillColor="@color/hvac_temperature_decrease_arrow_color"
+ android:pathData="M24,3.5H0V0.5H24V3.5Z"/>
+ </vector>
+ </aapt:attr>
+ </item>
+ <item>
+ <aapt:attr name="android:drawable">
+ <ripple android:color="?android:attr/colorControlHighlight"/>
+ </aapt:attr>
+ </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_default_background.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_default_background.xml
new file mode 100644
index 0000000..84f502b
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_default_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_selected="true"
+ android:drawable="@drawable/hvac_button_on_bg"/>
+ <item android:drawable="@drawable/hvac_button_off_bg"/>
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_heat_background.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_heat_background.xml
new file mode 100644
index 0000000..09d091e
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_heat_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_selected="true"
+ android:drawable="@drawable/hvac_button_heat_on_bg"/>
+ <item android:drawable="@drawable/hvac_button_off_bg"/>
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_increase_button.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_increase_button.xml
new file mode 100644
index 0000000..630727d
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_increase_button.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <item>
+ <aapt:attr name="android:drawable">
+ <vector android:width="@dimen/hvac_temperature_button_size"
+ android:height="@dimen/hvac_temperature_button_size"
+ android:viewportWidth="64"
+ android:viewportHeight="64">
+ <path
+ android:pathData="M32,0L32,0A32,32 0,0 1,64 32L64,32A32,32 0,0 1,32 64L32,64A32,32 0,0 1,0 32L0,32A32,32 0,0 1,32 0z"
+ android:fillColor="@color/hvac_temperature_adjust_button_color"/>
+ </vector>
+ </aapt:attr>
+ </item>
+ <item
+ android:gravity="center"
+ android:width="24dp"
+ android:height="24dp">
+ <aapt:attr name="android:drawable">
+ <vector android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@color/hvac_temperature_increase_arrow_color"
+ android:pathData="M24,13.5H13.5V24H10.5V13.5H0V10.5H10.5V0H13.5V10.5H24V13.5Z"/>
+ </vector>
+ </aapt:attr>
+ </item>
+ <item>
+ <aapt:attr name="android:drawable">
+ <ripple android:color="?android:attr/colorControlHighlight"/>
+ </aapt:attr>
+ </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_panel_bg.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_panel_bg.xml
new file mode 100644
index 0000000..f0cd3bd
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/hvac_panel_bg.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/hvac_background_color"/>
+
+ <!-- android:radius must be defined even with overrides. -->
+ <corners
+ android:radius="1dp"
+ android:topLeftRadius="@dimen/hvac_panel_bg_radius"
+ android:topRightRadius="@dimen/hvac_panel_bg_radius"
+ android:bottomLeftRadius="0dp"
+ android:bottomRightRadius="0dp"/>
+</shape>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_ac_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_ac_off.xml
new file mode 100644
index 0000000..5458c73
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_ac_off.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M42.0001,22H35.6601L40.7401,16.92C41.5201,16.14 41.5201,14.88 40.7401,14.1C39.9601,13.32 38.6801,13.32 37.9001,14.1L30.0001,22H26.0001V18L33.9001,10.1C34.6801,9.32 34.6801,8.04 33.9001,7.26C33.1201,6.48 31.8601,6.48 31.0801,7.26L26.0001,12.34V6C26.0001,4.9 25.1001,4 24.0001,4C22.9001,4 22.0001,4.9 22.0001,6V12.34L16.9201,7.26C16.1401,6.48 14.8801,6.48 14.1001,7.26C13.3201,8.04 13.3201,9.32 14.1001,10.1L22.0001,18V20.34L27.6601,26H30.0001L37.9001,33.9C38.6801,34.68 39.9601,34.68 40.7401,33.9C41.5201,33.12 41.5201,31.86 40.7401,31.08L35.6601,26H42.0001C43.1001,26 44.0001,25.1 44.0001,24C44.0001,22.9 43.1001,22 42.0001,22Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+ <path
+ android:pathData="M1.5801,11.24L12.3401,22H6.0001C4.9001,22 4.0001,22.9 4.0001,24C4.0001,25.1 4.9001,26 6.0001,26H12.3401L7.2601,31.08C6.4801,31.86 6.4801,33.12 7.2601,33.9C8.0401,34.68 9.3201,34.68 10.1001,33.9L17.1801,26.82L21.1801,30.82L14.1001,37.9C13.3201,38.68 13.3201,39.96 14.1001,40.74C14.8801,41.52 16.1401,41.52 16.9201,40.74L22.0001,35.66V42C22.0001,43.1 22.9001,44 24.0001,44C25.1001,44 26.0001,43.1 26.0001,42V35.66L38.3401,48L41.1601,45.18L4.4001,8.4L1.5801,11.24Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_ac_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_ac_on.xml
new file mode 100644
index 0000000..f86563e
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_ac_on.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M42,22H35.66L40.74,16.92C41.52,16.14 41.52,14.88 40.74,14.1C39.96,13.32 38.68,13.32 37.9,14.1L30,22H26V18L33.9,10.1C34.68,9.32 34.68,8.04 33.9,7.26C33.12,6.48 31.86,6.48 31.08,7.26L26,12.34V6C26,4.9 25.1,4 24,4C22.9,4 22,4.9 22,6V12.34L16.92,7.26C16.14,6.48 14.88,6.48 14.1,7.26C13.32,8.04 13.32,9.32 14.1,10.1L22,18V22H18L10.1,14.1C9.32,13.32 8.04,13.32 7.26,14.1C6.48,14.88 6.48,16.14 7.26,16.92L12.34,22H6C4.9,22 4,22.9 4,24C4,25.1 4.9,26 6,26H12.34L7.26,31.08C6.48,31.86 6.48,33.12 7.26,33.9C8.04,34.68 9.32,34.68 10.1,33.9L18,26H22V30L14.1,37.9C13.32,38.68 13.32,39.96 14.1,40.74C14.88,41.52 16.14,41.52 16.92,40.74L22,35.66V42C22,43.1 22.9,44 24,44C25.1,44 26,43.1 26,42V35.66L31.08,40.74C31.86,41.52 33.12,41.52 33.9,40.74C34.68,39.96 34.68,38.68 33.9,37.9L26,30V26H30L37.9,33.9C38.68,34.68 39.96,34.68 40.74,33.9C41.52,33.12 41.52,31.86 40.74,31.08L35.66,26H42C43.1,26 44,25.1 44,24C44,22.9 43.1,22 42,22Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M42,22H35.66L40.74,16.92C41.52,16.14 41.52,14.88 40.74,14.1C39.96,13.32 38.68,13.32 37.9,14.1L30,22H26V18L33.9,10.1C34.68,9.32 34.68,8.04 33.9,7.26C33.12,6.48 31.86,6.48 31.08,7.26L26,12.34V6C26,4.9 25.1,4 24,4C22.9,4 22,4.9 22,6V12.34L16.92,7.26C16.14,6.48 14.88,6.48 14.1,7.26C13.32,8.04 13.32,9.32 14.1,10.1L22,18V22H18L10.1,14.1C9.32,13.32 8.04,13.32 7.26,14.1C6.48,14.88 6.48,16.14 7.26,16.92L12.34,22H6C4.9,22 4,22.9 4,24C4,25.1 4.9,26 6,26H12.34L7.26,31.08C6.48,31.86 6.48,33.12 7.26,33.9C8.04,34.68 9.32,34.68 10.1,33.9L18,26H22V30L14.1,37.9C13.32,38.68 13.32,39.96 14.1,40.74C14.88,41.52 16.14,41.52 16.92,40.74L22,35.66V42C22,43.1 22.9,44 24,44C25.1,44 26,43.1 26,42V35.66L31.08,40.74C31.86,41.52 33.12,41.52 33.9,40.74C34.68,39.96 34.68,38.68 33.9,37.9L26,30V26H30L37.9,33.9C38.68,34.68 39.96,34.68 40.74,33.9C41.52,33.12 41.52,31.86 40.74,31.08L35.66,26H42C43.1,26 44,25.1 44,24C44,22.9 43.1,22 42,22Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_off.xml
new file mode 100644
index 0000000..3cf394e
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_off.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_wide_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="96"
+ android:viewportHeight="49">
+ <path
+ android:pathData="M40.209,16.7912L38.789,15.3812L36.999,17.1712L36.999,3.0012L34.999,3.0012L34.999,17.1712L33.209,15.3812L31.789,16.7912L35.999,21.0012L40.209,16.7912Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+ <path
+ android:pathData="M34.0338,32.7879L24.8823,46.6596C24.5032,47.2343 24.9153,48 25.6038,48C27.1419,48 28.6223,47.4142 29.744,46.3617L39.1124,37.5709C40.9662,35.8313 43.413,34.8632 45.9551,34.8632H54.2319C56.5346,34.8632 58.6342,33.5453 59.6353,31.4715L66.7164,16.8024C67.3096,15.5736 66.4143,14.1474 65.0499,14.1474C64.0736,14.1474 63.155,14.6096 62.5732,15.3935L55.9967,24.2545C54.1102,26.7962 51.1319,28.2947 47.9666,28.2947H42.3809C39.0203,28.2947 35.8844,29.9828 34.0338,32.7879Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+ <path
+ android:pathData="M61.9985,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_on.xml
new file mode 100644
index 0000000..a4c1eb2
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_feet_on.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_wide_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="96"
+ android:viewportHeight="49">
+ <path
+ android:pathData="M40.209,16.7912L38.789,15.3812L36.999,17.1712L36.999,3.0012L34.999,3.0012L34.999,17.1712L33.209,15.3812L31.789,16.7912L35.999,21.0012L40.209,16.7912Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M34.0338,32.7879L24.8823,46.6596C24.5032,47.2343 24.9153,48 25.6038,48C27.1419,48 28.6223,47.4142 29.744,46.3617L39.1124,37.5709C40.9662,35.8313 43.413,34.8632 45.9551,34.8632H54.2319C56.5346,34.8632 58.6342,33.5453 59.6353,31.4715L66.7164,16.8024C67.3096,15.5736 66.4143,14.1474 65.0499,14.1474C64.0736,14.1474 63.155,14.6096 62.5732,15.3935L55.9967,24.2545C54.1102,26.7962 51.1319,28.2947 47.9666,28.2947H42.3809C39.0203,28.2947 35.8844,29.9828 34.0338,32.7879Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M61.9985,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_off.xml
new file mode 100644
index 0000000..981958a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_off.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_wide_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="96"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M40.79,7.79L39.38,9.21L41.17,11H27V13H41.17L39.38,14.79L40.79,16.21L45,12L40.79,7.79Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+ <path
+ android:pathData="M34.0357,32.7879L24.8843,46.6596C24.5051,47.2343 24.9173,48 25.6058,48C27.1439,48 28.6243,47.4142 29.746,46.3617L39.1144,37.5709C40.9682,35.8313 43.4149,34.8632 45.9571,34.8632H54.2339C56.5366,34.8632 58.6362,33.5453 59.6372,31.4715L66.7184,16.8024C67.3115,15.5736 66.4162,14.1474 65.0518,14.1474C64.0756,14.1474 63.157,14.6096 62.5752,15.3935L55.9986,24.2545C54.1122,26.7962 51.1338,28.2947 47.9686,28.2947H42.3829C39.0223,28.2947 35.8864,29.9828 34.0357,32.7879Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+ <path
+ android:pathData="M62.0005,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_on.xml
new file mode 100644
index 0000000..4dc3272
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_head_on.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_wide_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="96"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M40.79,7.79L39.38,9.21L41.17,11H27V13H41.17L39.38,14.79L40.79,16.21L45,12L40.79,7.79Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M34.0357,32.7879L24.8843,46.6596C24.5051,47.2343 24.9173,48 25.6058,48C27.1439,48 28.6243,47.4142 29.746,46.3617L39.1144,37.5709C40.9682,35.8313 43.4149,34.8632 45.9571,34.8632H54.2339C56.5366,34.8632 58.6362,33.5453 59.6372,31.4715L66.7184,16.8024C67.3115,15.5736 66.4162,14.1474 65.0518,14.1474C64.0756,14.1474 63.157,14.6096 62.5752,15.3935L55.9986,24.2545C54.1122,26.7962 51.1338,28.2947 47.9686,28.2947H42.3829C39.0223,28.2947 35.8864,29.9828 34.0357,32.7879Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M62.0005,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_off.xml
new file mode 100644
index 0000000..2cdfa90
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_off.xml
@@ -0,0 +1,53 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_wide_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="96"
+ android:viewportHeight="49">
+ <path
+ android:pathData="M34.0338,32.7879L24.8823,46.6596C24.5032,47.2343 24.9153,48 25.6038,48C27.1419,48 28.6223,47.4142 29.744,46.3617L39.1124,37.5709C40.9662,35.8313 43.413,34.8632 45.9551,34.8632H54.2319C56.5346,34.8632 58.6342,33.5453 59.6353,31.4715L66.7164,16.8024C67.3096,15.5736 66.4143,14.1474 65.0499,14.1474C64.0736,14.1474 63.155,14.6096 62.5732,15.3935L55.9967,24.2545C54.1102,26.7962 51.1319,28.2947 47.9666,28.2947H42.3809C39.0203,28.2947 35.8844,29.9828 34.0338,32.7879Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+ <path
+ android:pathData="M61.9985,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+ <path
+ android:pathData="M41.5603,10.0488C37.5686,12.8863 45.4033,16.6289 41.5603,19.0972"
+ android:strokeWidth="2"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M37.0374,10.0488C33.0456,12.8863 40.8804,16.6289 37.0374,19.0972"
+ android:strokeWidth="2"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M32.5135,10.0488C28.5217,12.8863 36.3565,16.629 32.5135,19.0973"
+ android:strokeWidth="2"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M28.8859,15.7037H27.804L25.0881,6.2391C24.7768,5.1544 25.3126,4.0062 26.3439,3.5479C30.1667,1.8493 33.7188,1 37,1C40.2812,1 43.8333,1.8493 47.6561,3.5479C48.6874,4.0061 49.2232,5.1544 48.9119,6.2391L46.196,15.7037H45.1141"
+ android:strokeLineJoin="round"
+ android:strokeWidth="2"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_on.xml
new file mode 100644
index 0000000..a3510b1
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_airflow_windshield_on.xml
@@ -0,0 +1,53 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_wide_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="96"
+ android:viewportHeight="49">
+ <path
+ android:pathData="M34.0338,32.7879L24.8823,46.6596C24.5032,47.2343 24.9153,48 25.6038,48C27.1419,48 28.6223,47.4142 29.744,46.3617L39.1124,37.5709C40.9662,35.8313 43.413,34.8632 45.9551,34.8632H54.2319C56.5346,34.8632 58.6342,33.5453 59.6353,31.4715L66.7164,16.8024C67.3096,15.5736 66.4143,14.1474 65.0499,14.1474C64.0736,14.1474 63.155,14.6096 62.5732,15.3935L55.9967,24.2545C54.1102,26.7962 51.1319,28.2947 47.9666,28.2947H42.3809C39.0203,28.2947 35.8844,29.9828 34.0338,32.7879Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M61.9985,5.0526a5,5.0526 0,1 0,10 0a5,5.0526 0,1 0,-10 0z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M41.5603,10.0488C37.5686,12.8863 45.4033,16.6289 41.5603,19.0972"
+ android:strokeWidth="2"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M37.0374,10.0488C33.0456,12.8863 40.8804,16.6289 37.0374,19.0972"
+ android:strokeWidth="2"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M32.5135,10.0488C28.5217,12.8863 36.3565,16.629 32.5135,19.0973"
+ android:strokeWidth="2"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M28.8859,15.7037H27.804L25.0881,6.2391C24.7768,5.1544 25.3126,4.0062 26.3439,3.5479C30.1667,1.8493 33.7188,1 37,1C40.2812,1 43.8333,1.8493 47.6561,3.5479C48.6874,4.0061 49.2232,5.1544 48.9119,6.2391L46.196,15.7037H45.1141"
+ android:strokeLineJoin="round"
+ android:strokeWidth="2"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_off.xml
new file mode 100644
index 0000000..65a4b12
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_off.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M0.2653,30L4.6853,17.828H7.3203L11.7573,30H9.2243L8.2383,27.093H3.7843L2.7983,30H0.2653ZM5.5183,21.942L4.4983,24.985H7.5243L6.5043,21.942L6.0793,20.48H5.9433L5.5183,21.942ZM17.9681,30.272C16.4721,30.272 15.2934,29.8243 14.4321,28.929C13.5821,28.0337 13.1571,26.7927 13.1571,25.206V17.828H15.4181V25.359C15.4181,26.2203 15.6334,26.8947 16.0641,27.382C16.4947,27.858 17.1294,28.096 17.9681,28.096C18.8067,28.096 19.4357,27.858 19.8551,27.382C20.2857,26.8947 20.5011,26.2203 20.5011,25.359V17.828H22.7621V25.206C22.7621,26.226 22.5694,27.1157 22.1841,27.875C21.7987,28.6343 21.2491,29.2237 20.5351,29.643C19.8211,30.0623 18.9654,30.272 17.9681,30.272ZM28.299,30V20.004H24.559V17.828H34.317V20.004H30.577V30H28.299ZM41.1274,30.272C39.926,30.272 38.8834,30 37.9994,29.456C37.1154,28.9007 36.4297,28.147 35.9424,27.195C35.455,26.2317 35.2114,25.138 35.2114,23.914C35.2114,22.6787 35.455,21.585 35.9424,20.633C36.4297,19.681 37.1154,18.933 37.9994,18.389C38.8834,17.8337 39.926,17.556 41.1274,17.556C42.3287,17.556 43.3714,17.8337 44.2554,18.389C45.1394,18.933 45.825,19.681 46.3124,20.633C46.7997,21.585 47.0434,22.6787 47.0434,23.914C47.0434,25.138 46.7997,26.2317 46.3124,27.195C45.825,28.147 45.1394,28.9007 44.2554,29.456C43.3714,30 42.3287,30.272 41.1274,30.272ZM41.1274,28.096C42.204,28.096 43.071,27.7333 43.7284,27.008C44.397,26.2827 44.7314,25.2513 44.7314,23.914C44.7314,22.5767 44.397,21.5453 43.7284,20.82C43.071,20.0947 42.204,19.732 41.1274,19.732C40.0507,19.732 39.178,20.0947 38.5094,20.82C37.852,21.5453 37.5234,22.5767 37.5234,23.914C37.5234,25.2513 37.852,26.2827 38.5094,27.008C39.178,27.7333 40.0507,28.096 41.1274,28.096Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_on.xml
new file mode 100644
index 0000000..c847a95
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_auto_on.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M0.2653,30L4.6853,17.828H7.3203L11.7573,30H9.2243L8.2383,27.093H3.7843L2.7983,30H0.2653ZM5.5183,21.942L4.4983,24.985H7.5243L6.5043,21.942L6.0793,20.48H5.9433L5.5183,21.942ZM17.9681,30.272C16.4721,30.272 15.2934,29.8243 14.4321,28.929C13.5821,28.0337 13.1571,26.7927 13.1571,25.206V17.828H15.4181V25.359C15.4181,26.2203 15.6334,26.8947 16.0641,27.382C16.4947,27.858 17.1294,28.096 17.9681,28.096C18.8067,28.096 19.4357,27.858 19.8551,27.382C20.2857,26.8947 20.5011,26.2203 20.5011,25.359V17.828H22.7621V25.206C22.7621,26.226 22.5694,27.1157 22.1841,27.875C21.7987,28.6343 21.2491,29.2237 20.5351,29.643C19.8211,30.0623 18.9654,30.272 17.9681,30.272ZM28.299,30V20.004H24.559V17.828H34.317V20.004H30.577V30H28.299ZM41.1274,30.272C39.926,30.272 38.8834,30 37.9994,29.456C37.1154,28.9007 36.4297,28.147 35.9424,27.195C35.455,26.2317 35.2114,25.138 35.2114,23.914C35.2114,22.6787 35.455,21.585 35.9424,20.633C36.4297,19.681 37.1154,18.933 37.9994,18.389C38.8834,17.8337 39.926,17.556 41.1274,17.556C42.3287,17.556 43.3714,17.8337 44.2554,18.389C45.1394,18.933 45.825,19.681 46.3124,20.633C46.7997,21.585 47.0434,22.6787 47.0434,23.914C47.0434,25.138 46.7997,26.2317 46.3124,27.195C45.825,28.147 45.1394,28.9007 44.2554,29.456C43.3714,30 42.3287,30.272 41.1274,30.272ZM41.1274,28.096C42.204,28.096 43.071,27.7333 43.7284,27.008C44.397,26.2827 44.7314,25.2513 44.7314,23.914C44.7314,22.5767 44.397,21.5453 43.7284,20.82C43.071,20.0947 42.204,19.732 41.1274,19.732C40.0507,19.732 39.178,20.0947 38.5094,20.82C37.852,21.5453 37.5234,22.5767 37.5234,23.914C37.5234,25.2513 37.852,26.2827 38.5094,27.008C39.178,27.7333 40.0507,28.096 41.1274,28.096Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_off.xml
new file mode 100644
index 0000000..29bae07
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_off.xml
@@ -0,0 +1,47 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_wide_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="96"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M55.4836,23.5C49.4334,27.8006 61.3082,33.4732 55.4836,37.2143"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M48.6281,23.5C42.5779,27.8006 54.4527,33.4732 48.6281,37.2143"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M41.7707,23.5C35.7205,27.8006 47.5953,33.4732 41.7707,37.2143"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M36,32.5H32C30.8954,32.5 30,31.6046 30,30.5V13C30,11.8954 30.8954,11 32,11H64.5C65.6046,11 66.5,11.8954 66.5,13V30.5C66.5,31.6046 65.6046,32.5 64.5,32.5H62"
+ android:strokeLineJoin="round"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_on.xml
new file mode 100644
index 0000000..ca35897
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_rear_on.xml
@@ -0,0 +1,47 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_wide_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="96"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M55.4836,23.5C49.4334,27.8006 61.3082,33.4732 55.4836,37.2143"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M48.6281,23.5C42.5779,27.8006 54.4527,33.4732 48.6281,37.2143"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M41.7707,23.5C35.7205,27.8006 47.5953,33.4732 41.7707,37.2143"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M36,32.5H32C30.8954,32.5 30,31.6046 30,30.5V13C30,11.8954 30.8954,11 32,11H64.5C65.6046,11 66.5,11.8954 66.5,13V30.5C66.5,31.6046 65.6046,32.5 64.5,32.5H62"
+ android:strokeLineJoin="round"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_off.xml
new file mode 100644
index 0000000..99a55de
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_off.xml
@@ -0,0 +1,47 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_wide_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="96"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M54.1007,23.7148C48.0506,28.0154 59.9254,33.6881 54.1007,37.4291"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M47.2453,23.7148C41.1951,28.0154 53.0699,33.6881 47.2453,37.4291"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M40.3878,23.7148C34.3377,28.0154 46.2125,33.6881 40.3878,37.4291"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M34.8897,32.2857H33.2499L29.1335,17.9407C28.6618,16.2966 29.4739,14.5563 31.0369,13.8618C36.831,11.2873 42.2146,10 47.1878,10C52.161,10 57.5447,11.2873 63.3387,13.8618C64.9018,14.5563 65.7139,16.2966 65.2421,17.9406L61.1257,32.2857H59.486"
+ android:strokeLineJoin="round"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_on.xml
new file mode 100644
index 0000000..9728826
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_defroster_windshield_on.xml
@@ -0,0 +1,47 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_wide_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="96"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M54.1007,23.7148C48.0506,28.0154 59.9254,33.6881 54.1007,37.4291"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M47.2453,23.7148C41.1951,28.0154 53.0699,33.6881 47.2453,37.4291"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M40.3878,23.7148C34.3377,28.0154 46.2125,33.6881 40.3878,37.4291"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M34.8897,32.2857H33.2499L29.1335,17.9407C28.6618,16.2966 29.4739,14.5563 31.0369,13.8618C36.831,11.2873 42.2146,10 47.1878,10C52.161,10 57.5447,11.2873 63.3387,13.8618C64.9018,14.5563 65.7139,16.2966 65.2421,17.9406L61.1257,32.2857H59.486"
+ android:strokeLineJoin="round"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_high.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_high.xml
new file mode 100644
index 0000000..650fd9e
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_high.xml
@@ -0,0 +1,32 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_tall_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="100">
+ <path
+ android:pathData="M11.522,0H9.708C8.311,0 7.033,0.705 6.408,1.821C4.369,5.463 3.834,9.63 4.898,13.59L11.268,37.284C11.404,37.79 12.136,37.932 12.495,37.52L16.362,33.081C19.399,29.596 19.855,24.842 17.528,20.946L12.459,12.461C11.081,10.155 11.271,7.354 12.949,5.213L13.276,4.797C14.324,3.461 14.146,1.646 12.854,0.492C12.5,0.177 12.022,0 11.522,0ZM38.065,36.811H25.871C23.839,36.811 21.858,37.378 20.205,38.432L14.336,42.174C13.636,42.62 13.552,43.521 14.161,44.065L15.714,45.451C17.542,47.083 20.021,48 22.607,48H38.065C41.142,48 43.636,45.773 43.636,43.027V41.784C43.636,39.037 41.142,36.811 38.065,36.811ZM39.06,13.841C38.405,14.32 38.089,14.748 37.933,15.092C37.782,15.428 37.739,15.787 37.799,16.216C37.933,17.181 38.537,18.271 39.372,19.674L39.506,19.899C40.228,21.107 41.121,22.603 41.377,24.089C41.525,24.942 41.481,25.861 41.069,26.767C40.66,27.663 39.952,28.411 39.001,29.04C38.31,29.497 37.38,29.307 36.923,28.616C36.466,27.925 36.656,26.994 37.347,26.537C37.96,26.132 38.221,25.78 38.339,25.523C38.451,25.276 38.488,24.99 38.421,24.599C38.265,23.692 37.648,22.643 36.794,21.209L36.775,21.176C36.019,19.906 35.059,18.293 34.827,16.63C34.703,15.738 34.778,14.795 35.197,13.862C35.613,12.938 36.319,12.129 37.289,11.42C37.957,10.931 38.896,11.076 39.385,11.745C39.874,12.414 39.729,13.352 39.06,13.841ZM31.797,15.092C31.952,14.748 32.269,14.32 32.924,13.841C33.592,13.352 33.738,12.414 33.248,11.745C32.759,11.076 31.821,10.931 31.152,11.42C30.183,12.129 29.476,12.938 29.061,13.862C28.641,14.795 28.566,15.738 28.691,16.63C28.923,18.293 29.883,19.906 30.639,21.176L30.658,21.209C31.511,22.643 32.128,23.692 32.285,24.599C32.352,24.99 32.315,25.276 32.202,25.523C32.085,25.78 31.823,26.132 31.211,26.537C30.52,26.994 30.33,27.925 30.787,28.616C31.243,29.307 32.174,29.497 32.865,29.04C33.816,28.411 34.524,27.663 34.932,26.767C35.345,25.861 35.388,24.942 35.241,24.089C34.985,22.603 34.092,21.107 33.37,19.899L33.236,19.674C32.401,18.271 31.797,17.181 31.662,16.216C31.602,15.787 31.646,15.428 31.797,15.092ZM26.787,13.841C26.132,14.32 25.816,14.748 25.661,15.092C25.51,15.428 25.466,15.787 25.526,16.216C25.66,17.181 26.264,18.271 27.1,19.674L27.234,19.899C27.955,21.107 28.848,22.603 29.105,24.089C29.252,24.942 29.208,25.861 28.796,26.767C28.388,27.663 27.68,28.411 26.729,29.04C26.038,29.497 25.107,29.307 24.65,28.616C24.193,27.925 24.383,26.994 25.074,26.537C25.687,26.132 25.949,25.78 26.066,25.523C26.178,25.276 26.216,24.99 26.148,24.599C25.992,23.692 25.375,22.643 24.522,21.209L24.502,21.176C23.747,19.906 22.786,18.293 22.555,16.63C22.43,15.738 22.505,14.795 22.925,13.862C23.34,12.938 24.046,12.129 25.016,11.42C25.684,10.931 26.623,11.076 27.112,11.745C27.601,12.414 27.456,13.352 26.787,13.841Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_low.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_low.xml
new file mode 100644
index 0000000..a89ba0a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_low.xml
@@ -0,0 +1,33 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_tall_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="100">
+ <path
+ android:pathData="M11.522,0H9.708C8.311,0 7.033,0.705 6.408,1.821C4.369,5.463 3.834,9.63 4.898,13.59L11.268,37.284C11.404,37.79 12.136,37.932 12.495,37.52L16.362,33.081C19.399,29.596 19.855,24.842 17.528,20.946L12.459,12.461C11.081,10.155 11.271,7.354 12.949,5.213L13.276,4.797C14.324,3.461 14.146,1.646 12.854,0.492C12.5,0.177 12.022,0 11.522,0ZM38.065,36.811H25.871C23.839,36.811 21.858,37.378 20.205,38.432L14.336,42.174C13.636,42.62 13.552,43.521 14.161,44.065L15.714,45.451C17.542,47.083 20.021,48 22.607,48H38.065C41.142,48 43.636,45.773 43.636,43.027V41.784C43.636,39.037 41.142,36.811 38.065,36.811ZM39.06,13.841C38.405,14.32 38.089,14.748 37.933,15.092C37.782,15.428 37.739,15.787 37.799,16.216C37.933,17.181 38.537,18.271 39.372,19.674L39.506,19.899C40.228,21.107 41.121,22.603 41.377,24.089C41.525,24.942 41.481,25.861 41.069,26.767C40.66,27.663 39.952,28.411 39.001,29.04C38.31,29.497 37.38,29.307 36.923,28.616C36.466,27.925 36.656,26.994 37.347,26.537C37.96,26.132 38.221,25.78 38.339,25.523C38.451,25.276 38.488,24.99 38.421,24.599C38.265,23.692 37.648,22.643 36.794,21.209L36.775,21.176C36.019,19.906 35.059,18.293 34.827,16.63C34.703,15.738 34.778,14.795 35.197,13.862C35.613,12.938 36.319,12.129 37.289,11.42C37.957,10.931 38.896,11.076 39.385,11.745C39.874,12.414 39.729,13.352 39.06,13.841ZM31.797,15.092C31.952,14.748 32.269,14.32 32.924,13.841C33.592,13.352 33.738,12.414 33.248,11.745C32.759,11.076 31.821,10.931 31.152,11.42C30.183,12.129 29.476,12.938 29.061,13.862C28.641,14.795 28.566,15.738 28.691,16.63C28.923,18.293 29.883,19.906 30.639,21.176L30.658,21.209C31.511,22.643 32.128,23.692 32.285,24.599C32.352,24.99 32.315,25.276 32.202,25.523C32.085,25.78 31.823,26.132 31.211,26.537C30.52,26.994 30.33,27.925 30.787,28.616C31.243,29.307 32.174,29.497 32.865,29.04C33.816,28.411 34.524,27.663 34.932,26.767C35.345,25.861 35.388,24.942 35.241,24.089C34.985,22.603 34.092,21.107 33.37,19.899L33.236,19.674C32.401,18.271 31.797,17.181 31.662,16.216C31.602,15.787 31.646,15.428 31.797,15.092ZM26.787,13.841C26.132,14.32 25.816,14.748 25.661,15.092C25.51,15.428 25.466,15.787 25.526,16.216C25.66,17.181 26.264,18.271 27.1,19.674L27.234,19.899C27.955,21.107 28.848,22.603 29.105,24.089C29.252,24.942 29.208,25.861 28.796,26.767C28.388,27.663 27.68,28.411 26.729,29.04C26.038,29.497 25.107,29.307 24.65,28.616C24.193,27.925 24.383,26.994 25.074,26.537C25.687,26.132 25.949,25.78 26.066,25.523C26.178,25.276 26.216,24.99 26.148,24.599C25.992,23.692 25.375,22.643 24.522,21.209L24.502,21.176C23.747,19.906 22.786,18.293 22.555,16.63C22.43,15.738 22.505,14.795 22.925,13.862C23.34,12.938 24.046,12.129 25.016,11.42C25.684,10.931 26.623,11.076 27.112,11.745C27.601,12.414 27.456,13.352 26.787,13.841Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
+ android:fillColor="@color/hvac_on_icon_fill_color"
+ android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_off.xml
new file mode 100644
index 0000000..9117cac
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_driver_seat_heat_off.xml
@@ -0,0 +1,34 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_tall_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="100">
+ <path
+ android:pathData="M11.522,0H9.708C8.311,0 7.033,0.705 6.408,1.821C4.369,5.463 3.834,9.63 4.898,13.59L11.268,37.284C11.404,37.79 12.136,37.932 12.495,37.52L16.362,33.081C19.399,29.596 19.855,24.842 17.528,20.946L12.459,12.461C11.081,10.155 11.271,7.354 12.949,5.213L13.276,4.797C14.324,3.461 14.146,1.646 12.854,0.492C12.5,0.177 12.022,0 11.522,0ZM38.065,36.811H25.871C23.839,36.811 21.858,37.378 20.205,38.432L14.336,42.174C13.636,42.62 13.552,43.521 14.161,44.065L15.714,45.451C17.542,47.083 20.021,48 22.607,48H38.065C41.142,48 43.636,45.773 43.636,43.027V41.784C43.636,39.037 41.142,36.811 38.065,36.811ZM39.06,13.841C38.405,14.32 38.089,14.748 37.933,15.092C37.782,15.428 37.739,15.787 37.799,16.216C37.933,17.181 38.537,18.271 39.372,19.674L39.506,19.899C40.228,21.107 41.121,22.603 41.377,24.089C41.525,24.942 41.481,25.861 41.069,26.767C40.66,27.663 39.952,28.411 39.001,29.04C38.31,29.497 37.38,29.307 36.923,28.616C36.466,27.925 36.656,26.994 37.347,26.537C37.96,26.132 38.221,25.78 38.339,25.523C38.451,25.276 38.488,24.99 38.421,24.599C38.265,23.692 37.648,22.643 36.794,21.209L36.775,21.176C36.019,19.906 35.059,18.293 34.827,16.63C34.703,15.738 34.778,14.795 35.197,13.862C35.613,12.938 36.319,12.129 37.289,11.42C37.957,10.931 38.896,11.076 39.385,11.745C39.874,12.414 39.729,13.352 39.06,13.841ZM31.797,15.092C31.952,14.748 32.269,14.32 32.924,13.841C33.592,13.352 33.738,12.414 33.248,11.745C32.759,11.076 31.821,10.931 31.152,11.42C30.183,12.129 29.476,12.938 29.061,13.862C28.641,14.795 28.566,15.738 28.691,16.63C28.923,18.293 29.883,19.906 30.639,21.176L30.658,21.209C31.511,22.643 32.128,23.692 32.285,24.599C32.352,24.99 32.315,25.276 32.202,25.523C32.085,25.78 31.823,26.132 31.211,26.537C30.52,26.994 30.33,27.925 30.787,28.616C31.243,29.307 32.174,29.497 32.865,29.04C33.816,28.411 34.524,27.663 34.932,26.767C35.345,25.861 35.388,24.942 35.241,24.089C34.985,22.603 34.092,21.107 33.37,19.899L33.236,19.674C32.401,18.271 31.797,17.181 31.662,16.216C31.602,15.787 31.646,15.428 31.797,15.092ZM26.787,13.841C26.132,14.32 25.816,14.748 25.661,15.092C25.51,15.428 25.466,15.787 25.526,16.216C25.66,17.181 26.264,18.271 27.1,19.674L27.234,19.899C27.955,21.107 28.848,22.603 29.105,24.089C29.252,24.942 29.208,25.861 28.796,26.767C28.388,27.663 27.68,28.411 26.729,29.04C26.038,29.497 25.107,29.307 24.65,28.616C24.193,27.925 24.383,26.994 25.074,26.537C25.687,26.132 25.949,25.78 26.066,25.523C26.178,25.276 26.216,24.99 26.148,24.599C25.992,23.692 25.375,22.643 24.522,21.209L24.502,21.176C23.747,19.906 22.786,18.293 22.555,16.63C22.43,15.738 22.505,14.795 22.925,13.862C23.34,12.938 24.046,12.129 25.016,11.42C25.684,10.931 26.623,11.076 27.112,11.745C27.601,12.414 27.456,13.352 26.787,13.841Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
+ android:fillColor="@color/hvac_off_icon_fill_color"
+ android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
+ <path
+ android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
+ android:fillColor="@color/hvac_off_icon_fill_color"
+ android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_fan_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_fan_off.xml
new file mode 100644
index 0000000..649195b
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_fan_off.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M32.68,16.72L28.1,18.36C27.74,18.1 27.34,17.86 26.94,17.68C27.28,16.02 28.2,14.52 29.66,13.56C33.7,10.88 32.36,4 26.78,4C18,4 14.32,10.02 16.72,15.32L18.36,19.9C18.1,20.26 17.86,20.66 17.68,21.06C16.02,20.72 14.52,19.8 13.56,18.34C10.88,14.3 4,15.64 4,21.22C4,30.02 10.02,33.7 15.32,31.28L19.9,29.64C20.26,29.9 20.66,30.14 21.06,30.32C20.72,31.98 19.8,33.48 18.34,34.44C14.3,37.12 15.64,44 21.22,44C30.02,44 33.7,37.98 31.28,32.68L29.64,28.1C29.9,27.74 30.14,27.34 30.32,26.94C31.98,27.28 33.48,28.2 34.44,29.66C37.12,33.68 43.98,32.34 43.98,26.76C44,18 37.98,14.32 32.68,16.72ZM24,27C22.34,27 21,25.66 21,24C21,22.34 22.34,21 24,21C25.66,21 27,22.34 27,24C27,25.66 25.66,27 24,27Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_fan_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_fan_on.xml
new file mode 100644
index 0000000..62bb353
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_fan_on.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M32.68,16.72L28.1,18.36C27.74,18.1 27.34,17.86 26.94,17.68C27.28,16.02 28.2,14.52 29.66,13.56C33.7,10.88 32.36,4 26.78,4C18,4 14.32,10.02 16.72,15.32L18.36,19.9C18.1,20.26 17.86,20.66 17.68,21.06C16.02,20.72 14.52,19.8 13.56,18.34C10.88,14.3 4,15.64 4,21.22C4,30.02 10.02,33.7 15.32,31.28L19.9,29.64C20.26,29.9 20.66,30.14 21.06,30.32C20.72,31.98 19.8,33.48 18.34,34.44C14.3,37.12 15.64,44 21.22,44C30.02,44 33.7,37.98 31.28,32.68L29.64,28.1C29.9,27.74 30.14,27.34 30.32,26.94C31.98,27.28 33.48,28.2 34.44,29.66C37.12,33.68 43.98,32.34 43.98,26.76C44,18 37.98,14.32 32.68,16.72ZM24,27C22.34,27 21,25.66 21,24C21,22.34 22.34,21 24,21C25.66,21 27,22.34 27,24C27,25.66 25.66,27 24,27Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_off.xml
new file mode 100644
index 0000000..48baaf5
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_off.xml
@@ -0,0 +1,49 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M41.1457,5.5455C35.3706,9.6506 46.7056,15.0654 41.1457,18.6364"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M35.6907,5.5455C29.9155,9.6506 41.2505,15.0654 35.6907,18.6364"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M30.2356,5.5455C24.4604,9.6506 35.7955,15.0654 30.2356,18.6364"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M24,6.5455C12,6.5455 6.5454,16.9091 6.5454,24C6.5454,31.0909 12,41.4546 24,41.4546C36,41.4546 41.4545,32.5005 41.4545,25.0005"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_off_icon_fill_color"/>
+ <path
+ android:pathData="M14.0005,24.0005L8.0005,24.5005L8.5005,28.0005L18.296,28.7261C20.3848,28.8809 22.0005,30.6207 22.0005,32.7152V41.0005H26.0005V32.7722C26.0005,30.6543 27.6514,28.9034 29.7656,28.7791L43.0005,28.0005L43.5005,24.5005L34.0005,24.0005L30.0005,22.0005H18.0005L14.0005,24.0005Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_on.xml
new file mode 100644
index 0000000..425f2f6
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_heated_steering_on.xml
@@ -0,0 +1,49 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M41.1457,5.5455C35.3706,9.6506 46.7056,15.0654 41.1457,18.6364"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M35.6907,5.5455C29.9155,9.6506 41.2505,15.0654 35.6907,18.6364"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M30.2356,5.5455C24.4604,9.6506 35.7955,15.0654 30.2356,18.6364"
+ android:strokeWidth="3"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M24,6.5455C12,6.5455 6.5454,16.9091 6.5454,24C6.5454,31.0909 12,41.4546 24,41.4546C36,41.4546 41.4545,32.5005 41.4545,25.0005"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4"
+ android:fillColor="@android:color/transparent"
+ android:strokeColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M14.0005,24.0005L8.0005,24.5005L8.5005,28.0005L18.296,28.7261C20.3848,28.8809 22.0005,30.6207 22.0005,32.7152V41.0005H26.0005V32.7722C26.0005,30.6543 27.6514,28.9034 29.7656,28.7791L43.0005,28.0005L43.5005,24.5005L34.0005,24.0005L30.0005,22.0005H18.0005L14.0005,24.0005Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_minimize.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_minimize.xml
new file mode 100644
index 0000000..721cec4
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_minimize.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <vector
+ android:width="@dimen/system_bar_icon_drawing_size"
+ android:height="@dimen/system_bar_icon_drawing_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M12,15.375l-6,-6 1.4,-1.4 4.6,4.6 4.6,-4.6 1.4,1.4z"
+ android:fillColor="@color/car_nav_minimize_icon_fill_color"/>
+ </vector>
+ </item>
+</selector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_1.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_1.xml
new file mode 100644
index 0000000..00a1ed8
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_1.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M32.68,16.72L28.1,18.36C27.74,18.1 27.34,17.86 26.94,17.68C27.28,16.02 28.2,14.52 29.66,13.56C33.7,10.88 32.36,4 26.78,4C18,4 14.32,10.02 16.72,15.32L18.36,19.9C18.1,20.26 17.86,20.66 17.68,21.06C16.02,20.72 14.52,19.8 13.56,18.34C10.88,14.3 4,15.64 4,21.22C4,30.02 10.02,33.7 15.32,31.28L19.9,29.64C20.26,29.9 20.66,30.14 21.06,30.32C20.72,31.98 19.8,33.48 18.34,34.44C14.3,37.12 15.64,44 21.22,44C30.02,44 33.7,37.98 31.28,32.68L29.64,28.1C29.9,27.74 30.14,27.34 30.32,26.94C31.98,27.28 33.48,28.2 34.44,29.66C37.12,33.68 43.98,32.34 43.98,26.76C44,18 37.98,14.32 32.68,16.72ZM24,27C22.34,27 21,25.66 21,24C21,22.34 22.34,21 24,21C25.66,21 27,22.34 27,24C27,25.66 25.66,27 24,27Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M42.9713,47V38.792L41.3233,39.992L40.2673,38.376L43.4833,36.056H45.0673V47H42.9713Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_2.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_2.xml
new file mode 100644
index 0000000..3421517
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_2.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M32.68,16.72L28.1,18.36C27.74,18.1 27.34,17.86 26.94,17.68C27.28,16.02 28.2,14.52 29.66,13.56C33.7,10.88 32.36,4 26.78,4C18,4 14.32,10.02 16.72,15.32L18.36,19.9C18.1,20.26 17.86,20.66 17.68,21.06C16.02,20.72 14.52,19.8 13.56,18.34C10.88,14.3 4,15.64 4,21.22C4,30.02 10.02,33.7 15.32,31.28L19.9,29.64C20.26,29.9 20.66,30.14 21.06,30.32C20.72,31.98 19.8,33.48 18.34,34.44C14.3,37.12 15.64,44 21.22,44C30.02,44 33.7,37.98 31.28,32.68L29.64,28.1C29.9,27.74 30.14,27.34 30.32,26.94C31.98,27.28 33.48,28.2 34.44,29.66C37.12,33.68 43.98,32.34 43.98,26.76C44,18 37.98,14.32 32.68,16.72ZM24,27C22.34,27 21,25.66 21,24C21,22.34 22.34,21 24,21C25.66,21 27,22.34 27,24C27,25.66 25.66,27 24,27Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M39.3867,47V45.096C39.3867,45.096 39.4987,44.984 39.7227,44.76C39.9467,44.536 40.2294,44.2533 40.5707,43.912C40.9227,43.5707 41.2854,43.2133 41.6587,42.84C42.032,42.4667 42.3734,42.12 42.6827,41.8C42.992,41.48 43.2214,41.24 43.3707,41.08C43.744,40.6747 44.0054,40.3333 44.1547,40.056C44.304,39.7787 44.3787,39.4587 44.3787,39.096C44.3787,38.744 44.2454,38.4347 43.9787,38.168C43.712,37.9013 43.3387,37.768 42.8587,37.768C42.3787,37.768 42.0054,37.9067 41.7387,38.184C41.472,38.4613 41.2854,38.7707 41.1787,39.112L39.2907,38.328C39.408,37.9333 39.616,37.544 39.9147,37.16C40.224,36.776 40.624,36.456 41.1147,36.2C41.616,35.9333 42.208,35.8 42.8907,35.8C43.6374,35.8 44.2774,35.944 44.8107,36.232C45.3547,36.52 45.7707,36.904 46.0587,37.384C46.3574,37.864 46.5067,38.4027 46.5067,39C46.5067,39.6827 46.3414,40.312 46.0107,40.888C45.68,41.464 45.2694,41.992 44.7787,42.472C44.6187,42.6213 44.5014,42.7333 44.4267,42.808C44.3627,42.872 44.2934,42.936 44.2187,43C44.1547,43.064 44.0534,43.1653 43.9147,43.304C43.7867,43.432 43.584,43.6347 43.3067,43.912C43.04,44.1893 42.6614,44.5733 42.1707,45.064L42.2187,45.16H46.6507V47H39.3867Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_3.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_3.xml
new file mode 100644
index 0000000..58f47fe
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_3.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M32.68,16.72L28.1,18.36C27.74,18.1 27.34,17.86 26.94,17.68C27.28,16.02 28.2,14.52 29.66,13.56C33.7,10.88 32.36,4 26.78,4C18,4 14.32,10.02 16.72,15.32L18.36,19.9C18.1,20.26 17.86,20.66 17.68,21.06C16.02,20.72 14.52,19.8 13.56,18.34C10.88,14.3 4,15.64 4,21.22C4,30.02 10.02,33.7 15.32,31.28L19.9,29.64C20.26,29.9 20.66,30.14 21.06,30.32C20.72,31.98 19.8,33.48 18.34,34.44C14.3,37.12 15.64,44 21.22,44C30.02,44 33.7,37.98 31.28,32.68L29.64,28.1C29.9,27.74 30.14,27.34 30.32,26.94C31.98,27.28 33.48,28.2 34.44,29.66C37.12,33.68 43.98,32.34 43.98,26.76C44,18 37.98,14.32 32.68,16.72ZM24,27C22.34,27 21,25.66 21,24C21,22.34 22.34,21 24,21C25.66,21 27,22.34 27,24C27,25.66 25.66,27 24,27Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M42.9975,47.256C42.1122,47.256 41.3068,47.0213 40.5815,46.552C39.8562,46.072 39.3548,45.3467 39.0775,44.376L41.0615,43.592C41.3282,44.68 41.9735,45.224 42.9975,45.224C43.4455,45.224 43.8402,45.0907 44.1815,44.824C44.5228,44.5467 44.6935,44.1893 44.6935,43.752C44.6935,43.2827 44.5122,42.9147 44.1495,42.648C43.7868,42.3813 43.3068,42.248 42.7095,42.248H41.7655V40.344H42.6295C43.0668,40.344 43.4562,40.232 43.7975,40.008C44.1388,39.784 44.3095,39.4373 44.3095,38.968C44.3095,38.6053 44.1762,38.3067 43.9095,38.072C43.6535,37.8373 43.3122,37.72 42.8855,37.72C42.4162,37.72 42.0535,37.848 41.7975,38.104C41.5415,38.36 41.3655,38.6427 41.2695,38.952L39.3655,38.168C39.4935,37.8053 39.7068,37.4427 40.0055,37.08C40.3042,36.7173 40.6935,36.4133 41.1735,36.168C41.6535,35.9227 42.2295,35.8 42.9015,35.8C43.6055,35.8 44.2188,35.928 44.7415,36.184C45.2748,36.44 45.6908,36.792 45.9895,37.24C46.2882,37.6773 46.4375,38.1733 46.4375,38.728C46.4375,39.3573 46.2882,39.8747 45.9895,40.28C45.7015,40.6853 45.3868,40.9733 45.0455,41.144V41.272C45.5468,41.4747 45.9682,41.8 46.3095,42.248C46.6615,42.696 46.8375,43.2613 46.8375,43.944C46.8375,44.5733 46.6775,45.1387 46.3575,45.64C46.0375,46.1413 45.5895,46.536 45.0135,46.824C44.4375,47.112 43.7655,47.256 42.9975,47.256Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_4.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_4.xml
new file mode 100644
index 0000000..077d134
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_4.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M32.68,16.72L28.1,18.36C27.74,18.1 27.34,17.86 26.94,17.68C27.28,16.02 28.2,14.52 29.66,13.56C33.7,10.88 32.36,4 26.78,4C18,4 14.32,10.02 16.72,15.32L18.36,19.9C18.1,20.26 17.86,20.66 17.68,21.06C16.02,20.72 14.52,19.8 13.56,18.34C10.88,14.3 4,15.64 4,21.22C4,30.02 10.02,33.7 15.32,31.28L19.9,29.64C20.26,29.9 20.66,30.14 21.06,30.32C20.72,31.98 19.8,33.48 18.34,34.44C14.3,37.12 15.64,44 21.22,44C30.02,44 33.7,37.98 31.28,32.68L29.64,28.1C29.9,27.74 30.14,27.34 30.32,26.94C31.98,27.28 33.48,28.2 34.44,29.66C37.12,33.68 43.98,32.34 43.98,26.76C44,18 37.98,14.32 32.68,16.72ZM24,27C22.34,27 21,25.66 21,24C21,22.34 22.34,21 24,21C25.66,21 27,22.34 27,24C27,25.66 25.66,27 24,27Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M38.7282,44.984V43.288L43.5922,36.056H45.8642V43.032H47.2242V44.984H45.8642V47H43.7682V44.984H38.7282ZM41.0482,43.032H43.7682V39.176H43.6402L41.0482,43.032Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_5.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_5.xml
new file mode 100644
index 0000000..74b7fc2
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_5.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M32.68,16.72L28.1,18.36C27.74,18.1 27.34,17.86 26.94,17.68C27.28,16.02 28.2,14.52 29.66,13.56C33.7,10.88 32.36,4 26.78,4C18,4 14.32,10.02 16.72,15.32L18.36,19.9C18.1,20.26 17.86,20.66 17.68,21.06C16.02,20.72 14.52,19.8 13.56,18.34C10.88,14.3 4,15.64 4,21.22C4,30.02 10.02,33.7 15.32,31.28L19.9,29.64C20.26,29.9 20.66,30.14 21.06,30.32C20.72,31.98 19.8,33.48 18.34,34.44C14.3,37.12 15.64,44 21.22,44C30.02,44 33.7,37.98 31.28,32.68L29.64,28.1C29.9,27.74 30.14,27.34 30.32,26.94C31.98,27.28 33.48,28.2 34.44,29.66C37.12,33.68 43.98,32.34 43.98,26.76C44,18 37.98,14.32 32.68,16.72ZM24,27C22.34,27 21,25.66 21,24C21,22.34 22.34,21 24,21C25.66,21 27,22.34 27,24C27,25.66 25.66,27 24,27Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M42.9365,47.256C42.3925,47.256 41.8485,47.1493 41.3045,46.936C40.7712,46.7227 40.3018,46.3973 39.8965,45.96C39.4912,45.5227 39.2085,44.968 39.0485,44.296L40.9365,43.56C41.0538,44.0827 41.2832,44.5093 41.6245,44.84C41.9658,45.16 42.3978,45.32 42.9205,45.32C43.4218,45.32 43.8432,45.1493 44.1845,44.808C44.5365,44.4667 44.7125,44.0347 44.7125,43.512C44.7125,43 44.5472,42.5733 44.2165,42.232C43.8858,41.88 43.4538,41.704 42.9205,41.704C42.5898,41.704 42.2965,41.7733 42.0405,41.912C41.7845,42.0507 41.5658,42.2267 41.3845,42.44L39.3525,41.528L39.9765,36.056H46.1525V37.896H41.7205L41.3205,40.456L41.4485,40.488C41.6618,40.3067 41.9232,40.152 42.2325,40.024C42.5525,39.896 42.9258,39.832 43.3525,39.832C43.9605,39.832 44.5258,39.9867 45.0485,40.296C45.5712,40.6053 45.9925,41.0373 46.3125,41.592C46.6432,42.136 46.8085,42.776 46.8085,43.512C46.8085,44.2373 46.6432,44.8827 46.3125,45.448C45.9818,46.0133 45.5232,46.456 44.9365,46.776C44.3605,47.096 43.6938,47.256 42.9365,47.256Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_6.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_6.xml
new file mode 100644
index 0000000..503c42a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_6.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M32.68,16.72L28.1,18.36C27.74,18.1 27.34,17.86 26.94,17.68C27.28,16.02 28.2,14.52 29.66,13.56C33.7,10.88 32.36,4 26.78,4C18,4 14.32,10.02 16.72,15.32L18.36,19.9C18.1,20.26 17.86,20.66 17.68,21.06C16.02,20.72 14.52,19.8 13.56,18.34C10.88,14.3 4,15.64 4,21.22C4,30.02 10.02,33.7 15.32,31.28L19.9,29.64C20.26,29.9 20.66,30.14 21.06,30.32C20.72,31.98 19.8,33.48 18.34,34.44C14.3,37.12 15.64,44 21.22,44C30.02,44 33.7,37.98 31.28,32.68L29.64,28.1C29.9,27.74 30.14,27.34 30.32,26.94C31.98,27.28 33.48,28.2 34.44,29.66C37.12,33.68 43.98,32.34 43.98,26.76C44,18 37.98,14.32 32.68,16.72ZM24,27C22.34,27 21,25.66 21,24C21,22.34 22.34,21 24,21C25.66,21 27,22.34 27,24C27,25.66 25.66,27 24,27Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M43.0392,47.256C42.4312,47.256 41.8765,47.1493 41.3752,46.936C40.8845,46.712 40.4685,46.4187 40.1272,46.056C39.4445,45.3413 39.1032,44.4667 39.1032,43.432C39.1032,42.696 39.2472,42.024 39.5352,41.416C39.8339,40.808 40.2072,40.1787 40.6552,39.528C41.0925,38.8987 41.5299,38.2693 41.9672,37.64C42.4152,37 42.8579,36.3653 43.2952,35.736L44.9272,36.856C44.5539,37.3787 44.1752,37.9067 43.7912,38.44C43.4179,38.9627 43.0445,39.4907 42.6712,40.024L42.7672,40.12C43.0125,40.0133 43.2899,39.96 43.5992,39.96C44.1539,39.96 44.6819,40.1093 45.1832,40.408C45.6845,40.7067 46.0952,41.1227 46.4152,41.656C46.7352,42.1893 46.8952,42.808 46.8952,43.512C46.8952,44.2373 46.7139,44.8827 46.3512,45.448C45.9885,46.0133 45.5139,46.456 44.9272,46.776C44.3405,47.096 43.7112,47.256 43.0392,47.256ZM42.9912,45.336C43.3219,45.336 43.6259,45.2613 43.9032,45.112C44.1805,44.952 44.4045,44.7387 44.5752,44.472C44.7565,44.1947 44.8472,43.88 44.8472,43.528C44.8472,43.1653 44.7565,42.8507 44.5752,42.584C44.4045,42.3067 44.1752,42.0933 43.8872,41.944C43.6099,41.784 43.3112,41.704 42.9912,41.704C42.6819,41.704 42.3832,41.784 42.0952,41.944C41.8179,42.0933 41.5885,42.3067 41.4072,42.584C41.2365,42.8507 41.1512,43.1653 41.1512,43.528C41.1512,43.88 41.2365,44.1947 41.4072,44.472C41.5885,44.7387 41.8179,44.952 42.0952,45.112C42.3725,45.2613 42.6712,45.336 42.9912,45.336Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_7.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_7.xml
new file mode 100644
index 0000000..bcf9158
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_7.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M32.68,16.72L28.1,18.36C27.74,18.1 27.34,17.86 26.94,17.68C27.28,16.02 28.2,14.52 29.66,13.56C33.7,10.88 32.36,4 26.78,4C18,4 14.32,10.02 16.72,15.32L18.36,19.9C18.1,20.26 17.86,20.66 17.68,21.06C16.02,20.72 14.52,19.8 13.56,18.34C10.88,14.3 4,15.64 4,21.22C4,30.02 10.02,33.7 15.32,31.28L19.9,29.64C20.26,29.9 20.66,30.14 21.06,30.32C20.72,31.98 19.8,33.48 18.34,34.44C14.3,37.12 15.64,44 21.22,44C30.02,44 33.7,37.98 31.28,32.68L29.64,28.1C29.9,27.74 30.14,27.34 30.32,26.94C31.98,27.28 33.48,28.2 34.44,29.66C37.12,33.68 43.98,32.34 43.98,26.76C44,18 37.98,14.32 32.68,16.72ZM24,27C22.34,27 21,25.66 21,24C21,22.34 22.34,21 24,21C25.66,21 27,22.34 27,24C27,25.66 25.66,27 24,27Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M41.5709,47.256L39.8109,46.28L44.3709,38.12L44.3069,38.024H39.2829V36.056H46.7229V38.12C45.8695,39.6347 45.0109,41.1547 44.1469,42.68C43.2829,44.2053 42.4242,45.7307 41.5709,47.256Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_8.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_8.xml
new file mode 100644
index 0000000..7d70370
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_mode_fan_8.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M32.68,16.72L28.1,18.36C27.74,18.1 27.34,17.86 26.94,17.68C27.28,16.02 28.2,14.52 29.66,13.56C33.7,10.88 32.36,4 26.78,4C18,4 14.32,10.02 16.72,15.32L18.36,19.9C18.1,20.26 17.86,20.66 17.68,21.06C16.02,20.72 14.52,19.8 13.56,18.34C10.88,14.3 4,15.64 4,21.22C4,30.02 10.02,33.7 15.32,31.28L19.9,29.64C20.26,29.9 20.66,30.14 21.06,30.32C20.72,31.98 19.8,33.48 18.34,34.44C14.3,37.12 15.64,44 21.22,44C30.02,44 33.7,37.98 31.28,32.68L29.64,28.1C29.9,27.74 30.14,27.34 30.32,26.94C31.98,27.28 33.48,28.2 34.44,29.66C37.12,33.68 43.98,32.34 43.98,26.76C44,18 37.98,14.32 32.68,16.72ZM24,27C22.34,27 21,25.66 21,24C21,22.34 22.34,21 24,21C25.66,21 27,22.34 27,24C27,25.66 25.66,27 24,27Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M43.0087,47.256C42.23,47.256 41.542,47.112 40.9447,46.824C40.3474,46.5253 39.878,46.1307 39.5367,45.64C39.206,45.1387 39.0407,44.5787 39.0407,43.96C39.0407,43.32 39.2114,42.776 39.5527,42.328C39.894,41.8693 40.278,41.5227 40.7047,41.288V41.16C40.3634,40.9253 40.0594,40.616 39.7927,40.232C39.5367,39.8373 39.4087,39.3893 39.4087,38.888C39.4087,38.3013 39.5634,37.7787 39.8727,37.32C40.182,36.8507 40.6087,36.4827 41.1527,36.216C41.6967,35.9387 42.3154,35.8 43.0087,35.8C43.702,35.8 44.3154,35.9387 44.8487,36.216C45.3927,36.4827 45.8194,36.8507 46.1287,37.32C46.4487,37.7787 46.6087,38.3013 46.6087,38.888C46.6087,39.3893 46.4754,39.8373 46.2087,40.232C45.9527,40.616 45.6487,40.9253 45.2967,41.16V41.288C45.734,41.5227 46.1234,41.8693 46.4647,42.328C46.806,42.776 46.9767,43.32 46.9767,43.96C46.9767,44.5787 46.806,45.1387 46.4647,45.64C46.134,46.1307 45.67,46.5253 45.0727,46.824C44.486,47.112 43.798,47.256 43.0087,47.256ZM43.0087,40.376C43.446,40.376 43.814,40.2533 44.1127,40.008C44.422,39.752 44.5767,39.4213 44.5767,39.016C44.5767,38.6 44.422,38.2747 44.1127,38.04C43.814,37.7947 43.446,37.672 43.0087,37.672C42.5607,37.672 42.182,37.7947 41.8727,38.04C41.574,38.2747 41.4247,38.6 41.4247,39.016C41.4247,39.4213 41.574,39.752 41.8727,40.008C42.182,40.2533 42.5607,40.376 43.0087,40.376ZM43.0087,45.32C43.5527,45.32 43.9954,45.176 44.3367,44.888C44.6887,44.6 44.8647,44.2213 44.8647,43.752C44.8647,43.304 44.6887,42.936 44.3367,42.648C43.9847,42.36 43.542,42.216 43.0087,42.216C42.4754,42.216 42.0274,42.36 41.6647,42.648C41.3127,42.936 41.1367,43.304 41.1367,43.752C41.1367,44.2213 41.3127,44.6 41.6647,44.888C42.0167,45.176 42.4647,45.32 43.0087,45.32Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_high.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_high.xml
new file mode 100644
index 0000000..0d02e85
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_high.xml
@@ -0,0 +1,32 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_tall_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="100">
+ <path
+ android:pathData="M36.48,0H38.294C39.691,0 40.969,0.705 41.594,1.821C43.633,5.463 44.168,9.63 43.104,13.59L36.734,37.284C36.598,37.79 35.866,37.932 35.507,37.52L31.64,33.081C28.603,29.596 28.147,24.842 30.474,20.946L35.543,12.461C36.921,10.155 36.731,7.354 35.053,5.213L34.726,4.797C33.678,3.461 33.856,1.646 35.148,0.492C35.501,0.177 35.98,0 36.48,0ZM9.936,36.811H22.131C24.163,36.811 26.144,37.378 27.797,38.432L33.666,42.174C34.366,42.62 34.45,43.521 33.841,44.065L32.288,45.451C30.46,47.083 27.981,48 25.395,48H9.936C6.86,48 4.366,45.773 4.366,43.027V41.784C4.366,39.037 6.86,36.811 9.936,36.811ZM23.104,13.84C22.449,14.319 22.133,14.746 21.978,15.091C21.827,15.427 21.783,15.786 21.843,16.215C21.977,17.179 22.581,18.27 23.417,19.673L23.551,19.898C24.272,21.106 25.166,22.602 25.422,24.088C25.569,24.941 25.525,25.86 25.113,26.766C24.705,27.662 23.997,28.41 23.046,29.039C22.354,29.496 21.424,29.306 20.967,28.615C20.51,27.924 20.7,26.993 21.391,26.536C22.004,26.131 22.266,25.779 22.383,25.522C22.495,25.275 22.533,24.989 22.465,24.598C22.309,23.691 21.692,22.641 20.839,21.207L20.819,21.175C20.063,19.905 19.103,18.292 18.871,16.629C18.747,15.737 18.822,14.794 19.242,13.861C19.657,12.937 20.363,12.128 21.333,11.419C22.001,10.93 22.94,11.075 23.429,11.744C23.918,12.412 23.773,13.351 23.104,13.84ZM15.844,15.091C15.999,14.746 16.316,14.319 16.97,13.84C17.639,13.351 17.784,12.412 17.295,11.744C16.806,11.075 15.867,10.93 15.199,11.419C14.229,12.128 13.523,12.937 13.108,13.861C12.688,14.794 12.613,15.737 12.738,16.629C12.969,18.292 13.929,19.905 14.685,21.175L14.705,21.207C15.558,22.641 16.175,23.691 16.331,24.598C16.399,24.989 16.361,25.275 16.249,25.522C16.132,25.779 15.87,26.131 15.257,26.536C14.566,26.993 14.376,27.924 14.833,28.615C15.29,29.306 16.221,29.496 16.912,29.039C17.862,28.41 18.571,27.662 18.979,26.766C19.392,25.86 19.435,24.941 19.288,24.088C19.031,22.602 18.138,21.106 17.417,19.898L17.283,19.673C16.447,18.27 15.843,17.179 15.709,16.215C15.649,15.786 15.693,15.427 15.844,15.091ZM10.832,13.84C10.178,14.319 9.861,14.746 9.706,15.091C9.555,15.427 9.511,15.786 9.571,16.215C9.705,17.179 10.31,18.27 11.145,19.673L11.279,19.898C12,21.106 12.894,22.602 13.15,24.088C13.297,24.941 13.254,25.86 12.841,26.766C12.433,27.662 11.725,28.41 10.774,29.039C10.083,29.496 9.152,29.306 8.695,28.615C8.239,27.924 8.428,26.993 9.12,26.536C9.732,26.131 9.994,25.779 10.111,25.522C10.224,25.275 10.261,24.989 10.194,24.598C10.037,23.691 9.42,22.641 8.567,21.207L8.548,21.175C7.792,19.905 6.832,18.292 6.6,16.629C6.475,15.737 6.55,14.794 6.97,13.861C7.385,12.937 8.091,12.128 9.061,11.419C9.73,10.93 10.668,11.075 11.157,11.744C11.646,12.412 11.501,13.351 10.832,13.84Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_low.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_low.xml
new file mode 100644
index 0000000..b95b31a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_low.xml
@@ -0,0 +1,33 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_tall_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="100">
+ <path
+ android:pathData="M36.48,0H38.294C39.691,0 40.969,0.705 41.594,1.821C43.633,5.463 44.168,9.63 43.104,13.59L36.734,37.284C36.598,37.79 35.866,37.932 35.507,37.52L31.64,33.081C28.603,29.596 28.147,24.842 30.474,20.946L35.543,12.461C36.921,10.155 36.731,7.354 35.053,5.213L34.726,4.797C33.678,3.461 33.856,1.646 35.148,0.492C35.501,0.177 35.98,0 36.48,0ZM9.936,36.811H22.131C24.163,36.811 26.144,37.378 27.797,38.432L33.666,42.174C34.366,42.62 34.45,43.521 33.841,44.065L32.288,45.451C30.46,47.083 27.981,48 25.395,48H9.936C6.86,48 4.366,45.773 4.366,43.027V41.784C4.366,39.037 6.86,36.811 9.936,36.811ZM23.104,13.84C22.449,14.319 22.133,14.746 21.978,15.091C21.827,15.427 21.783,15.786 21.843,16.215C21.977,17.179 22.581,18.27 23.417,19.673L23.551,19.898C24.272,21.106 25.166,22.602 25.422,24.088C25.569,24.941 25.525,25.86 25.113,26.766C24.705,27.662 23.997,28.41 23.046,29.039C22.354,29.496 21.424,29.306 20.967,28.615C20.51,27.924 20.7,26.993 21.391,26.536C22.004,26.131 22.266,25.779 22.383,25.522C22.495,25.275 22.533,24.989 22.465,24.598C22.309,23.691 21.692,22.641 20.839,21.207L20.819,21.175C20.063,19.905 19.103,18.292 18.871,16.629C18.747,15.737 18.822,14.794 19.242,13.861C19.657,12.937 20.363,12.128 21.333,11.419C22.001,10.93 22.94,11.075 23.429,11.744C23.918,12.412 23.773,13.351 23.104,13.84ZM15.844,15.091C15.999,14.746 16.316,14.319 16.97,13.84C17.639,13.351 17.784,12.412 17.295,11.744C16.806,11.075 15.867,10.93 15.199,11.419C14.229,12.128 13.523,12.937 13.108,13.861C12.688,14.794 12.613,15.737 12.738,16.629C12.969,18.292 13.929,19.905 14.685,21.175L14.705,21.207C15.558,22.641 16.175,23.691 16.331,24.598C16.399,24.989 16.361,25.275 16.249,25.522C16.132,25.779 15.87,26.131 15.257,26.536C14.566,26.993 14.376,27.924 14.833,28.615C15.29,29.306 16.221,29.496 16.912,29.039C17.862,28.41 18.571,27.662 18.979,26.766C19.392,25.86 19.435,24.941 19.288,24.088C19.031,22.602 18.138,21.106 17.417,19.898L17.283,19.673C16.447,18.27 15.843,17.179 15.709,16.215C15.649,15.786 15.693,15.427 15.844,15.091ZM10.832,13.84C10.178,14.319 9.861,14.746 9.706,15.091C9.555,15.427 9.511,15.786 9.571,16.215C9.705,17.179 10.31,18.27 11.145,19.673L11.279,19.898C12,21.106 12.894,22.602 13.15,24.088C13.297,24.941 13.254,25.86 12.841,26.766C12.433,27.662 11.725,28.41 10.774,29.039C10.083,29.496 9.152,29.306 8.695,28.615C8.239,27.924 8.428,26.993 9.12,26.536C9.732,26.131 9.994,25.779 10.111,25.522C10.224,25.275 10.261,24.989 10.194,24.598C10.037,23.691 9.42,22.641 8.567,21.207L8.548,21.175C7.792,19.905 6.832,18.292 6.6,16.629C6.475,15.737 6.55,14.794 6.97,13.861C7.385,12.937 8.091,12.128 9.061,11.419C9.73,10.93 10.668,11.075 11.157,11.744C11.646,12.412 11.501,13.351 10.832,13.84Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
+ android:fillColor="@color/hvac_on_icon_fill_color"
+ android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_off.xml
new file mode 100644
index 0000000..cbf3fd1
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_passenger_seat_heat_off.xml
@@ -0,0 +1,34 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_tall_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="100">
+ <path
+ android:pathData="M36.48,0H38.294C39.691,0 40.969,0.705 41.594,1.821C43.633,5.463 44.168,9.63 43.104,13.59L36.734,37.284C36.598,37.79 35.866,37.932 35.507,37.52L31.64,33.081C28.603,29.596 28.147,24.842 30.474,20.946L35.543,12.461C36.921,10.155 36.731,7.354 35.053,5.213L34.726,4.797C33.678,3.461 33.856,1.646 35.148,0.492C35.501,0.177 35.98,0 36.48,0ZM9.936,36.811H22.131C24.163,36.811 26.144,37.378 27.797,38.432L33.666,42.174C34.366,42.62 34.45,43.521 33.841,44.065L32.288,45.451C30.46,47.083 27.981,48 25.395,48H9.936C6.86,48 4.366,45.773 4.366,43.027V41.784C4.366,39.037 6.86,36.811 9.936,36.811ZM23.104,13.84C22.449,14.319 22.133,14.746 21.978,15.091C21.827,15.427 21.783,15.786 21.843,16.215C21.977,17.179 22.581,18.27 23.417,19.673L23.551,19.898C24.272,21.106 25.166,22.602 25.422,24.088C25.569,24.941 25.525,25.86 25.113,26.766C24.705,27.662 23.997,28.41 23.046,29.039C22.354,29.496 21.424,29.306 20.967,28.615C20.51,27.924 20.7,26.993 21.391,26.536C22.004,26.131 22.266,25.779 22.383,25.522C22.495,25.275 22.533,24.989 22.465,24.598C22.309,23.691 21.692,22.641 20.839,21.207L20.819,21.175C20.063,19.905 19.103,18.292 18.871,16.629C18.747,15.737 18.822,14.794 19.242,13.861C19.657,12.937 20.363,12.128 21.333,11.419C22.001,10.93 22.94,11.075 23.429,11.744C23.918,12.412 23.773,13.351 23.104,13.84ZM15.844,15.091C15.999,14.746 16.316,14.319 16.97,13.84C17.639,13.351 17.784,12.412 17.295,11.744C16.806,11.075 15.867,10.93 15.199,11.419C14.229,12.128 13.523,12.937 13.108,13.861C12.688,14.794 12.613,15.737 12.738,16.629C12.969,18.292 13.929,19.905 14.685,21.175L14.705,21.207C15.558,22.641 16.175,23.691 16.331,24.598C16.399,24.989 16.361,25.275 16.249,25.522C16.132,25.779 15.87,26.131 15.257,26.536C14.566,26.993 14.376,27.924 14.833,28.615C15.29,29.306 16.221,29.496 16.912,29.039C17.862,28.41 18.571,27.662 18.979,26.766C19.392,25.86 19.435,24.941 19.288,24.088C19.031,22.602 18.138,21.106 17.417,19.898L17.283,19.673C16.447,18.27 15.843,17.179 15.709,16.215C15.649,15.786 15.693,15.427 15.844,15.091ZM10.832,13.84C10.178,14.319 9.861,14.746 9.706,15.091C9.555,15.427 9.511,15.786 9.571,16.215C9.705,17.179 10.31,18.27 11.145,19.673L11.279,19.898C12,21.106 12.894,22.602 13.15,24.088C13.297,24.941 13.254,25.86 12.841,26.766C12.433,27.662 11.725,28.41 10.774,29.039C10.083,29.496 9.152,29.306 8.695,28.615C8.239,27.924 8.428,26.993 9.12,26.536C9.732,26.131 9.994,25.779 10.111,25.522C10.224,25.275 10.261,24.989 10.194,24.598C10.037,23.691 9.42,22.641 8.567,21.207L8.548,21.175C7.792,19.905 6.832,18.292 6.6,16.629C6.475,15.737 6.55,14.794 6.97,13.861C7.385,12.937 8.091,12.128 9.061,11.419C9.73,10.93 10.668,11.075 11.157,11.744C11.646,12.412 11.501,13.351 10.832,13.84Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"
+ android:fillType="evenOdd"/>
+ <path
+ android:pathData="M24,90L24,90A5,5 0,0 1,29 95L29,95A5,5 0,0 1,24 100L24,100A5,5 0,0 1,19 95L19,95A5,5 0,0 1,24 90z"
+ android:fillColor="@color/hvac_off_icon_fill_color"
+ android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
+ <path
+ android:pathData="M24,68L24,68A5,5 0,0 1,29 73L29,73A5,5 0,0 1,24 78L24,78A5,5 0,0 1,19 73L19,73A5,5 0,0 1,24 68z"
+ android:fillColor="@color/hvac_off_icon_fill_color"
+ android:fillAlpha="@dimen/hvac_heat_or_cool_off_alpha"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_power_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_power_off.xml
new file mode 100644
index 0000000..aef5c8a
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_power_off.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M26.0001,4H22.0001V24H26.0001V4ZM33.9201,14.1L36.7401,11.28C43.7601,18.32 43.7601,29.7 36.7401,36.72C29.7201,43.76 18.3201,43.76 11.3001,36.74C4.2601,29.7 4.2601,18.3 11.2801,11.28L14.1001,14.08C8.6401,19.54 8.6601,28.44 14.1201,33.9C19.5601,39.34 28.4401,39.34 33.9001,33.88C39.3601,28.42 39.3801,19.56 33.9201,14.1Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"
+ android:fillType="evenOdd"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_power_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_power_on.xml
new file mode 100644
index 0000000..359ab84
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_power_on.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M26.0001,4H22.0001V24H26.0001V4ZM33.9201,14.1L36.7401,11.28C43.7601,18.32 43.7601,29.7 36.7401,36.72C29.7201,43.76 18.3201,43.76 11.3001,36.74C4.2601,29.7 4.2601,18.3 11.2801,11.28L14.1001,14.08C8.6401,19.54 8.6601,28.44 14.1201,33.9C19.5601,39.34 28.4401,39.34 33.9001,33.88C39.3601,28.42 39.3801,19.56 33.9201,14.1Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"
+ android:fillType="evenOdd"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_off.xml
new file mode 100644
index 0000000..dbe02ed
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_off.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M12,27C12,23.14 15.5,20 19.8,20H32.34L27.16,25.18L30,28L40,18L30,8L27.18,10.82L32.34,16H19.8C13.3,16 8,20.94 8,27C8,33.06 13.3,38 19.8,38H34V34H19.8C15.5,34 12,30.86 12,27Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_on.xml
new file mode 100644
index 0000000..55aa087
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_recirculate_on.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M12,27C12,23.14 15.5,20 19.8,20H32.34L27.16,25.18L30,28L40,18L30,8L27.18,10.82L32.34,16H19.8C13.3,16 8,20.94 8,27C8,33.06 13.3,38 19.8,38H34V34H19.8C15.5,34 12,30.86 12,27Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_off.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_off.xml
new file mode 100644
index 0000000..97698d7
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_off.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M33.6001,24.0001L40.8001,16.8001L33.6001,9.6001V14.4001H9.6001V19.2001H33.6001V24.0001Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+ <path
+ android:pathData="M14.4002,24L7.2002,31.2L14.4002,38.4V33.6H38.4002V28.8H14.4002V24Z"
+ android:fillColor="@color/hvac_off_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_on.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_on.xml
new file mode 100644
index 0000000..ab3b3ab
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/ic_sync_on.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/hvac_panel_icon_dimen"
+ android:height="@dimen/hvac_panel_icon_dimen"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:pathData="M33.6001,24.0001L40.8001,16.8001L33.6001,9.6001V14.4001H9.6001V19.2001H33.6001V24.0001Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+ <path
+ android:pathData="M14.4002,24L7.2002,31.2L14.4002,38.4V33.6H38.4002V28.8H14.4002V24Z"
+ android:fillColor="@color/hvac_on_icon_fill_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/nav_bar_button_background.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/nav_bar_button_background.xml
new file mode 100644
index 0000000..7c8b669
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/drawable/nav_bar_button_background.xml
@@ -0,0 +1,56 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <item android:state_selected="true">
+ <layer-list>
+ <item>
+ <ripple android:color="@color/car_ui_ripple_color">
+ <item>
+ <shape android:shape="rectangle">
+ <size android:width="@dimen/system_bar_button_size"
+ android:height="@dimen/system_bar_button_size"/>
+ <corners
+ android:radius="@dimen/system_bar_button_corner_radius"/>
+ <solid
+ android:color="?android:attr/colorAccent"/>
+ </shape>
+ </item>
+ </ripple>
+ </item>
+ <item android:drawable="@drawable/ic_minimize"
+ android:gravity="center"/>
+ </layer-list>
+ </item>
+ <item>
+ <layer-list>
+ <item>
+ <ripple android:color="@color/car_ui_ripple_color">
+ <item>
+ <shape android:shape="rectangle">
+ <size android:width="@dimen/system_bar_button_size"
+ android:height="@dimen/system_bar_button_size"/>
+ <corners
+ android:radius="@dimen/system_bar_button_corner_radius"/>
+ <solid
+ android:color="@color/car_nav_icon_background_color"/>
+ </shape>
+ </item>
+ </ripple>
+ </item>
+ </layer-list>
+ </item>
+</selector>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_bottom_system_bar.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_bottom_system_bar.xml
new file mode 100644
index 0000000..b5dcecb
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_bottom_system_bar.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<com.android.systemui.car.systembar.CarSystemBarView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:systemui="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/system_bar_background"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:id="@+id/nav_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <com.android.systemui.car.hvac.TemperatureControlView
+ android:id="@+id/driver_hvac"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_gravity="start"
+ android:gravity="start|center_vertical"
+ systemui:hvacAreaId="49">
+ <include layout="@layout/adjustable_temperature_view"/>
+ </com.android.systemui.car.hvac.TemperatureControlView>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:layoutDirection="ltr">
+
+ <com.android.systemui.car.systembar.DisplayAreaButton
+ android:id="@+id/grid_nav"
+ style="@style/SystemBarButton"
+ systemui:componentNames="com.android.car.carlauncher/.AppGridActivity"
+ systemui:icon="@drawable/car_ic_apps"
+ systemui:highlightWhenSelected="true"
+ systemui:toggleSelected="true"
+ systemui:intent="intent:#Intent;component=com.android.car.carlauncher/.AppGridActivity;launchFlags=0x24000000;end"
+ systemui:clearBackStack="true"/>
+
+ <com.android.systemui.car.systembar.DisplayAreaButton
+ android:id="@+id/standalone_notifications"
+ style="@style/SystemBarButton"
+ systemui:componentNames="com.android.car.notification/.CarNotificationCenterActivity"
+ systemui:packages="com.android.car.notification"
+ systemui:icon="@drawable/car_ic_notification"
+ systemui:highlightWhenSelected="true"
+ systemui:toggleSelected="true"
+ systemui:intent="intent:#Intent;component=com.android.car.notification/.CarNotificationCenterActivity;launchFlags=0x24000000;end"
+ systemui:longIntent="intent:#Intent;component=com.android.car.bugreport/.BugReportActivity;end"/>
+
+ <com.android.systemui.car.systembar.HvacButton
+ android:id="@+id/hvac"
+ style="@style/SystemBarButton"
+ systemui:icon="@drawable/car_ic_hvac"
+ systemui:highlightWhenSelected="true"
+ systemui:broadcast="true"/>
+
+ <com.android.systemui.car.systembar.AssitantButton
+ android:id="@+id/assist"
+ style="@style/SystemBarButton"
+ systemui:icon="@drawable/car_ic_mic"
+ systemui:highlightWhenSelected="true"
+ systemui:useDefaultAppIconForRole="true"/>
+ </LinearLayout>
+
+ <com.android.systemui.car.hvac.TemperatureControlView
+ android:id="@+id/passenger_hvac"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="end"
+ android:layout_weight="1"
+ android:gravity="end|center_vertical"
+ systemui:hvacAreaId="68">
+ <include layout="@layout/adjustable_temperature_view"/>
+ </com.android.systemui.car.hvac.TemperatureControlView>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/occlusion_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:layoutDirection="ltr"
+ android:visibility="gone">
+ <com.android.systemui.car.hvac.TemperatureControlView
+ android:id="@+id/driver_hvac"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_gravity="start"
+ android:gravity="start|center_vertical"
+ systemui:hvacAreaId="49">
+ <include layout="@layout/adjustable_temperature_view"/>
+ </com.android.systemui.car.hvac.TemperatureControlView>
+
+ <com.android.systemui.car.hvac.TemperatureControlView
+ android:id="@+id/passenger_hvac"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="end"
+ android:layout_weight="1"
+ android:gravity="end|center_vertical"
+ systemui:hvacAreaId="68">
+ <include layout="@layout/adjustable_temperature_view"/>
+ </com.android.systemui.car.hvac.TemperatureControlView>
+ </LinearLayout>
+</com.android.systemui.car.systembar.CarSystemBarView>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_top_system_bar.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_top_system_bar.xml
new file mode 100644
index 0000000..83b759b
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/car_top_system_bar.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<com.android.systemui.car.systembar.CarSystemBarView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:systemui="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/car_top_bar"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/status_bar_background"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <com.android.systemui.car.systembar.CarSystemBarButton
+ android:id="@+id/user_name"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_marginStart="@dimen/car_padding_3"
+ android:layout_gravity="start"
+ android:gravity="start"
+ android:orientation="horizontal"
+ systemui:intent="intent:#Intent;component=com.android.car.settings/.profiles.ProfileSwitcherActivity;launchFlags=0x24000000;end">
+ <ImageView
+ android:id="@+id/user_avatar"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:src="@drawable/car_ic_user_icon"
+ android:layout_marginEnd="@dimen/system_bar_user_icon_padding"/>
+ <TextView
+ android:id="@+id/user_name_text"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:textAppearance="@style/TextAppearance.SystemBar.Username"
+ android:maxLines="1"
+ android:maxLength="10"/>
+ </com.android.systemui.car.systembar.CarSystemBarButton>
+
+ <com.android.systemui.statusbar.policy.Clock
+ android:id="@+id/clock"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:paddingStart="@dimen/car_padding_2"
+ android:paddingEnd="@dimen/car_padding_2"
+ android:elevation="5dp"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.SystemBar.Clock"
+ systemui:amPmStyle="normal"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="end"
+ android:orientation="horizontal"
+ android:gravity="end">
+
+ <include layout="@layout/mic_privacy_chip"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_vertical"/>
+
+ <com.android.systemui.car.systembar.CarSystemBarButton
+ android:id="@+id/system_icon_area"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_marginEnd="@dimen/car_padding_3"
+ android:layout_gravity="end"
+ android:gravity="end"
+ systemui:intent="intent:#Intent;component=com.android.car.settings/.common.CarSettingActivities$HomepageActivity;launchFlags=0x24000000;end">
+
+ <com.android.systemui.statusbar.phone.StatusIconContainer
+ android:id="@+id/statusIcons"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="fitCenter"
+ android:gravity="center"
+ android:orientation="horizontal"/>
+
+ </com.android.systemui.car.systembar.CarSystemBarButton>
+ </LinearLayout>
+</com.android.systemui.car.systembar.CarSystemBarView>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/fan_direction.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/fan_direction.xml
new file mode 100644
index 0000000..1770796
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/fan_direction.xml
@@ -0,0 +1,110 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:systemui="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <com.android.systemui.car.hvac.toggle.HvacIntegerToggleButton
+ android:id="@+id/direction_face"
+ android:layout_width="@dimen/hvac_panel_airflow_button_width_1"
+ android:layout_height="@dimen/hvac_panel_button_dimen"
+ android:layout_weight="1"
+ android:background="@drawable/hvac_default_background"
+ systemui:hvacAreaId="117"
+ systemui:hvacPropertyId="356517121"
+ systemui:hvacToggleOffButtonDrawable="@drawable/ic_airflow_head_off"
+ systemui:hvacToggleOnButtonDrawable="@drawable/ic_airflow_head_on"
+ systemui:hvacTurnOffIfAutoOn="true"
+ systemui:onValue="1"
+ systemui:offValue="0"
+ systemui:preventToggleOff="true"/>
+ <View
+ android:layout_width="32dp"
+ android:layout_height="match_parent"/>
+ <com.android.systemui.car.hvac.toggle.HvacIntegerToggleButton
+ android:id="@+id/direction_floor"
+ android:layout_width="@dimen/hvac_panel_airflow_button_width_1"
+ android:layout_height="@dimen/hvac_panel_button_dimen"
+ android:layout_weight="1"
+ android:background="@drawable/hvac_default_background"
+ systemui:hvacAreaId="117"
+ systemui:hvacPropertyId="356517121"
+ systemui:hvacToggleOffButtonDrawable="@drawable/ic_airflow_feet_off"
+ systemui:hvacToggleOnButtonDrawable="@drawable/ic_airflow_feet_on"
+ systemui:hvacTurnOffIfAutoOn="true"
+ systemui:onValue="2"
+ systemui:offValue="0"
+ systemui:preventToggleOff="true"/>
+ <View
+ android:layout_width="32dp"
+ android:layout_height="match_parent"/>
+ <com.android.systemui.car.hvac.toggle.HvacIntegerToggleButton
+ android:id="@+id/direction_defrost_front_and_floor"
+ android:layout_width="@dimen/hvac_panel_airflow_button_width_1"
+ android:layout_height="@dimen/hvac_panel_button_dimen"
+ android:layout_weight="1"
+ android:background="@drawable/hvac_default_background"
+ systemui:hvacAreaId="117"
+ systemui:hvacPropertyId="356517121"
+ systemui:hvacToggleOffButtonDrawable="@drawable/ic_airflow_windshield_off"
+ systemui:hvacToggleOnButtonDrawable="@drawable/ic_airflow_windshield_on"
+ systemui:hvacTurnOffIfAutoOn="true"
+ systemui:onValue="6"
+ systemui:offValue="0"
+ systemui:preventToggleOff="true"/>
+ </LinearLayout>
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="32dp"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
+ android:id="@+id/direction_defrost_front"
+ android:layout_width="@dimen/hvac_panel_airflow_button_width_2"
+ android:layout_height="@dimen/hvac_panel_button_dimen"
+ android:layout_weight="1"
+ android:background="@drawable/hvac_default_background"
+ systemui:hvacAreaId="1"
+ systemui:hvacPropertyId="320865540"
+ systemui:hvacToggleOffButtonDrawable="@drawable/ic_defroster_windshield_off"
+ systemui:hvacToggleOnButtonDrawable="@drawable/ic_defroster_windshield_on"
+ systemui:hvacTurnOffIfAutoOn="true"
+ systemui:onValue="1"
+ systemui:offValue="0"/>
+ <View
+ android:layout_width="32dp"
+ android:layout_height="match_parent"/>
+ <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
+ android:id="@+id/direction_defrost_rear"
+ android:layout_width="@dimen/hvac_panel_airflow_button_width_2"
+ android:layout_height="@dimen/hvac_panel_button_dimen"
+ android:layout_weight="1"
+ android:background="@drawable/hvac_default_background"
+ systemui:hvacAreaId="2"
+ systemui:hvacPropertyId="320865540"
+ systemui:hvacToggleOffButtonDrawable="@drawable/ic_defroster_rear_off"
+ systemui:hvacToggleOnButtonDrawable="@drawable/ic_defroster_rear_on"
+ systemui:hvacTurnOffIfAutoOn="true"
+ systemui:onValue="1"
+ systemui:offValue="0"/>
+ </LinearLayout>
+</merge>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_container.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_container.xml
new file mode 100644
index 0000000..86df240
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_container.xml
@@ -0,0 +1,192 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<com.android.car.ui.FocusArea
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:systemui="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/hvac_panel_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/hvac_panel_full_expanded_height"
+ android:layout_gravity="bottom">
+ <com.android.systemui.car.hvac.HvacPanelView
+ android:id="@+id/hvac_panel"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/hvac_panel_bg">
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/top_guideline"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_begin="@dimen/hvac_panel_buttons_guideline"/>
+
+ <!-- ************************ -->
+ <!-- First group of buttons. -->
+ <!-- ************************ -->
+
+ <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
+ android:id="@+id/cooling_on_off"
+ android:layout_width="@dimen/hvac_panel_button_dimen"
+ android:layout_height="@dimen/hvac_panel_long_button_dimen"
+ android:layout_marginLeft="@dimen/hvac_panel_button_external_margin"
+ android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
+ android:background="@drawable/hvac_default_background"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/top_guideline"
+ systemui:hvacAreaId="117"
+ systemui:hvacPropertyId="354419973"
+ systemui:hvacToggleOffButtonDrawable="@drawable/ic_ac_off"
+ systemui:hvacToggleOnButtonDrawable="@drawable/ic_ac_on"
+ systemui:hvacTurnOffIfAutoOn="true"/>
+
+ <com.android.systemui.car.hvac.SeatTemperatureLevelButton
+ android:id="@+id/seat_heater_driver_on_off"
+ android:layout_width="@dimen/hvac_panel_button_dimen"
+ android:layout_height="@dimen/hvac_panel_long_button_dimen"
+ android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
+ android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
+ android:background="@drawable/hvac_heat_background"
+ app:layout_constraintRight_toRightOf="@+id/hvac_on_off"
+ app:layout_constraintTop_toBottomOf="@+id/top_guideline"
+ systemui:hvacAreaId="1"
+ systemui:seatTemperatureType="heating"
+ systemui:seatTemperatureIconDrawableList="@array/hvac_driver_seat_heat_icons"/>
+
+ <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
+ android:id="@+id/hvac_on_off"
+ android:layout_width="@dimen/hvac_panel_long_button_dimen"
+ android:layout_height="@dimen/hvac_panel_button_dimen"
+ android:layout_marginBottom="@dimen/hvac_panel_button_external_bottom_margin"
+ android:layout_marginLeft="@dimen/hvac_panel_button_external_margin"
+ android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
+ android:background="@drawable/hvac_default_background"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/cooling_on_off"
+ systemui:hvacAreaId="117"
+ systemui:hvacPropertyId="354419984"
+ systemui:hvacToggleOffButtonDrawable="@drawable/ic_power_off"
+ systemui:hvacToggleOnButtonDrawable="@drawable/ic_power_on"
+ systemui:hvacTurnOffIfPowerOff="false"/>
+
+ <!-- ************************ -->
+ <!-- Second group of buttons. -->
+ <!-- ************************ -->
+
+ <LinearLayout
+ android:id="@+id/airflow_group"
+ android:layout_width="@dimen/hvac_panel_slider_width"
+ android:layout_height="0dp"
+ android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
+ android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
+ android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
+ android:orientation="vertical"
+ app:layout_constraintLeft_toRightOf="@+id/seat_heater_driver_on_off"
+ app:layout_constraintRight_toLeftOf="@+id/seat_heater_passenger_on_off"
+ app:layout_constraintTop_toBottomOf="@+id/top_guideline">
+ <include layout="@layout/fan_direction"/>
+ </LinearLayout>
+
+ <com.android.systemui.car.hvac.custom.FanSpeedSeekBar
+ android:id="@+id/fan_speed_control"
+ android:layout_width="@dimen/hvac_panel_slider_width"
+ android:layout_height="@dimen/hvac_panel_button_dimen"
+ android:layout_marginBottom="@dimen/hvac_panel_button_external_bottom_margin"
+ android:layout_marginLeft="@dimen/hvac_panel_button_internal_margin"
+ android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
+ android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
+ android:progressDrawable="@drawable/fan_speed_seek_bar"
+ android:thumb="@drawable/fan_speed_seek_bar_thumb"
+ android:maxHeight="@dimen/hvac_panel_button_dimen"
+ android:minHeight="@dimen/hvac_panel_button_dimen"
+ android:background="@drawable/fan_speed_seek_bar_background"
+ android:splitTrack="false"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="@+id/airflow_group"
+ app:layout_constraintRight_toRightOf="@+id/airflow_group"
+ app:layout_constraintTop_toBottomOf="@+id/airflow_group"/>
+
+ <!-- ************************* -->
+ <!-- Third group of buttons. -->
+ <!-- ************************* -->
+
+ <com.android.systemui.car.hvac.SeatTemperatureLevelButton
+ android:id="@+id/seat_heater_passenger_on_off"
+ android:layout_width="@dimen/hvac_panel_button_dimen"
+ android:layout_height="@dimen/hvac_panel_long_button_dimen"
+ android:layout_marginRight="@dimen/hvac_panel_button_internal_margin"
+ android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
+ android:background="@drawable/hvac_heat_background"
+ app:layout_constraintLeft_toLeftOf="@+id/hvac_driver_passenger_sync"
+ app:layout_constraintTop_toBottomOf="@+id/top_guideline"
+ systemui:hvacAreaId="4"
+ systemui:seatTemperatureType="heating"
+ systemui:seatTemperatureIconDrawableList="@array/hvac_passenger_seat_heat_icons"/>
+
+ <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
+ android:id="@+id/recycle_air_on_off"
+ android:layout_width="@dimen/hvac_panel_button_dimen"
+ android:layout_height="@dimen/hvac_panel_button_dimen"
+ android:layout_marginRight="@dimen/hvac_panel_button_external_margin"
+ android:layout_marginTop="@dimen/hvac_panel_button_external_top_margin"
+ android:background="@drawable/hvac_default_background"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/top_guideline"
+ systemui:hvacAreaId="117"
+ systemui:hvacPropertyId="354419976"
+ systemui:hvacTurnOffIfAutoOn="true"
+ systemui:hvacToggleOnButtonDrawable="@drawable/ic_recirculate_on"
+ systemui:hvacToggleOffButtonDrawable="@drawable/ic_recirculate_off"/>
+
+ <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
+ android:id="@+id/auto_temperature_on_off"
+ android:layout_width="@dimen/hvac_panel_button_dimen"
+ android:layout_height="@dimen/hvac_panel_button_dimen"
+ android:layout_marginRight="@dimen/hvac_panel_button_external_margin"
+ android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
+ android:background="@drawable/hvac_default_background"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/recycle_air_on_off"
+ systemui:hvacAreaId="117"
+ systemui:hvacPropertyId="354419978"
+ systemui:hvacToggleOnButtonDrawable="@drawable/ic_auto_on"
+ systemui:hvacToggleOffButtonDrawable="@drawable/ic_auto_off"/>
+
+ <com.android.systemui.car.hvac.toggle.HvacBooleanToggleButton
+ android:id="@+id/hvac_driver_passenger_sync"
+ android:layout_width="@dimen/hvac_panel_long_button_dimen"
+ android:layout_height="@dimen/hvac_panel_button_dimen"
+ android:layout_marginBottom="@dimen/hvac_panel_button_external_bottom_margin"
+ android:layout_marginRight="@dimen/hvac_panel_button_external_margin"
+ android:layout_marginTop="@dimen/hvac_panel_button_internal_margin"
+ android:background="@drawable/hvac_default_background"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/auto_temperature_on_off"
+ systemui:hvacAreaId="117"
+ systemui:hvacPropertyId="354419977"
+ systemui:hvacToggleOffButtonDrawable="@drawable/ic_sync_off"
+ systemui:hvacToggleOnButtonDrawable="@drawable/ic_sync_on"
+ systemui:hvacTurnOffIfAutoOn="true"/>
+
+ <include
+ layout="@layout/hvac_panel_handle_bar"/>
+ </com.android.systemui.car.hvac.HvacPanelView>
+</com.android.car.ui.FocusArea>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_handle_bar.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_handle_bar.xml
new file mode 100644
index 0000000..62a1e81
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/hvac_panel_handle_bar.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <FrameLayout
+ android:id="@+id/handle_bar"
+ android:layout_width="@dimen/hvac_panel_handle_bar_container_width"
+ android:layout_height="@dimen/hvac_panel_handle_bar_container_height"
+ android:layout_gravity="center"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintHorizontal_chainStyle="packed">
+ <View
+ android:layout_width="@dimen/hvac_panel_handle_bar_width"
+ android:layout_height="@dimen/hvac_panel_handle_bar_height"
+ android:layout_marginTop="@dimen/hvac_panel_handle_bar_margin_top"
+ android:layout_gravity="top|center_horizontal"
+ android:background="@drawable/hvac_panel_handle_bar"/>
+ </FrameLayout>
+</merge>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/text_toast.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/text_toast.xml
new file mode 100644
index 0000000..87eb12c
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/layout/text_toast.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:maxWidth="@*android:dimen/toast_width"
+ android:background="@android:drawable/toast_frame"
+ android:elevation="@*android:dimen/toast_elevation"
+ android:paddingEnd="@dimen/toast_margin"
+ android:paddingTop="@dimen/toast_margin"
+ android:paddingBottom="@dimen/toast_margin"
+ android:paddingStart="@dimen/toast_margin"
+ android:layout_marginBottom="@dimen/toast_bottom_margin">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/toast_icon_dimen"
+ android:layout_height="@dimen/toast_icon_dimen"
+ android:layout_marginEnd="@dimen/toast_margin"/>
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@*android:style/TextAppearance.Toast"/>
+</LinearLayout>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/arrays.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/arrays.xml
new file mode 100644
index 0000000..828003f
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/arrays.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <array name="hvac_driver_seat_heat_icons">
+ <item>@drawable/ic_driver_seat_heat_off</item>
+ <item>@drawable/ic_driver_seat_heat_low</item>
+ <item>@drawable/ic_driver_seat_heat_high</item>
+ </array>
+ <array name="hvac_passenger_seat_heat_icons">
+ <item>@drawable/ic_passenger_seat_heat_off</item>
+ <item>@drawable/ic_passenger_seat_heat_low</item>
+ <item>@drawable/ic_passenger_seat_heat_high</item>
+ </array>
+ <array name="hvac_fan_speed_icons">
+ <item>@drawable/fan_speed_seek_bar_thumb_1</item>
+ <item>@drawable/fan_speed_seek_bar_thumb_2</item>
+ <item>@drawable/fan_speed_seek_bar_thumb_3</item>
+ <item>@drawable/fan_speed_seek_bar_thumb_4</item>
+ <item>@drawable/fan_speed_seek_bar_thumb_5</item>
+ <item>@drawable/fan_speed_seek_bar_thumb_6</item>
+ <item>@drawable/fan_speed_seek_bar_thumb_7</item>
+ <item>@drawable/fan_speed_seek_bar_thumb_8</item>
+ </array>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/attrs.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/attrs.xml
new file mode 100644
index 0000000..120905f
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/attrs.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <declare-styleable name="FanSpeedSeekBar">
+ <!-- List of drawables that will be shown when the seat heat level button is clicked.
+ This list should have exactly R.integer.hvac_seat_heat_level_count items.
+ The first item should have the "off" drawable. -->
+ <attr name="fanSpeedThumbIcons" format="reference"/>
+ </declare-styleable>
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/colors.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/colors.xml
new file mode 100644
index 0000000..3f62d35
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/colors.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="rear_view_camera_button_background">#CCFFFFFF</color>
+ <color name="rear_view_camera_exit_icon_color">@android:color/black</color>
+
+ <color name="car_nav_icon_fill_color">#FFFFFF</color>
+ <color name="car_nav_icon_background_color">#282A2D</color>
+ <color name="car_nav_minimize_icon_fill_color">@android:color/black</color>
+
+ <drawable name="system_bar_background">#000000</drawable>
+ <color name="system_bar_text_color">#E8EAED</color>
+ <color name="hvac_temperature_adjust_button_color">#282A2D</color>
+ <color name="hvac_temperature_decrease_arrow_color">#E8EAED</color>
+ <color name="hvac_temperature_increase_arrow_color">#E8EAED</color>
+
+ <color name="status_bar_background_color">#00000000</color>
+ <drawable name="status_bar_background">@color/status_bar_background_color</drawable>
+
+ <color name="hvac_background_color">#202124</color>
+ <color name="hvac_master_switch_color">@color/car_nav_icon_fill_color</color>
+ <color name="hvac_on_icon_fill_color">@android:color/black</color>
+ <color name="hvac_off_icon_fill_color">@android:color/white</color>
+ <color name="hvac_on_cooling_background_color">#6BF0FF</color>
+ <color name="hvac_on_heating_background_color">#EE675C</color>
+ <color name="hvac_on_background_color">#6BF0FF</color>
+ <color name="hvac_off_background_color">#3C4043</color>
+ <color name="hvac_panel_handle_bar_color">#3C4043</color>
+
+ <color name="dark_mode_icon_color_single_tone">@color/car_nav_icon_fill_color</color>
+ <color name="light_mode_icon_color_single_tone">@color/car_nav_icon_fill_color</color>
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/config.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/config.xml
new file mode 100644
index 0000000..75918eb
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/config.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <string name="config_systemUIFactoryComponent" translatable="false">
+ com.android.systemui.CarUiPortraitSystemUIFactory
+ </string>
+
+ <!-- Car System UI's OverlayViewsMediator.
+ Whenever a new class is added, make sure to also add that class to OverlayWindowModule. -->
+ <string-array name="config_carSystemUIOverlayViewsMediators" translatable="false">
+ <item>com.android.systemui.car.hvac.AutoDismissHvacPanelOverlayViewMediator</item>
+ <item>com.android.systemui.car.keyguard.CarKeyguardViewMediator</item>
+ <item>com.android.systemui.car.userswitcher.FullscreenUserSwitcherViewMediator</item>
+ <item>com.android.systemui.car.userswitcher.UserSwitchTransitionViewMediator</item>
+ </string-array>
+
+ <integer name="hvac_num_fan_speeds">8</integer>
+
+ <integer name="config_hvacAutoDismissDurationMs">15000</integer>
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/dimens.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/dimens.xml
new file mode 100644
index 0000000..f900e57
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/dimens.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <!-- dimensions for rear view camera -->
+ <dimen name="rear_view_camera_width">1020dp</dimen>
+ <dimen name="rear_view_camera_height">720dp</dimen>
+
+ <dimen name="rear_view_camera_exit_button_width">48dp</dimen>
+ <dimen name="rear_view_camera_exit_button_height">48dp</dimen>
+ <dimen name="rear_view_camera_exit_button_margin">24dp</dimen>
+ <dimen name="rear_view_camera_exit_icon_width">26dp</dimen>
+ <dimen name="rear_view_camera_exit_icon_height">26dp</dimen>
+
+ <dimen name="hvac_container_padding">24dp</dimen>
+ <dimen name="hvac_temperature_text_size">56sp</dimen>
+ <dimen name="hvac_temperature_text_padding">12dp</dimen>
+ <dimen name="hvac_temperature_button_size">64dp</dimen>
+
+ <dimen name="system_bar_icon_drawing_size">56dp</dimen>
+ <dimen name="system_bar_button_size">88dp</dimen>
+ <!-- Margin between the system bar buttons -->
+ <dimen name="system_bar_button_margin">40dp</dimen>
+ <!-- Padding between the system bar button and the icon within it -->
+ <dimen name="system_bar_button_padding">16dp</dimen>
+ <dimen name="system_bar_button_corner_radius">24dp</dimen>
+
+ <dimen name="system_bar_user_icon_drawing_size">44dp</dimen>
+ <dimen name="status_bar_system_icon_spacing">32dp</dimen>
+
+ <dimen name="system_bar_minimize_icon_height">17dp</dimen>
+ <dimen name="system_bar_minimize_icon_width">28dp</dimen>
+
+ <dimen name="hvac_panel_handle_bar_container_height">64dp</dimen>
+ <dimen name="hvac_panel_handle_bar_container_width">728dp</dimen>
+ <dimen name="hvac_panel_handle_bar_height">6dp</dimen>
+ <dimen name="hvac_panel_handle_bar_margin_top">17dp</dimen>
+ <dimen name="hvac_panel_handle_bar_width">120dp</dimen>
+ <dimen name="hvac_panel_bg_radius">24dp</dimen>
+ <dimen name="hvac_panel_off_button_radius">24dp</dimen>
+ <dimen name="hvac_panel_on_button_radius">24dp</dimen>
+ <dimen name="hvac_panel_seek_bar_radius">44dp</dimen>
+ <dimen name="hvac_panel_full_expanded_height">456dp</dimen>
+ <dimen name="hvac_panel_title_margin">36dp</dimen>
+ <dimen name="hvac_panel_buttons_guideline">@dimen/hvac_panel_handle_bar_container_height</dimen>
+
+ <dimen name="hvac_panel_icon_dimen">48dp</dimen>
+ <dimen name="hvac_panel_tall_icon_dimen">108dp</dimen>
+ <dimen name="hvac_panel_wide_icon_dimen">96dp</dimen>
+
+ <dimen name="hvac_panel_button_dimen">88dp</dimen>
+ <dimen name="hvac_panel_long_button_dimen">208dp</dimen>
+ <dimen name="hvac_panel_airflow_button_width_1">210dp</dimen>
+ <dimen name="hvac_panel_airflow_button_width_2">332dp</dimen>
+ <dimen name="hvac_panel_slider_width">696dp</dimen>
+
+ <dimen name="hvac_panel_button_external_margin">24dp</dimen>
+ <dimen name="hvac_panel_button_external_top_margin">16dp</dimen>
+ <dimen name="hvac_panel_button_external_bottom_margin">48dp</dimen>
+ <dimen name="hvac_panel_button_internal_margin">32dp</dimen>
+
+ <item name="hvac_heat_or_cool_off_alpha" format="float" type="dimen">0.3</item>
+
+ <dimen name="toast_margin">24dp</dimen>
+ <dimen name="toast_icon_dimen">48dp</dimen>
+ <dimen name="toast_bottom_margin">32dp</dimen>
+
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/integers.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/integers.xml
new file mode 100644
index 0000000..282373c
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/integers.xml
@@ -0,0 +1,19 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <integer name="hvac_seat_heat_level_count">3</integer>
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/strings.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/strings.xml
new file mode 100644
index 0000000..c9cff07
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Format for temperature in the temperature control view (No decimal) -->
+ <string name="hvac_temperature_format_fahrenheit" translatable="false">%.0f</string>
+
+ <!-- HVAC panel header [CHAR LIMIT=40]-->
+ <string name="hvac_panel_header">Comfort controls</string>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/styles.xml b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/styles.xml
new file mode 100644
index 0000000..b5f46f8
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/res/values/styles.xml
@@ -0,0 +1,40 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!--
+ Note on selected/unselected icons:
+ The icon is always tinted with @color/car_nav_icon_fill_color_selected in @layout/car_system_bar_button
+ Unselected: keep this behavior so all icons have consistent color (eg. tint a multi-colored default app icon)
+ Selected: set selected alpha 0, making icon transparent. Use state list nav_bar_button_background to show selected icon (in addition to background).
+ -->
+ <style name="SystemBarButton">
+ <item name="android:layout_width">@dimen/system_bar_button_size</item>
+ <item name="android:layout_height">@dimen/system_bar_button_size</item>
+ <item name="android:background">@drawable/nav_bar_button_background</item>
+ <item name="android:gravity">center</item>
+ <item name="unselectedAlpha">1.0</item>
+ <item name="selectedAlpha">0</item>
+ </style>
+
+ <style name="TextAppearance.SystemBar.Username"
+ parent="@android:style/TextAppearance.DeviceDefault">
+ <item name="android:textSize">@dimen/car_body1_size</item>
+ <item name="android:textColor">@color/system_bar_text_color</item>
+ </style>
+</resources>
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitGlobalRootComponent.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitGlobalRootComponent.java
new file mode 100644
index 0000000..af85885
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitGlobalRootComponent.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 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.systemui;
+
+import com.android.systemui.dagger.GlobalModule;
+import com.android.systemui.dagger.WMModule;
+
+import javax.inject.Singleton;
+
+import dagger.Component;
+
+/**
+ * Root Component for Dagger injection for CarUiPortraitSystemUI
+ */
+@Singleton
+@Component(
+ modules = {
+ GlobalModule.class,
+ CarUiPortraitSysUIComponentModule.class,
+ WMModule.class
+ })
+interface CarUiPortraitGlobalRootComponent extends CarGlobalRootComponent {
+ @Component.Builder
+ interface Builder extends CarGlobalRootComponent.Builder {
+ CarUiPortraitGlobalRootComponent build();
+ }
+
+ @Override
+ CarUiPortraitSysUIComponent.Builder getSysUIComponent();
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponent.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponent.java
new file mode 100644
index 0000000..25e488f
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponent.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.systemui;
+
+import com.android.systemui.dagger.DependencyProvider;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.SystemUIModule;
+
+import dagger.Subcomponent;
+
+/**
+ * Dagger Subcomponent for Core SysUI.
+ */
+@SysUISingleton
+@Subcomponent(modules = {
+ CarComponentBinder.class,
+ DependencyProvider.class,
+ SystemUIModule.class,
+ CarSystemUIModule.class,
+ CarUiPortraitSystemUIBinder.class})
+public interface CarUiPortraitSysUIComponent extends CarSysUIComponent {
+ /**
+ * Builder for a CarSysUIComponent.
+ */
+ @Subcomponent.Builder
+ interface Builder extends CarSysUIComponent.Builder {
+ CarUiPortraitSysUIComponent build();
+ }
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponentModule.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponentModule.java
new file mode 100644
index 0000000..196c917
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSysUIComponentModule.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 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.systemui;
+
+import dagger.Binds;
+import dagger.Module;
+
+/**
+ * Dagger module for including the CarUiPortraitSysUIComponent.
+ *
+ * TODO(b/162923491): Remove or otherwise refactor this module. This is a stop gap.
+ */
+@Module(subcomponents = {CarUiPortraitSysUIComponent.class})
+public abstract class CarUiPortraitSysUIComponentModule {
+ @Binds
+ abstract CarGlobalRootComponent bindSystemUIRootComponent(
+ CarUiPortraitGlobalRootComponent systemUIRootComponent);
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIBinder.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIBinder.java
new file mode 100644
index 0000000..255d70e
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIBinder.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2021 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.systemui;
+
+import com.android.systemui.car.window.ExtendedOverlayWindowModule;
+
+import dagger.Module;
+
+/** Binder for AAECarSystemUI specific {@link SystemUI} modules and components. */
+@Module(includes = {ExtendedOverlayWindowModule.class})
+abstract class CarUiPortraitSystemUIBinder extends CarSystemUIBinder {
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIFactory.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIFactory.java
new file mode 100644
index 0000000..ef75f48
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/CarUiPortraitSystemUIFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2021 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.systemui;
+
+import android.content.Context;
+
+import com.android.systemui.dagger.GlobalRootComponent;
+
+/**
+ * Class factory to provide AAECarSystemUI specific SystemUI components.
+ */
+public class CarUiPortraitSystemUIFactory extends CarSystemUIFactory {
+ @Override
+ protected GlobalRootComponent buildGlobalRootComponent(Context context) {
+ return DaggerCarUiPortraitGlobalRootComponent.builder().context(context).build();
+ }
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewController.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewController.java
new file mode 100644
index 0000000..bf0b34d
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewController.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 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.systemui.car.hvac;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Handler;
+
+import com.android.systemui.R;
+import com.android.systemui.car.CarDeviceProvisionedController;
+import com.android.systemui.car.window.OverlayViewGlobalStateController;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.wm.shell.animation.FlingAnimationUtils;
+
+import javax.inject.Inject;
+
+/**
+ * An extension of {@link HvacPanelOverlayViewController} which auto dismisses the panel if there
+ * is no activity for some configured amount of time.
+ */
+@SysUISingleton
+public class AutoDismissHvacPanelOverlayViewController extends HvacPanelOverlayViewController {
+
+ private final Resources mResources;
+ private final Handler mHandler;
+
+ private HvacPanelView mHvacPanelView;
+ private int mAutoDismissDurationMs;
+
+ private final Runnable mAutoDismiss = () -> {
+ if (isPanelExpanded()) {
+ toggle();
+ }
+ };
+
+ @Inject
+ public AutoDismissHvacPanelOverlayViewController(Context context,
+ @Main Resources resources,
+ HvacController hvacController,
+ OverlayViewGlobalStateController overlayViewGlobalStateController,
+ FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
+ CarDeviceProvisionedController carDeviceProvisionedController,
+ @Main Handler handler) {
+ super(context, resources, hvacController, overlayViewGlobalStateController,
+ flingAnimationUtilsBuilder, carDeviceProvisionedController);
+ mResources = resources;
+ mHandler = handler;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mAutoDismissDurationMs = mResources.getInteger(R.integer.config_hvacAutoDismissDurationMs);
+
+ mHvacPanelView = getLayout().findViewById(R.id.hvac_panel);
+ mHvacPanelView.setMotionEventHandler(event -> {
+ if (!isPanelExpanded()) {
+ return;
+ }
+
+ mHandler.removeCallbacks(mAutoDismiss);
+ mHandler.postDelayed(mAutoDismiss, mAutoDismissDurationMs);
+ });
+ }
+
+ @Override
+ protected void onAnimateExpandPanel() {
+ super.onAnimateExpandPanel();
+
+ mHandler.postDelayed(mAutoDismiss, mAutoDismissDurationMs);
+ }
+
+ @Override
+ protected void onAnimateCollapsePanel() {
+ super.onAnimateCollapsePanel();
+
+ mHandler.removeCallbacks(mAutoDismiss);
+ }
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewMediator.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewMediator.java
new file mode 100644
index 0000000..ce30c43
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/AutoDismissHvacPanelOverlayViewMediator.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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.systemui.car.hvac;
+
+import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.car.systembar.CarSystemBarController;
+import com.android.systemui.dagger.SysUISingleton;
+
+import javax.inject.Inject;
+
+/**
+ * Instance of {@link HvacPanelOverlayViewMediator} which uses {@link
+ * AutoDismissHvacPanelOverlayViewController}.
+ */
+@SysUISingleton
+public class AutoDismissHvacPanelOverlayViewMediator extends HvacPanelOverlayViewMediator {
+
+ @Inject
+ public AutoDismissHvacPanelOverlayViewMediator(
+ CarSystemBarController carSystemBarController,
+ AutoDismissHvacPanelOverlayViewController hvacPanelOverlayViewController,
+ BroadcastDispatcher broadcastDispatcher) {
+ super(carSystemBarController, hvacPanelOverlayViewController, broadcastDispatcher);
+ }
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/custom/FanSpeedSeekBar.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/custom/FanSpeedSeekBar.java
new file mode 100644
index 0000000..bc7f4f2
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/hvac/custom/FanSpeedSeekBar.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2021 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.systemui.car.hvac.custom;
+
+import static android.car.VehiclePropertyIds.HVAC_AUTO_ON;
+import static android.car.VehiclePropertyIds.HVAC_FAN_SPEED;
+import static android.car.VehiclePropertyIds.HVAC_POWER_ON;
+
+import android.car.hardware.CarPropertyValue;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.widget.SeekBar;
+
+import androidx.annotation.ArrayRes;
+
+import com.android.systemui.R;
+import com.android.systemui.car.hvac.HvacController;
+import com.android.systemui.car.hvac.HvacPropertySetter;
+import com.android.systemui.car.hvac.HvacView;
+
+/** Custom seek bar to control fan speed. */
+public class FanSpeedSeekBar extends SeekBar implements HvacView {
+
+ private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG;
+ private static final String TAG = "FanSpeedSeekBar";
+
+ private final SparseArray<Drawable> mIcons = new SparseArray<>();
+
+ private HvacPropertySetter mHvacPropertySetter;
+ private int mHvacGlobalAreaId;
+
+ private boolean mPowerOn;
+ private boolean mAutoOn;
+
+ private float mOnAlpha;
+ private float mOffAlpha;
+
+ private final OnSeekBarChangeListener mSeekBarChangeListener = new OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ int prevProgress = getProgress();
+ // Limit updates to the hvac property to be only those that come from the user in order
+ // to avoid an infinite loop.
+ if (shouldAllowControl() && fromUser && progress == prevProgress) {
+ mHvacPropertySetter.setHvacProperty(HVAC_FAN_SPEED, getAreaId(), progress);
+ } else if (progress != prevProgress) {
+ // There is an edge case with seek bar touch handling that can lead to an
+ // inconsistent state of the progress state and UI. We need to set the progress to
+ // a different value before setting it to the value we expect in order to ensure
+ // that the update doesn't get dropped.
+ setProgress(progress);
+ setProgress(prevProgress);
+ updateUI();
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // no-op.
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // no-op.
+ }
+ };
+
+ public FanSpeedSeekBar(Context context) {
+ super(context);
+ init(null);
+ }
+
+ public FanSpeedSeekBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(attrs);
+ }
+
+ public FanSpeedSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(attrs);
+ }
+
+ public FanSpeedSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init(attrs);
+ }
+
+ private void init(AttributeSet attrs) {
+ int speeds = mContext.getResources().getInteger(R.integer.hvac_num_fan_speeds);
+ if (speeds < 1) {
+ throw new IllegalArgumentException("The nuer of fan speeds should be > 1");
+ }
+
+ setMin(1);
+ incrementProgressBy(1);
+ setMax(speeds);
+ int thumbRadius = mContext.getResources().getDimensionPixelSize(
+ R.dimen.hvac_panel_seek_bar_radius);
+ setPadding(thumbRadius, 0, thumbRadius, 0);
+ mHvacGlobalAreaId = mContext.getResources().getInteger(R.integer.hvac_global_area_id);
+
+ mOnAlpha = mContext.getResources().getFloat(R.dimen.hvac_turned_on_alpha);
+ mOffAlpha = mContext.getResources().getFloat(R.dimen.hvac_turned_off_alpha);
+
+ if (attrs == null) {
+ return;
+ }
+
+ // Get fan speed thumb drawables.
+ TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.FanSpeedSeekBar);
+ @ArrayRes int drawableListRes = typedArray.getResourceId(
+ R.styleable.FanSpeedSeekBar_fanSpeedThumbIcons,
+ R.array.hvac_fan_speed_icons);
+
+ TypedArray fanSpeedThumbIcons = mContext.getResources().obtainTypedArray(drawableListRes);
+ if (fanSpeedThumbIcons.length() != speeds) {
+ throw new IllegalArgumentException(
+ "R.styeable.SeatHeatLevelButton_seatHeaterIconDrawableList should have the "
+ + "same length as R.integer.hvac_seat_heat_level_count");
+ }
+
+ for (int i = 0; i < speeds; i++) {
+ mIcons.set(i + 1, fanSpeedThumbIcons.getDrawable(i));
+ }
+ fanSpeedThumbIcons.recycle();
+ typedArray.recycle();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ setOnSeekBarChangeListener(mSeekBarChangeListener);
+ }
+
+ @Override
+ public void setHvacPropertySetter(HvacPropertySetter hvacPropertySetter) {
+ mHvacPropertySetter = hvacPropertySetter;
+ }
+
+ @Override
+ public void onHvacTemperatureUnitChanged(boolean usesFahrenheit) {
+ // no-op.
+ }
+
+ @Override
+ public void onPropertyChanged(CarPropertyValue value) {
+ if (value == null) {
+ if (DEBUG) {
+ Log.w(TAG, "onPropertyChanged: received null value");
+ }
+ return;
+ }
+
+ if (DEBUG) {
+ Log.w(TAG, "onPropertyChanged: property id: " + value.getPropertyId());
+ Log.w(TAG, "onPropertyChanged: area id: " + value.getAreaId());
+ Log.w(TAG, "onPropertyChanged: value: " + value.getValue());
+ }
+
+ if (value.getPropertyId() == HVAC_FAN_SPEED) {
+ int level = (int) value.getValue();
+ setProgress(level, /* animate= */ true);
+ }
+
+ if (value.getPropertyId() == HVAC_POWER_ON) {
+ mPowerOn = (boolean) value.getValue();
+ }
+
+ if (value.getPropertyId() == HVAC_AUTO_ON) {
+ mAutoOn = (boolean) value.getValue();
+ }
+
+ updateUI();
+ }
+
+ @Override
+ public @HvacController.HvacProperty Integer getHvacPropertyToView() {
+ return HVAC_FAN_SPEED;
+ }
+
+ @Override
+ public @HvacController.AreaId Integer getAreaId() {
+ return mHvacGlobalAreaId;
+ }
+
+ private void updateUI() {
+ int progress = getProgress();
+ setThumb(mIcons.get(progress));
+ setSelected(progress > 0);
+ setAlpha(shouldAllowControl() ? mOnAlpha : mOffAlpha);
+ // Steal touch events if shouldn't allow control.
+ setOnTouchListener(shouldAllowControl() ? null : (v, event) -> true);
+ }
+
+ private boolean shouldAllowControl() {
+ return mPowerOn && !mAutoOn;
+ }
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/systembar/DisplayAreaButton.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/systembar/DisplayAreaButton.java
new file mode 100644
index 0000000..1412903
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/systembar/DisplayAreaButton.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 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.systemui.car.systembar;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.AttributeSet;
+
+/** A CarSystemBarButton that controls a display area. */
+public class DisplayAreaButton extends CarSystemBarButton {
+
+ // TODO(b/194334719): Remove when display area logic is moved into systemui
+ private static final String DISPLAY_AREA_VISIBILITY_CHANGED =
+ "com.android.car.carlauncher.displayarea.DISPLAY_AREA_VISIBILITY_CHANGED";
+ private static final String INTENT_EXTRA_IS_DISPLAY_AREA_VISIBLE =
+ "EXTRA_IS_DISPLAY_AREA_VISIBLE";
+
+ /**
+ * A broadcast receiver to listen when the display area is closed via swipe.
+ * When the display area logic is moved from launcher into system ui, the DisplayAreaButton
+ * should be notified of changes in the panel's visibility directly, rather than using a
+ * broadcast.
+ */
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ boolean isDisplayAreaVisible = intent.getBooleanExtra(
+ INTENT_EXTRA_IS_DISPLAY_AREA_VISIBLE, /* defaultValue= */ true);
+ if (getSelected() && !isDisplayAreaVisible) {
+ context.getMainExecutor().execute(() -> setSelected(/* selected= */ false));
+ }
+ }
+ };
+
+ public DisplayAreaButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ context.registerReceiver(mBroadcastReceiver,
+ new IntentFilter(DISPLAY_AREA_VISIBILITY_CHANGED));
+ }
+}
diff --git a/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/window/ExtendedOverlayWindowModule.java b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/window/ExtendedOverlayWindowModule.java
new file mode 100644
index 0000000..21f1500
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/CarUiPortraitSystemUI/src/com/android/systemui/car/window/ExtendedOverlayWindowModule.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 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.systemui.car.window;
+
+import com.android.systemui.car.hvac.AutoDismissHvacPanelOverlayViewMediator;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.multibindings.ClassKey;
+import dagger.multibindings.IntoMap;
+
+/** Lists additional {@link OverlayViewMediator} that apply to the CarUiPortraitSystemUI. */
+@Module
+public abstract class ExtendedOverlayWindowModule {
+
+ /** Injects RearViewCameraViewMediator. */
+ @Binds
+ @IntoMap
+ @ClassKey(AutoDismissHvacPanelOverlayViewMediator.class)
+ public abstract OverlayViewMediator bindAutoDismissHvacPanelViewMediator(
+ AutoDismissHvacPanelOverlayViewMediator overlayViewsMediator);
+}
diff --git a/car_product/car_ui_portrait/apps/HideApps/Android.mk b/car_product/car_ui_portrait/apps/HideApps/Android.mk
new file mode 100644
index 0000000..74cdcaa
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/HideApps/Android.mk
@@ -0,0 +1,34 @@
+#
+# Copyright (C) 2021 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+LOCAL_PACKAGE_NAME := CarUiPortraitHideApps
+LOCAL_SDK_VERSION := current
+
+# Add packages here to remove them from the build
+LOCAL_OVERRIDES_PACKAGES := \
+ CarRotaryController \
+ RotaryPlayground \
+ RotaryIME \
+ CarRotaryImeRRO \
+
+LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
+LOCAL_LICENSE_CONDITIONS := notice
+include $(BUILD_PACKAGE)
diff --git a/car_product/car_ui_portrait/apps/HideApps/AndroidManifest.xml b/car_product/car_ui_portrait/apps/HideApps/AndroidManifest.xml
new file mode 100644
index 0000000..a234af5
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/HideApps/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.caruiportrait.hideapps">
+ <application
+ android:allowBackup="false"
+ android:debuggable="false"
+ android:label="CarUiPortraitHideApps">
+ </application>
+</manifest>
diff --git a/car_product/car_ui_portrait/apps/car_ui_portrait_apps.mk b/car_product/car_ui_portrait/apps/car_ui_portrait_apps.mk
new file mode 100644
index 0000000..8e01ccc
--- /dev/null
+++ b/car_product/car_ui_portrait/apps/car_ui_portrait_apps.mk
@@ -0,0 +1,26 @@
+#
+# Copyright (C) 2021 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.
+#
+
+# All apps that should be included in CarUiPortrait builds
+PRODUCT_PACKAGES += \
+ CarUiPortraitSettings \
+ CarUiPortraitSystemUI \
+ CarNotification \
+ PaintBooth \
+
+# All apps to be excluded in car_ui_portrait builds should be specified as part of CarUiPortraitHideApps.
+PRODUCT_PACKAGES += \
+ CarUiPortraitHideApps
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/bootanimation/README b/car_product/car_ui_portrait/bootanimation/README
new file mode 100644
index 0000000..2ce687f
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/README
@@ -0,0 +1,68 @@
+# Boot Animation
+
+The boot animation format is described in full by [FORMAT.md](https://android.googlesource.com/platform/frameworks/base/+/master/cmds/bootanimation/FORMAT.md)
+
+## Command to create the zip:
+zip -0qry -i \*.txt \*.png \*.wav @ ../bootanimation.zip *.txt part*
+
+## zipfile layout
+
+The `bootanimation.zip` archive file includes:
+
+ desc.txt - a text file
+ part0 \
+ part1 \ directories full of PNG frames
+ ... /
+ partN /
+
+## desc.txt format
+
+The first line defines the general parameters of the animation:
+
+ WIDTH HEIGHT FPS [PROGRESS]
+
+ * **WIDTH:** animation width (pixels)
+ * **HEIGHT:** animation height (pixels)
+ * **FPS:** frames per second, e.g. 60
+ * **PROGRESS:** whether to show a progress percentage on the last part
+ + The percentage will be displayed with an x-coordinate of 'c', and a
+ y-coordinate set to 1/3 of the animation height.
+
+It is followed by a number of rows of the form:
+
+ TYPE COUNT PAUSE PATH [FADE [#RGBHEX [CLOCK1 [CLOCK2]]]]
+
+ * **TYPE:** a single char indicating what type of animation segment this is:
+ + `p` -- this part will play unless interrupted by the end of the boot
+ + `c` -- this part will play to completion, no matter what
+ + `f` -- same as `p` but in addition the specified number of frames is being faded out while
+ continue playing. Only the first interrupted `f` part is faded out, other subsequent `f`
+ parts are skipped
+ * **COUNT:** how many times to play the animation, or 0 to loop forever until boot is complete
+ * **PAUSE:** number of FRAMES to delay after this part ends
+ * **PATH:** directory in which to find the frames for this part (e.g. `part0`)
+ * **FADE:** _(ONLY FOR `f` TYPE)_ number of frames to fade out when interrupted where `0` means
+ _immediately_ which makes `f ... 0` behave like `p` and doesn't count it as a fading
+ part
+ * **RGBHEX:** _(OPTIONAL)_ a background color, specified as `#RRGGBB`
+ * **CLOCK1, CLOCK2:** _(OPTIONAL)_ the coordinates at which to draw the current time (for watches):
+ + If only `CLOCK1` is provided it is the y-coordinate of the clock and the x-coordinate
+ defaults to `c`
+ + If both `CLOCK1` and `CLOCK2` are provided then `CLOCK1` is the x-coordinate and `CLOCK2` is
+ the y-coodinate
+ + Values can be either a positive integer, a negative integer, or `c`
+ - `c` -- will centre the text
+ - `n` -- will position the text n pixels from the start; left edge for x-axis, bottom edge
+ for y-axis
+ - `-n` -- will position the text n pixels from the end; right edge for x-axis, top edge
+ for y-axis
+ - Examples:
+ * `-24` or `c -24` will position the text 24 pixels from the top of the screen,
+ centred horizontally
+ * `16 c` will position the text 16 pixels from the left of the screen, centred
+ vertically
+ * `-32 32` will position the text such that the bottom right corner is 32 pixels above
+ and 32 pixels left of the edges of the screen
+
+There is also a special TYPE, `$SYSTEM`, that loads `/system/media/bootanimation.zip`
+and plays that.
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/bootanimation/bootanimation.zip b/car_product/car_ui_portrait/bootanimation/bootanimation.zip
new file mode 100644
index 0000000..2843730
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/bootanimation.zip
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/desc.txt b/car_product/car_ui_portrait/bootanimation/parts/desc.txt
new file mode 100644
index 0000000..abfa895
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/desc.txt
@@ -0,0 +1,3 @@
+1224 2175 24
+c 1 0 part0
+p 0 0 part1
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00023.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00023.png
new file mode 100644
index 0000000..29499a6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00023.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00024.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00024.png
new file mode 100644
index 0000000..29499a6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00024.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00025.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00025.png
new file mode 100644
index 0000000..7d1314e
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00025.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00026.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00026.png
new file mode 100644
index 0000000..f50bbda
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00026.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00027.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00027.png
new file mode 100644
index 0000000..2a90baf
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00027.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00028.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00028.png
new file mode 100644
index 0000000..02ed17b
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00028.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00029.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00029.png
new file mode 100644
index 0000000..8a22023
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00029.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00030.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00030.png
new file mode 100644
index 0000000..6cf674b
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00030.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00031.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00031.png
new file mode 100644
index 0000000..482193d
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00031.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00032.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00032.png
new file mode 100644
index 0000000..aa224f8
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00032.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00033.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00033.png
new file mode 100644
index 0000000..4cd9877
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00033.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00034.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00034.png
new file mode 100644
index 0000000..af5ab61
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00034.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00035.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00035.png
new file mode 100644
index 0000000..691c0df
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00035.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00036.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00036.png
new file mode 100644
index 0000000..ec45285
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00036.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00037.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00037.png
new file mode 100644
index 0000000..8962084
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00037.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00038.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00038.png
new file mode 100644
index 0000000..1640075
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00038.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00039.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00039.png
new file mode 100644
index 0000000..130924a
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00039.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00040.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00040.png
new file mode 100644
index 0000000..19802da
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00040.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00041.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00041.png
new file mode 100644
index 0000000..dfd77a0
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00041.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00042.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00042.png
new file mode 100644
index 0000000..c97189e
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00042.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00043.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00043.png
new file mode 100644
index 0000000..664c312
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00043.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00044.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00044.png
new file mode 100644
index 0000000..275e75c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00044.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00045.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00045.png
new file mode 100644
index 0000000..4a98b77
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00045.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00046.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00046.png
new file mode 100644
index 0000000..f62b3bc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00046.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00047.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00047.png
new file mode 100644
index 0000000..23915b9
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00047.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00048.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00048.png
new file mode 100644
index 0000000..ad64499
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00048.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00049.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00049.png
new file mode 100644
index 0000000..b259965
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00049.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00050.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00050.png
new file mode 100644
index 0000000..7e17a93
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00050.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00051.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00051.png
new file mode 100644
index 0000000..eaa32b6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00051.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00052.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00052.png
new file mode 100644
index 0000000..c683cb8
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00052.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00053.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00053.png
new file mode 100644
index 0000000..d7eb3fd
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00053.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00054.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00054.png
new file mode 100644
index 0000000..aaeb6db
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00054.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00055.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00055.png
new file mode 100644
index 0000000..8b577e4
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00055.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00056.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00056.png
new file mode 100644
index 0000000..953cd95
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00056.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00057.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00057.png
new file mode 100644
index 0000000..52e5ec3
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00057.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00058.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00058.png
new file mode 100644
index 0000000..681a0f9
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00058.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00059.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00059.png
new file mode 100644
index 0000000..83840b6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00059.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00060.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00060.png
new file mode 100644
index 0000000..8d8eb51
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00060.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00061.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00061.png
new file mode 100644
index 0000000..f4abe38
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00061.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00062.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00062.png
new file mode 100644
index 0000000..d7f9e56
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00062.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00063.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00063.png
new file mode 100644
index 0000000..42d0bd0
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00063.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00064.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00064.png
new file mode 100644
index 0000000..d52b3e6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00064.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00065.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00065.png
new file mode 100644
index 0000000..16c454e
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00065.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00066.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00066.png
new file mode 100644
index 0000000..e1c0b1d
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00066.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00067.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00067.png
new file mode 100644
index 0000000..15935ff
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00067.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00068.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00068.png
new file mode 100644
index 0000000..981883c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00068.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00069.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00069.png
new file mode 100644
index 0000000..43f1c9f
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00069.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00070.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00070.png
new file mode 100644
index 0000000..addd8c1
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00070.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00071.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00071.png
new file mode 100644
index 0000000..0d964e9
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00071.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00072.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00072.png
new file mode 100644
index 0000000..e1ea8ce
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00072.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00073.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00073.png
new file mode 100644
index 0000000..4314159
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00073.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00074.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00074.png
new file mode 100644
index 0000000..c9c9543
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00074.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00075.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00075.png
new file mode 100644
index 0000000..3db4b49
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00075.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00076.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00076.png
new file mode 100644
index 0000000..cf76b15
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00076.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00077.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00077.png
new file mode 100644
index 0000000..45213e3
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00077.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00078.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00078.png
new file mode 100644
index 0000000..86c56a7
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00078.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00079.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00079.png
new file mode 100644
index 0000000..82c4f55
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00079.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00080.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00080.png
new file mode 100644
index 0000000..d4d8deb
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00080.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00081.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00081.png
new file mode 100644
index 0000000..5e50025
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00081.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00082.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00082.png
new file mode 100644
index 0000000..b840d1d
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00082.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00083.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00083.png
new file mode 100644
index 0000000..0398cdc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00083.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00084.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00084.png
new file mode 100644
index 0000000..0d8228e
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00084.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00085.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00085.png
new file mode 100644
index 0000000..8496bc6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00085.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00086.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00086.png
new file mode 100644
index 0000000..7885233
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00086.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00087.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00087.png
new file mode 100644
index 0000000..751f94b
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00087.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00088.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00088.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00088.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00089.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00089.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00089.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00090.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00090.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00090.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00091.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00091.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00091.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00092.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00092.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00092.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00093.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00093.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00093.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00094.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00094.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00094.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part0/00095.png b/car_product/car_ui_portrait/bootanimation/parts/part0/00095.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part0/00095.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00096.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00096.png
new file mode 100644
index 0000000..33ad84f
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00096.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00097.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00097.png
new file mode 100644
index 0000000..765ec1a
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00097.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00098.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00098.png
new file mode 100644
index 0000000..9947c94
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00098.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00099.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00099.png
new file mode 100644
index 0000000..db0ac24
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00099.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00100.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00100.png
new file mode 100644
index 0000000..ae93d95
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00100.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00101.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00101.png
new file mode 100644
index 0000000..1b729c0
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00101.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00102.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00102.png
new file mode 100644
index 0000000..22b9527
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00102.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00103.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00103.png
new file mode 100644
index 0000000..8ce8992
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00103.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00104.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00104.png
new file mode 100644
index 0000000..f16dbc2
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00104.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00105.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00105.png
new file mode 100644
index 0000000..5ef6b89
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00105.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00106.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00106.png
new file mode 100644
index 0000000..53ce2bc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00106.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00107.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00107.png
new file mode 100644
index 0000000..fd8ffef
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00107.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00108.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00108.png
new file mode 100644
index 0000000..49b39ed
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00108.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00109.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00109.png
new file mode 100644
index 0000000..aaa55db
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00109.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00110.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00110.png
new file mode 100644
index 0000000..3442634
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00110.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00111.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00111.png
new file mode 100644
index 0000000..7aecef1
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00111.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00112.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00112.png
new file mode 100644
index 0000000..a5278ba
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00112.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00113.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00113.png
new file mode 100644
index 0000000..7e3dc9e
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00113.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00114.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00114.png
new file mode 100644
index 0000000..9b93101
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00114.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00115.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00115.png
new file mode 100644
index 0000000..ab90998
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00115.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00116.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00116.png
new file mode 100644
index 0000000..38b3f8f
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00116.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00117.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00117.png
new file mode 100644
index 0000000..5fc2e5c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00117.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00118.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00118.png
new file mode 100644
index 0000000..ccb9eea
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00118.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00119.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00119.png
new file mode 100644
index 0000000..f910866
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00119.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00120.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00120.png
new file mode 100644
index 0000000..ecf8c0c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00120.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00121.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00121.png
new file mode 100644
index 0000000..a0199e5
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00121.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00122.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00122.png
new file mode 100644
index 0000000..a697982
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00122.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00123.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00123.png
new file mode 100644
index 0000000..3c88c58
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00123.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00124.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00124.png
new file mode 100644
index 0000000..626e5e5
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00124.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00125.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00125.png
new file mode 100644
index 0000000..ae9d3df
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00125.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00126.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00126.png
new file mode 100644
index 0000000..ae5f460
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00126.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00127.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00127.png
new file mode 100644
index 0000000..146759c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00127.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00128.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00128.png
new file mode 100644
index 0000000..064f01d
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00128.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00129.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00129.png
new file mode 100644
index 0000000..73d88d5
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00129.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00130.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00130.png
new file mode 100644
index 0000000..689df07
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00130.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00131.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00131.png
new file mode 100644
index 0000000..247f995
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00131.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00132.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00132.png
new file mode 100644
index 0000000..f8c5a02
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00132.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00133.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00133.png
new file mode 100644
index 0000000..a0379c7
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00133.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00134.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00134.png
new file mode 100644
index 0000000..90b94ec
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00134.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00135.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00135.png
new file mode 100644
index 0000000..5230de4
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00135.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00136.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00136.png
new file mode 100644
index 0000000..3eb96d5
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00136.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00137.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00137.png
new file mode 100644
index 0000000..37774d9
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00137.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00138.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00138.png
new file mode 100644
index 0000000..01c95d0
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00138.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00139.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00139.png
new file mode 100644
index 0000000..f496ce3
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00139.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00140.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00140.png
new file mode 100644
index 0000000..0e13dfb
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00140.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00141.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00141.png
new file mode 100644
index 0000000..46cad3a
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00141.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00142.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00142.png
new file mode 100644
index 0000000..ab40ddc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00142.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00143.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00143.png
new file mode 100644
index 0000000..a70a6cc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00143.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00144.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00144.png
new file mode 100644
index 0000000..e357b73
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00144.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00145.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00145.png
new file mode 100644
index 0000000..813fa51
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00145.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00146.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00146.png
new file mode 100644
index 0000000..469f0a6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00146.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00147.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00147.png
new file mode 100644
index 0000000..9f3f355
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00147.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00148.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00148.png
new file mode 100644
index 0000000..e4f3c2b
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00148.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00149.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00149.png
new file mode 100644
index 0000000..434a379
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00149.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00150.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00150.png
new file mode 100644
index 0000000..12ef618
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00150.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00151.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00151.png
new file mode 100644
index 0000000..a4d2e09
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00151.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00152.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00152.png
new file mode 100644
index 0000000..c657de6
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00152.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00153.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00153.png
new file mode 100644
index 0000000..c745f96
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00153.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00154.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00154.png
new file mode 100644
index 0000000..5f389dc
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00154.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00155.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00155.png
new file mode 100644
index 0000000..50f51b7
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00155.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00156.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00156.png
new file mode 100644
index 0000000..c905a6c
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00156.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00157.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00157.png
new file mode 100644
index 0000000..23f509d
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00157.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00158.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00158.png
new file mode 100644
index 0000000..cbd9ebe
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00158.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00159.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00159.png
new file mode 100644
index 0000000..afe61e4
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00159.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00160.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00160.png
new file mode 100644
index 0000000..35eadb5
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00160.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00161.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00161.png
new file mode 100644
index 0000000..6d43b14
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00161.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00162.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00162.png
new file mode 100644
index 0000000..f5ac837
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00162.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00163.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00163.png
new file mode 100644
index 0000000..657f59a
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00163.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00164.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00164.png
new file mode 100644
index 0000000..6352deb
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00164.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00165.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00165.png
new file mode 100644
index 0000000..8150c65
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00165.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00166.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00166.png
new file mode 100644
index 0000000..1199a55
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00166.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00167.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00167.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00167.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00168.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00168.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00168.png
Binary files differ
diff --git a/car_product/car_ui_portrait/bootanimation/parts/part1/00169.png b/car_product/car_ui_portrait/bootanimation/parts/part1/00169.png
new file mode 100644
index 0000000..7d96719
--- /dev/null
+++ b/car_product/car_ui_portrait/bootanimation/parts/part1/00169.png
Binary files differ
diff --git a/car_product/car_ui_portrait/car_ui_portrait.ini b/car_product/car_ui_portrait/car_ui_portrait.ini
new file mode 100644
index 0000000..7804eca
--- /dev/null
+++ b/car_product/car_ui_portrait/car_ui_portrait.ini
@@ -0,0 +1,11 @@
+hw.audioInput=yes
+hw.lcd.density=160
+hw.gpu.enabled=yes
+hw.camera.back=none
+hw.camera.front=none
+hw.mainKeys=no
+skin.dynamic=yes
+skin.name=1224x2175
+skin.path=1224x2175
+hw.lcd.width=1224
+hw.lcd.height=2175
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/overlay/README b/car_product/car_ui_portrait/overlay/README
new file mode 100644
index 0000000..e49b1b0
--- /dev/null
+++ b/car_product/car_ui_portrait/overlay/README
@@ -0,0 +1,2 @@
+This project currently using the old approach for static RROs targeting the android package due to
+b/186753067. Please use the current approach for RROs for any other application/package.
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/Android.bp b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/Android.bp
new file mode 100644
index 0000000..2a4357c
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/Android.bp
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+ name: "CarEvsCameraPreviewAppRRO",
+ resource_dirs: ["res"],
+ platform_apis: true,
+ certificate: "platform",
+ aaptflags: [
+ "--no-resource-deduping",
+ "--no-resource-removal"
+ ],
+ static_libs: [
+ "androidx-constraintlayout_constraintlayout",
+ "androidx-constraintlayout_constraintlayout-solver",
+ ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/AndroidManifest.xml
new file mode 100644
index 0000000..c878ee1
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.car.evs.caruiportrait.rro">
+ <application android:hasCode="false"/>
+ <overlay android:priority="20"
+ android:targetName="CarEvsCameraPreviewApp"
+ android:targetPackage="com.google.android.car.evs"
+ android:resourcesMap="@xml/overlays"
+ android:isStatic="true"/>
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/close_bg.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/close_bg.xml
new file mode 100644
index 0000000..9a4596b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/close_bg.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="?android:attr/colorBackground"/>
+ <corners android:radius="@dimen/close_button_radius"/>
+</shape>
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/ic_close.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/ic_close.xml
new file mode 100644
index 0000000..5874541
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/drawable/ic_close.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/close_icon_dimen"
+ android:height="@dimen/close_icon_dimen"
+ android:viewportWidth="26"
+ android:viewportHeight="26">
+ <path
+ android:pathData="M25.8327 2.75199L23.2477 0.166992L12.9994 10.4153L2.75102 0.166992L0.166016 2.75199L10.4144 13.0003L0.166016 23.2487L2.75102 25.8337L12.9994 15.5853L23.2477 25.8337L25.8327 23.2487L15.5844 13.0003L25.8327 2.75199Z"
+ android:fillColor="?android:attr/textColorPrimary"/>
+</vector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/layout/evs_preview_activity.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/layout/evs_preview_activity.xml
new file mode 100644
index 0000000..bfd6370
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/layout/evs_preview_activity.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent">
+ <LinearLayout
+ android:id="@+id/evs_preview_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent"
+ android:orientation="vertical"/>
+
+ <ImageButton
+ android:id="@+id/close_button"
+ android:layout_width="@dimen/close_button_dimen"
+ android:layout_height="@dimen/close_button_dimen"
+ android:layout_marginLeft="@dimen/close_button_margin"
+ android:layout_marginTop="@dimen/close_button_margin"
+ android:background="@drawable/close_bg"
+ android:scaleType="center"
+ android:alpha="0.5"
+ android:src="@drawable/ic_close"/>
+</FrameLayout>
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/config.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/config.xml
new file mode 100644
index 0000000..532ba92
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/config.xml
@@ -0,0 +1,23 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <!-- Shade of the background behind the camera window. 1.0 for fully opaque, 0.0 for fully
+ transparent. -->
+ <item name="config_cameraBackgroundScrim" format="float" type="dimen">0.7</item>
+
+ <!-- In-plane rotation angle of the rearview camera device in degree -->
+ <integer name="config_evsRearviewCameraInPlaneRotationAngle">180</integer>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/dimens.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/dimens.xml
new file mode 100644
index 0000000..ea3ce97
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/dimens.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <!-- dimensions for evs camera preview in the system window -->
+ <dimen name="camera_preview_width">1224dp</dimen>
+ <dimen name="camera_preview_height">720dp</dimen>
+
+ <dimen name="close_icon_dimen">28dp</dimen>
+ <dimen name="close_button_dimen">80dp</dimen>
+ <dimen name="close_button_margin">24dp</dimen>
+ <dimen name="close_button_radius">20dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/strings.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/strings.xml
new file mode 100644
index 0000000..77cc4d1
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <string name="app_name">Rearview Camera</string>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..13266b6
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarEvsCameraPreviewAppRRO/res/xml/overlays.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<overlay>
+ <item target="layout/evs_preview_activity" value="@layout/evs_preview_activity"/>
+
+ <item target="string/app_name" value="@string/app_name"/>
+
+ <item target="dimen/camera_preview_width" value="@dimen/camera_preview_width"/>
+ <item target="dimen/camera_preview_height" value="@dimen/camera_preview_height"/>
+
+ <item target="id/evs_preview_container" value="@id/evs_preview_container"/>
+ <item target="id/close_button" value="@id/close_button"/>
+
+ <item target="dimen/config_cameraBackgroundScrim" value="@dimen/config_cameraBackgroundScrim"/>
+ <item target="integer/config_evsRearviewCameraInPlaneRotationAngle" value="@integer/config_evsRearviewCameraInPlaneRotationAngle"/>
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/Android.bp
new file mode 100644
index 0000000..28a8c0c
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/Android.bp
@@ -0,0 +1,25 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+ name: "CarUiPortraitCarServiceRRO",
+ resource_dirs: ["res"],
+ platform_apis: true,
+ aaptflags: [
+ "--no-resource-deduping",
+ "--no-resource-removal"
+ ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/AndroidManifest.xml
new file mode 100644
index 0000000..3831ee7
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.caruiportrait.rro">
+ <application android:hasCode="false" />
+ <overlay android:priority="20"
+ android:targetPackage="com.android.car"
+ android:resourcesMap="@xml/overlays"
+ android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/values/config.xml b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/values/config.xml
new file mode 100644
index 0000000..43b6e65
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/values/config.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <!--
+ Specifies optional features that can be enabled by this image. Note that vhal can disable
+ them depending on product variation.
+ Feature name can be either service name defined in Car.*_SERVICE for Car*Manager or any
+ optional feature defined under @OptionalFeature annotation.
+ Note that '/' is used to have subfeature under main feature like "MAIN_FEATURE/SUB_FEATURE".
+
+ Some examples are:
+ <item>storage_monitoring</item>
+ <item>com.android.car.user.CarUserNoticeService</item>
+ <item>com.example.Feature/SubFeature</item>
+
+ The default list defined below will enable all optional features defined.
+ -->
+ <string-array translatable="false" name="config_allowed_optional_car_features">
+ <item>car_navigation_service</item>
+ <item>cluster_service</item>
+ <item>com.android.car.user.CarUserNoticeService</item>
+ <item>diagnostic</item>
+ <item>storage_monitoring</item>
+ <item>vehicle_map_service</item>
+ <item>car_evs_service</item>
+ <item>car_telemetry_service</item>
+ </string-array>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..61d1bc5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitCarServiceRRO/res/xml/overlays.xml
@@ -0,0 +1,18 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<overlay>
+ <item target="array/config_allowed_optional_car_features" value="@array/config_allowed_optional_car_features"/>
+</overlay>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/Android.bp
new file mode 100644
index 0000000..92bd722
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/Android.bp
@@ -0,0 +1,31 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+ name: "CarUiPortraitDialerRRO",
+ resource_dirs: ["res"],
+ platform_apis: true,
+ aaptflags: [
+ "--no-resource-deduping",
+ "--no-resource-removal"
+ ],
+ static_libs: [
+ "androidx-constraintlayout_constraintlayout",
+ "androidx-constraintlayout_constraintlayout-solver",
+ "car-apps-common",
+ "car-ui-lib",
+ ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/AndroidManifest.xml
new file mode 100644
index 0000000..fea65f9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.dialer.caruiportrait.rro">
+ <application android:hasCode="false"/>
+ <overlay android:priority="20"
+ android:targetName="CarDialerApp"
+ android:targetPackage="com.android.car.dialer"
+ android:resourcesMap="@xml/overlays"
+ android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/dialer_ripple_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/dialer_ripple_background.xml
new file mode 100644
index 0000000..131afd5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/dialer_ripple_background.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid
+ android:color="@color/keypad_background_color" />
+ <size
+ android:width="@dimen/dialer_keypad_button_size"
+ android:height="@dimen/dialer_keypad_button_size"/>
+ </shape>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_arrow_right.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_arrow_right.xml
new file mode 100644
index 0000000..b02823b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_arrow_right.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="88dp"
+ android:height="88dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="?android:attr/textColorPrimary"
+ android:pathData="M12,6c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2m0,10c2.7,0 5.8,1.29 6,2L6,18c0.23,-0.72 3.31,-2 6,-2m0,-12C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
+</vector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_backspace.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_backspace.xml
new file mode 100644
index 0000000..4770409
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_backspace.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="32dp"
+ android:height="32dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M22,3L7,3c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.9,0.89 1.59,0.89h15c1.1,0 2,-0.9 2,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM22,19L7.07,19L2.4,12l4.66,-7L22,5v14zM10.41,17L14,13.41 17.59,17 19,15.59 15.41,12 19,8.41 17.59,7 14,10.59 10.41,7 9,8.41 12.59,12 9,15.59z"/>
+</vector>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_bluetooth.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_bluetooth.xml
new file mode 100644
index 0000000..13f130e
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_bluetooth.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="?android:attr/textColorPrimary"
+ android:pathData="M17.71 7.71L12 2h-1v7.59L6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 11 14.41V22h1l5.71-5.71-4.3-4.29 4.3-4.29zM13 5.83l1.88 1.88L13 9.59V5.83zm1.88 10.46L13 18.17v-3.76l1.88 1.88z"/>
+</vector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_phone.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_phone.xml
new file mode 100644
index 0000000..3e2e824
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/ic_phone.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="40dp"
+ android:height="40dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:pathData="M0 0h24v24H0z" />
+ <path
+ android:fillColor="?android:attr/textColorPrimary"
+ android:pathData="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27 .67 -.36 1.02-.24 1.12
+.37 2.33 .57 3.57 .57 .55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17
+0-.55 .45 -1 1-1h3.5c.55 0 1 .45 1 1 0 1.25 .2 2.45 .57 3.57 .11 .35 .03 .74-.25
+1.02l-2.2 2.2z" />
+</vector>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/icon_call_button.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/icon_call_button.xml
new file mode 100644
index 0000000..d048af9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/icon_call_button.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:shape="rectangle">
+ <solid
+ android:color="#29cb86" />
+ <corners android:radius="100dp"/>
+ <size
+ android:width="416dp"
+ android:height="@dimen/dialer_keypad_button_size"/>
+ </shape>
+ </item>
+ <item android:drawable="@drawable/ic_phone" android:gravity="center"/>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/keypad_default_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/keypad_default_background.xml
new file mode 100644
index 0000000..131afd5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/keypad_default_background.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid
+ android:color="@color/keypad_background_color" />
+ <size
+ android:width="@dimen/dialer_keypad_button_size"
+ android:height="@dimen/dialer_keypad_button_size"/>
+ </shape>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/restricted_dialing_mode_label_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/restricted_dialing_mode_label_background.xml
new file mode 100644
index 0000000..ea5a715
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/drawable/restricted_dialing_mode_label_background.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+ <corners android:radius="4dp"/>
+ <solid android:color="@color/car_red_500a"/>
+</shape>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_fragment_with_type_down.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_fragment_with_type_down.xml
new file mode 100644
index 0000000..d192bc9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_fragment_with_type_down.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="100dp">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="45dp"
+ android:layout_marginBottom="48dp"
+ android:layout_alignParentTop="true"
+ android:layout_alignLeft="@id/dialpad_fragment"
+ android:layout_alignRight="@id/dialpad_fragment"
+ android:textAppearance="@style/TextAppearance.DialNumber"
+ android:gravity="center"/>
+
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/list_view"
+ android:layout_width="536dp"
+ android:layout_height="266dp"
+ android:layout_marginBottom="10dp"
+ android:layout_marginLeft="70dp"
+ android:layout_below="@id/title"/>
+ <fragment
+ android:id="@+id/dialpad_fragment"
+ android:name="com.android.car.dialer.ui.dialpad.KeypadFragment"
+ android:layout_height="456dp"
+ android:layout_width="416dp"
+ android:layout_marginLeft="120dp"
+ android:layout_below="@id/list_view"/>
+
+ <RelativeLayout
+ android:layout_height="@dimen/dialer_keypad_button_size"
+ android:layout_width="0dp"
+ android:layout_below="@id/dialpad_fragment"
+ android:layout_marginTop="38dp"
+ android:layout_alignLeft="@id/dialpad_fragment"
+ android:layout_alignRight="@id/dialpad_fragment">
+
+ <ImageView
+ android:id="@+id/call_button"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:adjustViewBounds="true"
+ android:scaleType="fitXY"
+ android:src="@drawable/icon_call_button"
+ android:layout_toLeftOf="@id/delete_button"/>
+
+ <ImageButton
+ android:id="@+id/delete_button"
+ android:layout_width="@dimen/dialer_keypad_button_size"
+ android:layout_height="@dimen/dialer_keypad_button_size"
+ style="@style/DialpadSecondaryButton"
+ android:src="@drawable/ic_backspace"
+ android:layout_marginLeft="64dp"
+ android:visibility="gone"
+ android:layout_alignParentRight="true"/>
+ </RelativeLayout>
+
+ <include
+ layout="@layout/dialpad_user_profile"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_marginTop="10dp"
+ android:layout_below="@id/title"
+ android:layout_centerHorizontal="true"/>
+
+ <include
+ layout="@layout/restricted_dialing_mode_label"
+ android:id="@+id/restricted_dialing_mode_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="8dp"
+ android:layout_below="@id/title"
+ android:visibility="invisible"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_user_profile.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_user_profile.xml
new file mode 100644
index 0000000..bb4e11f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/dialpad_user_profile.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/display_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DialpadDisplayName"
+ android:singleLine="true"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentTop="true"/>
+
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:singleLine="true"
+ android:layout_marginTop="12dp"
+ android:layout_below="@id/display_name"
+ android:layout_centerHorizontal="true"/>
+
+ <ImageView
+ android:id="@+id/dialpad_contact_avatar"
+ android:layout_height="@dimen/dialpad_contact_avatar_size"
+ android:layout_width="@dimen/dialpad_contact_avatar_size"
+ android:layout_below="@id/label"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"/>
+
+ <TextView
+ android:id="@+id/dialpad_contact_initials"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="10dp"
+ android:layout_height="10dp"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/incall_dialpad_fragment.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/incall_dialpad_fragment.xml
new file mode 100644
index 0000000..e7b31ed
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/incall_dialpad_fragment.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:singleLine="true"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentTop="true"/>
+
+ <Chronometer
+ android:id="@+id/call_state"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:singleLine="true"
+ android:layout_marginTop="40dp"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@id/title"/>
+
+ <com.android.car.ui.FocusArea
+ android:id="@+id/dialpad_focus_area"
+ android:layout_height="456dp"
+ android:layout_width="416dp"
+ android:layout_marginBottom="171dp"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentBottom="true">
+
+ <fragment
+ android:id="@+id/dialpad_fragment"
+ android:name="com.android.car.dialer.ui.dialpad.KeypadFragment"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"/>
+
+ </com.android.car.ui.FocusArea>
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/no_hfp.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/no_hfp.xml
new file mode 100644
index 0000000..881d0d5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/no_hfp.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <RelativeLayout
+ android:id="@+id/no_hfp_error_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <Button
+ android:id="@+id/emergency_call_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/emergency_button_text"
+ android:minWidth="156dp"
+ android:minHeight="76dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:textColor="#ffd50000"
+ android:layout_marginBottom="@dimen/car_ui_padding_4"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"/>
+
+ <ImageView
+ android:id="@+id/error_icon"
+ android:layout_width="96dp"
+ android:layout_height="96dp"
+ android:src="@drawable/ic_bluetooth"
+ android:layout_marginBottom="@dimen/car_ui_padding_3"
+ android:layout_alignLeft="@id/error_string"
+ android:layout_alignRight="@id/error_string"
+ android:layout_above="@id/error_string"
+ android:gravity="center"/>
+
+ <TextView
+ android:id="@+id/error_string"
+ style="@style/FullScreenErrorMessageStyle"
+ android:text="@string/no_hfp"
+ android:layout_centerInParent="true"/>
+
+ <com.android.car.apps.common.UxrButton
+ android:id="@+id/connect_bluetooth_button"
+ style="@style/FullScreenErrorButtonStyle"
+ android:background="@color/keypad_background_color"
+ android:text="@string/connect_bluetooth_button_text"
+ android:layout_marginTop="@dimen/car_ui_padding_5"
+ android:layout_below="@id/error_string"
+ android:layout_centerHorizontal="true"/>
+ </RelativeLayout>
+
+</FrameLayout>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/ongoing_call_fragment.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/ongoing_call_fragment.xml
new file mode 100644
index 0000000..0e76d6b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/ongoing_call_fragment.xml
@@ -0,0 +1,55 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.car.apps.common.BackgroundImageView
+ android:id="@+id/background_image"
+ android:layout_width="0dp"
+ android:layout_height="0dp"/>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent">
+
+ <fragment
+ android:name="com.android.car.dialer.ui.dialpad.InCallDialpadFragment"
+ android:id="@+id/incall_dialpad_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="0dp">
+
+ <include
+ layout="@layout/user_profile_large"
+ android:id="@+id/user_profile_container"/>
+
+ </LinearLayout>
+
+ <fragment
+ android:id="@+id/onhold_user_profile"
+ android:name="com.android.car.dialer.ui.activecall.OnHoldCallUserProfileFragment"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"/>
+
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/restricted_dialing_mode_label.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/restricted_dialing_mode_label.xml
new file mode 100644
index 0000000..6a23fd5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/restricted_dialing_mode_label.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/restricted_dialing_mode_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:padding="8dp"
+ android:textAppearance="@style/TextAppearance.Body2"
+ android:text="@string/restricted_dialing_mode_label"
+ android:alpha="0.8"
+ android:background="@drawable/restricted_dialing_mode_label_background"/>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/type_down_list_item.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/type_down_list_item.xml
new file mode 100644
index 0000000..68cf567
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/type_down_list_item.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/contact_result"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:layout_width="match_parent"
+ android:layout_height="112dp">
+
+ <ImageView
+ android:id="@+id/contact_picture"
+ android:layout_width="72dp"
+ android:layout_height="72dp"
+ android:scaleType="centerCrop"
+ android:layout_centerVertical="true"
+ android:layout_alignParentLeft="true"/>
+
+ <TextView
+ android:id="@+id/contact_name"
+ android:layout_marginStart="24dp"
+ android:layout_marginTop="14dp"
+ android:layout_marginBottom="8dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.ContactResultTitle"
+ android:duplicateParentState="true"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/contact_picture"/>
+
+ <TextView
+ android:id="@+id/phone_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="14dp"
+ android:theme="@style/Theme.CarUi.WithToolbar"
+ android:singleLine="true"
+ android:layout_alignParentBottom="true"
+ android:layout_alignLeft="@id/contact_name"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/user_profile_large.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/user_profile_large.xml
new file mode 100644
index 0000000..f59ee9e
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/layout/user_profile_large.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="start|center_vertical"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/user_profile_avatar"
+ android:layout_width="196dp"
+ android:layout_height="196dp"
+ android:scaleType="fitCenter"/>
+
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="fill_parent"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/user_profile_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"/>
+ <TextView
+ android:id="@+id/user_profile_phone_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"/>
+
+ <!-- A textView without a fixed width will call requestLayout() every time its text is changed -->
+ <!-- So we have to make this Chronometer match_parent to avoid redrawing the whole screen -->
+ <Chronometer
+ android:id="@+id/user_profile_call_state"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"/>
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values-night/colors.xml
new file mode 100644
index 0000000..937c35b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values-night/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="keypad_background_color">#282A2D</color>
+ <color name="divider_color">#2e3134</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/colors.xml
new file mode 100644
index 0000000..9b5d97c
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="keypad_background_color">#E8EAED</color>
+ <color name="divider_color">#E8EAED</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/configs.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/configs.xml
new file mode 100644
index 0000000..39e81f6
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/configs.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <integer name="config_dialed_number_gravity">1</integer>
+
+ <bool name="config_always_show_incall_dialpad">true</bool>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/dimens.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/dimens.xml
new file mode 100644
index 0000000..108cfb1
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <dimen name="dialer_keypad_button_size">96dp</dimen>
+ <dimen name="dialpad_contact_avatar_size">64dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/strings.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/strings.xml
new file mode 100644
index 0000000..5f273dc
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="restricted_dialing_mode_label">Dialpad usage is restricted while driving</string>
+ <string name="emergency_button_text">Emergency</string>
+ <string name="connect_bluetooth_button_text">Connect to Bluetooth</string>
+ <string name="no_hfp">
+ To complete your call, first connect your phone to your car via Bluetooth.
+ </string>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/styles.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/styles.xml
new file mode 100644
index 0000000..d67d485
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/values/styles.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Phone -->
+ <style name="KeypadButtonStyle">
+ <item name="android:clickable">true</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginTop">12dp</item>
+ <item name="android:layout_marginRight">32dp</item>
+ <item name="android:layout_marginBottom">12dp</item>
+ <item name="android:layout_marginLeft">32dp</item>
+ <item name="android:background">@drawable/keypad_default_background</item>
+ <item name="android:focusable">true</item>
+ </style>
+
+ <style name="TextAppearance.DialNumber" parent="android:style/TextAppearance">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textSize">32sp</item>
+ </style>
+
+ <style name="SubheaderText" parent="android:style/TextAppearance">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textSize">24sp</item>
+ <item name="android:textFontWeight">500</item>
+ <item name="android:textStyle">normal</item>
+ </style>
+
+ <style name="AddFavoriteText">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
+ <!-- Call history -->
+ <style name="TextAppearance.CallLogTitleDefault" parent="TextAppearance.Body1">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+ <!-- Customized text color for missed calls can be added here -->
+ <style name="TextAppearance.CallLogTitleMissedCall" parent="TextAppearance.Body1">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
+ <style name="DialpadSecondaryButton">
+ <item name="android:background">@drawable/dialer_ripple_background</item>
+ <item name="android:scaleType">centerInside</item>
+ <item name="android:tint">?android:attr/textColorPrimary</item>
+ </style>
+
+ <style name="TextAppearance.DialpadDisplayName" parent="TextAppearance.Body1"/>
+
+ <style name="TextAppearance.ContactResultTitle" parent="TextAppearance.Body1">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
+ <style name="TextAppearance.TypeDownListSpan" parent="TextAppearance.Body3">
+ <item name="android:textSize">32sp</item>
+ <item name="android:textColor">#29cb86</item>
+ </style>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..2064bc4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitDialerRRO/res/xml/overlays.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<overlay>
+ <item target="drawable/ic_phone" value="@drawable/ic_phone"/>
+ <item target="drawable/icon_call_button" value="@drawable/icon_call_button"/>
+ <item target="drawable/ic_backspace" value="@drawable/ic_backspace"/>
+ <item target="drawable/dialer_ripple_background" value="@drawable/dialer_ripple_background"/>
+ <item target="drawable/restricted_dialing_mode_label_background" value="@drawable/restricted_dialing_mode_label_background"/>
+ <item target="drawable/ic_arrow_right" value="@drawable/ic_arrow_right"/>
+ <item target="drawable/ic_bluetooth" value="@drawable/ic_bluetooth"/>
+
+ <item target="id/dialpad_fragment" value="@id/dialpad_fragment" />
+ <item target="id/call_button" value="@id/call_button" />
+ <item target="id/title" value="@id/title" />
+ <item target="id/delete_button" value="@id/delete_button" />
+ <item target="id/display_name" value="@id/display_name" />
+ <item target="id/label" value="@id/label" />
+ <item target="id/dialpad_contact_avatar" value="@id/dialpad_contact_avatar" />
+ <item target="id/dialpad_contact_initials" value="@id/dialpad_contact_initials" />
+ <item target="id/list_view" value="@id/list_view" />
+ <item target="id/restricted_dialing_mode_label" value="@id/restricted_dialing_mode_label" />
+ <item target="id/contact_picture" value="@id/contact_picture" />
+ <item target="id/contact_result" value="@id/contact_result" />
+ <item target="id/contact_name" value="@id/contact_name" />
+ <item target="id/phone_number" value="@id/phone_number" />
+ <item target="id/no_hfp_error_container" value="@id/no_hfp_error_container" />
+ <item target="id/emergency_call_button" value="@id/emergency_call_button" />
+ <item target="id/error_icon" value="@id/error_icon" />
+ <item target="id/error_string" value="@id/error_string" />
+ <item target="id/connect_bluetooth_button" value="@id/connect_bluetooth_button" />
+ <item target="id/background_image" value="@id/background_image" />
+ <item target="id/incall_dialpad_fragment" value="@id/incall_dialpad_fragment" />
+ <item target="id/user_profile_container" value="@id/user_profile_container" />
+ <item target="id/onhold_user_profile" value="@id/onhold_user_profile" />
+ <item target="id/user_profile_avatar" value="@id/user_profile_avatar" />
+ <item target="id/user_profile_title" value="@id/user_profile_title" />
+ <item target="id/user_profile_phone_number" value="@id/user_profile_phone_number" />
+ <item target="id/user_profile_call_state" value="@id/user_profile_call_state" />
+ <item target="id/call_state" value="@id/call_state" />
+ <item target="id/dialpad_focus_area" value="@id/dialpad_focus_area" />
+
+ <item target="color/divider_color" value="@color/divider_color" />
+ <item target="color/hero_button_background_color" value="@color/hero_button_background_color" />
+
+ <item target="bool/config_always_show_incall_dialpad" value="@bool/config_always_show_incall_dialpad" />
+
+ <item target="integer/config_dialed_number_gravity" value="@integer/config_dialed_number_gravity" />
+
+ <item target="layout/dialpad_fragment_with_type_down" value="@layout/dialpad_fragment_with_type_down"/>
+ <item target="layout/dialpad_user_profile" value="@layout/dialpad_user_profile"/>
+ <item target="layout/type_down_list_item" value="@layout/type_down_list_item"/>
+ <item target="layout/restricted_dialing_mode_label" value="@layout/restricted_dialing_mode_label"/>
+ <item target="layout/no_hfp" value="@layout/no_hfp"/>
+ <item target="layout/ongoing_call_fragment" value="@layout/ongoing_call_fragment"/>
+ <item target="layout/user_profile_large" value="@layout/user_profile_large"/>
+ <item target="layout/incall_dialpad_fragment" value="@layout/incall_dialpad_fragment"/>
+
+ <item target="style/KeypadButtonStyle" value="@style/KeypadButtonStyle"/>
+ <item target="style/TextAppearance.DialNumber" value="@style/TextAppearance.DialNumber"/>
+ <item target="style/SubheaderText" value="@style/SubheaderText"/>
+ <item target="style/AddFavoriteText" value="@style/AddFavoriteText"/>
+ <item target="style/TextAppearance.CallLogTitleDefault" value="@style/TextAppearance.CallLogTitleDefault"/>
+ <item target="style/TextAppearance.DialpadDisplayName" value="@style/TextAppearance.DialpadDisplayName"/>
+ <item target="style/TextAppearance.ContactResultTitle" value="@style/TextAppearance.ContactResultTitle"/>
+ <item target="style/TextAppearance.TypeDownListSpan" value="@style/TextAppearance.TypeDownListSpan"/>
+
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/Android.bp
new file mode 100644
index 0000000..7e3df0a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/Android.bp
@@ -0,0 +1,31 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+ name: "CarUiPortraitLauncherRRO",
+ resource_dirs: ["res"],
+ platform_apis: true,
+ aaptflags: [
+ "--no-resource-deduping",
+ "--no-resource-removal"
+ ],
+ static_libs: [
+ "androidx.cardview_cardview",
+ "androidx-constraintlayout_constraintlayout",
+ "car-media-common",
+ "car-apps-common",
+ ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/AndroidManifest.xml
new file mode 100644
index 0000000..1969da8
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.carlauncher.caruiportrait.rro">
+ <application android:hasCode="false"/>
+ <overlay android:priority="20"
+ android:targetName="CarLauncher"
+ android:targetPackage="com.android.car.carlauncher"
+ android:resourcesMap="@xml/overlays"
+ android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/control_bar_image_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/control_bar_image_background.xml
new file mode 100644
index 0000000..9a3c589
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/control_bar_image_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/control_bar_image_background_radius"/>
+</shape>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/default_audio_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/default_audio_background.xml
new file mode 100644
index 0000000..c1fd0e9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/default_audio_background.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/default_audio_background_color"/>
+ </shape>
+ </item>
+ <item android:bottom="@dimen/default_audio_icon_padding"
+ android:top="@dimen/default_audio_icon_padding"
+ android:right="@dimen/default_audio_icon_padding"
+ android:left="@dimen/default_audio_icon_padding">
+ <shape android:shape="oval">
+ <stroke
+ android:width="@dimen/default_audio_icon_outer_ring_thickness"
+ android:color="@color/default_audio_background_image_color"/>
+ <size
+ android:width="@dimen/default_audio_icon_outer_ring_size"
+ android:height="@dimen/default_audio_icon_outer_ring_size"/>
+ </shape>
+ </item>
+ <item android:gravity="center"
+ android:drawable="@drawable/ic_play_music"/>
+</layer-list>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/ic_play_music.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/ic_play_music.xml
new file mode 100644
index 0000000..91190e7
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/drawable/ic_play_music.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/default_audio_icon_inner_icon_size"
+ android:height="@dimen/default_audio_icon_inner_icon_size"
+ android:viewportWidth="40"
+ android:viewportHeight="40">
+ <path
+ android:pathData="M40,20C40,8.96 31.04,0 20,0C8.96,0 0,8.96 0,20C0,31.04 8.96,40 20,40C31.04,40 40,31.04 40,20ZM28,14H22V25C22,27.76 19.76,30 17,30C14.24,30 12,27.76 12,25C12,22.24 14.24,20 17,20C18.14,20 19.16,20.38 20,21.02V10H28V14Z"
+ android:fillColor="@color/default_audio_background_image_color"/>
+</vector>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout-land/car_launcher.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout-land/car_launcher.xml
new file mode 100644
index 0000000..7f79ef1
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout-land/car_launcher.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:layoutDirection="ltr">
+
+ <com.android.car.ui.FocusArea
+ android:id="@+id/bottom_card"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:padding="10dp"
+ android:layoutDirection="locale"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_only.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_only.xml
new file mode 100644
index 0000000..fbd91ba
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_only.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<!-- Layout for a DescriptiveTextView. Required by HomeCardFragment, but currently not used by the CarUiPortrait launcher. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:orientation="horizontal">
+
+ <include layout="@layout/descriptive_text"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"/>
+
+ <TextView
+ android:id="@+id/tap_for_more_text"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:singleLine="true"
+ android:text="Tap for more"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:visibility="gone"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_with_controls.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_with_controls.xml
new file mode 100644
index 0000000..90a9042
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_descriptive_text_with_controls.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<!-- Layout for a DescriptiveTextWithControlsView. Required by HomeCardFragment, but currently not used by the CarUiPortrait launcher. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+
+ <include layout="@layout/descriptive_text"
+ android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_gravity="start|center_vertical"/>
+
+ <LinearLayout
+ android:id="@+id/button_trio"
+ android:gravity="center"
+ android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:orientation="horizontal"
+ android:layout_gravity="end|center_vertical">
+
+ <ImageButton
+ android:id="@+id/button_left"
+ android:layout_height="@dimen/control_bar_action_icon_size"
+ android:layout_width="@dimen/control_bar_action_icon_size"
+ android:background="@android:color/transparent"
+ android:scaleType="centerInside"/>
+
+ <Space
+ android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ android:id="@+id/button_center"
+ android:layout_height="@dimen/control_bar_action_icon_size"
+ android:layout_width="@dimen/control_bar_action_icon_size"
+ android:background="@android:color/transparent"
+ android:scaleType="centerInside"/>
+
+ <Space
+ android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ android:id="@+id/button_right"
+ android:layout_height="@dimen/control_bar_action_icon_size"
+ android:layout_width="@dimen/control_bar_action_icon_size"
+ android:background="@android:color/transparent"
+ android:scaleType="centerInside"/>
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_media.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_media.xml
new file mode 100644
index 0000000..932a22f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_media.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<!-- Layout specifically for the media card, which uses media-specific playback_controls.xml -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:orientation="horizontal">
+
+ <include layout="@layout/descriptive_text"
+ android:id="@+id/media_descriptive_text"
+ android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_gravity="start"/>
+
+ <FrameLayout
+ android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_gravity="end">
+
+ <com.android.car.media.common.PlaybackControlsActionBar
+ android:id="@+id/media_playback_controls_bar"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ app:enableOverflow="true"
+ app:columns="@integer/playback_controls_bar_columns"/>
+ </FrameLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_text_block.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_text_block.xml
new file mode 100644
index 0000000..f08886b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_content_text_block.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<!-- Layout for a TextBlockView. Required by HomeCardFragment, but currently not used by the CarUiPortrait launcher. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/text_block"
+ android:gravity="start"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+ <TextView
+ android:id="@+id/tap_for_more_text"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:singleLine="true"
+ android:text="Tap for more"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:visibility="gone"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_fragment.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_fragment.xml
new file mode 100644
index 0000000..5078a1b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/card_fragment.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<androidx.cardview.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/card_view"
+ android:background="?android:attr/colorBackgroundFloating"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:visibility="gone">
+
+ <FrameLayout
+ android:id="@+id/card_background"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <RelativeLayout
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+
+ <FrameLayout
+ android:id="@+id/control_bar_image_container"
+ android:layout_height="@dimen/control_bar_image_size"
+ android:layout_width="@dimen/control_bar_image_size"
+ android:layout_centerVertical="true"
+ android:layout_marginStart="@dimen/card_icon_margin_start">
+
+ <com.android.car.apps.common.CrossfadeImageView
+ android:id="@+id/card_background_image"
+ android:background="@drawable/control_bar_image_background"
+ android:clipToOutline="true"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"/>
+
+ <ImageView
+ android:id="@+id/card_icon"
+ android:layout_height="@dimen/control_bar_app_icon_size"
+ android:layout_width="@dimen/control_bar_app_icon_size"
+ android:layout_gravity="bottom|end"
+ android:layout_marginEnd="@dimen/control_bar_app_icon_margin"
+ android:layout_marginBottom="@dimen/control_bar_app_icon_margin"
+ android:scaleType="centerInside"/>
+ </FrameLayout>
+
+ <!-- Do not show app name -->
+ <TextView
+ android:id="@+id/card_name"
+ android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:visibility="gone"
+ android:layout_centerVertical="true"
+ android:layout_toEndOf="@id/card_icon"/>
+
+ <FrameLayout
+ android:layout_height="match_parent"
+ android:layout_width="0dp"
+ android:layout_toEndOf="@id/control_bar_image_container"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="@dimen/card_content_margin_start">
+
+ <ViewStub android:id="@+id/media_layout"
+ android:inflatedId="@+id/media_layout"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:visibility="gone"
+ android:layout="@layout/card_content_media"/>
+
+ <!-- Following ViewStubs are required by the HomeCardFragment, but are currently unused
+ as the portrait launcher only shows an audio card and the respective media layout. -->
+ <ViewStub android:id="@+id/descriptive_text_layout"
+ android:inflatedId="@+id/descriptive_text_layout"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:visibility="gone"
+ android:layout="@layout/card_content_descriptive_text_only"/>
+
+ <ViewStub android:id="@+id/text_block_layout"
+ android:inflatedId="@+id/text_block_layout"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:visibility="gone"
+ android:layout="@layout/card_content_text_block"/>
+
+ <ViewStub android:id="@+id/descriptive_text_with_controls_layout"
+ android:inflatedId="@+id/descriptive_text_with_controls_layout"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent"
+ android:visibility="gone"
+ android:layout="@layout/card_content_descriptive_text_with_controls"/>
+
+ </FrameLayout>
+ </RelativeLayout>
+</androidx.cardview.widget.CardView>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/descriptive_text.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/descriptive_text.xml
new file mode 100644
index 0000000..1c55cca
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/descriptive_text.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+
+ <!-- optional_image is required by the HomeCardFragment. Intentionally not shown by setting
+ 0 height and width. -->
+ <ImageView
+ android:id="@+id/optional_image"
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:visibility="gone"
+ android:layout_alignParentStart="true"/>
+
+ <TextView
+ android:id="@+id/primary_text"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginTop="@dimen/descriptive_text_vertical_margin"/>
+
+ <Chronometer
+ android:id="@+id/optional_timer"
+ android:visibility="gone"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentBottom="true"
+ android:layout_marginBottom="@dimen/descriptive_text_vertical_margin"/>
+
+ <TextView
+ android:id="@+id/optional_timer_separator"
+ android:visibility="gone"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:text="@string/ongoing_call_duration_text_separator"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_toEndOf="@id/optional_timer"
+ android:layout_alignParentBottom="true"
+ android:layout_marginBottom="@dimen/descriptive_text_vertical_margin"/>
+
+ <TextView
+ android:id="@+id/secondary_text"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_toEndOf="@id/optional_timer_separator"
+ android:layout_alignParentBottom="true"
+ android:layout_marginBottom="@dimen/descriptive_text_vertical_margin"/>
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/title_bar_display_area_view.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/title_bar_display_area_view.xml
new file mode 100644
index 0000000..940e413
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/layout/title_bar_display_area_view.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/title_bar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/title_bar_display_area_height"
+ android:background="@color/title_bar_display_area_background_color">
+ <View
+ android:layout_width="120dp"
+ android:layout_height="6dp"
+ android:background="@color/title_bar_display_area_handle_bar_color"
+ android:layout_marginTop="17dp"
+ android:layout_centerInParent="true" />
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"/>
+</RelativeLayout>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values-night/colors.xml
new file mode 100644
index 0000000..1ea47a5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values-night/colors.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <color name="default_audio_background_image_color">#515355</color>
+ <color name="default_audio_background_color">#1E2125</color>
+ <color name="title_bar_display_area_handle_bar_color">#5f6368</color>
+ <color name="title_bar_display_area_background_color">#000000</color>
+
+ <color name="icon_tint">#e8eaed</color>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/colors.xml
new file mode 100644
index 0000000..57ff2c3
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/colors.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <color name="default_audio_background_image_color">#DADCE0</color>
+ <color name="default_audio_background_color">#BDC1C6</color>
+ <color name="title_bar_display_area_handle_bar_color">#9aa0a6</color>
+ <color name="title_bar_display_area_background_color">#ffffff</color>
+
+ <color name="icon_tint">#000000</color>
+ <color name="media_button_tint">@color/icon_tint</color>
+ <color name="dialer_button_icon_color">@color/icon_tint</color>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/config.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/config.xml
new file mode 100644
index 0000000..72d87d8
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/config.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <!-- A list of package names that provide the cards to display on the home screen -->
+ <string-array name="config_homeCardModuleClasses" translatable="false">
+ <item>com.android.car.carlauncher.homescreen.audio.AudioCard</item>
+ </string-array>
+
+ <string-array name="config_foregroundDAComponents" translatable="false">
+ <item>com.android.car.carlauncher/.AppGridActivity</item>
+ <item>com.android.car.notification/.CarNotificationCenterActivity</item>
+ </string-array>
+
+ <string-array name="config_ignoreOpeningForegroundDA" translatable="false">
+ <item>com.android.car.carlauncher/.CarLauncher</item>
+ <item>com.android.car.carlauncher/.ControlBarActivity</item>
+ <item>com.android.car.settings/.FallbackHome</item>
+ <item>com.google.android.gms/.auth.uiflows.common.UnpackingRedirectActivity</item>
+ <item>com.google.android.gms/.auth.auto.SignInMethodActivity</item>
+ <item>com.google.android.gms/.auth.uiflows.minutemaid.MinuteMaidActivity</item>
+ <item>android.car.cluster/android.car.cluster.FakeFreeNavigationActivity</item>
+ <item>android.car.cluster/android.car.cluster.MainClusterActivity</item>
+ <item>android.car.usb.handler/android.car.usb.handler.UsbHostManagementActivity</item>
+ </string-array>
+
+ <!-- TODO(b/202413464): Move GAS components to the separate RRO. -->
+ <!-- The ComponentName of Assistant VoicePlate Activity, the Activity will be placed in
+ VoicePlate TDA -->
+ <string name="config_assistantVoicePlateActivity" translatable="false">
+ com.google.android.carassistant/com.google.android.libraries.assistant.auto.tng.assistant.ui.activity.AutoAssistantActivity
+ </string>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/dimens.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/dimens.xml
new file mode 100644
index 0000000..9dcb0e9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/dimens.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <dimen name="card_icon_margin_start">8dp</dimen>
+ <dimen name="card_content_margin_start">16dp</dimen>
+
+ <dimen name="descriptive_text_vertical_margin">23dp</dimen>
+
+ <dimen name="control_bar_image_size">120dp</dimen>
+ <dimen name="control_bar_app_icon_size">36dp</dimen>
+ <dimen name="control_bar_app_icon_margin">6dp</dimen>
+
+ <dimen name="control_bar_action_icon_size">88dp</dimen>
+
+ <!--Percent by which to blur the image used for the card's background as a float between 0 and 1, where 0 is not blurred-->
+ <dimen name="card_background_image_blur_radius" format="float">0</dimen>
+
+ <!-- screen height of the device. This is used when custom policy is provided using
+ config_deviceSpecificDisplayAreaPolicyProvider -->
+ <dimen name="total_screen_height">2175dp</dimen>
+ <!-- screen width of the device. This is used when custom policy is provided using
+ config_deviceSpecificDisplayAreaPolicyProvider -->
+ <dimen name="total_screen_width">1224dp</dimen>
+
+ <dimen name="control_bar_height">136dp</dimen>
+ <dimen name="control_bar_padding">0dp</dimen>
+ <!-- This height is from the top of navbar not accounting for the control bar height. -->
+ <dimen name="default_app_display_area_height">1055dp</dimen>
+ <dimen name="title_bar_display_area_height">40dp</dimen>
+ <!-- This value is 500dp + (top of title bar) -->
+ <dimen name="title_bar_display_area_touch_drag_threshold">1204dp</dimen>
+
+ <dimen name="default_audio_icon_padding">26dp</dimen>
+ <dimen name="default_audio_icon_outer_ring_size">66dp</dimen>
+ <dimen name="default_audio_icon_outer_ring_thickness">8dp</dimen>
+ <dimen name="default_audio_icon_inner_icon_size">40dp</dimen>
+
+ <dimen name="control_bar_image_background_radius">24dp</dimen>
+
+ <dimen name="button_tap_target_size">88dp</dimen>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/integers.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/integers.xml
new file mode 100644
index 0000000..7d54116
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/integers.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <!-- Number of buttons shown for the media playback controls bar -->
+ <integer name="playback_controls_bar_columns">3</integer>
+ <!-- Entry/exit animation transition speed in milliseconds. This is the animation of foreground DA.-->
+ <integer name="enter_exit_animation_foreground_display_area_duration_ms">500</integer>
+</resources>
+
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/strings.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/strings.xml
new file mode 100644
index 0000000..2c0a471
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <string name="ongoing_call_duration_text_separator"> • </string>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..241e6ae
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitLauncherRRO/res/xml/overlays.xml
@@ -0,0 +1,70 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<overlay>
+ <item target="color/dialer_button_icon_color" value="@color/dialer_button_icon_color"/>
+ <item target="color/icon_tint" value="@color/icon_tint" />
+ <item target="color/media_button_tint" value="@color/media_button_tint"/>
+
+ <item target="id/bottom_card" value="@id/bottom_card" />
+ <item target="id/card_background" value="@id/card_background" />
+ <item target="id/card_background_image" value="@id/card_background_image" />
+ <item target="id/card_icon" value="@id/card_icon" />
+ <item target="id/card_name" value="@id/card_name" />
+ <item target="id/card_view" value="@id/card_view" />
+ <item target="id/descriptive_text_layout" value="@id/descriptive_text_layout" />
+ <item target="id/text_block_layout" value="@id/text_block_layout" />
+ <item target="id/descriptive_text_with_controls_layout" value="@id/descriptive_text_with_controls_layout" />
+ <item target="id/media_descriptive_text" value="@id/media_descriptive_text" />
+ <item target="id/media_layout" value="@id/media_layout"/>
+ <item target="id/media_playback_controls_bar" value="@id/media_playback_controls_bar" />
+ <item target="id/optional_image" value="@id/optional_image" />
+ <item target="id/optional_timer" value="@id/optional_timer"/>
+ <item target="id/optional_timer_separator" value="@id/optional_timer_separator"/>
+ <item target="id/primary_text" value="@id/primary_text" />
+ <item target="id/secondary_text" value="@id/secondary_text"/>
+ <item target="id/title" value="@id/title"/>
+
+ <item target="id/button_trio" value="@id/button_trio"/>
+ <item target="id/button_center" value="@id/button_center"/>
+ <item target="id/button_left" value="@id/button_left"/>
+ <item target="id/button_right" value="@id/button_right"/>
+
+ <item target="integer/enter_exit_animation_foreground_display_area_duration_ms" value="@integer/enter_exit_animation_foreground_display_area_duration_ms"/>
+
+ <item target="layout/card_content_media" value="@layout/card_content_media" />
+ <item target="layout/card_fragment" value="@layout/card_fragment" />
+ <item target="layout/car_launcher" value="@layout/car_launcher"/>
+ <item target="layout/descriptive_text" value="@layout/descriptive_text" />
+ <item target="layout/title_bar_display_area_view" value="@layout/title_bar_display_area_view" />
+
+ <item target="dimen/card_background_image_blur_radius" value="@dimen/card_background_image_blur_radius" />
+ <item target="dimen/control_bar_height" value="@dimen/control_bar_height"/>
+ <item target="dimen/control_bar_padding" value="@dimen/control_bar_padding"/>
+ <item target="dimen/default_app_display_area_height" value="@dimen/default_app_display_area_height"/>
+ <item target="dimen/button_tap_target_size" value="@dimen/button_tap_target_size"/>
+ <item target="dimen/title_bar_display_area_height" value="@dimen/title_bar_display_area_height"/>
+ <item target="dimen/title_bar_display_area_touch_drag_threshold" value="@dimen/title_bar_display_area_touch_drag_threshold"/>
+ <item target="dimen/total_screen_height" value="@dimen/total_screen_height"/>
+ <item target="dimen/total_screen_width" value="@dimen/total_screen_width"/>
+
+ <item target="drawable/default_audio_background" value="@drawable/default_audio_background"/>
+
+ <item target="string/config_assistantVoicePlateActivity" value="@string/config_assistantVoicePlateActivity"/>
+
+ <item target="array/config_homeCardModuleClasses" value="@array/config_homeCardModuleClasses"/>
+ <item target="array/config_foregroundDAComponents" value="@array/config_foregroundDAComponents"/>
+ <item target="array/config_ignoreOpeningForegroundDA" value="@array/config_ignoreOpeningForegroundDA"/>
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/Android.bp
new file mode 100644
index 0000000..a6beaa0
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/Android.bp
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+ name: "CarUiPortraitMediaRRO",
+ resource_dirs: ["res"],
+ platform_apis: true,
+ aaptflags: [
+ "--no-resource-deduping",
+ "--no-resource-removal"
+ ],
+ static_libs: [
+ "androidx-constraintlayout_constraintlayout",
+ "car-apps-common",
+
+ ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/AndroidManifest.xml
new file mode 100644
index 0000000..fa12e4f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.media.caruiportrait.rro">
+ <application android:hasCode="false"/>
+ <overlay android:priority="20"
+ android:targetName="CarMediaApp"
+ android:targetPackage="com.android.car.media"
+ android:resourcesMap="@xml/overlays"
+ android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/drawable/image_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/drawable/image_background.xml
new file mode 100644
index 0000000..e5f9fa5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/drawable/image_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/image_radius"/>
+</shape>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/layout/fragment_error.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/layout/fragment_error.xml
new file mode 100644
index 0000000..dc03e18
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/layout/fragment_error.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="0dp"
+ android:layout_height="0dp">
+ <Space
+ android:id="@+id/ui_content_start_guideline"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_marginLeft="0dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ />
+
+ <Space
+ android:id="@+id/ui_content_top_guideline"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_marginTop="96dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ <Space
+ android:id="@+id/ui_content_end_guideline"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_marginRight="0dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ />
+
+ <Space
+ android:id="@+id/ui_content_bottom_guideline"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_marginBottom="0dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <com.android.car.apps.common.UxrTextView
+ android:id="@+id/error_message"
+ android:layout_width="520dp"
+ android:layout_height="44dp"
+ android:gravity="center"
+ android:layout_marginTop="440dp"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"/>
+
+ <com.android.car.apps.common.UxrButton
+ android:id="@+id/error_button"
+ android:layout_width="760dp"
+ android:layout_height="88dp"
+ android:background="@color/button_background_color"
+ android:textAlignment="center"
+ android:gravity="center"
+ android:layout_marginTop="120dp"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@id/error_message"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values-night/colors.xml
new file mode 100644
index 0000000..83db0e3
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values-night/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="button_background_color">#282A2D</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/bools.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/bools.xml
new file mode 100644
index 0000000..8f979df
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/bools.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Now playing and mini playback controls will be shown in sysui instead of media center. -->
+ <bool name="show_mini_playback_controls">false</bool>
+ <bool name="switch_to_playback_view_when_playable_item_is_clicked">false</bool>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/colors.xml
new file mode 100644
index 0000000..14bc52b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="button_background_color">#E8EAED</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/dimens.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/dimens.xml
new file mode 100644
index 0000000..9726095
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <dimen name="image_radius">24dp</dimen>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/styles.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/styles.xml
new file mode 100644
index 0000000..dbc8eec
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/values/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <style name="MediaIconContainerStyle">
+ <item name="android:background">@drawable/image_background</item>
+ <item name="android:clipToOutline">true</item>
+ </style>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..01f2039
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitMediaRRO/res/xml/overlays.xml
@@ -0,0 +1,47 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<overlay>
+ <item target="attr/layout_constraintBottom_toBottomOf" value="@attr/layout_constraintBottom_toBottomOf"/>
+ <item target="attr/layout_constraintBottom_toTopOf" value="@attr/layout_constraintBottom_toTopOf"/>
+ <item target="attr/layout_constraintEnd_toEndOf" value="@attr/layout_constraintEnd_toEndOf"/>
+ <item target="attr/layout_constraintEnd_toStartOf" value="@attr/layout_constraintEnd_toStartOf"/>
+ <item target="attr/layout_constraintGuide_begin" value="@attr/layout_constraintGuide_begin"/>
+ <item target="attr/layout_constraintGuide_end" value="@attr/layout_constraintGuide_end"/>
+ <item target="attr/layout_constraintHorizontal_bias" value="@attr/layout_constraintHorizontal_bias"/>
+ <item target="attr/layout_constraintLeft_toLeftOf" value="@attr/layout_constraintLeft_toLeftOf"/>
+ <item target="attr/layout_constraintLeft_toRightOf" value="@attr/layout_constraintLeft_toRightOf"/>
+ <item target="attr/layout_constraintRight_toLeftOf" value="@attr/layout_constraintRight_toLeftOf"/>
+ <item target="attr/layout_constraintRight_toRightOf" value="@attr/layout_constraintRight_toRightOf"/>
+ <item target="attr/layout_constraintStart_toEndOf" value="@attr/layout_constraintStart_toEndOf"/>
+ <item target="attr/layout_constraintStart_toStartOf" value="@attr/layout_constraintStart_toStartOf"/>
+ <item target="attr/layout_constraintTop_toBottomOf" value="@attr/layout_constraintTop_toBottomOf"/>
+ <item target="attr/layout_constraintTop_toTopOf" value="@attr/layout_constraintTop_toTopOf"/>
+
+ <item target="bool/show_mini_playback_controls" value="@bool/show_mini_playback_controls" />
+ <item target="bool/switch_to_playback_view_when_playable_item_is_clicked" value="@bool/switch_to_playback_view_when_playable_item_is_clicked" />
+
+ <item target="id/error_message" value="@id/error_message" />
+ <item target="id/error_button" value="@id/error_button" />
+ <item target="id/ui_content_start_guideline" value="@id/ui_content_start_guideline" />
+ <item target="id/ui_content_top_guideline" value="@id/ui_content_top_guideline" />
+ <item target="id/ui_content_end_guideline" value="@id/ui_content_end_guideline" />
+ <item target="id/ui_content_bottom_guideline" value="@id/ui_content_bottom_guideline" />
+
+ <item target="layout/fragment_error" value="@layout/fragment_error"/>
+
+ <item target="style/MediaIconContainerStyle" value="@style/MediaIconContainerStyle" />
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/Android.bp
new file mode 100644
index 0000000..960301a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+ name: "CarUiPortraitNotificationRRO",
+ resource_dirs: ["res"],
+ platform_apis: true,
+ aaptflags: [
+ "--no-resource-deduping",
+ "--no-resource-removal"
+ ],
+ static_libs: [
+ "CarNotificationLib",
+ ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/AndroidManifest.xml
new file mode 100644
index 0000000..47e6a4a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.notification.caruiportrait.rro">
+ <application android:hasCode="false"/>
+ <overlay android:priority="20"
+ android:targetName="CarNotification"
+ android:targetPackage="com.android.car.notification"
+ android:resourcesMap="@xml/overlays"
+ android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/call_headsup_notification_template.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/call_headsup_notification_template.xml
new file mode 100644
index 0000000..41095c4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/call_headsup_notification_template.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<com.android.car.ui.FocusArea
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <androidx.cardview.widget.CardView
+ android:id="@+id/card_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ app:cardCornerRadius="@dimen/notification_card_radius">
+
+ <RelativeLayout
+ android:id="@+id/inner_template_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/car_notification_card_inner_top_margin">
+
+ <com.android.car.notification.template.CarNotificationHeaderView
+ android:id="@+id/notification_header"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ app:isHeadsUp="true"/>
+
+ <com.android.car.notification.template.CarNotificationBodyView
+ android:id="@+id/notification_body"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/notification_touch_target_size"
+ android:gravity="center_vertical"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="@dimen/card_body_margin_start"
+ app:maxLines="@integer/config_headsUpNotificationMaxBodyLines"
+ app:showBigIcon="true"
+ app:isHeadsUp="true"/>
+
+ <FrameLayout
+ android:id="@+id/notification_actions_wrapper"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/notification_body">
+
+ <com.android.car.notification.template.CarNotificationActionsView
+ android:id="@+id/notification_actions"
+ style="@style/NotificationActionViewLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:categoryCall="true"/>
+ </FrameLayout>
+ </RelativeLayout>
+ </androidx.cardview.widget.CardView>
+</com.android.car.ui.FocusArea>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_body_view.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_body_view.xml
new file mode 100644
index 0000000..a373e8a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_body_view.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <ImageView
+ android:id="@+id/notification_body_icon"
+ android:layout_width="@dimen/notification_touch_target_size"
+ android:layout_height="@dimen/notification_touch_target_size"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginStart="@dimen/body_big_icon_margin"
+ style="@style/NotificationBodyImageIcon"/>
+
+ <TextView
+ android:id="@+id/notification_body_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toEndOf="@id/notification_body_icon"
+ android:layout_alignTop="@id/notification_body_icon"
+ android:layout_marginStart="@dimen/card_start_margin"
+ android:layout_alignWithParentIfMissing="true"
+ android:layout_marginTop="8dp"
+ style="@style/NotificationBodyTitleText"/>
+
+ <TextView
+ android:id="@+id/notification_body_content"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toEndOf="@id/notification_body_icon"
+ android:layout_below="@id/notification_body_title"
+ android:layout_marginStart="@dimen/card_start_margin"
+ android:layout_marginTop="4dp"
+ android:layout_alignWithParentIfMissing="true"
+ style="@style/NotificationBodyContentText"/>
+</merge>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_header_view.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_header_view.xml
new file mode 100644
index 0000000..7a77a81
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_headsup_notification_header_view.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<!-- Do not show app icon or name for HUNs -->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <ImageView
+ android:id="@+id/app_icon"
+ android:layout_width="0dp"
+ android:layout_height="0dp"/>
+
+ <TextView
+ android:id="@+id/header_text"
+ android:layout_width="0dp"
+ android:layout_height="0dp"/>
+</merge>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_notification_actions_view.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_notification_actions_view.xml
new file mode 100644
index 0000000..a3c2ce3
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/car_notification_actions_view.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <com.android.car.notification.template.CarNotificationActionButton
+ android:id="@+id/action_1"
+ style="@style/NotificationActionButton1"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="@dimen/action_button_height"
+ android:minWidth="@dimen/action_button_min_width"
+ android:paddingBottom="@dimen/action_button_padding_bottom"
+ android:paddingTop="@dimen/action_button_padding_top"
+ android:visibility="gone"/>
+
+ <com.android.car.notification.template.CarNotificationActionButton
+ android:id="@+id/action_2"
+ style="@style/NotificationActionButton2"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/action_button_height"
+ android:layout_marginStart="@dimen/action_button_spacing_start"
+ android:minWidth="@dimen/action_button_min_width"
+ android:paddingBottom="@dimen/action_button_padding_bottom"
+ android:paddingTop="@dimen/action_button_padding_top"
+ android:visibility="gone"/>
+
+ <com.android.car.notification.template.CarNotificationActionButton
+ android:id="@+id/action_3"
+ style="@style/NotificationActionButton3"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="@dimen/action_button_height"
+ android:layout_marginStart="@dimen/action_button_spacing_start"
+ android:minWidth="@dimen/action_button_min_width"
+ android:paddingBottom="@dimen/action_button_padding_bottom"
+ android:paddingTop="@dimen/action_button_padding_top"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+</merge>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/headsup_container_bottom.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/headsup_container_bottom.xml
new file mode 100644
index 0000000..66f7085
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/headsup_container_bottom.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/notification_headsup"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.android.car.notification.headsup.HeadsUpContainerView
+ android:id="@+id/headsup_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/headsup_notification_bottom_margin"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/message_headsup_notification_template.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/message_headsup_notification_template.xml
new file mode 100644
index 0000000..4fd0492
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/message_headsup_notification_template.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<com.android.car.ui.FocusArea
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <androidx.cardview.widget.CardView
+ android:id="@+id/card_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ app:cardCornerRadius="@dimen/notification_card_radius">
+
+ <RelativeLayout
+ android:id="@+id/inner_template_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/car_notification_card_inner_top_margin">
+
+ <com.android.car.notification.template.CarNotificationHeaderView
+ android:id="@+id/notification_header"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentStart="true"
+ app:isHeadsUp="true"/>
+
+ <com.android.car.notification.template.CarNotificationBodyView
+ android:id="@+id/notification_body"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/notification_touch_target_size"
+ android:gravity="center_vertical"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentEnd="true"
+ android:layout_marginStart="@dimen/card_body_margin_start"
+ app:isHeadsUp="true"
+ app:maxLines="@integer/config_headsUpNotificationMaxBodyLines"
+ app:showBigIcon="true"/>
+
+ <FrameLayout
+ android:id="@+id/notification_actions_wrapper"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/notification_body">
+
+ <com.android.car.notification.template.CarNotificationActionsView
+ android:id="@+id/notification_actions"
+ style="@style/NotificationActionViewLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ </FrameLayout>
+ </RelativeLayout>
+ </androidx.cardview.widget.CardView>
+</com.android.car.ui.FocusArea>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/notification_center_activity.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/notification_center_activity.xml
new file mode 100644
index 0000000..83a6525
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/layout/notification_center_activity.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.car.ui.FocusParkingView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <com.android.car.ui.FocusArea
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.car.notification.CarNotificationView
+ android:id="@+id/notification_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <FrameLayout
+ android:id="@+id/exit_button_container"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/empty_notification_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toTopOf="@id/manage_button"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
+ android:text="@string/empty_notification_header"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:visibility="gone"/>
+
+ <Button
+ android:id="@+id/manage_button"
+ style="@style/ManageButton"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/manage_button_height"
+ android:layout_marginTop="@dimen/manage_button_top_margin"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/empty_notification_text"
+ app:layout_constraintVertical_chainStyle="packed"
+ android:text="@string/manage_text"
+ android:visibility="gone"/>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/notifications"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:orientation="vertical"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/notification_center_title"/>
+ </com.android.car.notification.CarNotificationView>
+ </com.android.car.ui.FocusArea>
+</FrameLayout>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values-night/colors.xml
new file mode 100644
index 0000000..0cc16f9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values-night/colors.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <color name="action_button_background_color">#2e3134</color>
+ <color name="primary_text_color">@android:color/system_neutral1_50</color>
+ <color name="secondary_text_color">@android:color/system_neutral2_400</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/colors.xml
new file mode 100644
index 0000000..6cf3e5a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/colors.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <color name="action_button_background_color">#e8eaed</color>
+ <color name="clear_all_button_background_color">@color/action_button_background_color</color>
+ <color name="primary_text_color">@android:color/system_neutral1_900</color>
+ <color name="secondary_text_color">@android:color/system_neutral2_500</color>
+ <color name="icon_tint">@color/primary_text_color</color>
+
+ <color name="call_accept_button">#29cb86</color>
+ <color name="call_decline_button">#e46962</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/config.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/config.xml
new file mode 100644
index 0000000..9e25a62
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/config.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <bool name="config_showHeadsUpNotificationOnBottom">true</bool>
+
+ <string name="config_headsUpNotificationAnimationHelper" translatable="false">
+ com.android.car.notification.headsup.animationhelper.CarHeadsUpNotificationBottomAnimationHelper</string>
+
+ <!-- If false, small icon will be used to distinguish the app, large icon will be used
+ in notification body and notification header will be shown.-->
+ <bool name="config_useLauncherIcon">false</bool>
+
+ <!-- Whether to show header for the notifications center -->
+ <bool name="config_showHeaderForNotifications">true</bool>
+
+ <!-- Whether to show footer for the notifications center -->
+ <bool name="config_showFooterForNotifications">false</bool>
+
+ <!-- Whether to show Recents/Older header for notifications list -->
+ <bool name="config_showRecentAndOldHeaders">false</bool>
+
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/dimens.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/dimens.xml
new file mode 100644
index 0000000..32659ce
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/dimens.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <!-- Horizontal margin for HUNs -->
+ <dimen name="notification_headsup_card_margin_horizontal">0dp</dimen>
+ <dimen name="car_notification_card_inner_top_margin">8dp</dimen>
+
+ <!-- Card View -->
+ <dimen name="card_start_margin">16dp</dimen>
+ <dimen name="card_end_margin">8dp</dimen>
+ <dimen name="card_body_margin_bottom">8dp</dimen>
+ <dimen name="card_body_margin_start">16dp</dimen>
+ <dimen name="card_header_margin_bottom">8dp</dimen>
+ <dimen name="notification_card_radius">24dp</dimen>
+ <dimen name="headsup_notification_bottom_margin">160dp</dimen>
+
+ <!-- Icons -->
+ <dimen name="notification_touch_target_size">120dp</dimen>
+ <dimen name="body_big_icon_margin">8dp</dimen>
+
+ <!-- Action View -->
+ <dimen name="action_button_height">88dp</dimen>
+ <dimen name="action_button_radius">24dp</dimen>
+ <dimen name="action_view_left_margin">8dp</dimen>
+ <dimen name="action_view_right_margin">8dp</dimen>
+ <dimen name="action_button_padding_bottom">8dp</dimen>
+
+ <dimen name="card_min_bottom_padding">8dp</dimen>
+ <dimen name="card_min_top_padding">8dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/strings.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/strings.xml
new file mode 100644
index 0000000..981ffdc
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <!-- The assistant action label to read aloud a message notification and optionally prompt user to respond [CHAR_LIMIT=20]-->
+ <string name="assist_action_play_label">Play message</string>
+
+ <!-- Notification header text displayed on top of the notification center shade [CHAR_LIMIT=25] -->
+ <string name="notification_header">Notification Center</string>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/styles.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/styles.xml
new file mode 100644
index 0000000..2727581
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/styles.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <style name="NotificationBodyTitleText" parent="@android:TextAppearance.DeviceDefault.Large">
+ <item name="android:maxLines">1</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:textAlignment">viewStart</item>
+ </style>
+
+ <style name="NotificationBodyContentText" parent="@android:TextAppearance.DeviceDefault.Small">
+ <item name="android:maxLines">1</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:textAlignment">viewStart</item>
+ </style>
+
+ <style name="NotificationActionViewLayout">
+ <item name="android:layout_gravity">center</item>
+ <item name="android:layout_marginLeft">8dp</item>
+ <item name="android:layout_marginRight">8dp</item>
+ <item name="android:layout_marginTop">8dp</item>
+ <item name="android:layout_marginBottom">8dp</item>
+ </style>
+
+ <style name="NotificationActionButtonBase" parent="@android:Widget.DeviceDefault.Button.Borderless.Colored">
+ <item name="android:minWidth">@dimen/action_button_min_width</item>
+ <item name="android:gravity">center</item>
+ <item name="android:textSize">32sp</item>
+ <item name="android:textAllCaps">false</item>
+ <item name="android:textColor">@color/notification_accent_color</item>
+ <item name="android:maxLines">1</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:background">@drawable/action_button_background</item>
+ </style>
+
+ <style name="NotificationActionButtonText" parent="@android:TextAppearance.DeviceDefault.Large">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textAllCaps">false</item>
+ <item name="android:maxLines">1</item>
+ <item name="android:ellipsize">end</item>
+ </style>
+
+ <style name="ClearAllButton" parent="@android:Widget.DeviceDefault.Button.Borderless.Colored">
+ <item name="android:minWidth">@dimen/clear_all_button_min_width</item>
+ <item name="android:paddingStart">@dimen/clear_all_button_padding</item>
+ <item name="android:paddingEnd">@dimen/clear_all_button_padding</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:gravity">center</item>
+ <item name="android:textAllCaps">false</item>
+ <item name="android:background">@drawable/clear_all_button_background</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/themes.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/themes.xml
new file mode 100644
index 0000000..36f372b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/values/themes.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <style name="Theme.DeviceDefault.NoActionBar.Notification" parent="@android:Theme.DeviceDefault.NoActionBar">
+ </style>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..18124ea
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitNotificationRRO/res/xml/overlays.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<overlay>
+ <item target="attr/categoryCall" value="@attr/categoryCall"/>
+ <item target="attr/cardCornerRadius" value="@attr/cardCornerRadius"/>
+ <item target="attr/isHeadsUp" value="@attr/isHeadsUp"/>
+ <item target="attr/maxLines" value="@attr/maxLines"/>
+ <item target="attr/showBigIcon" value="@attr/showBigIcon"/>
+
+ <item target="bool/config_showHeadsUpNotificationOnBottom" value="@bool/config_showHeadsUpNotificationOnBottom" />
+ <item target="bool/config_useLauncherIcon" value="@bool/config_useLauncherIcon"/>
+ <item target="bool/config_showHeaderForNotifications" value="@bool/config_showHeaderForNotifications"/>
+ <item target="bool/config_showFooterForNotifications" value="@bool/config_showFooterForNotifications"/>
+ <item target="bool/config_showRecentAndOldHeaders" value="@bool/config_showRecentAndOldHeaders"/>
+
+ <item target="color/icon_tint" value="@color/icon_tint"/>
+ <item target="color/call_accept_button" value="@color/call_accept_button"/>
+ <item target="color/call_decline_button" value="@color/call_decline_button"/>
+
+ <item target="color/action_button_background_color" value="@color/action_button_background_color"/>
+ <item target="color/clear_all_button_background_color" value="@color/clear_all_button_background_color"/>
+ <item target="color/primary_text_color" value="@color/primary_text_color"/>
+ <item target="color/secondary_text_color" value="@color/secondary_text_color"/>
+
+ <item target="dimen/action_button_height" value="@dimen/action_button_height" />
+ <item target="dimen/action_button_radius" value="@dimen/action_button_radius" />
+ <item target="dimen/action_button_padding_bottom" value="@dimen/action_button_padding_bottom"/>
+ <item target="dimen/action_view_left_margin" value="@dimen/action_view_left_margin" />
+ <item target="dimen/action_view_right_margin" value="@dimen/action_view_right_margin" />
+ <item target="dimen/body_big_icon_margin" value="@dimen/body_big_icon_margin"/>
+ <item target="dimen/car_notification_card_inner_top_margin" value="@dimen/car_notification_card_inner_top_margin"/>
+ <item target="dimen/card_start_margin" value="@dimen/card_start_margin" />
+ <item target="dimen/card_body_margin_bottom" value="@dimen/card_body_margin_bottom" />
+ <item target="dimen/card_end_margin" value="@dimen/card_start_margin" />
+ <item target="dimen/card_header_margin_bottom" value="@dimen/card_header_margin_bottom" />
+ <item target="dimen/notification_card_radius" value="@dimen/notification_card_radius"/>
+ <item target="dimen/notification_headsup_card_margin_horizontal" value="@dimen/notification_headsup_card_margin_horizontal" />
+ <item target="dimen/notification_touch_target_size" value="@dimen/notification_touch_target_size"/>
+
+ <item target="id/action_1" value="@id/action_1" />
+ <item target="id/action_2" value="@id/action_2" />
+ <item target="id/action_3" value="@id/action_3" />
+ <item target="id/card_view" value="@id/card_view" />
+ <item target="id/headsup_content" value="@id/headsup_content"/>
+ <item target="id/inner_template_view" value="@id/inner_template_view" />
+ <item target="id/notification_actions" value="@id/notification_actions" />
+ <item target="id/notification_actions_wrapper" value="@id/notification_actions_wrapper" />
+ <item target="id/notification_body" value="@id/notification_body"/>
+ <item target="id/notification_header" value="@id/notification_header"/>
+ <item target="id/notification_headsup" value="@id/notification_headsup"/>
+ <item target="id/notification_view" value="@id/notification_view"/>
+ <item target="id/notifications" value="@id/notifications"/>
+ <item target="id/manage_button" value="@id/manage_button"/>
+ <item target="id/empty_notification_text" value="@id/empty_notification_text"/>
+ <item target="id/exit_button_container" value="@id/exit_button_container"/>
+
+ <item target="layout/car_notification_actions_view" value="@layout/car_notification_actions_view"/>
+ <item target="layout/headsup_container_bottom" value="@layout/headsup_container_bottom"/>
+ <item target="layout/message_headsup_notification_template" value="@layout/message_headsup_notification_template" />
+ <item target="layout/notification_center_activity" value="@layout/notification_center_activity"/>
+
+ <item target="string/assist_action_play_label" value="@string/assist_action_play_label"/>
+ <item target="string/config_headsUpNotificationAnimationHelper" value="@string/config_headsUpNotificationAnimationHelper" />
+ <item target="string/notification_header" value="@string/notification_header"/>
+
+ <item target="style/ClearAllButton" value="@style/ClearAllButton"/>
+ <item target="style/NotificationActionButtonBase" value="@style/NotificationActionButtonBase"/>
+ <item target="style/NotificationActionViewLayout" value="@style/NotificationActionViewLayout"/>
+ <item target="style/NotificationBodContentText" value="@style/NotificationBodyContentText" />
+ <item target="style/NotificationBodyTitleText" value="@style/NotificationBodyTitleText" />
+ <item target="style/NotificationActionButtonText" value="@style/NotificationActionButtonText"/>
+ <item target="style/Theme.DeviceDefault.NoActionBar.Notification" value="@style/Theme.DeviceDefault.NoActionBar.Notification"/>
+
+ <item target="dimen/card_min_bottom_padding" value="@dimen/card_min_bottom_padding"/>
+ <item target="dimen/card_min_top_padding" value="@dimen/card_min_top_padding"/>
+
+ <item target="layout/car_headsup_notification_header_view" value="@layout/car_headsup_notification_header_view"/>
+ <item target="id/app_icon" value="@id/app_icon"/>
+ <item target="id/header_text" value="@id/header_text"/>
+
+ <item target="layout/car_headsup_notification_body_view" value="@layout/car_headsup_notification_body_view"/>
+ <item target="id/notification_body_icon" value="@id/notification_body_icon"/>
+ <item target="id/notification_body_title" value="@id/notification_body_title"/>
+ <item target="id/notification_body_content" value="@id/notification_body_content"/>
+
+ <item target="layout/call_headsup_notification_template" value="@layout/call_headsup_notification_template"/>
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/Android.bp
new file mode 100644
index 0000000..b63e2fe
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/Android.bp
@@ -0,0 +1,25 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+ name: "CarUiPortraitSettingsProviderRRO",
+ resource_dirs: ["res"],
+ platform_apis: true,
+ aaptflags: [
+ "--no-resource-deduping",
+ "--no-resource-removal"
+ ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/AndroidManifest.xml
new file mode 100644
index 0000000..807a91a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.providers.settings.caruiportrait.rro">
+ <application android:hasCode="false" />
+ <overlay android:priority="20"
+ android:targetName="SettingsProvider"
+ android:targetPackage="com.android.providers.settings"
+ android:resourcesMap="@xml/overlays"
+ android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/values/defaults.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/values/defaults.xml
new file mode 100644
index 0000000..39181e4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/values/defaults.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <!-- Set default screen orientation. -->
+ <integer name="def_user_rotation">0</integer>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..23a2903
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsProviderRRO/res/xml/overlays.xml
@@ -0,0 +1,18 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<overlay>
+ <item target="integer/def_user_rotation" value="@integer/def_user_rotation"/>
+</overlay>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/Android.bp b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/Android.bp
new file mode 100644
index 0000000..5b9f206
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/Android.bp
@@ -0,0 +1,25 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+android_app {
+ name: "CarUiPortraitSettingsRRO",
+ resource_dirs: ["res"],
+ platform_apis: true,
+ aaptflags: [
+ "--no-resource-deduping",
+ "--no-resource-removal"
+ ],
+}
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/AndroidManifest.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/AndroidManifest.xml
new file mode 100644
index 0000000..8d15347
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.settings.caruiportrait.rro">
+ <application android:hasCode="false"/>
+ <overlay android:priority="20"
+ android:targetName="CarSettings"
+ android:targetPackage="com.android.car.settings"
+ android:resourcesMap="@xml/overlays"
+ android:isStatic="true" />
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/animator/trans_fade_in.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/animator/trans_fade_in.xml
new file mode 100644
index 0000000..76c08d9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/animator/trans_fade_in.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:interpolator="@android:interpolator/decelerate_quint"
+ android:propertyName="alpha"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType"/>
+</set>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/animator/trans_fade_out.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/animator/trans_fade_out.xml
new file mode 100644
index 0000000..482327a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/animator/trans_fade_out.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:interpolator="@android:interpolator/decelerate_quint"
+ android:propertyName="alpha"
+ android:valueFrom="1.0"
+ android:valueTo="0.0"
+ android:valueType="floatType"/>
+</set>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_background.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_background.xml
new file mode 100644
index 0000000..59391e2
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_background.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true" android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackground"/>
+ </shape>
+ </item>
+ <item android:state_focused="true">
+ <shape android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackground"/>
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackground"/>
+ </shape>
+ </item>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_highlight.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_highlight.xml
new file mode 100644
index 0000000..0f72dd2
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/drawable/top_level_preference_highlight.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:right="100dip">
+ <shape
+ android:shape="rectangle" >
+ <solid android:color="#91AFC6" />
+ </shape>
+ </item>
+ <item android:left="8dip">
+ <shape
+ android:shape="rectangle" >
+ <solid android:color="@color/top_level_preference_highlight_background_color" />
+ </shape>
+ </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/layout/top_level_preference.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/layout/top_level_preference.xml
new file mode 100644
index 0000000..65b753a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/layout/top_level_preference.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/top_level_preference_background"
+ android:clipToPadding="false"
+ android:minHeight="96dp"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:tag="carUiPreference"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+ <com.android.car.ui.uxr.DrawableStateImageView
+ android:id="@android:id/icon"
+ android:layout_width="44dp"
+ android:layout_height="44dp"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:layout_marginStart="23dp"
+ android:scaleType="fitCenter"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_marginStart="33dp"
+ android:layout_toEndOf="@android:id/icon"
+ android:layout_toStartOf="@android:id/widget_frame"
+ android:orientation="vertical">
+
+ <com.android.car.ui.uxr.DrawableStateTextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"/>
+
+ <com.android.car.ui.uxr.DrawableStateTextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ </LinearLayout>
+
+ <!-- Preference should place its actual preference widget here. -->
+ <FrameLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values-night/colors.xml
new file mode 100644
index 0000000..74014e9
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values-night/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="top_level_preference_highlight_background_color">#282A2D</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values/colors.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values/colors.xml
new file mode 100644
index 0000000..d209af4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="top_level_preference_highlight_background_color">#E8EAED</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/xml/overlays.xml
new file mode 100644
index 0000000..cc1dc2f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/CarUiPortraitSettingsRRO/res/xml/overlays.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<overlay>
+ <item target="drawable/top_level_preference_background" value="@drawable/top_level_preference_background"/>
+ <item target="drawable/top_level_preference_highlight" value="@drawable/top_level_preference_highlight"/>
+
+ <item target="layout/top_level_preference" value="@layout/top_level_preference"/>
+
+ <item target="animator/trans_left_in" value="@animator/trans_fade_in"/>
+ <item target="animator/trans_left_out" value="@animator/trans_fade_out"/>
+ <item target="animator/trans_right_in" value="@animator/trans_fade_in"/>
+ <item target="animator/trans_right_out" value="@animator/trans_fade_out"/>
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/android/Android.bp b/car_product/car_ui_portrait/rro/android/Android.bp
new file mode 100644
index 0000000..73878ea
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/Android.bp
@@ -0,0 +1,36 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+runtime_resource_overlay {
+ name: "CarUiPortraitFrameworkResRRO",
+ resource_dirs: ["res"],
+ certificate: "platform",
+ manifest: "AndroidManifest.xml",
+ system_ext_specific: true,
+}
+
+android_app {
+ name: "CarUiPortraitFrameworkResRROTest",
+ resource_dirs: ["res"],
+ platform_apis: true,
+ manifest: "AndroidManifest-test.xml",
+ aaptflags: [
+ "--no-resource-deduping",
+ "--no-resource-removal"
+ ],
+}
diff --git a/car_product/car_ui_portrait/rro/android/AndroidManifest-test.xml b/car_product/car_ui_portrait/rro/android/AndroidManifest-test.xml
new file mode 100644
index 0000000..0895ea8
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/AndroidManifest-test.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.caruiportrait.rro.test">
+ <application android:hasCode="false" />
+ <overlay
+ android:targetPackage="android"
+ android:priority="21"
+ android:category="caruiportrait.rro"/>
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/android/AndroidManifest.xml b/car_product/car_ui_portrait/rro/android/AndroidManifest.xml
new file mode 100644
index 0000000..41a3491
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.caruiportrait.rro">
+ <application android:hasCode="false" />
+ <overlay
+ android:targetPackage="android"
+ android:isStatic="true"
+ android:priority="20"
+ android:category="caruiportrait.rro"/>
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/android/res/anim/fade_in.xml b/car_product/car_ui_portrait/rro/android/res/anim/fade_in.xml
new file mode 100644
index 0000000..bff68d0
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/anim/fade_in.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:interpolator/decelerate_quad">
+
+ <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:duration="@android:integer/config_longAnimTime"/>
+</set>
diff --git a/car_product/car_ui_portrait/rro/android/res/anim/fade_out.xml b/car_product/car_ui_portrait/rro/android/res/anim/fade_out.xml
new file mode 100644
index 0000000..b1b60db
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/anim/fade_out.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:interpolator/decelerate_quad">
+
+ <alpha android:fromAlpha="1.0" android:toAlpha="0.0"
+ android:duration="@android:integer/config_longAnimTime"/>
+</set>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_on_accent_device_default.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_on_accent_device_default.xml
new file mode 100644
index 0000000..24f27f2
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_on_accent_device_default.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see primary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:color="@*android:color/system_neutral1_900"/>
+ <item android:color="@*android:color/system_neutral1_400"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_dark.xml
new file mode 100644
index 0000000..c82f99c
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_dark.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see primary_text_material_dark.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:color="@*android:color/system_neutral1_500"/>
+ <item android:color="@*android:color/system_neutral1_50"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_light.xml
new file mode 100644
index 0000000..c9b5a6b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_primary_device_default_light.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see primary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:color="@*android:color/system_neutral1_400"/>
+ <item android:color="@*android:color/system_neutral1_900"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_dark.xml
new file mode 100644
index 0000000..470ed75
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_dark.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see secondary_text_material_dark.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@*android:color/system_neutral2_200"/>
+ <item android:color="@*android:color/system_neutral2_200"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_light.xml
new file mode 100644
index 0000000..30759cc
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_secondary_device_default_light.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see secondary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@*android:color/system_neutral2_700"/>
+ <item android:color="@*android:color/system_neutral2_700"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_dark.xml
new file mode 100644
index 0000000..f3d50db
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_dark.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see tertiary_text_material_dark.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@*android:color/system_neutral2_400"/>
+ <item android:color="@*android:color/system_neutral2_400"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_light.xml
new file mode 100644
index 0000000..638084e
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color-night/text_color_tertiary_device_default_light.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see tertiary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@*android:color/system_neutral2_500"/>
+ <item android:color="@*android:color/system_neutral2_500"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/btn_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color/btn_device_default_dark.xml
new file mode 100644
index 0000000..037fe3e
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/btn_device_default_dark.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@*android:color/system_accent1_200"/>
+ <item android:color="@*android:color/system_accent1_200"/>
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/android/res/color/overview_background.xml b/car_product/car_ui_portrait/rro/android/res/color/overview_background.xml
new file mode 100644
index 0000000..1e98723
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/overview_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/transparent" />
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/android/res/color/overview_background_dark.xml b/car_product/car_ui_portrait/rro/android/res/color/overview_background_dark.xml
new file mode 100644
index 0000000..1e98723
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/overview_background_dark.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/transparent" />
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_on_accent_device_default.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_on_accent_device_default.xml
new file mode 100644
index 0000000..c9b5a6b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_on_accent_device_default.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see primary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:color="@*android:color/system_neutral1_400"/>
+ <item android:color="@*android:color/system_neutral1_900"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_dark.xml
new file mode 100644
index 0000000..0bb281f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_dark.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see primary_text_material_dark.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:color="@*android:color/system_neutral1_400"/>
+ <item android:color="@*android:color/system_neutral1_900"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_light.xml
new file mode 100644
index 0000000..9729379
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_primary_device_default_light.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see primary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:color="@*android:color/system_neutral1_500"/>
+ <item android:color="@*android:color/system_neutral1_50"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_dark.xml
new file mode 100644
index 0000000..4b6483f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_dark.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see secondary_text_material_dark.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@*android:color/system_neutral2_700"/>
+ <item android:color="@*android:color/system_neutral2_700"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_light.xml
new file mode 100644
index 0000000..0d9f871
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_secondary_device_default_light.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see secondary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@*android:color/system_neutral2_200"/>
+ <item android:color="@*android:color/system_neutral2_200"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_dark.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_dark.xml
new file mode 100644
index 0000000..1cf8447
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_dark.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see tertiary_text_material_dark.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@*android:color/system_neutral2_500"/>
+ <item android:color="@*android:color/system_neutral2_500"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_light.xml b/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_light.xml
new file mode 100644
index 0000000..45160e3
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/color/text_color_tertiary_device_default_light.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Please see tertiary_text_material_light.xml -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@*android:color/system_neutral2_400"/>
+ <item android:color="@*android:color/system_neutral2_400"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/drawable/alert_dialog_bg.xml b/car_product/car_ui_portrait/rro/android/res/drawable/alert_dialog_bg.xml
new file mode 100644
index 0000000..b01931d
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/drawable/alert_dialog_bg.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <corners android:radius="@dimen/alert_dialog_corner_radius"/>
+</shape>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/android/res/drawable/btn_borderless_car.xml b/car_product/car_ui_portrait/rro/android/res/drawable/btn_borderless_car.xml
new file mode 100644
index 0000000..0693426
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/drawable/btn_borderless_car.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_focused="true">
+ <inset android:insetLeft="@*android:dimen/button_inset_horizontal_material"
+ android:insetTop="@*android:dimen/button_inset_vertical_material"
+ android:insetRight="@*android:dimen/button_inset_horizontal_material"
+ android:insetBottom="@*android:dimen/button_inset_vertical_material">
+ <selector>
+ <item android:state_focused="true" android:state_pressed="true">
+ <shape android:shape="rectangle">
+ <corners android:radius="?android:attr/buttonCornerRadius" />
+ <solid android:color="#8A94CBFF"/>
+ <stroke android:width="4dp"
+ android:color="#94CBFF"/>
+ <padding android:left="@*android:dimen/button_padding_horizontal_material"
+ android:top="@*android:dimen/button_padding_vertical_material"
+ android:right="@*android:dimen/button_padding_horizontal_material"
+ android:bottom="@*android:dimen/button_padding_vertical_material" />
+ </shape>
+ </item>
+ <item android:state_focused="true">
+ <shape android:shape="rectangle">
+ <corners android:radius="?android:attr/buttonCornerRadius" />
+ <solid android:color="#3D94CBFF"/>
+ <stroke android:width="8dp" android:color="#94CBFF"/>
+ <padding android:left="@*android:dimen/button_padding_horizontal_material"
+ android:top="@*android:dimen/button_padding_vertical_material"
+ android:right="@*android:dimen/button_padding_horizontal_material"
+ android:bottom="@*android:dimen/button_padding_vertical_material" />
+ </shape>
+ </item>
+ </selector>
+ </inset>
+ </item>
+ <item>
+ <ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item android:id="@android:id/mask"
+ android:drawable="@*android:drawable/btn_default_mtrl_shape" />
+ </ripple>
+ </item>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/android/res/drawable/car_dialog_button_background.xml b/car_product/car_ui_portrait/rro/android/res/drawable/car_dialog_button_background.xml
new file mode 100644
index 0000000..a551f99
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/drawable/car_dialog_button_background.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape>
+ <solid android:color="@color/car_alert_dialog_action_button_color"/>
+ <corners android:radius="@dimen/alert_dialog_button_corner_radius"/>
+ </shape>
+ </item>
+ <item>
+ <ripple android:color="?android:attr/colorControlHighlight">
+ <item android:id="@android:id/mask">
+ <color android:color="@*android:color/car_white_1000"/>
+ </item>
+ </ripple>
+ </item>
+</layer-list>
diff --git a/car_product/car_ui_portrait/rro/android/res/drawable/toast_frame.xml b/car_product/car_ui_portrait/rro/android/res/drawable/toast_frame.xml
new file mode 100644
index 0000000..762f1cd
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/drawable/toast_frame.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?android:attr/colorBackgroundFloating" />
+ <corners android:radius="@dimen/toast_corner_radius" />
+</shape>
diff --git a/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog.xml b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog.xml
new file mode 100644
index 0000000..1d0f32d
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<com.android.internal.widget.AlertDialogLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@*android:id/parentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="400dp"
+ android:maxHeight="916dp"
+ android:gravity="start|top"
+ android:background="@color/alert_dialog_background_color"
+ android:orientation="vertical">
+ <include layout="@layout/car_alert_dialog_title" />
+ <FrameLayout
+ android:id="@*android:id/contentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/car_alert_dialog_margin"
+ android:layout_marginEnd="@dimen/car_alert_dialog_margin">
+ <ScrollView
+ android:id="@*android:id/scrollView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <Space
+ android:id="@*android:id/textSpacerNoTitle"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="36dp"/>
+ <TextView
+ android:id="@android:id/message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textColor="@color/alert_dialog_message_text_color"/>
+ <!-- we don't need this spacer, but the id needs to be here for compatibility -->
+ <Space
+ android:id="@*android:id/textSpacerNoButtons"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="0dp" />
+ </LinearLayout>
+ </ScrollView>
+ </FrameLayout>
+ <FrameLayout
+ android:id="@*android:id/customPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="48dp">
+ <FrameLayout
+ android:id="@android:id/custom"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </FrameLayout>
+ <include
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/button_layout_height"
+ layout="@layout/car_alert_dialog_button_bar" />
+</com.android.internal.widget.AlertDialogLayout>
diff --git a/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_button_bar.xml b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_button_bar.xml
new file mode 100644
index 0000000..72294cc
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_button_bar.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@*android:id/buttonPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scrollbarAlwaysDrawVerticalTrack="true"
+ android:scrollIndicators="top|bottom"
+ android:fillViewport="true"
+ style="?android:attr/buttonBarStyle">
+ <com.android.internal.widget.ButtonBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layoutDirection="locale"
+ android:layout_marginTop="@dimen/car_alert_dialog_margin"
+ android:layout_marginLeft="@dimen/car_alert_dialog_margin"
+ android:layout_marginRight="@dimen/car_alert_dialog_margin"
+ android:orientation="horizontal"
+ android:gravity="center">
+ <Button
+ android:id="@android:id/button3"
+ android:background="@drawable/car_dialog_button_background"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/button_layout_height"
+ android:layout_marginRight="@dimen/car_alert_dialog_button_margin"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/alert_dialog_message_text_color"/>
+ <Button
+ android:id="@android:id/button2"
+ android:background="@drawable/car_dialog_button_background"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/button_layout_height"
+ android:layout_marginRight="@dimen/car_alert_dialog_button_margin"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/alert_dialog_message_text_color"/>
+ <Button
+ android:id="@android:id/button1"
+ android:background="@drawable/car_dialog_button_background"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/button_layout_height"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/alert_dialog_message_text_color"/>
+ <Space
+ android:id="@*android:id/spacer"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:visibility="gone" />
+ </com.android.internal.widget.ButtonBarLayout>
+</ScrollView>
diff --git a/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_title.xml b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_title.xml
new file mode 100644
index 0000000..3e08602
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/layout-car/car_alert_dialog_title.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@*android:id/topPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+ <!-- If the client uses a customTitle, it will be added here. -->
+ <RelativeLayout
+ android:id="@*android:id/title_template"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/car_card_header_height"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="44dp"
+ android:layout_height="44dp"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:scaleType="fitCenter"
+ android:src="@null" />
+ <com.android.internal.widget.DialogTitle
+ android:id="@*android:id/alertTitle"
+ android:maxLines="1"
+ android:ellipsize="none"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toEndOf="@+id/icon"
+ android:textAlignment="viewStart"
+ android:textColor="@color/alert_dialog_message_text_color"
+ android:layout_centerVertical="true"/>
+ </RelativeLayout>
+ <Space
+ android:id="@*android:id/titleDividerNoCustom"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="0dp" />
+</LinearLayout>
diff --git a/car_product/car_ui_portrait/rro/android/res/layout/transient_notification.xml b/car_product/car_ui_portrait/rro/android/res/layout/transient_notification.xml
new file mode 100644
index 0000000..2eeaa7a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/layout/transient_notification.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:maxWidth="@dimen/toast_width"
+ android:background="@drawable/toast_frame"
+ android:elevation="@dimen/toast_elevation"
+ android:paddingEnd="@dimen/toast_margin"
+ android:paddingTop="@dimen/toast_margin"
+ android:paddingBottom="@dimen/toast_margin"
+ android:paddingStart="@dimen/toast_margin"
+ android:layout_marginBottom="@dimen/toast_bottom_margin">
+
+ <TextView
+ android:id="@android:id/message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance_Toast"/>
+</LinearLayout>
diff --git a/car_product/car_ui_portrait/rro/android/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/android/res/values-night/colors.xml
new file mode 100644
index 0000000..2a70fa4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values-night/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="alert_dialog_background_color">#202124</color>
+ <color name="alert_dialog_message_text_color">#fff</color>
+ <color name="car_alert_dialog_action_button_color">#2E3134</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values-night/colors_device_default.xml b/car_product/car_ui_portrait/rro/android/res/values-night/colors_device_default.xml
new file mode 100644
index 0000000..2328a0b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values-night/colors_device_default.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <color name="primary_device_default_dark">@*android:color/system_neutral1_900</color>
+ <color name="primary_device_default_light">@*android:color/system_neutral1_50</color>
+ <color name="primary_device_default_settings">@*android:color/system_neutral1_900</color>
+ <color name="primary_device_default_settings_light">@*android:color/primary_device_default_light</color>
+ <color name="primary_dark_device_default_dark">@*android:color/primary_device_default_dark</color>
+ <color name="primary_dark_device_default_light">@*android:color/primary_device_default_light</color>
+ <color name="primary_dark_device_default_settings">@*android:color/primary_device_default_dark</color>
+ <color name="primary_dark_device_default_settings_light">@*android:color/primary_device_default_light</color>
+ <color name="secondary_device_default_settings">@*android:color/secondary_material_settings</color>
+ <color name="secondary_device_default_settings_light">@*android:color/secondary_material_settings_light</color>
+ <color name="tertiary_device_default_settings">@*android:color/tertiary_material_settings</color>
+ <color name="quaternary_device_default_settings">@*android:color/quaternary_material_settings</color>
+ <color name="navigation_bar_divider_device_default_settings">#1f000000</color>
+
+ <!-- Accent colors -->
+ <color name="accent_device_default_light">@*android:color/system_accent1_600</color>
+ <color name="accent_device_default_dark">@*android:color/system_accent1_100</color>
+ <color name="accent_device_default">@*android:color/accent_device_default_light</color>
+ <color name="accent_primary_device_default">@*android:color/system_accent1_100</color>
+ <color name="accent_secondary_device_default">@*android:color/system_accent2_100</color>
+ <color name="accent_tertiary_device_default">@*android:color/system_accent3_100</color>
+
+ <!-- Accent variants -->
+ <color name="accent_primary_variant_light_device_default">@*android:color/system_accent1_600</color>
+ <color name="accent_secondary_variant_light_device_default">@*android:color/system_accent2_600</color>
+ <color name="accent_tertiary_variant_light_device_default">@*android:color/system_accent3_600</color>
+ <color name="accent_primary_variant_dark_device_default">@*android:color/system_accent1_300</color>
+ <color name="accent_secondary_variant_dark_device_default">@*android:color/system_accent2_300</color>
+ <color name="accent_tertiary_variant_dark_device_default">@*android:color/system_accent3_300</color>
+
+ <!-- Background colors -->
+ <color name="background_device_default_dark">@*android:color/system_neutral1_1000</color>
+ <color name="background_device_default_light">@*android:color/system_neutral1_50</color>
+ <color name="background_floating_device_default_dark">@*android:color/car_grey_900</color>
+ <color name="background_floating_device_default_light">@*android:color/background_device_default_light</color>
+
+ <!-- Surface colors -->
+ <color name="surface_header_dark">@*android:color/system_neutral1_700</color>
+ <color name="surface_header_light">@*android:color/system_neutral1_100</color>
+ <color name="surface_variant_dark">@*android:color/system_neutral1_700</color>
+ <color name="surface_variant_light">@*android:color/system_neutral2_100</color>
+ <color name="surface_dark">@*android:color/system_neutral1_800</color>
+ <color name="surface_highlight_light">@*android:color/system_neutral1_0</color>
+
+ <!-- Please refer to text_color_[primary]_device_default_[light].xml for text colors-->
+ <color name="foreground_device_default_light">@*android:color/text_color_primary_device_default_light</color>
+ <color name="foreground_device_default_dark">@*android:color/text_color_primary_device_default_dark</color>
+
+ <!-- Error color -->
+ <color name="error_color_device_default_dark">@*android:color/error_color_material_dark</color>
+ <color name="error_color_device_default_light">@*android:color/error_color_material_light</color>
+
+ <color name="list_divider_color_light">@*android:color/system_neutral1_200</color>
+ <color name="list_divider_color_dark">@*android:color/system_neutral1_700</color>
+ <color name="list_divider_opacity_device_default_light">@android:color/white</color>
+ <color name="list_divider_opacity_device_default_dark">@android:color/white</color>
+
+ <color name="loading_gradient_background_color_dark">#44484C</color>
+ <color name="loading_gradient_background_color_light">#F8F9FA</color>
+ <color name="loading_gradient_highlight_color_dark">#4D5155</color>
+ <color name="loading_gradient_highlight_color_light">#F1F3F4</color>
+
+ <color name="edge_effect_device_default_light">@android:color/black</color>
+ <color name="edge_effect_device_default_dark">@android:color/white</color>
+
+ <color name="floating_background_color">@*android:color/car_grey_900</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values-sw900dp/dimens.xml b/car_product/car_ui_portrait/rro/android/res/values-sw900dp/dimens.xml
new file mode 100644
index 0000000..177ff6d
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values-sw900dp/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <!-- Height of the bottom navigation / climate bar. -->
+ <!-- TODO: remove-->
+ <dimen name="navigation_bar_height">160dp</dimen>
+ <dimen name="navigation_bar_height_landscape">160dp</dimen>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/colors.xml b/car_product/car_ui_portrait/rro/android/res/values/colors.xml
new file mode 100644
index 0000000..22c0af0
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/colors.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="car_alert_dialog_action_button_color">#dadce0</color>
+ <color name="car_card_ripple_background_dark">?android:attr/colorControlHighlight</color>
+ <color name="car_card_ripple_background_light">?android:attr/colorControlHighlight</color>
+
+ <color name="system_accent1_0">#ffffff</color>
+ <!-- duped-->
+ <color name="system_accent1_10">#defbff</color>
+ <color name="system_accent1_50">#defbff</color>
+ <color name="system_accent1_100">#acf6fe</color>
+ <color name="system_accent1_200">#6bf0ff</color>
+ <color name="system_accent1_300">#00e8fe</color>
+ <color name="system_accent1_400">#00e1fa</color>
+ <color name="system_accent1_500">#00daf8</color>
+ <color name="system_accent1_600">#00c9e3</color>
+ <color name="system_accent1_700">#00b2c7</color>
+ <color name="system_accent1_800">#009eae</color>
+ <color name="system_accent1_900">#00797f</color>
+ <color name="system_accent1_1000">#000000</color>
+
+ <color name="system_accent2_0">#ffffff</color>
+ <color name="system_accent2_10">#e5f2ff</color>
+ <color name="system_accent2_50">#e5f2ff</color>
+ <color name="system_accent2_100">#c8deed</color>
+ <color name="system_accent2_200">#aec7da</color>
+ <color name="system_accent2_300">#91afc6</color>
+ <color name="system_accent2_400">#7b9cb5</color>
+ <color name="system_accent2_500">#648aa6</color>
+ <color name="system_accent2_600">#567b94</color>
+ <color name="system_accent2_700">#46667c</color>
+ <color name="system_accent2_800">#385366</color>
+ <color name="system_accent2_900">#263d4e</color>
+ <color name="system_accent2_1000">#000000</color>
+
+ <color name="system_accent3_0">#ffffff</color>
+ <color name="system_accent3_10">#e2f8ed</color>
+ <color name="system_accent3_50">#e2f8ed</color>
+ <color name="system_accent3_100">#b9eed2</color>
+ <color name="system_accent3_200">#8de2b7</color>
+ <color name="system_accent3_300">#5cd69b</color>
+ <color name="system_accent3_400">#29cb86</color>
+ <color name="system_accent3_500">#00c171</color>
+ <color name="system_accent3_600">#00b166</color>
+ <color name="system_accent3_700">#009e59</color>
+ <color name="system_accent3_800">#008c4d</color>
+ <color name="system_accent3_900">#006c37</color>
+ <color name="system_accent3_1000">#000000</color>
+
+ <color name="error_color_device_default_dark">#ec928e</color> <!-- Material Red 300 -->
+ <color name="error_color_device_default_light">#b3261e</color> <!-- Material Red 600 -->
+
+ <color name="list_divider_color">#2E3134</color>
+ <color name="alert_dialog_background_color">#f1f3f4</color>
+ <color name="alert_dialog_message_text_color">#000</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/colors_device_default.xml b/car_product/car_ui_portrait/rro/android/res/values/colors_device_default.xml
new file mode 100644
index 0000000..c9e8d2d
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/colors_device_default.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!--title bars-->
+ <color name="primary_device_default_dark">@*android:color/system_neutral1_200</color>
+ <color name="primary_device_default_light">@*android:color/system_neutral1_300</color>
+
+ <color name="primary_device_default_settings">@*android:color/system_neutral1_200</color>
+ <color name="primary_device_default_settings_light">@*android:color/primary_device_default_light</color>
+ <color name="primary_dark_device_default_dark">@*android:color/primary_device_default_dark</color>
+ <color name="primary_dark_device_default_light">@*android:color/primary_device_default_light</color>
+ <color name="primary_dark_device_default_settings">@*android:color/primary_device_default_dark</color>
+ <color name="primary_dark_device_default_settings_light">@*android:color/primary_device_default_light</color>
+ <color name="secondary_device_default_settings">@*android:color/secondary_material_settings</color>
+ <color name="secondary_device_default_settings_light">@*android:color/secondary_material_settings_light</color>
+ <color name="tertiary_device_default_settings">@*android:color/tertiary_material_settings</color>
+ <color name="quaternary_device_default_settings">@*android:color/quaternary_material_settings</color>
+ <color name="navigation_bar_divider_device_default_settings">#1f000000</color>
+
+ <!-- Accent colors edit -->
+ <color name="accent_device_default_light">@*android:color/system_accent1_200</color>
+ <color name="accent_device_default_dark">@*android:color/system_accent1_200</color>
+ <color name="accent_device_default">@*android:color/accent_device_default_light</color>
+ <color name="accent_primary_device_default">@*android:color/system_accent1_200</color>
+ <color name="accent_secondary_device_default">@*android:color/system_accent2_300</color>
+ <color name="accent_tertiary_device_default">@*android:color/system_accent3_300</color>
+
+ <!-- Accent variants edit -->
+ <color name="accent_primary_variant_light_device_default">@*android:color/system_accent1_200</color>
+ <color name="accent_secondary_variant_light_device_default">@*android:color/system_accent2_300</color>
+ <color name="accent_tertiary_variant_light_device_default">@*android:color/system_accent3_300</color>
+ <color name="accent_primary_variant_dark_device_default">@*android:color/system_accent1_300</color>
+ <color name="accent_secondary_variant_dark_device_default">@*android:color/system_accent2_300</color>
+ <color name="accent_tertiary_variant_dark_device_default">@*android:color/system_accent3_300</color>
+
+ <!-- Background colors -->
+ <color name="background_device_default_dark">@*android:color/system_neutral1_0</color>
+ <color name="background_device_default_light">@*android:color/system_neutral1_900</color>
+ <color name="background_floating_device_default_dark">@*android:color/car_grey_300</color>
+ <color name="background_floating_device_default_light">@*android:color/background_device_default_light</color>
+
+ <!-- Surface colors -->
+ <color name="surface_header_dark">@*android:color/system_neutral1_100</color>
+ <color name="surface_header_light">@*android:color/system_neutral1_700</color>
+ <color name="surface_variant_dark">@*android:color/system_neutral1_100</color>
+ <color name="surface_variant_light">@*android:color/system_neutral2_700</color>
+ <color name="surface_dark">@*android:color/system_neutral1_200</color>
+ <color name="surface_highlight_light">@*android:color/system_neutral1_1000</color>
+
+ <!-- Please refer to text_color_[primary]_device_default_[light].xml for text colors-->
+ <color name="foreground_device_default_light">@*android:color/text_color_primary_device_default_light</color>
+ <color name="foreground_device_default_dark">@*android:color/text_color_primary_device_default_dark</color>
+
+ <color name="list_divider_color_light">@*android:color/system_neutral1_700</color>
+ <color name="list_divider_color_dark">@*android:color/system_neutral1_200</color>
+ <color name="list_divider_opacity_device_default_light">@android:color/black</color>
+ <color name="list_divider_opacity_device_default_dark">@android:color/black</color>
+
+ <color name="loading_gradient_background_color_dark">#F8F9FA</color>
+ <color name="loading_gradient_background_color_light">#44484C</color>
+ <color name="loading_gradient_highlight_color_dark">#F1F3F4</color>
+ <color name="loading_gradient_highlight_color_light">#4D5155</color>
+
+ <color name="edge_effect_device_default_light">@android:color/white</color>
+ <color name="edge_effect_device_default_dark">@android:color/black</color>
+
+ <color name="floating_background_color">@*android:color/car_grey_300</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/config.xml b/car_product/car_ui_portrait/rro/android/res/values/config.xml
new file mode 100644
index 0000000..8ab71a7
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/config.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- IME should not hide nav bar -->
+ <bool name="config_automotiveHideNavBarForKeyboard">false</bool>
+
+ <!-- Class name of the device specific implementation of DisplayAreaPolicy.Provider
+ or empty if the default should be used. -->
+ <string translatable="false" name="config_deviceSpecificDisplayAreaPolicyProvider">
+ com.android.server.wm.CarDisplayAreaPolicyProvider
+ </string>
+
+ <!-- Colon separated list of package names that should be granted Notification Listener access -->
+ <string name="config_defaultListenerAccessPackages" translatable="false">com.android.car.notification</string>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/dimens.xml b/car_product/car_ui_portrait/rro/android/res/values/dimens.xml
new file mode 100644
index 0000000..7a325ad
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/dimens.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <!-- Height of the status bar -->
+ <dimen name="status_bar_height">92dp</dimen>
+ <!-- Height of the bottom navigation / climate bar. -->
+ <dimen name="navigation_bar_height">160dp</dimen>
+ <dimen name="navigation_bar_height_landscape">160dp</dimen>
+
+ <!-- ****** Alert dialog dimens ***** -->
+
+ <!-- Dialog corner radius -->
+ <dimen name="alert_dialog_corner_radius">24dp</dimen>
+ <!-- Dialog button corner radius -->
+ <dimen name="alert_dialog_button_corner_radius">16dp</dimen>
+ <!-- Dialog header size -->
+ <dimen name="car_card_header_height">88dp</dimen>
+ <!-- Dialog image size -->
+ <dimen name="car_alert_dialog_title_image_size">@dimen/car_card_header_height</dimen>
+ <!-- Default dialog margin -->
+ <dimen name="car_alert_dialog_margin">36dp</dimen>
+ <!-- Dialog button layout height -->
+ <dimen name="button_layout_height">88dp</dimen>
+ <!-- Default dialog button margin -->
+ <dimen name="car_alert_dialog_button_margin">24dp</dimen>
+
+ <!-- ****** Toast dimens ***** -->
+
+ <!-- Toast corner radius -->
+ <dimen name="toast_corner_radius">24dp</dimen>
+ <!-- Toast margin -->
+ <dimen name="toast_margin">24dp</dimen>
+ <!-- Toast elevation -->
+ <dimen name="toast_elevation">2dp</dimen>
+ <!-- Toast max width -->
+ <dimen name="toast_width">760dp</dimen>
+ <!-- Toast y offset (should be the same as the height of the audio bar -->
+ <dimen name="toast_y_offset">136dp</dimen>
+ <dimen name="toast_bottom_margin">32dp</dimen>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/styles.xml b/car_product/car_ui_portrait/rro/android/res/values/styles.xml
new file mode 100644
index 0000000..c32411b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/styles.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+
+<resources>
+ <style name="DialogActionButton">
+ <item name="android:textSize">32sp</item>
+ <item name="android:textColor">@android:color/white</item>
+ <item name="android:textStyle">normal</item>
+ <item name="android:background">@drawable/car_dialog_button_background</item>
+ </style>
+
+ <style name="TextAppearance_Toast">
+ <item name="android:textColorHighlight">?android:textColorHighlight</item>
+ <item name="android:textColorHint">?android:textColorHint</item>
+ <item name="android:textColorLink">?android:textColorLink</item>
+ <item name="android:textStyle">normal</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ <item name="android:textSize">28sp</item>
+ <item name="android:lineHeight">36sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
+ <!-- Override the default activity transitions. We have to do a full copy and not just inherit
+ and override because we're replacing the default style across the system.
+ -->
+ <style name="Animation.Activity" parent="*android:Animation.Material.Activity">
+ <item name="android:activityOpenEnterAnimation">@*android:anim/fade_in</item>
+ <item name="android:activityOpenExitAnimation">@*android:anim/fade_out</item>
+ <item name="android:activityCloseEnterAnimation">@*android:anim/fade_in</item>
+ <item name="android:activityCloseExitAnimation">@*android:anim/fade_out</item>
+ <item name="android:taskOpenEnterAnimation">@*android:anim/fade_in</item>
+ <item name="android:taskOpenExitAnimation">@*android:anim/fade_out</item>
+ <item name="android:launchTaskBehindTargetAnimation">@*android:anim/launch_task_behind_target</item>
+ <item name="android:launchTaskBehindSourceAnimation">@*android:anim/launch_task_behind_source</item>
+ <item name="android:taskCloseEnterAnimation">@*android:anim/fade_in</item>
+ <item name="android:taskCloseExitAnimation">@*android:anim/fade_out</item>
+ <item name="android:taskToFrontEnterAnimation">@*android:anim/fade_in</item>
+ <item name="android:taskToFrontExitAnimation">@*android:anim/fade_out</item>
+ <item name="android:taskToBackEnterAnimation">@*android:anim/task_close_enter</item>
+ <item name="android:taskToBackExitAnimation">@*android:anim/task_close_exit</item>
+ <item name="android:wallpaperOpenEnterAnimation">@*android:anim/wallpaper_open_enter</item>
+ <item name="android:wallpaperOpenExitAnimation">@*android:anim/wallpaper_open_exit</item>
+ <item name="android:wallpaperCloseEnterAnimation">@*android:anim/wallpaper_close_enter</item>
+ <item name="android:wallpaperCloseExitAnimation">@*android:anim/wallpaper_close_exit</item>
+ <item name="android:wallpaperIntraOpenEnterAnimation">@*android:anim/wallpaper_intra_open_enter</item>
+ <item name="android:wallpaperIntraOpenExitAnimation">@*android:anim/wallpaper_intra_open_exit</item>
+ <item name="android:wallpaperIntraCloseEnterAnimation">@*android:anim/wallpaper_intra_close_enter</item>
+ <item name="android:wallpaperIntraCloseExitAnimation">@*android:anim/wallpaper_intra_close_exit</item>
+ <item name="android:fragmentOpenEnterAnimation">@*android:animator/fragment_open_enter</item>
+ <item name="android:fragmentOpenExitAnimation">@*android:animator/fragment_open_exit</item>
+ <item name="android:fragmentCloseEnterAnimation">@*android:animator/fragment_close_enter</item>
+ <item name="android:fragmentCloseExitAnimation">@*android:animator/fragment_close_exit</item>
+ <item name="android:fragmentFadeEnterAnimation">@*android:animator/fragment_fade_enter</item>
+ <item name="android:fragmentFadeExitAnimation">@*android:animator/fragment_fade_exit</item>
+ </style>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/styles_device_default.xml b/car_product/car_ui_portrait/rro/android/res/values/styles_device_default.xml
new file mode 100644
index 0000000..fadcfd5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/styles_device_default.xml
@@ -0,0 +1,164 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+
+<!--
+ This is an override of frameworks/base/core/res/res/values/styles_device_default.xml
+ It is how the device default is changed to match the desired look for a car theme.
+-->
+<resources>
+
+ <style name="TextAppearance.DeviceDefault" parent="android:TextAppearance.Material.Large">
+ <item name="android:textSize">@*android:dimen/car_body3_size</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ </style>
+ <style name="TextAppearance.DeviceDefault.Inverse" parent="android:TextAppearance.Material.Inverse">
+ <item name="android:textSize">@*android:dimen/car_body3_size</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ </style>
+
+ <style name="TextAppearance.DeviceDefault.Large" parent="android:TextAppearance.Material.Large">
+ <item name="android:textSize">@*android:dimen/car_body1_size</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ </style>
+ <style name="TextAppearance.DeviceDefault.Large.Inverse" parent="android:TextAppearance.Material.Large.Inverse">
+ <item name="android:textSize">@*android:dimen/car_body1_size</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ </style>
+
+ <style name="TextAppearance.DeviceDefault.Medium" parent="android:TextAppearance.Material.Medium">
+ <item name="android:textSize">@*android:dimen/car_body2_size</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+ </style>
+ <style name="TextAppearance.DeviceDefault.Medium.Inverse" parent="android:TextAppearance.Material.Medium.Inverse">
+ <item name="android:textSize">@*android:dimen/car_body2_size</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item>
+ </style>
+
+ <style name="TextAppearance.DeviceDefault.Small" parent="android:TextAppearance.Material.Small">
+ <item name="android:textSize">@*android:dimen/car_body4_size</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ </style>
+ <style name="TextAppearance.DeviceDefault.Small.Inverse" parent="android:TextAppearance.Material.Small.Inverse">
+ <item name="android:textSize">@*android:dimen/car_body4_size</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ </style>
+
+ <style name="TextAppearance.DeviceDefault.Subhead" parent="android:TextAppearance.Material.Subhead">
+ <item name="android:textSize">@*android:dimen/car_body1_size</item>
+ <item name="android:fontFamily">@*android:string/config_bodyFontFamily</item>
+ </style>
+
+ <style name="TextAppearance.DeviceDefault.Widget.Button.Borderless.Colored"
+ parent="android:TextAppearance.DeviceDefault.Widget.Button">
+ <item name="android:textColor">@*android:color/car_borderless_button_text_color</item>
+ </style>
+
+ <style name="DialogWindowTitle.DeviceDefault" parent="*android:DialogWindowTitle.Material">
+ <item name="android:textAppearance">@*android:style/TextAppearance.DeviceDefault.DialogWindowTitle</item>
+ </style>
+ <style name="TextAppearance.DeviceDefault.DialogWindowTitle" parent="android:TextAppearance.Material.DialogWindowTitle">
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@*android:dimen/car_body2_size</item>
+ <item name="android:textColor">@*android:color/car_body2</item>
+ </style>
+ <style name="TextAppearance.Material.DialogWindowTitle" parent="android:TextAppearance.Material.Title" />
+ <style name="TextAppearance.Material.Title" parent="android:TextAppearance.Material">
+ <item name="android:textSize">@*android:dimen/text_size_title_material</item>
+ <item name="android:fontFamily">@*android:string/font_family_title_material</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
+ <style name="TextAppearance.DeviceDefault.Widget.Button" parent="android:TextAppearance.Material.Widget.Button">
+ <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+ <item name="android:textAllCaps">@*android:bool/config_buttonTextAllCaps</item>
+ <item name="android:textSize">@*android:dimen/car_action1_size</item>
+ <item name="android:textColor">@*android:color/car_button_text_color</item>
+ </style>
+
+ <style name="Widget.DeviceDefault.TextView" parent="android:Widget.Material.TextView">
+ <item name="android:ellipsize">none</item>
+ <item name="android:textSize">@*android:dimen/car_body1_size</item>
+ <item name="android:requiresFadingEdge">horizontal</item>
+ <item name="android:fadingEdgeLength">@*android:dimen/car_textview_fading_edge_length</item>
+ </style>
+
+ <style name="Widget.DeviceDefault.Button" parent="android:Widget.Material.Button">
+ <item name="android:singleLine">true</item>
+ <item name="android:ellipsize">none</item>
+ <item name="android:requiresFadingEdge">horizontal</item>
+ <item name="android:fadingEdgeLength">@*android:dimen/car_textview_fading_edge_length</item>
+ <item name="android:background">@*android:drawable/car_button_background</item>
+ <item name="android:layout_height">@*android:dimen/car_button_height</item>
+ <item name="android:minWidth">@*android:dimen/car_button_min_width</item>
+ <item name="android:paddingStart">@*android:dimen/car_button_horizontal_padding</item>
+ <item name="android:paddingEnd">@*android:dimen/car_button_horizontal_padding</item>
+ </style>
+
+ <style name="Widget.DeviceDefault.Button.Borderless" parent="android:Widget.Material.Button.Borderless">
+ <item name="android:background">@drawable/btn_borderless_car</item>
+ </style>
+
+ <style name="Widget.DeviceDefault.CompoundButton.CheckBox" parent="android:Widget.Material.CompoundButton.CheckBox">
+ <item name="android:button">@*android:drawable/car_checkbox</item>
+ </style>
+
+ <style name="Widget.DeviceDefault.CompoundButton.Switch" parent="android:Widget.Material.CompoundButton.Switch">
+ <item name="android:thumb">@*android:drawable/car_switch_thumb</item>
+ <item name="android:track">@*android:drawable/car_switch_track</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ </style>
+
+ <style name="Widget.DeviceDefault.ProgressBar.Horizontal" parent="android:Widget.Material.ProgressBar.Horizontal">
+ <item name="android:minHeight">@*android:dimen/car_progress_bar_height</item>
+ <item name="android:maxHeight">@*android:dimen/car_progress_bar_height</item>
+ </style>
+
+ <style name="Widget.DeviceDefault.SeekBar" parent="android:Widget.Material.SeekBar">
+ <item name="android:progressDrawable">@*android:drawable/car_seekbar_track</item>
+ <item name="android:thumb">@*android:drawable/car_seekbar_thumb</item>
+ </style>
+
+ <style name="Widget.DeviceDefault.ActionBar.Solid" parent="android:Widget.Material.ActionBar.Solid">
+ <item name="android:textSize">@*android:dimen/car_body3_size</item>
+ </style>
+
+ <!-- Preference Styles -->
+ <style name="Preference.DeviceDefault" parent="*android:Preference.Material">
+ <item name="android:layout">@*android:layout/car_preference</item>
+ </style>
+ <style name="Preference.DeviceDefault.Category" parent="*android:Preference.Material.Category">
+ <item name="android:layout">@*android:layout/car_preference_category</item>
+ </style>
+ <style name="Preference.DeviceDefault.CheckBoxPreference" parent="*android:Preference.Material.CheckBoxPreference">
+ <item name="android:layout">@*android:layout/car_preference</item>
+ </style>
+ <style name="Preference.DeviceDefault.DialogPreference" parent="*android:Preference.Material.DialogPreference">
+ <item name="android:layout">@*android:layout/car_preference</item>
+ </style>
+ <style name="Preference.DeviceDefault.DialogPreference.EditTextPreference" parent="*android:Preference.Material.DialogPreference.EditTextPreference">
+ <item name="android:layout">@*android:layout/car_preference</item>
+ </style>
+ <style name="Preference.DeviceDefault.SwitchPreference" parent="*android:Preference.Material.SwitchPreference">
+ <item name="android:layout">@*android:layout/car_preference</item>
+ </style>
+
+ <!-- AlertDialog Style -->
+ <style name="AlertDialog.DeviceDefault" parent="*android:AlertDialog.Material">
+ <item name="android:layout">@*android:layout/car_alert_dialog</item>
+ </style>
+
+</resources>
diff --git a/car_product/car_ui_portrait/rro/android/res/values/themes_device_defaults.xml b/car_product/car_ui_portrait/rro/android/res/values/themes_device_defaults.xml
new file mode 100644
index 0000000..bcdfc26
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/android/res/values/themes_device_defaults.xml
@@ -0,0 +1,263 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+
+<!--
+ This is an override of frameworks/base/core/res/res/values/themes_device_defaults.xml
+ It is how the device default is changed to match the desired look for a car theme.
+-->
+<resources>
+ <style name="Theme.DeviceDefault" parent="*android:Theme.DeviceDefaultBase">
+ <!-- Text styles -->
+ <!-- TODO clean up -->
+
+ <item name="android:textAppearanceListItem">@*android:style/TextAppearance.DeviceDefault.Large</item>
+ <item name="android:textAppearanceListItemSmall">@*android:style/TextAppearance.DeviceDefault.Large</item>
+ <item name="android:textAppearanceListItemSecondary">@*android:style/TextAppearance.DeviceDefault.Small</item>
+
+ <item name="android:borderlessButtonStyle">@*android:style/Widget.DeviceDefault.Button.Borderless.Colored</item>
+ <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+ <item name="android:buttonStyle">@*android:style/Widget.DeviceDefault.Button</item>
+
+
+ <item name="android:listPreferredItemHeightSmall">@*android:dimen/car_single_line_list_item_height</item>
+
+
+ <item name="android:selectableItemBackground">@*android:drawable/item_background</item>
+
+ <item name="android:actionBarSize">@*android:dimen/car_app_bar_height</item>
+
+ <!-- Color palette -->
+ <item name="android:colorBackgroundFloating">@color/floating_background_color</item>
+ <item name="android:statusBarColor">@android:color/black</item>
+ <item name="android:colorButtonNormal">@color/btn_device_default_dark</item>
+ <item name="android:colorControlHighlight">@color/btn_device_default_dark</item>
+ <item name="android:colorControlNormal">@color/btn_device_default_dark</item>
+
+ <item name="android:listDivider">@color/list_divider_color</item>
+ <item name="android:alertDialogTheme">@android:style/Theme.DeviceDefault.Dialog.Alert</item>
+ </style>
+
+ <style name="Theme.DeviceDefault.Dialog" parent="android:Theme.Material.Dialog">
+ <item name="android:textAppearanceLarge">@*android:style/TextAppearance.DeviceDefault.Large</item>
+ <item name="android:textAppearanceMedium">@*android:style/TextAppearance.DeviceDefault.Medium</item>
+ <item name="android:textAppearanceSmall">@*android:style/TextAppearance.DeviceDefault.Small</item>
+ <item name="android:textAppearanceLargeInverse">@*android:style/TextAppearance.DeviceDefault.Large.Inverse</item>
+ <item name="android:textAppearanceMediumInverse">@*android:style/TextAppearance.DeviceDefault.Medium.Inverse</item>
+ <item name="android:textAppearanceSmallInverse">@*android:style/TextAppearance.DeviceDefault.Small.Inverse</item>
+ <item name="android:textAppearanceListItem">@*android:style/TextAppearance.DeviceDefault.Large</item>
+ <item name="android:textAppearanceListItemSmall">@*android:style/TextAppearance.DeviceDefault.Large</item>
+ <item name="android:textAppearanceListItemSecondary">@*android:style/TextAppearance.DeviceDefault.Small</item>
+ <item name="android:textAppearanceButton">@*android:style/Widget.DeviceDefault.Button</item>
+ <item name="android:borderlessButtonStyle">@*android:style/Widget.DeviceDefault.Button.Borderless.Colored</item>
+ <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+ <item name="android:buttonStyle">@*android:style/Widget.DeviceDefault.Button</item>
+ <item name="android:selectableItemBackground">@*android:drawable/item_background</item>
+ <item name="android:windowTitleStyle">?android:attr/textAppearanceLarge</item>
+ <!-- Color palette -->
+ <item name="android:colorButtonNormal">@color/btn_device_default_dark</item>
+ </style>
+
+ <style name="Theme.DeviceDefault.Dialog.NoActionBar" parent="android:Theme.DeviceDefault.Dialog">
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+ </style>
+
+ <style name="Theme.DeviceDefault.Dialog.Alert" parent="android:Theme.Material.Dialog.Alert">
+
+ <item name="android:textAppearanceLarge">@*android:style/TextAppearance.DeviceDefault.Large</item>
+ <item name="android:textAppearanceMedium">@*android:style/TextAppearance.DeviceDefault.Medium</item>
+ <item name="android:textAppearanceSmall">@*android:style/TextAppearance.DeviceDefault.Small</item>
+ <item name="android:textAppearanceButton">@*android:style/Widget.DeviceDefault.Button</item>
+ <item name="android:alertDialogStyle">@*android:style/AlertDialog.DeviceDefault</item>
+ <item name="android:borderlessButtonStyle">@*android:style/Widget.DeviceDefault.Button.Borderless.Colored</item>
+ <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+ <item name="android:buttonStyle">@*android:style/Widget.DeviceDefault.Button</item>
+ <item name="android:selectableItemBackground">@*android:drawable/item_background</item>
+ <item name="android:textAppearanceListItem">@*android:style/TextAppearance.DeviceDefault.Large</item>
+ <item name="android:textAppearanceListItemSmall">@*android:style/TextAppearance.DeviceDefault.Large</item>
+ <item name="android:textAppearanceListItemSecondary">@*android:style/TextAppearance.DeviceDefault.Small</item>
+ <item name="android:windowTitleStyle">?android:attr/textAppearanceLarge</item>
+ <!-- Color palette -->
+ <item name="android:colorButtonNormal">@color/btn_device_default_dark</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:textColorPrimary">@color/alert_dialog_message_text_color</item>
+ </style>
+
+ <style name="Theme.DeviceDefault.Settings.Dialog" parent="android:Theme.DeviceDefault.Dialog.Alert">
+ </style>
+
+ <!-- The light theme is defined to be the same as the default since currently there is only one
+ defined theme palette -->
+ <style name="Theme.DeviceDefault.Light" parent="android:Theme.DeviceDefault"/>
+ <style name="Theme.DeviceDefault.Light.Dialog" parent="android:Theme.DeviceDefault.Dialog"/>
+ <style name="Theme.DeviceDefault.Light.Dialog.Alert" parent="android:Theme.DeviceDefault.Dialog.Alert"/>
+ <style name="Theme.DeviceDefault.Light.Dialog.NoActionBar" parent="android:Theme.DeviceDefault.Dialog.NoActionBar"/>
+
+ <style name="Theme.DeviceDefault.Light.NoActionBar" parent="android:Theme.DeviceDefault.Light">
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+ </style>
+ <style name="Theme.DeviceDefault.NoActionBar" parent="android:Theme.DeviceDefault">
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+ </style>
+
+ <style name="Theme.DeviceDefault.InputMethod" parent="android:Theme.Material.InputMethod">
+ <!-- Color palette -->
+ <item name="android:colorAccent">@*android:color/accent_device_default_light</item>
+ <item name="android:colorBackground">@*android:color/primary_device_default_light</item>
+ <item name="android:listDivider">@*android:color/car_keyboard_divider_line</item>
+ <item name="android:selectableItemBackground">@*android:drawable/item_background</item>
+ <item name="android:textColorPrimary">@*android:color/car_keyboard_text_primary_color</item>
+ <item name="android:textColorSecondary">@*android:color/car_keyboard_text_secondary_color</item>
+ </style>
+
+ <style name="Theme.DeviceDefault.Settings" parent="android:Theme.DeviceDefault"/>
+ <style name="Theme.DeviceDefault.Settings.NoActionBar" parent="android:Theme.DeviceDefault.NoActionBar"/>
+
+ <style name="Theme.DeviceDefault.Light.DarkActionBar" parent="android:Theme.DeviceDefault"/>
+ <!-- DeviceDefault theme for the default system theme. -->
+ <style name="Theme.DeviceDefault.System" parent="android:Theme.DeviceDefault.Light.DarkActionBar" />
+
+ <!-- Theme used for the intent picker activity. -->
+ <style name="Theme.DeviceDefault.Resolver" parent="android:Theme.DeviceDefault">
+ <item name="android:windowEnterTransition">@empty</item>
+ <item name="android:windowExitTransition">@empty</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:backgroundDimEnabled">true</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:colorControlActivated">?*android:attr/colorControlHighlight</item>
+ <item name="android:listPreferredItemPaddingStart">?*android:attr/dialogPreferredPadding</item>
+ <item name="android:listPreferredItemPaddingEnd">?*android:attr/dialogPreferredPadding</item>
+
+ <!-- Dialog attributes -->
+ <item name="android:dialogCornerRadius">@*android:dimen/config_dialogCornerRadius</item>
+
+ <!-- Button styles -->
+ <item name="android:buttonCornerRadius">@*android:dimen/config_buttonCornerRadius</item>
+ <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+ <item name="android:borderlessButtonStyle">@*android:style/Widget.DeviceDefault.Button.Borderless.Colored</item>
+ <item name="android:buttonStyle">@*android:style/Widget.DeviceDefault.Button</item>
+
+ <!-- Color palette -->
+ <item name="android:colorButtonNormal">@color/btn_device_default_dark</item>
+
+ <!-- Progress bar attributes -->
+ <item name="*android:colorProgressBackgroundNormal">@*android:color/config_progress_background_tint</item>
+ <item name="*android:progressBarCornerRadius">@*android:dimen/config_progressBarCornerRadius</item>
+
+ <!-- Toolbar attributes -->
+ <item name="android:toolbarStyle">@*android:style/Widget.DeviceDefault.Toolbar</item>
+
+ <item name="*android:toastFrameBackground">@*android:drawable/toast_frame</item>
+ <item name="android:textAppearanceListItem">@android:style/TextAppearance.DeviceDefault.Large</item>
+ <item name="android:textAppearanceListItemSmall">@android:style/TextAppearance.DeviceDefault.Large</item>
+ <item name="android:textAppearanceListItemSecondary">@android:style/TextAppearance.DeviceDefault.Small</item>
+
+ <!-- Icon sizes -->
+ <item name="*android:iconfactoryIconSize">@*android:dimen/resolver_icon_size</item>
+ <item name="*android:iconfactoryBadgeSize">@*android:dimen/resolver_badge_size</item>
+ </style>
+
+
+ <!-- DeviceDefault theme for windows that want to have the user's selected wallpaper appear
+ behind them. -->
+ <style name="Theme.DeviceDefault.Wallpaper" parent="android:Theme.DeviceDefault">
+ <!-- Color palette -->
+
+ <!-- Dialog attributes -->
+ <item name="android:dialogCornerRadius">@*android:dimen/config_dialogCornerRadius</item>
+
+ <!-- Text styles -->
+ <item name="android:textAppearanceButton">@*android:style/TextAppearance.DeviceDefault.Widget.Button</item>
+
+ <!-- Button styles -->
+ <item name="android:buttonCornerRadius">@*android:dimen/config_buttonCornerRadius</item>
+ <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+
+ <!-- Progress bar attributes -->
+ <item name="*android:colorProgressBackgroundNormal">@*android:color/config_progress_background_tint</item>
+ <item name="*android:progressBarCornerRadius">@*android:dimen/config_progressBarCornerRadius</item>
+
+ <!-- Toolbar attributes -->
+ <item name="android:toolbarStyle">@*android:style/Widget.DeviceDefault.Toolbar</item>
+
+ <item name="android:windowBackground">@*android:color/transparent</item>
+ <item name="android:colorBackgroundCacheHint">@null</item>
+ <item name="android:windowShowWallpaper">true</item>
+ </style>
+
+ <!-- DeviceDefault theme for windows that want to have the user's selected wallpaper appear
+ behind them and without an action bar. -->
+ <style name="Theme.DeviceDefault.Wallpaper.NoTitleBar" parent="android:Theme.DeviceDefault.Wallpaper">
+ <!-- Color palette -->
+
+ <!-- Dialog attributes -->
+ <item name="android:dialogCornerRadius">@*android:dimen/config_dialogCornerRadius</item>
+
+ <!-- Text styles -->
+ <item name="android:textAppearanceButton">@*android:style/TextAppearance.DeviceDefault.Widget.Button</item>
+
+ <!-- Button styles -->
+ <item name="android:buttonCornerRadius">@*android:dimen/config_buttonCornerRadius</item>
+ <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+
+ <!-- Progress bar attributes -->
+ <item name="*android:colorProgressBackgroundNormal">@*android:color/config_progress_background_tint</item>
+ <item name="*android:progressBarCornerRadius">@*android:dimen/config_progressBarCornerRadius</item>
+
+ <!-- Toolbar attributes -->
+ <item name="android:toolbarStyle">@*android:style/Widget.DeviceDefault.Toolbar</item>
+
+ <item name="android:windowNoTitle">true</item>
+ </style>
+
+ <!-- DeviceDefault theme for panel windows. This removes all extraneous window decorations, so
+ you basically have an empty rectangle in which to place your content. It makes the window
+ floating, with a transparent background, and turns off dimming behind the window.
+ Used for Autofill screens.-->
+ <style name="Theme.DeviceDefault.Panel" parent="android:Theme.Material.Panel">
+ <!-- Color palette -->
+
+ <!-- Dialog attributes -->
+ <item name="android:dialogCornerRadius">@*android:dimen/config_dialogCornerRadius</item>
+
+ <!-- Text styles -->
+ <item name="android:textAppearanceButton">@*android:style/TextAppearance.DeviceDefault.Widget.Button</item>
+
+ <!-- Button styles -->
+ <item name="android:buttonCornerRadius">@*android:dimen/config_buttonCornerRadius</item>
+ <item name="android:buttonBarButtonStyle">@*android:style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog</item>
+
+ <!-- Progress bar attributes -->
+ <item name="*android:colorProgressBackgroundNormal">@*android:color/config_progress_background_tint</item>
+ <item name="*android:progressBarCornerRadius">@*android:dimen/config_progressBarCornerRadius</item>
+
+ <!-- Toolbar attributes -->
+ <item name="android:toolbarStyle">@*android:style/Widget.DeviceDefault.Toolbar</item>
+
+ <!-- Hide action bar -->
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+
+ <item name="android:selectableItemBackground">@*android:drawable/item_background</item>
+ </style>
+
+ <style name="Theme.DeviceDefault.Light.Panel" parent="android:Theme.DeviceDefault.Panel"/>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/Android.mk b/car_product/car_ui_portrait/rro/car-ui-customizations/Android.mk
new file mode 100644
index 0000000..96c6d30
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/Android.mk
@@ -0,0 +1,57 @@
+#
+# Copyright (C) 2021 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+CAR_UI_RRO_SET_NAME := generated_caruiportrait_customization
+CAR_UI_RRO_MANIFEST_FILE := $(LOCAL_PATH)/AndroidManifest.xml
+CAR_UI_RESOURCE_DIR := $(LOCAL_PATH)/res
+CAR_UI_RRO_TARGETS := \
+ com.android.car.ui.paintbooth \
+ com.google.android.car.ui.paintbooth \
+ com.google.android.carui.ats \
+ com.android.car.rotaryplayground \
+ com.android.car.themeplayground \
+ com.android.car.carlauncher \
+ com.android.car.home \
+ com.android.car.media \
+ com.android.car.radio \
+ com.android.car.calendar \
+ com.android.car.companiondevicesupport \
+ com.android.car.systemupdater \
+ com.android.car.dialer \
+ com.android.car.linkviewer \
+ com.android.car.settings \
+ com.android.car.voicecontrol \
+ com.android.car.faceenroll \
+ com.android.car.developeroptions \
+ com.android.managedprovisioning \
+ com.android.settings.intelligence \
+ com.google.android.apps.automotive.inputmethod \
+ com.google.android.apps.automotive.inputmethod.dev \
+ com.google.android.apps.automotive.templates.host \
+ com.google.android.embedded.projection \
+ com.google.android.gms \
+ com.google.android.gsf \
+ com.google.android.packageinstaller \
+ com.google.android.permissioncontroller \
+ com.google.android.carassistant \
+ com.google.android.tts \
+ com.android.htmlviewer \
+ com.android.vending \
+
+include packages/apps/Car/libs/car-ui-lib/generate_rros.mk
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/AndroidManifest.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/AndroidManifest.xml
new file mode 100644
index 0000000..9d3a1a4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="{{RRO_PACKAGE_NAME}}">
+ <application android:hasCode="false"/>
+ <overlay android:priority="10"
+ android:targetName="car-ui-lib"
+ android:targetPackage="{{TARGET_PACKAGE_NAME}}"
+ android:resourcesMap="@xml/overlays"
+ android:isStatic="true"
+ android:requiredSystemPropertyName="ro.build.characteristics"
+ android:requiredSystemPropertyValue="automotive"/>
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/README b/car_product/car_ui_portrait/rro/car-ui-customizations/README
new file mode 100644
index 0000000..1e14b27
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/README
@@ -0,0 +1,2 @@
+The values in this RRO are for modifying the car-ui-lib values and should be applied to all
+applications using car-ui-lib
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/product.mk b/car_product/car_ui_portrait/rro/car-ui-customizations/product.mk
new file mode 100644
index 0000000..a7c3808
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/product.mk
@@ -0,0 +1,51 @@
+#
+# Copyright (C) 2021 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.
+#
+
+# Inherit from this product to include the "Car Ui Portrait" RROs for CarUi
+# Include generated RROs
+PRODUCT_PACKAGES += \
+ generated_caruiportrait_customization-com-android-car-ui-paintbooth \
+ generated_caruiportrait_customization-com-google-android-car-ui-paintbooth \
+ generated_caruiportrait_customization-com-google-android-carui-ats \
+ generated_caruiportrait_customization-com-android-car-rotaryplayground \
+ generated_caruiportrait_customization-com-android-car-themeplayground \
+ generated_caruiportrait_customization-com-android-car-carlauncher \
+ generated_caruiportrait_customization-com-android-car-home \
+ generated_caruiportrait_customization-com-android-car-media \
+ generated_caruiportrait_customization-com-android-car-radio \
+ generated_caruiportrait_customization-com-android-car-calendar \
+ generated_caruiportrait_customization-com-android-car-companiondevicesupport \
+ generated_caruiportrait_customization-com-android-car-systemupdater \
+ generated_caruiportrait_customization-com-android-car-dialer \
+ generated_caruiportrait_customization-com-android-car-linkviewer \
+ generated_caruiportrait_customization-com-android-car-settings \
+ generated_caruiportrait_customization-com-android-car-voicecontrol \
+ generated_caruiportrait_customization-com-android-car-faceenroll \
+ generated_caruiportrait_customization-com-android-car-developeroptions \
+ generated_caruiportrait_customization-com-android-managedprovisioning \
+ generated_caruiportrait_customization-com-android-settings-intelligence \
+ generated_caruiportrait_customization-com-google-android-apps-automotive-inputmethod \
+ generated_caruiportrait_customization-com-google-android-apps-automotive-inputmethod-dev \
+ generated_caruiportrait_customization-com-google-android-apps-automotive-templates-host \
+ generated_caruiportrait_customization-com-google-android-embedded-projection \
+ generated_caruiportrait_customization-com-google-android-gms \
+ generated_caruiportrait_customization-com-google-android-gsf \
+ generated_caruiportrait_customization-com-google-android-packageinstaller \
+ generated_caruiportrait_customization-com-google-android-permissioncontroller \
+ generated_caruiportrait_customization-com-google-android-carassistant \
+ generated_caruiportrait_customization-com-google-android-tts \
+ generated_caruiportrait_customization-com-android-htmlviewer \
+ generated_caruiportrait_customization-com-android-vending
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_primary.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_primary.xml
new file mode 100644
index 0000000..e87a692
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_primary.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Copy of ?android:attr/textColorPrimary (frameworks/base/res/res/color/text_color_primary.xml)
+ but with a ux restricted state. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="?android:attr/textColorPrimary"/>
+ <item app:state_ux_restricted="true"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="?android:attr/textColorPrimary"/>
+ <item android:color="?android:attr/textColorPrimary"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_secondary.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_secondary.xml
new file mode 100644
index 0000000..0f1fcb5
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_text_color_secondary.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- Copy of ?android:attr/textColorSecondary (frameworks/base/res/res/color/text_color_secondary.xml)
+ but with a ux restricted state. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="?android:attr/textColorSecondary"/>
+ <item app:state_ux_restricted="true"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="?android:attr/textColorSecondary"/>
+ <item android:color="?android:attr/textColorSecondary"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_toolbar_tab_item_selector.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_toolbar_tab_item_selector.xml
new file mode 100644
index 0000000..02d4374
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/color/car_ui_toolbar_tab_item_selector.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/car_ui_text_color_primary" android:state_activated="true"/>
+ <item android:color="@color/car_ui_text_color_secondary"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_activity_background.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_activity_background.xml
new file mode 100644
index 0000000..f70ad67
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_activity_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:drawable="?android:attr/colorBackground"/>
+</layer-list>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_ic_down.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_ic_down.xml
new file mode 100644
index 0000000..e2d1b93
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_ic_down.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:pathData="M14.83,16.42L24,25.59l9.17,-9.17L36,19.25l-12,12 -12,-12z"
+ android:fillColor="#000000"/>
+</vector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_ic_up.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_ic_up.xml
new file mode 100644
index 0000000..c8cc84f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_ic_up.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:pathData="M14.83,30.83L24,21.66l9.17,9.17L36,28 24,16 12,28z"
+ android:fillColor="#000000"/>
+</vector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_scrollbar_thumb.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_scrollbar_thumb.xml
new file mode 100644
index 0000000..54922cf
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_recyclerview_scrollbar_thumb.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="#99000000" />
+ <corners android:radius="100dp"/>
+</shape>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_divider.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_divider.xml
new file mode 100644
index 0000000..9b47736
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_divider.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ ~
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <size android:width="16dp"/>
+</shape>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_background.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_background.xml
new file mode 100644
index 0000000..57ac917
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_background.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <size
+ android:width="40dp"
+ android:height="40dp"/>
+ <solid android:color="@android:color/transparent"/>
+</shape>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
new file mode 100644
index 0000000..9ac2a1f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ ~
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#27ffffff"
+ android:radius="48dp"/>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/layout/car_ui_alert_dialog_title_with_subtitle.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/layout/car_ui_alert_dialog_title_with_subtitle.xml
new file mode 100644
index 0000000..81cfb5c
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/layout/car_ui_alert_dialog_title_with_subtitle.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/title_template"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <!-- Leave this view here so that we don't get any null pointer errors in the alert dialog
+ class. -->
+ <ImageView
+ android:id="@+id/car_ui_alert_icon"
+ android:layout_width="96dp"
+ android:layout_height="96dp"
+ android:layout_marginStart="10dp"
+ android:layout_marginTop="@dimen/alert_dialog_margin"
+ android:scaleType="fitCenter"
+ android:tint="@color/car_ui_text_color_primary"
+ android:visibility="gone"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/alert_dialog_margin"
+ android:layout_marginEnd="@dimen/alert_dialog_margin"
+ android:layout_marginTop="@dimen/alert_dialog_margin"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/car_ui_alert_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance_CarUi_AlertDialog_Title" />
+
+ <TextView
+ android:id="@+id/car_ui_alert_subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textAppearance="@style/TextAppearance_CarUi_AlertDialog_Subtitle"/>
+ </LinearLayout>
+
+ <View
+ android:id="@+id/empty_space"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"/>
+</LinearLayout>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/attrs.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/attrs.xml
new file mode 100644
index 0000000..6b60f12
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/attrs.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+<resources>
+ <attr name="state_ux_restricted" format="boolean" />
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/dimens.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/dimens.xml
new file mode 100644
index 0000000..a3ce819
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+ <dimen name="car_ui_body1_size">32sp</dimen>
+ <dimen name="car_ui_body3_size">24sp</dimen>
+
+ <dimen name="alert_dialog_margin">36dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/drawables.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/drawables.xml
new file mode 100644
index 0000000..f44dbf0
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/drawables.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Toolbar background color -->
+ <drawable name="car_ui_toolbar_background">@*android:color/background_device_default_dark</drawable>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/styles.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/styles.xml
new file mode 100644
index 0000000..f154c0b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/styles.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="TextAppearance_CarUi_AlertDialog_Title" parent="android:TextAppearance.DeviceDefault">
+ <item name="android:textSize">32sp</item>
+ </style>
+ <style name="TextAppearance_CarUi_AlertDialog_Subtitle" parent="android:TextAppearance.DeviceDefault">
+ <item name="android:textSize">24sp</item>
+ </style>
+
+ <style name="TextAppearance.CarUi.PreferenceCategoryTitle" parent="android:TextAppearance.DeviceDefault">
+ <item name="android:fontFamily">sans-serif-medium</item>
+ <item name="android:textColor">?android:attr/colorAccent</item>
+ <item name="android:textSize">24sp</item>
+ </style>
+
+ <style name="TextAppearance.CarUi.PreferenceSummary" parent="android:TextAppearance.DeviceDefault">
+ <item name="android:textColor">?android:attr/textColorSecondary</item>
+ <item name="android:textSize">24sp</item>
+ </style>
+
+ <style name="TextAppearance.CarUi.PreferenceTitle" parent="android:TextAppearance.DeviceDefault">
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textSize">32sp</item>
+ </style>
+
+ <style name="TextAppearance.CarUi.Widget" parent="android:TextAppearance.DeviceDefault.Widget">
+ <item name="android:textAlignment">viewStart</item>
+ </style>
+
+ <style name="TextAppearance.CarUi.Widget.Toolbar"/>
+
+ <style name="TextAppearance.CarUi.Widget.Toolbar.Title">
+ <item name="android:singleLine">true</item>
+ <item name="android:textSize">32sp</item>
+ </style>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/themes.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/themes.xml
new file mode 100644
index 0000000..85dfc95
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/themes.xml
@@ -0,0 +1,18 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/values.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/values.xml
new file mode 100644
index 0000000..a09f6e8
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/values/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <bool name="car_ui_scrollbar_enable">false</bool>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-customizations/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/car-ui-customizations/res/xml/overlays.xml
new file mode 100644
index 0000000..0049818
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-customizations/res/xml/overlays.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+<overlay>
+ <item target="layout/car_ui_alert_dialog_title_with_subtitle" value="@layout/car_ui_alert_dialog_title_with_subtitle"/>
+
+ <item target="id/car_ui_alert_icon" value="@id/car_ui_alert_icon"/>
+ <item target="id/car_ui_alert_title" value="@id/car_ui_alert_title"/>
+ <item target="id/car_ui_alert_subtitle" value="@id/car_ui_alert_subtitle"/>
+
+ <item target="dimen/car_ui_body1_size" value="@dimen/car_ui_body1_size"/>
+ <item target="dimen/car_ui_body3_size" value="@dimen/car_ui_body3_size"/>
+ <item target="dimen/alert_dialog_margin" value="@dimen/alert_dialog_margin"/>
+
+ <item target="drawable/car_ui_recyclerview_ic_up" value="@drawable/car_ui_recyclerview_ic_up" />
+ <item target="drawable/car_ui_recyclerview_ic_down" value="@drawable/car_ui_recyclerview_ic_down" />
+ <item target="drawable/car_ui_recyclerview_scrollbar_thumb" value="@drawable/car_ui_recyclerview_scrollbar_thumb" />
+ <item target="drawable/car_ui_activity_background" value="@drawable/car_ui_activity_background" />
+ <item target="drawable/car_ui_toolbar_menu_item_icon_background" value="@drawable/car_ui_toolbar_menu_item_icon_background" />
+
+ <item target="color/car_ui_text_color_primary" value="@color/car_ui_text_color_primary" />
+ <item target="color/car_ui_text_color_secondary" value="@color/car_ui_text_color_secondary" />
+ <item target="color/car_ui_toolbar_tab_item_selector" value="@color/car_ui_toolbar_tab_item_selector" />
+
+ <item target="drawable/car_ui_toolbar_background" value="@drawable/car_ui_toolbar_background" />
+ <item target="drawable/car_ui_toolbar_menu_item_divider" value="@drawable/car_ui_toolbar_menu_item_divider" />
+ <item target="drawable/car_ui_toolbar_menu_item_icon_ripple" value="@drawable/car_ui_toolbar_menu_item_icon_ripple" />
+ <item target="bool/car_ui_scrollbar_enable" value="@bool/car_ui_scrollbar_enable" />
+
+ <item target="style/TextAppearance_CarUi_AlertDialog_Title" value="@style/TextAppearance_CarUi_AlertDialog_Title" />
+ <item target="style/TextAppearance_CarUi_AlertDialog_Subtitle" value="@style/TextAppearance_CarUi_AlertDialog_Subtitle" />
+ <item target="style/TextAppearance.CarUi.PreferenceCategoryTitle" value="@style/TextAppearance.CarUi.PreferenceCategoryTitle" />
+ <item target="style/TextAppearance.CarUi.PreferenceSummary" value="@style/TextAppearance.CarUi.PreferenceSummary" />
+ <item target="style/TextAppearance.CarUi.PreferenceTitle" value="@style/TextAppearance.CarUi.PreferenceTitle" />
+ <item target="style/TextAppearance.CarUi.Widget" value="@style/TextAppearance.CarUi.Widget" />
+ <item target="style/TextAppearance.CarUi.Widget.Toolbar" value="@style/TextAppearance.CarUi.Widget.Toolbar" />
+ <item target="style/TextAppearance.CarUi.Widget.Toolbar.Title" value="@style/TextAppearance.CarUi.Widget.Toolbar.Title" />
+
+ <item target="attr/state_ux_restricted" value="@attr/state_ux_restricted"/>
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/Android.mk b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/Android.mk
new file mode 100644
index 0000000..1b06791
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/Android.mk
@@ -0,0 +1,28 @@
+#
+# Copyright (C) 2021 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+CAR_UI_RRO_SET_NAME := generated_caruiportrait_toolbar
+CAR_UI_RRO_MANIFEST_FILE := $(LOCAL_PATH)/AndroidManifest.xml
+CAR_UI_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+CAR_UI_RRO_TARGETS := \
+ com.android.car.media \
+ com.android.car.dialer \
+
+include packages/apps/Car/libs/car-ui-lib/generate_rros.mk
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/AndroidManifest.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/AndroidManifest.xml
new file mode 100644
index 0000000..9d3a1a4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="{{RRO_PACKAGE_NAME}}">
+ <application android:hasCode="false"/>
+ <overlay android:priority="10"
+ android:targetName="car-ui-lib"
+ android:targetPackage="{{TARGET_PACKAGE_NAME}}"
+ android:resourcesMap="@xml/overlays"
+ android:isStatic="true"
+ android:requiredSystemPropertyName="ro.build.characteristics"
+ android:requiredSystemPropertyValue="automotive"/>
+</manifest>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/README b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/README
new file mode 100644
index 0000000..e199f7b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/README
@@ -0,0 +1,2 @@
+The values in this RRO are to change the placement of the car-ui toolbar as such it currently
+is only targeting a limited set of applications.
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/product.mk b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/product.mk
new file mode 100644
index 0000000..7b7cccd
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/product.mk
@@ -0,0 +1,21 @@
+#
+# Copyright (C) 2021 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.
+#
+
+# Inherit from this product to include the "Car Ui Portrait" RROs for CarUi
+# Include generated RROs
+PRODUCT_PACKAGES += \
+ generated_caruiportrait_toolbar-com-android-car-media \
+ generated_caruiportrait_toolbar-com-android-car-dialer \
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_primary.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_primary.xml
new file mode 100644
index 0000000..860f219
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_primary.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+-->
+<!-- Copy of ?android:attr/textColorPrimary (frameworks/base/res/res/color/text_color_primary.xml)
+ but with a ux restricted state. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="?android:attr/colorForeground"/>
+ <item app:state_ux_restricted="true"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="?android:attr/colorForeground"/>
+ <item android:color="?android:attr/colorForeground"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_secondary.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_secondary.xml
new file mode 100644
index 0000000..f99fc86
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_text_color_secondary.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+-->
+<!-- Copy of ?android:attr/textColorSecondary (frameworks/base/res/res/color/text_color_secondary.xml)
+ but with a ux restricted state. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="?android:attr/colorForeground"/>
+ <item app:state_ux_restricted="true"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="?android:attr/colorForeground"/>
+ <item android:color="?android:attr/colorForeground"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_toolbar_tab_item_selector.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_toolbar_tab_item_selector.xml
new file mode 100644
index 0000000..02d4374
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/car_ui_toolbar_tab_item_selector.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/car_ui_text_color_primary" android:state_activated="true"/>
+ <item android:color="@color/car_ui_text_color_secondary"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/tab_side_indicator_color.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/tab_side_indicator_color.xml
new file mode 100644
index 0000000..0cf2a1a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/color/tab_side_indicator_color.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#52CCB0" android:state_activated="true"/>
+ <item android:color="@android:color/transparent"/>
+</selector>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_divider.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_divider.xml
new file mode 100644
index 0000000..9b47736
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_divider.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ ~
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <size android:width="16dp"/>
+</shape>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
new file mode 100644
index 0000000..9ac2a1f
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/car_ui_toolbar_menu_item_icon_ripple.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ ~
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#27ffffff"
+ android:radius="48dp"/>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/tab_background.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/tab_background.xml
new file mode 100644
index 0000000..ffbeb18
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/drawable/tab_background.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_activated="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/tab_background_color"/>
+ </shape>
+ </item>
+ <item android:state_activated="false">
+ <shape android:shape="rectangle">
+ <solid android:color="@android:color/transparent"/>
+ </shape>
+ </item>
+</selector>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_base_layout_toolbar.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_base_layout_toolbar.xml
new file mode 100644
index 0000000..642ea29
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_base_layout_toolbar.xml
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<!-- This is for the two-row version of the toolbar -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tag="CarUiBaseLayoutToolbar">
+
+ <!-- When not in touch mode, if we clear focus in current window, Android will re-focus the
+ first focusable view in the window automatically. Adding a FocusParkingView to the window
+ can fix this issue, because it can take focus, and it is transparent and its default focus
+ highlight is disabled, so it's invisible to the user no matter whether it's focused or not.
+ -->
+ <com.android.car.ui.FocusParkingView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <FrameLayout
+ android:id="@+id/car_ui_base_layout_content_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toEndOf="@id/left_part_of_toolbar_focus_area"
+ app:layout_constraintEnd_toEndOf="parent"/>
+
+ <com.android.car.ui.FocusArea
+ android:id="@+id/top_part_of_toolbar_focus_area"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="90dp"
+ android:background="?android:attr/colorBackground"
+ android:tag="car_ui_top_inset"
+ app:layout_constraintTop_toTopOf="parent">
+ <com.android.car.ui.baselayout.ClickBlockingView
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+ <FrameLayout
+ android:id="@+id/car_ui_toolbar_nav_icon_container"
+ android:layout_width="90dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <ImageView
+ android:id="@+id/car_ui_toolbar_nav_icon"
+ android:layout_width="@dimen/car_ui_toolbar_nav_icon_size"
+ android:layout_height="@dimen/car_ui_toolbar_nav_icon_size"
+ android:layout_gravity="center"
+ android:scaleType="fitXY"
+ android:background="@drawable/car_ui_toolbar_menu_item_icon_ripple"
+ android:tint="?android:attr/textColorPrimary"/>
+
+ <ImageView
+ android:id="@+id/car_ui_toolbar_logo"
+ android:layout_width="@dimen/car_ui_toolbar_logo_size"
+ android:layout_height="@dimen/car_ui_toolbar_logo_size"
+ android:layout_gravity="center"
+ android:scaleType="fitXY" />
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/car_ui_toolbar_title_logo_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="24dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_toolbar_title_container"
+ app:layout_constraintHorizontal_chainStyle="packed">
+
+ <ImageView
+ android:id="@+id/car_ui_toolbar_title_logo"
+ android:layout_width="@dimen/car_ui_toolbar_logo_size"
+ android:layout_height="@dimen/car_ui_toolbar_logo_size"
+ android:layout_gravity="center"
+ android:scaleType="fitXY" />
+ </FrameLayout>
+
+ <LinearLayout android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:id="@+id/car_ui_toolbar_title_container"
+ android:orientation="vertical"
+ android:layout_marginStart="16dp"
+ app:layout_goneMarginStart="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_title_logo_container">
+ <TextView android:id="@+id/car_ui_toolbar_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.CarUi.Widget.Toolbar.Title"/>
+ <TextView android:id="@+id/car_ui_toolbar_subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/car_ui_toolbar_search_view_container"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/car_ui_toolbar_menu_items_container"
+ app:layout_constraintStart_toEndOf="@+id/car_ui_toolbar_nav_icon_container"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <LinearLayout
+ android:id="@+id/car_ui_toolbar_menu_items_container"
+ android:divider="@drawable/car_ui_toolbar_menu_item_divider"
+ android:showDividers="beginning|middle|end"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:orientation="horizontal"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <ProgressBar
+ android:id="@+id/car_ui_toolbar_progress_bar"
+ style="@android:style/Widget.DeviceDefault.ProgressBar.Horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:indeterminate="true"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <!-- Hairline across bottom of toolbar -->
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="2dp"
+ android:background="@color/divider_color"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </com.android.car.ui.FocusArea>
+
+ <com.android.car.ui.FocusArea
+ android:id="@+id/left_part_of_toolbar_focus_area"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:tag="car_ui_left_inset"
+ android:orientation="horizontal"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/top_part_of_toolbar_focus_area"
+ app:layout_constraintBottom_toBottomOf="parent">
+
+ <com.android.car.ui.toolbar.TabLayout
+ android:id="@+id/car_ui_toolbar_tabs"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:orientation="vertical"/>
+ </com.android.car.ui.FocusArea>
+
+ <!-- Hairline to the right of the tabs -->
+ <View
+ android:layout_width="2dp"
+ android:layout_height="0dp"
+ android:background="@color/divider_color"
+ android:focusable="false"
+ app:layout_constraintStart_toEndOf="@id/left_part_of_toolbar_focus_area"
+ app:layout_constraintTop_toBottomOf="@id/top_part_of_toolbar_focus_area"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_toolbar_tab_item.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_toolbar_tab_item.xml
new file mode 100644
index 0000000..63ac2b4
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/layout/car_ui_toolbar_tab_item.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="320dp"
+ android:layout_height="96dp"
+ android:background="@drawable/tab_background">
+
+ <View
+ android:layout_width="8dp"
+ android:layout_height="match_parent"
+ android:background="@color/tab_side_indicator_color"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <ImageView
+ android:id="@+id/car_ui_toolbar_tab_item_icon"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"
+ android:scaleType="fitCenter"
+ android:tint="@color/car_ui_toolbar_tab_item_selector"
+ android:tintMode="src_in"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/car_ui_toolbar_tab_item_text"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+ <TextView
+ android:id="@+id/car_ui_toolbar_tab_item_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textSize="28sp"
+ android:singleLine="true"
+ app:layout_constraintStart_toEndOf="@id/car_ui_toolbar_tab_item_icon"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-night/colors.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-night/colors.xml
new file mode 100644
index 0000000..52db35b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-night/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="tab_background_color">#282A2D</color>
+ <color name="divider_color">#2e3134</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-port/values.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-port/values.xml
new file mode 100644
index 0000000..299d726
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values-port/values.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+<resources>
+ <bool name="car_ui_toolbar_tab_flexible_layout">false</bool>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/attrs.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/attrs.xml
new file mode 100644
index 0000000..e06d40a
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/attrs.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+<resources>
+ <attr name="layout_constraintGuide_begin" format="dimension"/>
+ <attr name="layout_constraintGuide_end" format="dimension"/>
+ <attr name="layout_constraintGuide_percent" format="float"/>
+
+ <attr name="layout_constraintLeft_toLeftOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintLeft_toRightOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintRight_toLeftOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintRight_toRightOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintTop_toTopOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintTop_toBottomOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintBottom_toTopOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintBottom_toBottomOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintBaseline_toBaselineOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintStart_toEndOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintStart_toStartOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintEnd_toStartOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+ <attr name="layout_constraintEnd_toEndOf" format="reference|enum">
+ <enum name="parent" value="0"/>
+ </attr>
+
+ <attr name="layout_constraintHorizontal_bias" format="float"/>
+ <attr name="layout_constraintVertical_bias" format="float"/>
+
+ <attr name="layout_goneMarginLeft" format="dimension"/>
+ <attr name="layout_goneMarginTop" format="dimension"/>
+ <attr name="layout_goneMarginRight" format="dimension"/>
+ <attr name="layout_goneMarginBottom" format="dimension"/>
+ <attr name="layout_goneMarginStart" format="dimension"/>
+ <attr name="layout_goneMarginEnd" format="dimension"/>
+
+ <attr name="layout_constraintHorizontal_chainStyle" format="enum">
+ <enum name="spread" value="0"/>
+ <enum name="spread_inside" value="1"/>
+ <enum name="packed" value="2"/>
+ </attr>
+ <attr name="state_ux_restricted" format="boolean" />
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/bools.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/bools.xml
new file mode 100644
index 0000000..c502242
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/bools.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<resources>
+
+ <bool name="car_ui_toolbar_tabs_on_second_row">true</bool>
+
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/colors.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/colors.xml
new file mode 100644
index 0000000..8c7d1da
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <color name="tab_background_color">#E8EAED</color>
+ <color name="divider_color">#E8EAED</color>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/themes.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/themes.xml
new file mode 100644
index 0000000..62b5c1b
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/themes.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <style name="TextAppearance.CarUi.Widget" parent="android:TextAppearance.Material.Widget">
+ <item name="android:textAlignment">viewStart</item>
+ </style>
+
+ <style name="TextAppearance.CarUi.Widget.Toolbar"/>
+
+ <style name="TextAppearance.CarUi.Widget.Toolbar.Title">
+ <item name="android:singleLine">true</item>
+ <item name="android:textSize">32sp</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/values.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/values.xml
new file mode 100644
index 0000000..c8f85cf
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/values/values.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+<resources>
+ <bool name="car_ui_toolbar_logo_fills_nav_icon_space">false</bool>
+ <bool name="car_ui_toolbar_tab_flexible_layout">false</bool>
+ <bool name="car_ui_scrollbar_enable">false</bool>
+
+ <dimen name="car_ui_toolbar_logo_size">44dp</dimen>
+ <dimen name="car_ui_toolbar_nav_icon_size">44dp</dimen>
+</resources>
diff --git a/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/xml/overlays.xml b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/xml/overlays.xml
new file mode 100644
index 0000000..88772fb
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/res/xml/overlays.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+<overlay>
+ <item target="layout/car_ui_base_layout_toolbar" value="@layout/car_ui_base_layout_toolbar"/>
+ <item target="layout/car_ui_toolbar_tab_item" value="@layout/car_ui_toolbar_tab_item"/>
+
+ <item target="bool/car_ui_toolbar_logo_fills_nav_icon_space" value="@bool/car_ui_toolbar_logo_fills_nav_icon_space" />
+ <item target="bool/car_ui_toolbar_tab_flexible_layout" value="@bool/car_ui_toolbar_tab_flexible_layout" />
+ <item target="bool/car_ui_scrollbar_enable" value="@bool/car_ui_scrollbar_enable" />
+ <item target="bool/car_ui_toolbar_tabs_on_second_row" value="@bool/car_ui_toolbar_tabs_on_second_row" />
+
+ <item target="id/car_ui_toolbar_nav_icon_container" value="@id/car_ui_toolbar_nav_icon_container" />
+ <item target="id/car_ui_toolbar_nav_icon" value="@id/car_ui_toolbar_nav_icon" />
+ <item target="id/car_ui_toolbar_logo" value="@id/car_ui_toolbar_logo" />
+ <item target="id/car_ui_toolbar_title_logo_container" value="@id/car_ui_toolbar_title_logo_container" />
+ <item target="id/car_ui_toolbar_title_logo" value="@id/car_ui_toolbar_title_logo" />
+ <item target="id/car_ui_toolbar_title" value="@id/car_ui_toolbar_title" />
+ <item target="id/car_ui_toolbar_title_container" value="@id/car_ui_toolbar_title_container" />
+ <item target="id/car_ui_toolbar_subtitle" value="@id/car_ui_toolbar_subtitle" />
+ <item target="id/car_ui_toolbar_tabs" value="@id/car_ui_toolbar_tabs" />
+ <item target="id/car_ui_toolbar_menu_items_container" value="@id/car_ui_toolbar_menu_items_container" />
+ <item target="id/car_ui_toolbar_search_view_container" value="@id/car_ui_toolbar_search_view_container" />
+ <item target="id/car_ui_toolbar_progress_bar" value="@id/car_ui_toolbar_progress_bar" />
+ <item target="id/car_ui_base_layout_content_container" value="@id/car_ui_base_layout_content_container" />
+ <item target="id/car_ui_toolbar_tab_item_icon" value="@id/car_ui_toolbar_tab_item_icon" />
+ <item target="id/car_ui_toolbar_tab_item_text" value="@id/car_ui_toolbar_tab_item_text" />
+
+ <item target="attr/layout_constraintGuide_begin" value="@attr/layout_constraintGuide_begin"/>
+ <item target="attr/layout_constraintGuide_end" value="@attr/layout_constraintGuide_end"/>
+ <item target="attr/layout_constraintStart_toStartOf" value="@attr/layout_constraintStart_toStartOf"/>
+ <item target="attr/layout_constraintStart_toEndOf" value="@attr/layout_constraintStart_toEndOf"/>
+ <item target="attr/layout_constraintEnd_toStartOf" value="@attr/layout_constraintEnd_toStartOf"/>
+ <item target="attr/layout_constraintEnd_toEndOf" value="@attr/layout_constraintEnd_toEndOf"/>
+ <item target="attr/layout_constraintLeft_toLeftOf" value="@attr/layout_constraintLeft_toLeftOf"/>
+ <item target="attr/layout_constraintLeft_toRightOf" value="@attr/layout_constraintLeft_toRightOf"/>
+ <item target="attr/layout_constraintRight_toLeftOf" value="@attr/layout_constraintRight_toLeftOf"/>
+ <item target="attr/layout_constraintRight_toRightOf" value="@attr/layout_constraintRight_toRightOf"/>
+ <item target="attr/layout_constraintTop_toTopOf" value="@attr/layout_constraintTop_toTopOf"/>
+ <item target="attr/layout_constraintTop_toBottomOf" value="@attr/layout_constraintTop_toBottomOf"/>
+ <item target="attr/layout_constraintBottom_toTopOf" value="@attr/layout_constraintBottom_toTopOf"/>
+ <item target="attr/layout_constraintBottom_toBottomOf" value="@attr/layout_constraintBottom_toBottomOf"/>
+ <item target="attr/layout_constraintHorizontal_bias" value="@attr/layout_constraintHorizontal_bias"/>
+ <item target="attr/layout_goneMarginLeft" value="@attr/layout_goneMarginLeft"/>
+ <item target="attr/layout_goneMarginRight" value="@attr/layout_goneMarginRight"/>
+ <item target="attr/layout_goneMarginTop" value="@attr/layout_goneMarginTop"/>
+ <item target="attr/layout_goneMarginBottom" value="@attr/layout_goneMarginBottom"/>
+ <item target="attr/layout_goneMarginStart" value="@attr/layout_goneMarginStart"/>
+ <item target="attr/layout_goneMarginEnd" value="@attr/layout_goneMarginEnd"/>
+ <item target="attr/layout_constraintHorizontal_chainStyle" value="@attr/layout_constraintHorizontal_chainStyle"/>
+ <item target="attr/state_ux_restricted" value="@attr/state_ux_restricted"/>
+</overlay>
diff --git a/car_product/car_ui_portrait/rro/car_ui_portrait_rro.mk b/car_product/car_ui_portrait/rro/car_ui_portrait_rro.mk
new file mode 100644
index 0000000..5b06ed2
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/car_ui_portrait_rro.mk
@@ -0,0 +1,35 @@
+#
+# Copyright (C) 2021 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.
+#
+
+$(call inherit-product, packages/services/Car/car_product/car_ui_portrait/rro/car-ui-customizations/product.mk)
+$(call inherit-product, packages/services/Car/car_product/car_ui_portrait/rro/car-ui-toolbar-customizations/product.mk)
+
+# All RROs to be included in car_ui_portrait builds.
+PRODUCT_PACKAGES += \
+ CarEvsCameraPreviewAppRRO \
+ CarUiPortraitDialerRRO \
+ CarUiPortraitSettingsRRO \
+ CarUiPortraitMediaRRO \
+ CarUiPortraitLauncherRRO \
+ CarUiPortraitNotificationRRO \
+ CarUiPortraitCarServiceRRO \
+ CarUiPortraitFrameworkResRRO \
+ CarUiPortraitFrameworkResRROTest
+
+ifneq ($(INCLUDE_SEAHAWK_ONLY_RROS),)
+PRODUCT_PACKAGES += \
+ CarUiPortraitSettingsProviderRRO
+endif
diff --git a/car_product/car_ui_portrait/rro/common-res/res/color/car_ui_text_color_primary.xml b/car_product/car_ui_portrait/rro/common-res/res/color/car_ui_text_color_primary.xml
new file mode 100644
index 0000000..860f219
--- /dev/null
+++ b/car_product/car_ui_portrait/rro/common-res/res/color/car_ui_text_color_primary.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+-->
+<!-- Copy of ?android:attr/textColorPrimary (frameworks/base/res/res/color/text_color_primary.xml)
+ but with a ux restricted state. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item android:state_enabled="false"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="?android:attr/colorForeground"/>
+ <item app:state_ux_restricted="true"
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="?android:attr/colorForeground"/>
+ <item android:color="?android:attr/colorForeground"/>
+</selector>
diff --git a/car_product/car_ui_portrait/tools/export_emulator.py b/car_product/car_ui_portrait/tools/export_emulator.py
new file mode 100755
index 0000000..aa75f3a
--- /dev/null
+++ b/car_product/car_ui_portrait/tools/export_emulator.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+
+import subprocess
+import os
+import sys
+from shutil import copy2, copytree, rmtree
+from argparse import ArgumentParser as AP
+
+# Mostly adapted from https://cs.android.com/android/platform/superproject/+/master:device/generic/car/tools/run_local_avd.sh
+
+def fromTop(path):
+ return os.path.join(os.environ['ANDROID_BUILD_TOP'], path)
+
+def fromProductOut(path):
+ return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], path)
+
+def copyImages(outputDir, abi):
+ outputDir = os.path.join(outputDir, abi)
+ os.mkdir(outputDir)
+
+ try:
+ copy2(fromProductOut('system-qemu.img'), os.path.join(outputDir, 'system.img'))
+ copy2(fromProductOut('vendor-qemu.img'), os.path.join(outputDir, 'vendor.img'))
+ if os.path.isfile(fromProductOut('kernel-ranchu-64')):
+ copy2(fromProductOut('kernel-ranchu-64'), outputDir)
+ else:
+ copy2(fromProductOut('kernel-ranchu'), outputDir)
+ copy2(fromProductOut('ramdisk-qemu.img'), os.path.join(outputDir, 'ramdisk.img'))
+ copy2(fromProductOut('encryptionkey.img'), outputDir)
+ # take prebuilt userdata.img
+ # Ref: https://cs.android.com/android/platform/superproject/+/master:development/build/sdk.atree?q=userdata.img&ss=android%2Fplatform%2Fsuperproject:development%2Fbuild%2F
+ copy2(fromTop('device/generic/goldfish/data/etc/userdata.img'), outputDir)
+ copytree(fromProductOut('data'), os.path.join(outputDir, 'data'), dirs_exist_ok=True)
+ copy2(fromProductOut('system/build.prop'), os.path.join(outputDir, 'build.prop'))
+ copy2(fromProductOut('VerifiedBootParams.textproto'), outputDir)
+ copy2(fromProductOut('config.ini'), outputDir)
+ copy2(fromProductOut('advancedFeatures.ini'), outputDir)
+ except FileNotFoundError as f:
+ print("File not found: "+f.filename+", did you build android first?")
+ sys.exit(1)
+
+def readScreenDimens(configini):
+ width = 1080
+ height = 1920
+ density = 160
+ with open(configini, 'r') as f:
+ for line in f.readlines():
+ parts = line.split(' = ')
+ if len(parts) != 2:
+ continue
+ if parts[0] == 'hw.lcd.width':
+ width = parts[1]
+ if parts[0] == 'hw.lcd.height':
+ height = parts[1]
+ return (width, height, density)
+
+def buildAVD(outputDir, abi):
+ os.makedirs(os.path.join(outputDir, '.android/avd/my_car_avd.avd/'), exist_ok=True)
+ with open(os.path.join(outputDir, '.android/avd/my_car_avd.ini'), 'w') as f:
+ f.write('avd.ini.encoding=UTF-8\n')
+ f.write('path=required_but_we_want_to_use_path.rel_instead\n')
+ f.write('path.rel=avd/my_car_avd.avd\n')
+
+ width, height, density = readScreenDimens(fromProductOut('config.ini'))
+
+ with open(os.path.join(outputDir, '.android/avd/my_car_avd.avd/config.ini'), 'w') as f:
+ f.write(f'''
+image.sysdir.1 = unused_because_passing_-sysdir_to_emulator
+hw.lcd.density = {density}
+hw.lcd.width = {width}
+hw.lcd.height = {height}
+AvdId = my_car_avd
+avd.ini.displayname = my_car_avd
+hw.ramSize = 3584
+abi.type = {abi}
+
+tag.display = Automotive
+tag.id = android-automotive
+hw.device.manufacturer = google
+hw.device.name = hawk
+avd.ini.encoding = UTF-8
+disk.dataPartition.size = 6442450944
+fastboot.chosenSnapshotFile =
+fastboot.forceChosenSnapshotBoot = no
+fastboot.forceColdBoot = no
+fastboot.forceFastBoot = yes
+hw.accelerometer = no
+hw.arc = false
+hw.audioInput = yes
+hw.battery = no
+hw.camera.back = None
+hw.camera.front = None
+hw.cpu.arch = x86_64
+hw.cpu.ncore = 4
+hw.dPad = no
+hw.device.hash2 = MD5:1fdb01985c7b4d7c19ec309cc238b0f9
+hw.gps = yes
+hw.gpu.enabled = yes
+hw.gpu.mode = auto
+hw.initialOrientation = landscape
+hw.keyboard = yes
+hw.keyboard.charmap = qwerty2
+hw.keyboard.lid = false
+hw.mainKeys = no
+hw.sdCard = no
+hw.sensors.orientation = no
+hw.sensors.proximity = no
+hw.trackBall = no
+runtime.network.latency = none
+runtime.network.speed = full
+''')
+
+def genStartScript(outputDir):
+ filepath = os.path.join(outputDir, 'start_emu.sh')
+ with open(os.open(filepath, os.O_CREAT | os.O_WRONLY, 0o750), 'w') as f:
+ f.write(f'''
+# This file is auto-generated from export_emulator.py
+OS="$(uname -s)"
+if [[ $OS == "Linux" ]]; then
+ DEFAULT_ANDROID_SDK_ROOT="$HOME/Android/Sdk"
+elif [[ $OS == "Darwin" ]]; then
+ DEFAULT_ANDROID_SDK_ROOT="/Users/$USER/Library/Android/sdk"
+else
+ echo Sorry, this does not work on $OS
+ exit
+fi
+if [[ -z $ANDROID_SDK_ROOT ]]; then
+ ANDROID_SDK_ROOT="$DEFAULT_ANDROID_SDK_ROOT"
+fi
+if ! [[ -d $ANDROID_SDK_ROOT ]]; then
+ echo Could not find android SDK root. Did you install an SDK with android studio?
+ exit
+fi
+
+# TODO: this ANDROID_EMULATOR_HOME may need to not be changed.
+# we had to change it so we could find the avd by a relative path,
+# but changing it means makes it give an "emulator is out of date"
+# warning
+
+# TODO: You shouldn't need to pass -sysdir, it should be specified
+# in the avd ini file. But I couldn't figure out how to make that work
+# with a relative path.
+
+ANDROID_EMULATOR_HOME=$(dirname $0)/.android \
+ANDROID_AVD_HOME=.android/avd \
+ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT \
+$ANDROID_SDK_ROOT/emulator/emulator \
+-avd my_car_avd -sysdir x86_64 $@
+''')
+
+
+def main():
+ parser = AP(description="Export the current build as a sharable emulator")
+ parser.add_argument('-o', '--output', default="/tmp/exported_emulator",
+ help='Output folder. Defaults to /tmp/exported_emulator. Will wipe any existing contents!')
+ args = parser.parse_args()
+
+ if 'ANDROID_BUILD_TOP' not in os.environ or 'ANDROID_PRODUCT_OUT' not in os.environ:
+ print("Please run lunch first")
+ sys.exit(1)
+
+ if os.path.isfile(args.output):
+ print("Something already exists at "+args.output)
+ sys.exit(1)
+
+ if not os.path.isdir(os.path.dirname(args.output)):
+ print("Parent directory of "+args.output+" must already exist")
+ sys.exit(1)
+
+ rmtree(args.output, ignore_errors=True)
+ os.mkdir(args.output)
+
+ copyImages(args.output, 'x86_64')
+ buildAVD(args.output, 'x86_64')
+ genStartScript(args.output)
+ print("Done. Exported to "+args.output)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/car_product/init/init.car.rc b/car_product/init/init.car.rc
index a4880ab..4780dd4 100644
--- a/car_product/init/init.car.rc
+++ b/car_product/init/init.car.rc
@@ -1,6 +1,7 @@
# Insert car-specific startup services here
on post-fs-data
mkdir /data/system/car 0700 system system
+ mkdir /data/system/car/watchdog 0700 system system
# A property to enable EVS services conditionally
on property:persist.automotive.evs.mode=0
diff --git a/car_product/overlay-visual/OWNERS b/car_product/overlay-visual/OWNERS
new file mode 100644
index 0000000..4f53a30
--- /dev/null
+++ b/car_product/overlay-visual/OWNERS
@@ -0,0 +1,4 @@
+# Car UI Reference OWNERS
+hseog@google.com
+stenning@google.com
+igorr@google.com
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/color/car_seekbar_thumb_selected_background_color_selector.xml b/car_product/overlay-visual/frameworks/base/core/res/res/color/car_seekbar_thumb_selected_background_color_selector.xml
new file mode 100644
index 0000000..a01a016
--- /dev/null
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/color/car_seekbar_thumb_selected_background_color_selector.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_selected="true"
+ android:color="@color/car_seekbar_thumb_selected_background_color"/>
+ <item android:color="@android:color/transparent"/>
+</selector>
\ No newline at end of file
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/color/car_seekbar_thumb_selected_stroke_color_selector.xml b/car_product/overlay-visual/frameworks/base/core/res/res/color/car_seekbar_thumb_selected_stroke_color_selector.xml
new file mode 100644
index 0000000..a8ab141
--- /dev/null
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/color/car_seekbar_thumb_selected_stroke_color_selector.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_selected="true"
+ android:color="@color/car_seekbar_thumb_selected_stroke_color"/>
+ <item android:color="@android:color/transparent"/>
+</selector>
\ No newline at end of file
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/color/car_seekbar_thumb_selector.xml b/car_product/overlay-visual/frameworks/base/core/res/res/color/car_seekbar_thumb_selector.xml
new file mode 100644
index 0000000..4fdabcc
--- /dev/null
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/color/car_seekbar_thumb_selector.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="false"
+ android:color="@color/car_seekbar_thumb_disabled_on_dark"/>
+ <item android:color="@color/car_seekbar_thumb"/>
+</selector>
\ No newline at end of file
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/color/car_switch_thumb_selector.xml b/car_product/overlay-visual/frameworks/base/core/res/res/color/car_switch_thumb_selector.xml
new file mode 100644
index 0000000..8183224
--- /dev/null
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/color/car_switch_thumb_selector.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:color="?attr/colorSwitchThumbDisabled"
+ android:state_enabled="false" />
+ <item android:color="?attr/colorSwitchThumbNormal" />
+</selector>
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/color/car_switch_track_background_selector.xml b/car_product/overlay-visual/frameworks/base/core/res/res/color/car_switch_track_background_selector.xml
new file mode 100644
index 0000000..2321fa4
--- /dev/null
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/color/car_switch_track_background_selector.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_checked="true" android:state_enabled="false"
+ android:color="@color/car_switch_track_background_checked_disabled" />
+ <item
+ android:state_enabled="false"
+ android:color="@color/car_switch_track_background_unchecked_disabled" />
+ <item
+ android:state_checked="true" android:color="@color/car_switch_track_background_checked" />
+ <item android:color="@color/car_switch_track_background_unchecked" />
+</selector>
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_seekbar_thumb.xml b/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_seekbar_thumb.xml
new file mode 100644
index 0000000..493eeb2
--- /dev/null
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_seekbar_thumb.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="52dp"
+ android:height="52dp"
+ android:viewportWidth="52"
+ android:viewportHeight="52">
+ <path
+ android:pathData="M52,26A26,26 0,0 1,26 52,26 26,0 0,1 0,26 26,26 0,0 1,26 0,26 26,0 0,1 52,26Z"
+ android:fillAlpha="0.56">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:gradientRadius="26"
+ android:centerX="26"
+ android:centerY="26"
+ android:type="radial">
+ <item android:offset="0" android:color="#8F000000"/>
+ <item android:offset="0.92307687" android:color="#8F000000"/>
+ <item android:offset="1" android:color="#00000000"/>
+ </gradient>
+ </aapt:attr>
+ </path>
+ <path
+ android:pathData="M26,26m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"
+ android:fillColor="?attr/colorSeekbarThumb"/>
+</vector>
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_seekbar_thumb_selectable.xml b/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_seekbar_thumb_selectable.xml
new file mode 100644
index 0000000..2709180
--- /dev/null
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_seekbar_thumb_selectable.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:drawable="@drawable/car_seekbar_thumb"
+ android:gravity="center" />
+ <item
+ android:gravity="center"
+ android:width="@dimen/car_seekbar_thumb_size"
+ android:height="@dimen/car_seekbar_thumb_size">
+ <shape android:shape="oval">
+ <stroke
+ android:color="@color/car_seekbar_thumb_selected_stroke_color_selector"
+ android:width="@dimen/car_seekbar_thumb_selected_stroke_width" />
+ <solid android:color="@color/car_seekbar_thumb_selected_background_color_selector" />
+ </shape>
+ </item>
+</layer-list>
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_seekbar_track.xml b/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_seekbar_track.xml
new file mode 100644
index 0000000..394a132
--- /dev/null
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_seekbar_track.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@android:id/background"
+ android:top="@dimen/car_seekbar_track_vertical_padding"
+ android:bottom="@dimen/car_seekbar_track_vertical_padding">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/car_switch_thumb_outer_radius" />
+ <size android:height="@dimen/car_seekbar_track_height" />
+ <solid android:color="?attr/colorSeekBarTrackBackground" />
+ </shape>
+ </item>
+ <item
+ android:id="@android:id/secondaryProgress"
+ android:top="@dimen/car_seekbar_track_vertical_padding"
+ android:bottom="@dimen/car_seekbar_track_vertical_padding">
+ <scale android:scaleWidth="100%">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/car_switch_thumb_outer_radius" />
+ <size android:height="@dimen/car_seekbar_track_height" />
+ <solid android:color="?attr/colorSeekBarTrackProgressSecondary" />
+ </shape>
+ </scale>
+ </item>
+ <item
+ android:id="@android:id/progress"
+ android:top="@dimen/car_seekbar_track_vertical_padding"
+ android:bottom="@dimen/car_seekbar_track_vertical_padding">
+ <clip>
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/car_switch_thumb_outer_radius" />
+ <size android:height="@dimen/car_seekbar_track_height" />
+ <solid android:color="?attr/colorSeekBarTrackProgress" />
+ </shape>
+ </clip>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_switch_thumb.xml b/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_switch_thumb.xml
new file mode 100644
index 0000000..ae7a80c
--- /dev/null
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_switch_thumb.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+ android:paddingLeft="@dimen/car_switch_thumb_stroke_width"
+ android:paddingRight="@dimen/car_switch_thumb_stroke_width">
+ <item
+ android:gravity="center"
+ android:bottom="@dimen/car_switch_thumb_stroke_width"
+ android:left="@dimen/car_switch_thumb_stroke_width"
+ android:right="@dimen/car_switch_thumb_stroke_width"
+ android:top="@dimen/car_switch_thumb_stroke_width">
+ <shape android:shape="oval">
+ <size
+ android:width="@dimen/car_switch_thumb_size"
+ android:height="@dimen/car_switch_thumb_size" />
+ <solid android:color="@color/car_switch_thumb_selector"/>
+ </shape>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_switch_track.xml b/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_switch_track.xml
new file mode 100644
index 0000000..a342e86
--- /dev/null
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/drawable/car_switch_track.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:bottom="@dimen/car_switch_track_margin_vertical"
+ android:gravity="center"
+ android:top="@dimen/car_switch_track_margin_vertical">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/car_switch_thumb_outer_radius" />
+ <padding
+ android:left="@dimen/car_switch_thumb_stroke_width"
+ android:right="@dimen/car_switch_thumb_stroke_width" />
+ <size android:height="@dimen/car_switch_track_height" />
+ <solid android:color="?attr/colorSwitchTrack" />
+ </shape>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/values/attrs.xml b/car_product/overlay-visual/frameworks/base/core/res/res/values/attrs.xml
new file mode 100644
index 0000000..05506a7
--- /dev/null
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/values/attrs.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 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.
+-->
+<resources>
+ <attr name="colorSwitchThumbDisabled" format="color" />
+ <attr name="colorSwitchThumbNormal" format="color" />
+ <attr name="colorSwitchTrack" format="color" />
+ <attr name="colorSeekBarTrackBackground" format="color" />
+ <attr name="colorSeekBarTrackProgress" format="color" />
+ <attr name="colorSeekBarTrackProgressSecondary" format="color" />
+ <attr name="colorSeekbarThumb" format="color"/>
+</resources>
\ No newline at end of file
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/values/colors.xml b/car_product/overlay-visual/frameworks/base/core/res/res/values/colors.xml
index 94a0478..e6eae25 100644
--- a/car_product/overlay-visual/frameworks/base/core/res/res/values/colors.xml
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/values/colors.xml
@@ -37,4 +37,32 @@
icons. -->
<color name="notification_default_color">@*android:color/car_accent</color>
+ <!-- The Switch track background color when checked. -->
+ <color name="car_switch_track_background_checked">#66B5FF</color>
+ <!-- The Switch track background color when unchecked. -->
+ <color name="car_switch_track_background_unchecked">#757575</color>
+ <!-- The Switch track background color when checked and disabled. -->
+ <color name="car_switch_track_background_checked_disabled">#7566B5FF</color>
+ <!-- The Switch track background color when unchecked and disabled. -->
+ <color name="car_switch_track_background_unchecked_disabled">#75757575</color>
+ <!-- The Switch thumb color. -->
+ <color name="car_switch_thumb_color">#FFFFFF</color>
+ <!-- The Switch thumb color when disabled and using the dark theme. -->
+ <color name="car_switch_thumb_color_disabled_on_dark">#757575</color>
+ <!-- The Switch thumb color when disabled and using the light theme. -->
+ <color name="car_switch_thumb_color_disabled_on_light">@color/car_switch_thumb_color</color>
+ <!-- The Switch track's background color. -->
+ <color name="car_seekbar_track_background">#757575</color>
+ <!-- The Switch track's progress color. -->
+ <color name="car_seekbar_track_progress">#66B5FF</color>
+ <!-- The Switch track's secondary progress color. -->
+ <color name="car_seekbar_track_progress_secondary">#A6A6A9</color>
+ <!-- Color of the outer ring when the SeekBar is selected. -->
+ <color name="car_seekbar_thumb_selected_stroke_color">#94CBFF</color>
+ <!-- Color of the background of the ring when the SeekBar is selected. -->
+ <color name="car_seekbar_thumb_selected_background_color">#3D94CBFF</color>
+ <!-- The SeekBar thumb color. -->
+ <color name="car_seekbar_thumb">#FFFFFF</color>
+ <!-- The SeekBar thumb color when disabled. Use for the dark theme. -->
+ <color name="car_seekbar_thumb_disabled_on_dark">#757575</color>
</resources>
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/values/dimens.xml b/car_product/overlay-visual/frameworks/base/core/res/res/values/dimens.xml
index d7c2fa7..b1ab72a 100644
--- a/car_product/overlay-visual/frameworks/base/core/res/res/values/dimens.xml
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/values/dimens.xml
@@ -17,10 +17,10 @@
*/
-->
<resources>
- <dimen name="status_bar_height">68dp</dimen>
- <dimen name="status_bar_height_landscape">68dp</dimen>
- <dimen name="status_bar_height_portrait">68dp</dimen>
- <dimen name="car_qs_header_system_icons_area_height">68dp</dimen>
+ <dimen name="status_bar_height">76dp</dimen>
+ <dimen name="status_bar_height_landscape">76dp</dimen>
+ <dimen name="status_bar_height_portrait">76dp</dimen>
+ <dimen name="car_qs_header_system_icons_area_height">76dp</dimen>
<dimen name="navigation_bar_height">96dp</dimen>
<dimen name="navigation_bar_height_landscape">96dp</dimen>
@@ -134,4 +134,33 @@
<dimen name="floating_toolbar_menu_image_width">@*android:dimen/car_primary_icon_size</dimen>
<dimen name="floating_toolbar_menu_image_button_vertical_padding">@*android:dimen/car_padding_2</dimen>
<dimen name="floating_toolbar_text_size">@*android:dimen/car_body1_size</dimen>
+
+ <!-- Switch dimensions -->
+ <dimen name="car_switch_track_height">48dp</dimen>
+ <!-- Increases the vertical touch target size. -->
+ <dimen name="car_switch_track_margin_vertical">14dp</dimen>
+ <!-- Should be the half of car_switch_track_height. -->
+ <dimen name="car_switch_thumb_outer_radius">24dp</dimen>
+ <!-- The difference between the track size and the thumb size. -->
+ <dimen name="car_switch_thumb_stroke_width">4dp</dimen>
+ <!-- car_switch_thumb_size + car_switch_thumb_stroke_width == car_switch_track_height -->
+ <dimen name="car_switch_thumb_size">40dp</dimen>
+
+ <!-- SeekBar dimensions. -->
+ <!-- Allows thumb to extend out of the range of the track. -->
+ <!-- For more information see android.widget.SeekBar#attr_android:thumbOffset. -->
+ <dimen name="car_seekbar_thumb_offset">24dp</dimen>
+ <!-- Horizontal padding for the SeekBar.-->
+ <dimen name="car_seekbar_horizontal_padding">24dp</dimen>
+ <!-- The SeekBar track height.-->
+ <dimen name="car_seekbar_track_height">24dp</dimen>
+ <!-- Vertical padding that extends the touch area for the SeekBar. -->
+ <dimen name="car_seekbar_track_vertical_padding">26dp</dimen>
+ <!-- The stroke with of the ring around the SeekBar thumb when selected. -->
+ <dimen name="car_seekbar_thumb_selected_stroke_width">8dp</dimen>
+ <!-- The size of the SeekBar thumb. -->
+ <dimen name="car_seekbar_thumb_size">68dp</dimen>
+
+ <!-- Sets the Theme android:disabledAlpha value, Used by SeekBar. -->
+ <dimen name="car_disabled_alpha">0.46</dimen>
</resources>
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/values/styles_device_default.xml b/car_product/overlay-visual/frameworks/base/core/res/res/values/styles_device_default.xml
index 9db29ef..191b9ac 100644
--- a/car_product/overlay-visual/frameworks/base/core/res/res/values/styles_device_default.xml
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/values/styles_device_default.xml
@@ -106,9 +106,16 @@
<item name="android:button">@*android:drawable/car_checkbox</item>
</style>
- <style name="Widget.DeviceDefault.CompoundButton.Switch" parent="android:Widget.Material.CompoundButton.Switch">
- <item name="android:thumb">@*android:drawable/car_switch_thumb</item>
- <item name="android:track">@*android:drawable/car_switch_track</item>
+ <style name="Widget.DeviceDefault.CompoundButton.Switch"
+ parent="android:Widget.Material.CompoundButton.Switch">
+ <item name="android:thumb">@drawable/car_switch_thumb</item>
+ <item name="android:track">@drawable/car_switch_track</item>
+ </style>
+
+ <style name="Widget.DeviceDefault.Light.CompoundButton.Switch"
+ parent="android:Widget.Material.Light.CompoundButton.Switch">
+ <item name="android:thumb">@drawable/car_switch_thumb</item>
+ <item name="android:track">@drawable/car_switch_track</item>
</style>
<style name="Widget.DeviceDefault.ProgressBar.Horizontal" parent="android:Widget.Material.ProgressBar.Horizontal">
@@ -117,8 +124,27 @@
</style>
<style name="Widget.DeviceDefault.SeekBar" parent="android:Widget.Material.SeekBar">
- <item name="android:progressDrawable">@*android:drawable/car_seekbar_track</item>
- <item name="android:thumb">@*android:drawable/car_seekbar_thumb</item>
+ <item name="android:thumb">@drawable/car_seekbar_thumb_selectable</item>
+ <item name="android:progressDrawable">@drawable/car_seekbar_track</item>
+ <item name="android:splitTrack">false</item>
+ <item name="android:thumbOffset">@dimen/car_seekbar_thumb_offset</item>
+ <item name="android:paddingStart">@dimen/car_seekbar_horizontal_padding</item>
+ <item name="android:paddingEnd">@dimen/car_seekbar_horizontal_padding</item>
+ <item name="*android:useDisabledAlpha">true</item>
+ <item name="android:clipToOutline">false</item>
+ <item name="android:clipToPadding">false</item>
+ </style>
+
+ <style name="Widget.DeviceDefault.Light.SeekBar" parent="android:Widget.Material.Light.SeekBar">
+ <item name="android:thumb">@drawable/car_seekbar_thumb_selectable</item>
+ <item name="android:progressDrawable">@drawable/car_seekbar_track</item>
+ <item name="android:splitTrack">false</item>
+ <item name="android:thumbOffset">@dimen/car_seekbar_thumb_offset</item>
+ <item name="android:paddingStart">@dimen/car_seekbar_horizontal_padding</item>
+ <item name="android:paddingEnd">@dimen/car_seekbar_horizontal_padding</item>
+ <item name="*android:useDisabledAlpha">true</item>
+ <item name="android:clipToOutline">false</item>
+ <item name="android:clipToPadding">false</item>
</style>
<style name="Widget.DeviceDefault.ActionBar.Solid" parent="android:Widget.Material.ActionBar.Solid">
diff --git a/car_product/overlay-visual/frameworks/base/core/res/res/values/themes_device_defaults.xml b/car_product/overlay-visual/frameworks/base/core/res/res/values/themes_device_defaults.xml
index 61262a1..45c33d3 100644
--- a/car_product/overlay-visual/frameworks/base/core/res/res/values/themes_device_defaults.xml
+++ b/car_product/overlay-visual/frameworks/base/core/res/res/values/themes_device_defaults.xml
@@ -43,6 +43,15 @@
<item name="android:textAppearanceListItemSmall">@*android:style/TextAppearance.DeviceDefault.Large</item>
<item name="android:textAppearanceListItemSecondary">@*android:style/TextAppearance.DeviceDefault.Small</item>
<item name="android:actionBarSize">@*android:dimen/car_app_bar_height</item>
+
+ <item name="colorSwitchThumbNormal">@color/car_switch_thumb_color</item>
+ <item name="colorSwitchTrack">@color/car_switch_track_background_selector</item>
+ <item name="colorSwitchThumbDisabled">@color/car_switch_thumb_color_disabled_on_dark</item>
+ <item name="colorSeekBarTrackBackground">@color/car_seekbar_track_background</item>
+ <item name="colorSeekBarTrackProgress">@color/car_seekbar_track_progress</item>
+ <item name="colorSeekBarTrackProgressSecondary">@color/car_seekbar_track_progress_secondary</item>
+ <item name="colorSeekbarThumb">@color/car_seekbar_thumb_selector</item>
+ <item name="*android:disabledAlpha">@dimen/car_disabled_alpha</item>
</style>
<style name="Theme.DeviceDefault.Dialog" parent="android:Theme.Material.Dialog">
@@ -113,7 +122,17 @@
<!-- The light theme is defined to be the same as the default since currently there is only one
defined theme palette -->
- <style name="Theme.DeviceDefault.Light" parent="android:Theme.DeviceDefault"/>
+ <style name="Theme.DeviceDefault.Light" parent="android:Theme.DeviceDefault">
+ <item name="android:switchStyle">@style/Widget.DeviceDefault.Light.CompoundButton.Switch</item>
+ <item name="colorSwitchThumbNormal">@color/car_switch_thumb_color</item>
+ <item name="colorSwitchThumbDisabled">@color/car_switch_thumb_color_disabled_on_light</item>
+ <item name="colorSeekBarTrackBackground">@color/car_seekbar_track_background</item>
+ <item name="colorSeekBarTrackProgress">@color/car_seekbar_track_progress</item>
+ <item name="colorSeekBarTrackProgressSecondary">@color/car_seekbar_track_progress_secondary</item>
+ <item name="*android:disabledAlpha">@dimen/car_disabled_alpha</item>
+ <!-- To simulate transparency over light (white) background, we don't need a selector. -->
+ <item name="colorSeekbarThumb">@color/car_seekbar_thumb</item>
+ </style>
<style name="Theme.DeviceDefault.Light.Dialog" parent="android:Theme.DeviceDefault.Dialog"/>
<style name="Theme.DeviceDefault.Light.Dialog.Alert" parent="android:Theme.DeviceDefault.Dialog.Alert"/>
<style name="Theme.DeviceDefault.Light.Dialog.NoActionBar" parent="android:Theme.DeviceDefault.Dialog.NoActionBar"/>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-af/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-af/strings.xml
index abf8fd2..1242730 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-af/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-af/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Bestuurder"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"kry benaderde ligging net op die voorgrond"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Skakel mikrofoon aan"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-am/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-am/strings.xml
index 0f190c4..019cd32 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-am/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-am/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ነጂ"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ከፊት ለፊት ብቻ ግምታዊ አካባቢን ድረስ"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"ማይክሮፎንን አብራ"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ar/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ar/strings.xml
index aa51d5d..0a849a1 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ar/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ar/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"السائق"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"الوصول إلى الموقع الجغرافي التقريبي في الواجهة الأمامية فقط"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"تفعيل الميكروفون"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-as/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-as/strings.xml
index ea877d4..84a2388 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-as/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-as/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"চালক"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"কেৱল অগ্ৰভূমিত আনুমানিক অৱস্থান এক্সেছ কৰক"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"মাইক্ৰ’ফ’ন অন কৰক"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-az/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-az/strings.xml
index 3e2ccc1..a836051 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-az/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-az/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Sürücü"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"yalnız ön planda təqribi məkana daxil olun"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Mikrofonu aktiv edin"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-b+sr+Latn/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-b+sr+Latn/strings.xml
index a20c58e..6e1d36e 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-b+sr+Latn/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-b+sr+Latn/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Vozač"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"pristup približnoj lokaciji samo u prvom planu"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Uključi mikrofon"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-be/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-be/strings.xml
index d6e8de4..09b2d78 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-be/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-be/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Вадзіцель"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"доступ да прыблізнага месцазнаходжання толькі ў асноўным рэжыме"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Уключыць мікрафон"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-bg/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-bg/strings.xml
index 7a2fd23..1f72cf9 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-bg/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-bg/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Шофьор"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"достъп до приблизителното местоположение само на преден план"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Включване на микрофона"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-bn/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-bn/strings.xml
index e9725c8..31e0c0f 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-bn/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-bn/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ড্রাইভার"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"শুধুমাত্র অ্যাপটি খোলা থাকলে আপনার আনুমানিক লোকেশন অ্যাক্সেস করা"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"মাইক্রোফোন চালু করুন"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-bs/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-bs/strings.xml
index a20c58e..6e1d36e 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-bs/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-bs/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Vozač"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"pristup približnoj lokaciji samo u prvom planu"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Uključi mikrofon"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ca/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ca/strings.xml
index 8d24e22..45600ba 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ca/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ca/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Conductor"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"accedeix a la ubicació aproximada només en primer pla"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Activa el micròfon"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-cs/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-cs/strings.xml
index 2432df2..19a4398 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-cs/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-cs/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Řidič"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"přístup k přibližné poloze jen na popředí"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Zapnout mikrofon"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-da/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-da/strings.xml
index 731f251..46ca7f4 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-da/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-da/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Chauffør"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"få kun adgang til omtrentlig lokation i forgrunden"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Aktivér mikrofon"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-de/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-de/strings.xml
index 9febde6..b5e8eb5 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-de/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-de/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Fahrer"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"Nur bei Ausführung im Vordergrund auf den ungefähren Standort zugreifen"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Mikrofon einschalten"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-el/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-el/strings.xml
index fcfe739..955af7d 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-el/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-el/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Οδηγός"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"πρόσβαση στην κατά προσέγγιση τοποθεσία μόνο στο προσκήνιο"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Ενεργοποίηση μικροφώνου"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-en-rAU/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-en-rAU/strings.xml
index 85c4908..931327c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-en-rAU/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-en-rAU/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Driver"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"access approximate location only in the foreground"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Turn on microphone"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-en-rCA/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-en-rCA/strings.xml
index 85c4908..931327c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-en-rCA/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-en-rCA/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Driver"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"access approximate location only in the foreground"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Turn on microphone"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-en-rGB/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-en-rGB/strings.xml
index 85c4908..931327c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-en-rGB/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-en-rGB/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Driver"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"access approximate location only in the foreground"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Turn on microphone"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-en-rIN/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-en-rIN/strings.xml
index 85c4908..931327c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-en-rIN/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-en-rIN/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Driver"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"access approximate location only in the foreground"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Turn on microphone"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-en-rXC/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-en-rXC/strings.xml
index 7778984..e292c14 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-en-rXC/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-en-rXC/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Driver"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"access approximate location only in the foreground"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Turn on microphone"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-es-rUS/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-es-rUS/strings.xml
index 5666331..c36a675 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-es-rUS/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-es-rUS/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Conductor"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"acceder a la ubicación aproximada solo en primer plano"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Activar micrófono"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-es/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-es/strings.xml
index 3e3ad48..587af8d 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-es/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-es/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Conductor"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"acceder a la ubicación aproximada solo al estar en primer plano"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Activar micrófono"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-et/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-et/strings.xml
index 27ae02b..7e8416c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-et/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-et/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Sõitja"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"juurdepääs ligikaudsele asukohale ainult esiplaanil"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Lülita mikrofon sisse"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-eu/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-eu/strings.xml
index f7e62cf..fc2cc39 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-eu/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-eu/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Gidaria"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"atzitu gutxi gorabeherako kokapena aurreko planoan bakarrik"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Aktibatu mikrofonoa"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-fa/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-fa/strings.xml
index 4d257d6..100012c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-fa/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-fa/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"راننده"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"دسترسی به مکان تقریبی فقط در پیشزمینه"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"روشن کردن میکروفون"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-fi/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-fi/strings.xml
index b2d5135..bcc420d 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-fi/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-fi/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Kuljettaja"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"käyttää likimääräistä sijaintia vain etualalla"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Laita mikrofoni päälle"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-fr-rCA/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-fr-rCA/strings.xml
index 2ac4be7..8b0f9ed 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-fr-rCA/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-fr-rCA/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Conducteur"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"accéder à votre position approximative seulement en avant-plan"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Activer le microphone"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-fr/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-fr/strings.xml
index 81af986..bf2be7a 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-fr/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-fr/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Conducteur"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"accéder à la position approximative au premier plan uniquement"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Activer le micro"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-gl/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-gl/strings.xml
index 3754442..4817994 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-gl/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-gl/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Condutor"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"acceder á localización aproximada só en primeiro plano"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Activar micrófono"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-gu/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-gu/strings.xml
index 5c6ff84..9cf7aca 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-gu/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-gu/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ડ્રાઇવર"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ફૉરગ્રાઉન્ડમાં ફક્ત અંદાજિત સ્થાન ઍક્સેસ કરો"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"માઇક્રોફોન ચાલુ કરો"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-hi/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-hi/strings.xml
index 003478d..0f13434 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-hi/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-hi/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ड्राइवर"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"अनुमानित जगह की जानकारी सिर्फ़ तब ऐक्सेस करें, जब ऐप्लिकेशन स्क्रीन पर खुला हो"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"माइक्रोफ़ोन चालू करें"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-hr/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-hr/strings.xml
index 4cffd31..50e3b82 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-hr/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-hr/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Vozač"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"pristupiti približnoj lokaciji samo u prednjem planu"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Uključite mikrofon"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-hu/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-hu/strings.xml
index 52b7760..07d55c5 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-hu/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-hu/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Sofőr"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"megközelítőleges helyadatokhoz való hozzáférés csak előtérben"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Mikrofon bekapcsolása"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-hy/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-hy/strings.xml
index 68df140..0b4e067 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-hy/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-hy/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Վարորդ"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"տեղադրության մոտավոր տվյալների հասանելիություն միայն ֆոնային ռեժիմում"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Միացնել խոսափողը"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-in/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-in/strings.xml
index 21c21a8..034090b 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-in/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-in/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Pengemudi"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"akses perkiraan lokasi hanya saat di latar depan"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Aktifkan mikrofon"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-is/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-is/strings.xml
index 1eb3776..33d8243 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-is/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-is/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Ökumaður"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"aðgangur að áætlaðri staðsetningu aðeins í forgrunni"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Kveikja á hljóðnema"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-it/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-it/strings.xml
index c704544..c4a0027 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-it/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-it/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Autista"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"Accesso alla posizione approssimativa solo in primo piano"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Attiva il microfono"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-iw/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-iw/strings.xml
index a8d3bee..891c429 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-iw/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-iw/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"נהג/ת"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"קבלת גישה למיקום משוער בחזית בלבד"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"הפעלת המיקרופון"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ja/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ja/strings.xml
index eb41c01..09cd531 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ja/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ja/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ドライバー"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"フォアグラウンドでのみおおよその位置情報を取得"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"マイクを ON にする"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ka/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ka/strings.xml
index 4d26423..d72109c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ka/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ka/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"მძღოლი"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"მიახლოებით მდებარეობაზე წვდომა მხოლოდ წინა პლანზე"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"მიკროფონის ჩართვა"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-kk/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-kk/strings.xml
index b12a6d5..b3fdf5a 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-kk/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-kk/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Көлік жүргізуші"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"болжалды орналасқан жер туралы ақпаратқа тек ашық экранда кіру"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Микрофонды қосу"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-km/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-km/strings.xml
index 4975540..70f5781 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-km/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-km/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"អ្នកបើកបរ"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ចូលប្រើទីតាំងប្រហាក់ប្រហែលតែនៅផ្ទៃខាងមុខប៉ុណ្ណោះ"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"បើកមីក្រូហ្វូន"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-kn/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-kn/strings.xml
index 27e9a9e..359c573 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-kn/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-kn/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ಡ್ರೈವರ್"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ಮುನ್ನೆಲೆಯಲ್ಲಿ ಮಾತ್ರ ಅಂದಾಜು ಸ್ಥಳವನ್ನು ಪ್ರವೇಶಿಸಿ"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"ಮೈಕ್ರೋಫೋನ್ ಆನ್ ಮಾಡಿ"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ko/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ko/strings.xml
index 20facb1..5260947 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ko/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ko/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"운전자"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"포그라운드에서만 대략적인 위치에 액세스"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"마이크 사용 설정"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ky/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ky/strings.xml
index 1a43c49..5858cbd 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ky/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ky/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Айдоочу"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"болжолдуу аныкталган жайгашкан жерге активдүү режимде гана кирүүгө уруксат берүү"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Микрофонду күйгүзүү"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-lo/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-lo/strings.xml
index 877a259..4ca088f 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-lo/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-lo/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ຄົນຂັບລົດ"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ເຂົ້າເຖິງສະຖານທີ່ໂດຍປະມານເມື່ອຢູ່ໃນພື້ນໜ້າເທົ່ານັ້ນ"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"ເປີດໄມໂຄຣໂຟນ"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-lt/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-lt/strings.xml
index 134230f..f9f9163 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-lt/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-lt/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Vairuotojas"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"pasiekti apytikslę vietovę, tik kai programa veikia priekiniame plane"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Įjungti mikrofoną"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-lv/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-lv/strings.xml
index 3cc5c88..5cf45d8 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-lv/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-lv/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Vadītājs"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"piekļuve aptuvenai atrašanās vietai, tikai darbojoties priekšplānā"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Ieslēgt mikrofonu"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-mk/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-mk/strings.xml
index 3b1eda7..dde38ae 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-mk/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-mk/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Возач"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"пристап до приближната локација само во преден план"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Вклучи го микрофонот"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ml/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ml/strings.xml
index 7205182..883d61a 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ml/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ml/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ഡ്രൈവർ"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ഏകദേശ ലൊക്കേഷൻ ഫോർഗ്രൗണ്ടിൽ മാത്രം ആക്സസ് ചെയ്യുക"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"മൈക്രോഫോൺ ഓണാക്കുക"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-mn/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-mn/strings.xml
index dd1d6a2..9a38a23 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-mn/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-mn/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Жолооч"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ойролцоо байршилд зөвхөн дэлгэц дээр хандах"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Микрофоныг асаах"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-mr/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-mr/strings.xml
index a0619ec..96f9785 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-mr/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-mr/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ड्रायव्हर"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"फक्त फोअरग्राउंडमध्ये अंदाजे स्थान अॅक्सेस करा"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"मायक्रोफोन सुरू करा"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ms/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ms/strings.xml
index 7f22021..9c1d290 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ms/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ms/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Pemandu"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"akses lokasi anggaran hanya di latar depan"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Hidupkan mikrofon"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-my/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-my/strings.xml
index aea1568..075e0e0 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-my/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-my/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ယာဉ်မောင်းသူ"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"မျက်နှာစာတွင်သာ ခန့်မှန်းခြေ တည်နေရာ အသုံးပြုခြင်း"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"မိုက်ခရိုဖုန်း ဖွင့်ရန်"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-nb/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-nb/strings.xml
index 6e0de84..782c713 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-nb/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-nb/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Sjåfør"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"bare tilgang til omtrentlig posisjon i forgrunnen"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Slå på mikrofonen"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ne/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ne/strings.xml
index 76ca044..9dd934c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ne/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ne/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"चालक"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"अग्रभूमिमा मात्र अनुमानित स्थानमाथि पहुँच राख्नुहोस्"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"माइक्रोफोन अन गर्नुहोस्"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-nl/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-nl/strings.xml
index 873fb01..282ef51 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-nl/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-nl/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Chauffeur"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"alleen toegang tot geschatte locatie op de voorgrond"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Microfoon aanzetten"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-or/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-or/strings.xml
index 964683f..67d215b 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-or/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-or/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ଡ୍ରାଇଭର୍"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"କେବଳ ଫୋର୍ଗ୍ରାଉଣ୍ଡରେ ହାରାହାରି ଲୋକେସନ୍ ଆକ୍ସେସ୍ କରନ୍ତୁ"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"ମାଇକ୍ରୋଫୋନ ଚାଲୁ କରନ୍ତୁ"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-pa/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-pa/strings.xml
index 16541d3..2b6fd39 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-pa/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-pa/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ਡਰਾਈਵਰ"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"ਸਿਰਫ਼ ਫੋਰਗ੍ਰਾਊਂਡ ਵਿੱਚ ਅਨੁਮਾਨਿਤ ਟਿਕਾਣੇ ਤੱਕ ਪਹੁੰਚ ਕਰੋ"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਨੂੰ ਚਾਲੂ ਕਰੋ"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-pl/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-pl/strings.xml
index d0ff092..f96dbf3 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-pl/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-pl/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Kierowca"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"dostęp do przybliżonej lokalizacji tylko na pierwszym planie"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Włącz mikrofon"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-pt-rPT/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-pt-rPT/strings.xml
index 984000a..995d3ff 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-pt-rPT/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-pt-rPT/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Condutor"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"apenas aceder à localização aproximada em primeiro plano"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Ativar microfone"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-pt/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-pt/strings.xml
index 7ac9eef..80b30a0 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-pt/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-pt/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Motorista"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"acessar local aproximado apenas em primeiro plano"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Ativar microfone"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ro/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ro/strings.xml
index ec78db2..79dc141 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ro/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ro/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Șofer"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"să acceseze locația aproximativă numai în prim-plan"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Porniți microfonul"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ru/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ru/strings.xml
index ed49d76..f2e0e9c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ru/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ru/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Водитель"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"Доступ к приблизительному местоположению только в активном режиме"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Включить микрофон"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-si/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-si/strings.xml
index fcba27e..e5ab1ca 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-si/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-si/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"රියදුරු"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"පෙරබිම තුළ පමණක් ආසන්න ස්ථානයට ප්රවේශය"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"මයික්රෆෝනය ක්රියාත්මක කරන්න"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sk/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sk/strings.xml
index c9a990d..c005deb 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sk/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sk/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Vodič"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"prístup k približnej polohe iba v popredí"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Zapnúť mikrofón"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sl/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sl/strings.xml
index 918dda4..e1862a2 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sl/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sl/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Voznik"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"dostop do približne lokacije samo, ko deluje v ospredju"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Vklop mikrofona"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sq/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sq/strings.xml
index e4192e7..b156aa4 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sq/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sq/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Drejtuesi"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"qasu në vendndodhjen e përafërt vetëm në plan të parë"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Aktivizo mikrofonin"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sr/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sr/strings.xml
index 8cce889..039258c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sr/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sr/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Возач"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"приступ приближној локацији само у првом плану"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Укључи микрофон"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sv/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sv/strings.xml
index b5b4f63..1d15871 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sv/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sv/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Förare"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"endast åtkomst till ungefärlig plats i förgrunden"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Aktivera mikrofonen"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sw/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-sw/strings.xml
index 765973c..8e4d2b2 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-sw/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sw/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Dereva"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"kufikia mahali palipokadiriwa ikiwa tu programu imefunguliwa kwenye skrini"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Washa maikrofoni"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-sw600dp/config.xml b/car_product/overlay/frameworks/base/core/res/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..f7873a2
--- /dev/null
+++ b/car_product/overlay/frameworks/base/core/res/res/values-sw600dp/config.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Enable dynamic keyguard positioning for large-width screens. This will cause the keyguard
+ to be aligned to one side of the screen when in landscape mode. -->
+ <bool name="config_enableDynamicKeyguardPositioning">false</bool>
+</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ta/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ta/strings.xml
index 264f0c1..9036547 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ta/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ta/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"கார் உரிமையாளர்"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"முன்புலத்தில் இயங்கும்போது மட்டும் தோராயமான இருப்பிடத்தைக் கண்டறிதல்"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"மைக்ரோஃபோனை இயக்கு"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-te/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-te/strings.xml
index bf45f96..5e1da93 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-te/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-te/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"డ్రైవర్"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"స్క్రీన్పై ఉన్నప్పుడు మాత్రమే సమీప లొకేషన్ను యాక్సెస్ చేయండి"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"మైక్రోఫోన్ను ఆన్ చేయండి"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-th/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-th/strings.xml
index 8fae2ca..38f3fbe 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-th/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-th/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ผู้ขับรถ"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"เข้าถึงตำแหน่งโดยประมาณเมื่ออยู่เบื้องหน้าเท่านั้น"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"เปิดไมโครโฟน"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-tl/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-tl/strings.xml
index d425a87..0c85541 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-tl/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-tl/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Driver"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"i-access lang ang tinatantyang lokasyon sa foreground"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"I-on ang mikropono"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-tr/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-tr/strings.xml
index 244564e..a0ec764 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-tr/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-tr/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Sürücü"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"yalnızca ön planda yaklaşık konuma erişme"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Mikrofonu aç"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-uk/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-uk/strings.xml
index b46d679..d205c7c 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-uk/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-uk/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Водій"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"отримувати доступ до даних про приблизне місцезнаходження лише в активному режимі"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Увімкнути мікрофон"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-ur/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-ur/strings.xml
index 89fd655..256885e 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-ur/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-ur/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"ڈرائیور"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"صرف پیش منظر میں تخمینی مقام تک رسائی"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"مائیکروفون آن کریں"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-uz/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-uz/strings.xml
index 9914c10..5e4d8bf 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-uz/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-uz/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Haydovchi"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"taxminiy joylashuv axborotini olishga faqat old fonda ruxsat"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Mikrofonni yoqish"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-vi/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-vi/strings.xml
index d478052..8d95889 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-vi/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-vi/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Tài xế"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"chỉ truy cập thông tin vị trí gần đúng khi ứng dụng mở trên màn hình"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Bật micrô"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-zh-rCN/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-zh-rCN/strings.xml
index 06fe50d..203a6fa 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-zh-rCN/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-zh-rCN/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"司机"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"只有在前台运行时才能获取大致位置信息"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"开启麦克风"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-zh-rHK/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-zh-rHK/strings.xml
index fd93b30..4804542 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-zh-rHK/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-zh-rHK/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"司機"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"只在前景存取概略位置"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"開啟麥克風"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-zh-rTW/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-zh-rTW/strings.xml
index 59f09e1..eac10a2 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-zh-rTW/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-zh-rTW/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"駕駛"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"僅可在前景中取得概略位置"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"開啟麥克風"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values-zu/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values-zu/strings.xml
index adb9402..33cf0dc 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values-zu/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values-zu/strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="owner_name" msgid="3416113395996003764">"Umshayeli"</string>
<string name="permlab_accessCoarseLocation" msgid="2494909511737161237">"finyelela indawo enembile kuphela engaphambili"</string>
+ <string name="sensor_privacy_start_use_dialog_turn_on_button" msgid="2297855099350562159">"Vula imakrofoni"</string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/config.xml b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
index d7c566c..73326f1 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/config.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/config.xml
@@ -151,4 +151,15 @@
<!-- Whether this device is supporting the microphone toggle -->
<bool name="config_supportsMicToggle">true</bool>
+ <!-- Whether the airplane mode should be reset when device boots in non-safemode after exiting
+ from safemode.
+ This flag should be enabled only when the product does not have any UI to toggle airplane
+ mode like automotive devices.-->
+ <bool name="config_autoResetAirplaneMode">true</bool>
+
+ <!-- The component name of the activity for the companion-device-manager notification access
+ confirmation. -->
+ <string name="config_notificationAccessConfirmationActivity" translatable="false">
+ com.android.car.settings/com.android.car.settings.notifications.NotificationAccessConfirmationActivity
+ </string>
</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/policy_exempt_apps.xml b/car_product/overlay/frameworks/base/core/res/res/values/policy_exempt_apps.xml
index 9ab0ed3..27986a4 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/policy_exempt_apps.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/policy_exempt_apps.xml
@@ -20,7 +20,6 @@
device policies or APIs.
-->
<string-array translatable="false" name="policy_exempt_apps">
- <item>com.android.car.carlauncher</item>
<item>com.android.car.cluster.home</item>
<item>com.android.car.hvac</item>
<item>com.android.car.media</item>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/required_apps_managed_user.xml b/car_product/overlay/frameworks/base/core/res/res/values/required_apps_managed_user.xml
new file mode 100644
index 0000000..a04429e
--- /dev/null
+++ b/car_product/overlay/frameworks/base/core/res/res/values/required_apps_managed_user.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <string-array translatable="false" name="required_apps_managed_user">
+
+ <!-- NOTE: apps below were copied from phone, replacing the equivalent
+ car app when needed -->
+ <item>com.android.car.settings</item>
+ <item>com.android.systemui</item>
+ <item>com.android.car.dialer</item>
+ <item>com.android.contacts</item>
+ <item>com.android.stk</item>
+ <item>com.android.providers.downloads</item>
+ <item>com.android.providers.downloads.ui</item>
+ <item>com.android.documentsui</item>
+
+ <!-- Car-specific apps -->
+ <item>com.android.car.bugreport</item>
+ <item>com.android.car.acast.source</item>
+ <item>com.android.car.calendar</item>
+ <item>com.android.car.dialer</item>
+ <item>com.android.car.messenger</item>
+ <item>com.android.car.radio</item>
+ <item>com.android.car.speedbump</item>
+ <item>com.android.car.themeplayground</item>
+ <item>com.android.car.voicecontrol</item>
+
+ </string-array>
+</resources>
diff --git a/car_product/overlay/frameworks/base/core/res/res/values/strings.xml b/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
index 34fd92f..826c9f6 100644
--- a/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
+++ b/car_product/overlay/frameworks/base/core/res/res/values/strings.xml
@@ -19,4 +19,6 @@
<!-- Default name of the owner user [CHAR LIMIT=20] -->
<string name="owner_name">Driver</string>
<string name="permlab_accessCoarseLocation">access approximate location only in the foreground</string>
+ <!--- Action button in the dialog triggered if microphone is disabled but an app tried to access it. [CHAR LIMIT=60] -->
+ <string name="sensor_privacy_start_use_dialog_turn_on_button">Turn on microphone</string>
</resources>
diff --git a/car_product/rro/CarSystemUIEvsRRO/Android.bp b/car_product/rro/CarSystemUIEvsRRO/Android.bp
deleted file mode 100644
index ebb09ef..0000000
--- a/car_product/rro/CarSystemUIEvsRRO/Android.bp
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2021 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 {
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-android_app {
- name: "CarSystemUIEvsRRO",
- resource_dirs: ["res"],
- platform_apis: true,
- aaptflags: [
- "--no-resource-deduping",
- "--no-resource-removal"
- ],
-}
diff --git a/car_product/rro/CarSystemUIEvsRRO/AndroidManifest.xml b/car_product/rro/CarSystemUIEvsRRO/AndroidManifest.xml
deleted file mode 100644
index f1dc7b8..0000000
--- a/car_product/rro/CarSystemUIEvsRRO/AndroidManifest.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2021 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.
- -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.systemui.car.evs.rro">
- <application android:hasCode="false"/>
- <overlay android:targetName="CarSystemUI"
- android:targetPackage="com.android.systemui"
- android:resourcesMap="@xml/overlays"
- android:isStatic="true" />
-</manifest>
diff --git a/car_product/rro/CarSystemUIEvsRRO/res/values/config.xml b/car_product/rro/CarSystemUIEvsRRO/res/values/config.xml
deleted file mode 100644
index 4fab05c..0000000
--- a/car_product/rro/CarSystemUIEvsRRO/res/values/config.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2021 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.
- -->
-<resources>
- <string name="config_rearViewCameraActivity" translatable="false">
- com.google.android.car.evs/com.google.android.car.evs.CarEvsCameraPreviewActivity
- </string>
-</resources>
diff --git a/car_product/rro/CarSystemUIEvsRRO/res/xml/overlays.xml b/car_product/rro/CarSystemUIEvsRRO/res/xml/overlays.xml
deleted file mode 100644
index 36785dc..0000000
--- a/car_product/rro/CarSystemUIEvsRRO/res/xml/overlays.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<!--
- ~ Copyright (C) 2021 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.
- -->
-
-<overlay>
- <item target="string/config_rearViewCameraActivity"
- value="@string/config_rearViewCameraActivity"/>
-</overlay>
diff --git a/car_product/sepolicy/private/carservice_app.te b/car_product/sepolicy/private/carservice_app.te
index 04a5808..87fd0b7 100644
--- a/car_product/sepolicy/private/carservice_app.te
+++ b/car_product/sepolicy/private/carservice_app.te
@@ -103,3 +103,6 @@
allow carservice_app gpu_device:dir r_dir_perms;
allow carservice_app gpu_service:service_manager find;
binder_call(carservice_app, gpuservice)
+
+# Allow reading and writing /proc/loadavg/
+allow carservice_app proc_loadavg:file { open read getattr };
diff --git a/cpp/evs/manager/1.1/Enumerator.cpp b/cpp/evs/manager/1.1/Enumerator.cpp
index d6f532d..b33ddac 100644
--- a/cpp/evs/manager/1.1/Enumerator.cpp
+++ b/cpp/evs/manager/1.1/Enumerator.cpp
@@ -79,7 +79,7 @@
// Connect with the underlying hardware enumerator
mHwEnumerator = IEvsEnumerator::getService(hardwareServiceName);
- bool result = (mHwEnumerator.get() != nullptr);
+ bool result = (mHwEnumerator != nullptr);
if (result) {
// Get an internal display identifier.
mHwEnumerator->getDisplayIdList(
@@ -296,7 +296,7 @@
Return<void> Enumerator::closeCamera(const ::android::sp<IEvsCamera_1_0>& clientCamera) {
LOG(DEBUG) << __FUNCTION__;
- if (clientCamera.get() == nullptr) {
+ if (clientCamera == nullptr) {
LOG(ERROR) << "Ignoring call with null camera pointer.";
return Void();
}
@@ -317,6 +317,7 @@
// NOTE: This should drop our last reference to the camera, resulting in its
// destruction.
mActiveCameras.erase(halCamera->getId());
+ mHwEnumerator->closeCamera(halCamera->getHwCamera());
if (mMonitorEnabled) {
mClientsMonitor->unregisterClientToMonitor(halCamera->getId());
}
@@ -358,8 +359,7 @@
mEmulatedCameraDevices[id]);
}
} else {
- device = IEvsCamera_1_1::castFrom(mHwEnumerator->openCamera_1_1(id, streamCfg))
- .withDefault(nullptr);
+ device = mHwEnumerator->openCamera_1_1(id, streamCfg);
}
if (device == nullptr) {
diff --git a/cpp/evs/manager/1.1/HalCamera.cpp b/cpp/evs/manager/1.1/HalCamera.cpp
index e527840..47cad2d 100644
--- a/cpp/evs/manager/1.1/HalCamera.cpp
+++ b/cpp/evs/manager/1.1/HalCamera.cpp
@@ -63,7 +63,7 @@
}
-bool HalCamera::ownVirtualCamera(sp<VirtualCamera> virtualCamera) {
+bool HalCamera::ownVirtualCamera(sp<VirtualCamera>& virtualCamera) {
if (virtualCamera == nullptr) {
LOG(ERROR) << "Failed to create virtualCamera camera object";
@@ -86,20 +86,45 @@
return true;
}
-
-void HalCamera::disownVirtualCamera(sp<VirtualCamera> virtualCamera) {
+void HalCamera::disownVirtualCamera(sp<VirtualCamera>& virtualCamera) {
// Ignore calls with null pointers
- if (virtualCamera.get() == nullptr) {
+ if (virtualCamera == nullptr) {
LOG(WARNING) << "Ignoring disownVirtualCamera call with null pointer";
return;
}
// Remove the virtual camera from our client list
- unsigned clientCount = mClients.size();
+ const auto clientCount = mClients.size();
mClients.remove(virtualCamera);
if (clientCount != mClients.size() + 1) {
- LOG(ERROR) << "Couldn't find camera in our client list to remove it; "
- << "this client may be removed already.";
+ LOG(WARNING) << "Couldn't find camera in our client list to remove it; "
+ << "this client may be removed already.";
+ }
+
+ // Recompute the number of buffers required with the target camera removed from the list
+ if (!changeFramesInFlight(0)) {
+ LOG(ERROR) << "Error when trying to reduce the in flight buffer count";
+ }
+
+ // Update statistics
+ mUsageStats->updateNumClients(mClients.size());
+}
+
+void HalCamera::disownVirtualCamera(const VirtualCamera* clientToDisown) {
+ // Ignore calls with null pointers
+ if (clientToDisown == nullptr) {
+ LOG(WARNING) << "Ignoring disownVirtualCamera call with null pointer";
+ return;
+ }
+
+ // Remove the virtual camera from our client list
+ const auto clientCount = mClients.size();
+ mClients.remove_if([&clientToDisown](wp<VirtualCamera>& client) {
+ return client == clientToDisown;
+ });
+ if (clientCount == mClients.size()) {
+ LOG(WARNING) << "Couldn't find camera in our client list to remove it; "
+ << "this client may be removed already.";
}
// Recompute the number of buffers required with the target camera removed from the list
diff --git a/cpp/evs/manager/1.1/HalCamera.h b/cpp/evs/manager/1.1/HalCamera.h
index 1b05443..a63684a 100644
--- a/cpp/evs/manager/1.1/HalCamera.h
+++ b/cpp/evs/manager/1.1/HalCamera.h
@@ -58,7 +58,7 @@
// stream from the hardware camera and distribute it to the associated VirtualCamera objects.
class HalCamera : public IEvsCameraStream_1_1 {
public:
- HalCamera(sp<IEvsCamera_1_1> hwCamera,
+ HalCamera(sp<IEvsCamera_1_1>& hwCamera,
std::string deviceId = "",
int32_t recordId = 0,
Stream cfg = {})
@@ -75,8 +75,9 @@
// Factory methods for client VirtualCameras
sp<VirtualCamera> makeVirtualCamera();
- bool ownVirtualCamera(sp<VirtualCamera> virtualCamera);
- void disownVirtualCamera(sp<VirtualCamera> virtualCamera);
+ bool ownVirtualCamera(sp<VirtualCamera>& virtualCamera);
+ void disownVirtualCamera(sp<VirtualCamera>& virtualCamera);
+ void disownVirtualCamera(const VirtualCamera* virtualCamera);
// Implementation details
sp<IEvsCamera_1_0> getHwCamera() { return mHwCamera; };
diff --git a/cpp/evs/manager/1.1/VirtualCamera.cpp b/cpp/evs/manager/1.1/VirtualCamera.cpp
index a7e6329..2d416d2 100644
--- a/cpp/evs/manager/1.1/VirtualCamera.cpp
+++ b/cpp/evs/manager/1.1/VirtualCamera.cpp
@@ -86,6 +86,9 @@
// Give the underlying hardware camera the heads up that it might be time to stop
pHwCamera->clientStreamEnding(this);
+
+ // Retire from the participating HW camera's client list
+ pHwCamera->disownVirtualCamera(this);
}
// Join a capture thread
@@ -385,7 +388,6 @@
// This happens when either a new frame does not arrive
// before a timer expires or we're requested to stop
// capturing frames.
- LOG(DEBUG) << "Exiting a capture thread.";
break;
} else if (mStreamState == RUNNING) {
// Fetch frames and forward to the client
@@ -399,6 +401,9 @@
if (pHwCamera == nullptr) {
continue;
}
+ if (mFramesHeld[key].size() == 0) {
+ continue;
+ }
const auto frame = mFramesHeld[key].back();
if (frame.timestamp > lastFrameTimestamp) {
@@ -678,7 +683,7 @@
return EvsResult::INVALID_ARG;
}
- if (display.get() == nullptr) {
+ if (display == nullptr) {
LOG(ERROR) << __FUNCTION__
<< ": Passed display is invalid";
return EvsResult::INVALID_ARG;
diff --git a/cpp/evs/manager/sepolicy/private/evs_manager.te b/cpp/evs/manager/sepolicy/private/evs_manager.te
index d8dba4b..ae7ec68 100644
--- a/cpp/evs/manager/sepolicy/private/evs_manager.te
+++ b/cpp/evs/manager/sepolicy/private/evs_manager.te
@@ -15,3 +15,6 @@
# allow to use carservice_app
binder_call(evs_manager, carservice_app)
+
+# allow to use the graphics allocator
+allow evs_manager hal_graphics_allocator:fd use;
diff --git a/cpp/evs/sampleDriver/EvsV4lCamera.cpp b/cpp/evs/sampleDriver/EvsV4lCamera.cpp
index db6e4ee..8fb4882 100644
--- a/cpp/evs/sampleDriver/EvsV4lCamera.cpp
+++ b/cpp/evs/sampleDriver/EvsV4lCamera.cpp
@@ -28,13 +28,10 @@
#include <ui/GraphicBufferMapper.h>
#include <utils/SystemClock.h>
+namespace {
-namespace android {
-namespace hardware {
-namespace automotive {
-namespace evs {
-namespace V1_1 {
-namespace implementation {
+// The size of a pixel of RGBA format data in bytes
+constexpr auto kBytesPerPixelRGBA = 4;
// Default camera output image resolution
const std::array<int32_t, 2> kDefaultResolution = {640, 480};
@@ -43,6 +40,16 @@
// Safeguards against unreasonable resource consumption and provides a testable limit
static const unsigned MAX_BUFFERS_IN_FLIGHT = 100;
+}; // anonymous namespace
+
+
+namespace android {
+namespace hardware {
+namespace automotive {
+namespace evs {
+namespace V1_1 {
+namespace implementation {
+
EvsV4lCamera::EvsV4lCamera(const char *deviceName,
unique_ptr<ConfigManager::CameraInfo> &camInfo) :
mFramesAllowed(0),
@@ -739,7 +746,7 @@
android::base::unique_fd fd(open(filename.c_str(),
O_WRONLY | O_CREAT,
S_IRUSR | S_IWUSR | S_IRGRP));
- LOG(ERROR) << filename << ", " << fd;
+ LOG(INFO) << filename << ", " << fd;
if (fd == -1) {
PLOG(ERROR) << "Failed to open a file, " << filename;
} else {
@@ -775,6 +782,11 @@
bufDesc_1_1.timestamp =
pV4lBuff->timestamp.tv_sec * 1e+6 + pV4lBuff->timestamp.tv_usec;
+ const auto sizeInRGBA = pDesc->width * pDesc->height * kBytesPerPixelRGBA;
+ if (mColorSpaceConversionBuffer.size() < sizeInRGBA) {
+ mColorSpaceConversionBuffer.resize(sizeInRGBA);
+ }
+
// Lock our output buffer for writing
// TODO(b/145459970): Sometimes, physical camera device maps a buffer
// into the address that is about to be unmapped by another device; this
@@ -799,7 +811,8 @@
// Transfer the video image into the output buffer, making any needed
// format conversion along the way
- mFillBufferFromVideo(bufDesc_1_1, (uint8_t *)targetPixels, pData, mVideo.getStride());
+ mFillBufferFromVideo(bufDesc_1_1, (uint8_t *)targetPixels, pData,
+ mColorSpaceConversionBuffer.data(), mStride);
// Unlock the output buffer
mapper.unlock(bufDesc_1_1.buffer.nativeHandle);
diff --git a/cpp/evs/sampleDriver/EvsV4lCamera.h b/cpp/evs/sampleDriver/EvsV4lCamera.h
index 4aa8bc7..28bd7b8 100644
--- a/cpp/evs/sampleDriver/EvsV4lCamera.h
+++ b/cpp/evs/sampleDriver/EvsV4lCamera.h
@@ -143,7 +143,7 @@
// Which format specific function we need to use to move camera imagery into our output buffers
void(*mFillBufferFromVideo)(const BufferDesc& tgtBuff, uint8_t* tgt,
- void* imgData, unsigned imgStride);
+ void* imgData, void* buf, unsigned imgStride);
EvsResult doneWithFrame_impl(const uint32_t id, const buffer_handle_t handle);
@@ -166,6 +166,9 @@
// Frame counter
uint64_t mFrameCounter = 0;
+
+ // A buffer to hold an intermediate color conversion data
+ std::vector<uint8_t> mColorSpaceConversionBuffer;
};
} // namespace implementation
diff --git a/cpp/evs/sampleDriver/bufferCopy.cpp b/cpp/evs/sampleDriver/bufferCopy.cpp
index 098af61..253e39d 100644
--- a/cpp/evs/sampleDriver/bufferCopy.cpp
+++ b/cpp/evs/sampleDriver/bufferCopy.cpp
@@ -19,6 +19,14 @@
#include <android-base/logging.h>
#include <libyuv.h>
+namespace {
+
+inline constexpr size_t kYuv422BytesPerPixel = 2;
+inline constexpr size_t kRgbaBytesPerPixel = 4;
+
+}; // anonymous namespace
+
+
namespace android {
namespace hardware {
namespace automotive {
@@ -38,7 +46,8 @@
}
-void fillNV21FromNV21(const BufferDesc& tgtBuff, uint8_t* tgt, void* imgData, unsigned) {
+void fillNV21FromNV21(
+ const BufferDesc& tgtBuff, uint8_t* tgt, void* imgData, void*, unsigned) {
// The NV21 format provides a Y array of 8bit values, followed by a 1/2 x 1/2 interleave U/V array.
// It assumes an even width and height for the overall image, and a horizontal stride that is
// an even multiple of 16 bytes for both the Y and UV arrays.
@@ -57,7 +66,8 @@
}
-void fillNV21FromYUYV(const BufferDesc& tgtBuff, uint8_t* tgt, void* imgData, unsigned imgStride) {
+void fillNV21FromYUYV(
+ const BufferDesc& tgtBuff, uint8_t* tgt, void* imgData, void*, unsigned imgStride) {
// The YUYV format provides an interleaved array of pixel values with U and V subsampled in
// the horizontal direction only. Also known as interleaved 422 format. A 4 byte
// "macro pixel" provides the Y value for two adjacent pixels and the U and V values shared
@@ -120,15 +130,17 @@
}
-void fillRGBAFromYUYV(const BufferDesc& tgtBuff, uint8_t* tgt, void* imgData, unsigned imgStride) {
+void fillRGBAFromYUYV(
+ const BufferDesc& tgtBuff, uint8_t* tgt, void* imgData, void* buf, unsigned imgStride) {
const AHardwareBuffer_Desc* pDesc =
reinterpret_cast<const AHardwareBuffer_Desc*>(&tgtBuff.buffer.description);
// Converts YUY2ToARGB (little endian). Please note that libyuv uses the
// little endian while we're using the big endian in RGB format names.
- const auto dstStrideInBytes = pDesc->stride * 4; // 4-byte per pixel
+ const auto srcStrideInBytes = imgStride * kYuv422BytesPerPixel;
+ const auto dstStrideInBytes = pDesc->stride * kRgbaBytesPerPixel;
auto result = libyuv::YUY2ToARGB((const uint8_t*)imgData,
- imgStride, // input stride in bytes
- tgt,
+ srcStrideInBytes, // input stride in bytes
+ (uint8_t*)buf,
dstStrideInBytes, // output stride in bytes
pDesc->width,
pDesc->height);
@@ -137,10 +149,8 @@
return;
}
- // Swaps R and B pixels to convert BGRA to RGBA in place.
- // TODO(b/190783702): Consider allocating an extra space to store ARGB data
- // temporarily if below operation is too slow.
- result = libyuv::ABGRToARGB(tgt, dstStrideInBytes, tgt, dstStrideInBytes,
+ // Swaps R and B pixels to convert BGRA to RGBA
+ result = libyuv::ABGRToARGB((uint8_t*)buf, dstStrideInBytes, tgt, dstStrideInBytes,
pDesc->width, pDesc->height);
if (result) {
LOG(ERROR) << "Failed to convert BGRA to RGBA.";
@@ -148,31 +158,60 @@
}
-void fillYUYVFromYUYV(const BufferDesc& tgtBuff, uint8_t* tgt, void* imgData, unsigned imgStride) {
+void fillRGBAFromUYVY(
+ const BufferDesc& tgtBuff, uint8_t* tgt, void* imgData, void* buf, unsigned imgStride) {
const AHardwareBuffer_Desc* pDesc =
reinterpret_cast<const AHardwareBuffer_Desc*>(&tgtBuff.buffer.description);
- unsigned width = pDesc->width;
- unsigned height = pDesc->height;
- uint8_t* src = (uint8_t*)imgData;
- uint8_t* dst = (uint8_t*)tgt;
- unsigned srcStrideBytes = imgStride;
- unsigned dstStrideBytes = pDesc->stride * 2;
+ // Converts UYVYToARGB (little endian). Please note that libyuv uses the
+ // little endian while we're using the big endian in RGB format names.
+ const auto srcStrideInBytes = imgStride * kYuv422BytesPerPixel;
+ const auto dstStrideInBytes = pDesc->stride * kRgbaBytesPerPixel;
+ auto result = libyuv::UYVYToARGB(static_cast<const uint8_t*>(imgData),
+ srcStrideInBytes, // input stride in bytes
+ static_cast<uint8_t*>(buf),
+ dstStrideInBytes, // output stride in bytes
+ pDesc->width,
+ pDesc->height);
+ if (result) {
+ LOG(ERROR) << "Failed to convert UYVY to BGRA.";
+ return;
+ }
- for (unsigned r=0; r<height; r++) {
- // Copy a pixel row at a time (2 bytes per pixel, averaged over a YUYV macro pixel)
- memcpy(dst+r*dstStrideBytes, src+r*srcStrideBytes, width*2);
+ // Swaps R and B pixels to convert BGRA to RGBA
+ result = libyuv::ABGRToARGB(static_cast<uint8_t*>(buf), dstStrideInBytes, tgt,
+ dstStrideInBytes, pDesc->width, pDesc->height);
+ if (result) {
+ LOG(WARNING) << "Failed to convert BGRA to RGBA.";
}
}
-void fillYUYVFromUYVY(const BufferDesc& tgtBuff, uint8_t* tgt, void* imgData, unsigned imgStride) {
+void fillYUYVFromYUYV(
+ const BufferDesc& tgtBuff, uint8_t* tgt, void* imgData, void *, unsigned imgStride) {
+ const AHardwareBuffer_Desc* pDesc =
+ reinterpret_cast<const AHardwareBuffer_Desc*>(&tgtBuff.buffer.description);
+ const auto height = pDesc->height;
+ uint8_t* src = (uint8_t*)imgData;
+ uint8_t* dst = (uint8_t*)tgt;
+ const auto srcStrideBytes = imgStride * kYuv422BytesPerPixel;
+ const auto dstStrideBytes = pDesc->stride * kYuv422BytesPerPixel;
+
+ for (unsigned r=0; r<height; r++) {
+ // Copy a pixel row at a time (2 bytes per pixel, averaged over a YUYV macro pixel)
+ memcpy(dst+r*dstStrideBytes, src+r*srcStrideBytes, srcStrideBytes);
+ }
+}
+
+
+void fillYUYVFromUYVY(
+ const BufferDesc& tgtBuff, uint8_t* tgt, void* imgData, void *, unsigned imgStride) {
const AHardwareBuffer_Desc* pDesc =
reinterpret_cast<const AHardwareBuffer_Desc*>(&tgtBuff.buffer.description);
unsigned width = pDesc->width;
unsigned height = pDesc->height;
uint32_t* src = (uint32_t*)imgData;
uint32_t* dst = (uint32_t*)tgt;
- unsigned srcStridePixels = imgStride / 2;
+ unsigned srcStridePixels = imgStride;
unsigned dstStridePixels = pDesc->stride;
const int srcRowPadding32 = srcStridePixels/2 - width/2; // 2 bytes per pixel, 4 bytes per word
diff --git a/cpp/evs/sampleDriver/bufferCopy.h b/cpp/evs/sampleDriver/bufferCopy.h
index b07a619..a6bb512 100644
--- a/cpp/evs/sampleDriver/bufferCopy.h
+++ b/cpp/evs/sampleDriver/bufferCopy.h
@@ -30,19 +30,22 @@
void fillNV21FromNV21(const BufferDesc& tgtBuff, uint8_t* tgt,
- void* imgData, unsigned imgStride);
+ void* imgData, void* buf, unsigned imgStride);
void fillNV21FromYUYV(const BufferDesc& tgtBuff, uint8_t* tgt,
- void* imgData, unsigned imgStride);
+ void* imgData, void* buf, unsigned imgStride);
void fillRGBAFromYUYV(const BufferDesc& tgtBuff, uint8_t* tgt,
- void* imgData, unsigned imgStride);
+ void* imgData, void* buf, unsigned imgStride);
+
+void fillRGBAFromUYVY(const BufferDesc& tgtBuff, uint8_t* tgt,
+ void* imgData, void* buf, unsigned imgStride);
void fillYUYVFromYUYV(const BufferDesc& tgtBuff, uint8_t* tgt,
- void* imgData, unsigned imgStride);
+ void* imgData, void* buf, unsigned imgStride);
void fillYUYVFromUYVY(const BufferDesc& tgtBuff, uint8_t* tgt,
- void* imgData, unsigned imgStride);
+ void* imgData, void* buf, unsigned imgStride);
} // namespace implementation
} // namespace V1_1
diff --git a/cpp/evs/sampleDriver/sepolicy/private/evs_driver.te b/cpp/evs/sampleDriver/sepolicy/private/evs_driver.te
index 5b847fa..ab0c251 100644
--- a/cpp/evs/sampleDriver/sepolicy/private/evs_driver.te
+++ b/cpp/evs/sampleDriver/sepolicy/private/evs_driver.te
@@ -14,7 +14,11 @@
hal_client_domain(hal_evs_driver, hal_graphics_composer)
hal_client_domain(hal_evs_driver, hal_configstore)
+# Allow the driver to access EGL
allow hal_evs_driver gpu_device:chr_file rw_file_perms;
+allow hal_evs_driver gpu_device:dir search;
+
+# Allow the driver to use SurfaceFlinger
binder_call(hal_evs_driver, surfaceflinger);
allow hal_evs_driver surfaceflinger_service:service_manager find;
allow hal_evs_driver ion_device:chr_file r_file_perms;
@@ -25,4 +29,4 @@
# Allow the driver to use automotive display proxy service
allow hal_evs_driver automotive_display_service_server:binder call;
allow hal_evs_driver fwk_automotive_display_hwservice:hwservice_manager find;
-
+allow hal_evs_driver automotive_display_service:fd use;
diff --git a/cpp/evs/sampleDriver/sepolicy/private/surfaceflinger.te b/cpp/evs/sampleDriver/sepolicy/private/surfaceflinger.te
index ce51a0d..d7aba85 100644
--- a/cpp/evs/sampleDriver/sepolicy/private/surfaceflinger.te
+++ b/cpp/evs/sampleDriver/sepolicy/private/surfaceflinger.te
@@ -1,2 +1,5 @@
# Allow surfaceflinger to perform binder IPC to hal_evs_driver
binder_call(surfaceflinger, hal_evs_driver)
+
+# Allow surfaceflinger to perform binder IPC to automotive_display_service
+binder_call(surfaceflinger, automotive_display_service)
diff --git a/cpp/security/vehicle_binding_util/Android.bp b/cpp/security/vehicle_binding_util/Android.bp
new file mode 100644
index 0000000..bea8afa
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/Android.bp
@@ -0,0 +1,67 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_defaults {
+ name: "vehicle_binding_util_defaults",
+ cflags: [
+ "-Wall",
+ "-Wno-missing-field-initializers",
+ "-Werror",
+ "-Wno-unused-variable",
+ ],
+ shared_libs: [
+ "android.hardware.automotive.vehicle@2.0",
+ "libbase",
+ "libbinder",
+ "libcutils",
+ "libhidlbase",
+ "liblog",
+ "liblogwrap",
+ "libutils",
+ ],
+ static_libs: [
+ "libbase",
+ ],
+}
+
+cc_library_static {
+ name: "libvehicle_binding_util",
+ srcs: [
+ "src/VehicleBindingUtil.cpp",
+ ],
+ defaults: [
+ "vehicle_binding_util_defaults",
+ ],
+ export_include_dirs: [
+ "src",
+ ],
+}
+
+cc_binary {
+ name: "vehicle_binding_util",
+ defaults: [
+ "vehicle_binding_util_defaults",
+ ],
+ srcs: [
+ "src/main.cpp",
+ ],
+ init_rc: ["vehicle_binding_util.rc"],
+ static_libs: [
+ "libvehicle_binding_util",
+ ],
+}
diff --git a/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.cpp b/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.cpp
new file mode 100644
index 0000000..c15bd3c
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.cpp
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#include "VehicleBindingUtil.h"
+
+#include <android-base/logging.h>
+#include <android/hardware/automotive/vehicle/2.0/types.h>
+#include <cutils/properties.h> // for property_get
+#include <logwrap/logwrap.h>
+#include <utils/SystemClock.h>
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/random.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace security {
+namespace {
+
+using android::hardware::automotive::vehicle::V2_0::IVehicle;
+using android::hardware::automotive::vehicle::V2_0::StatusCode;
+using android::hardware::automotive::vehicle::V2_0::VehicleArea;
+using android::hardware::automotive::vehicle::V2_0::VehiclePropConfig;
+using android::hardware::automotive::vehicle::V2_0::VehicleProperty;
+using android::hardware::automotive::vehicle::V2_0::VehiclePropertyStatus;
+using android::hardware::automotive::vehicle::V2_0::VehiclePropValue;
+
+template <typename T>
+using hidl_vec = android::hardware::hidl_vec<T>;
+
+bool isSeedVhalPropertySupported(sp<IVehicle> vehicle) {
+ bool is_supported = false;
+
+ hidl_vec<int32_t> props = {
+ static_cast<int32_t>(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED)};
+ vehicle->getPropConfigs(props,
+ [&is_supported](StatusCode status,
+ hidl_vec<VehiclePropConfig> /*propConfigs*/) {
+ is_supported = (status == StatusCode::OK);
+ });
+ return is_supported;
+}
+
+std::string toHexString(const std::vector<uint8_t>& bytes) {
+ const char lookup[] = "0123456789abcdef";
+ std::string out;
+ out.reserve(bytes.size() * 2);
+ for (auto b : bytes) {
+ out += lookup[b >> 4];
+ out += lookup[b & 0xf];
+ }
+ return out;
+}
+
+BindingStatus setSeedVhalProperty(sp<IVehicle> vehicle, const std::vector<uint8_t>& seed) {
+ VehiclePropValue propValue;
+ propValue.timestamp = elapsedRealtimeNano();
+ propValue.areaId = toInt(VehicleArea::GLOBAL);
+ propValue.prop = toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED);
+ propValue.status = VehiclePropertyStatus::AVAILABLE;
+ propValue.value.bytes = seed;
+ StatusCode vhal_status = vehicle->set(propValue);
+ if (vhal_status == StatusCode::OK) {
+ return BindingStatus::OK;
+ }
+
+ LOG(ERROR) << "Unable to set the VHAL property: " << toString(vhal_status);
+ return BindingStatus::ERROR;
+}
+
+BindingStatus getSeedVhalProperty(sp<IVehicle> vehicle, std::vector<uint8_t>* seed) {
+ VehiclePropValue desired_prop;
+ desired_prop.prop = static_cast<int32_t>(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED);
+ BindingStatus status = BindingStatus::ERROR;
+ vehicle->get(desired_prop,
+ [&status, &seed](StatusCode prop_status, const VehiclePropValue& propValue) {
+ if (prop_status != StatusCode::OK) {
+ LOG(ERROR) << "Error reading vehicle property: " << toString(prop_status);
+ } else {
+ status = BindingStatus::OK;
+ *seed = std::vector<uint8_t>{propValue.value.bytes.begin(),
+ propValue.value.bytes.end()};
+ }
+ });
+
+ return status;
+}
+
+BindingStatus sendSeedToVold(const Executor& executor, const std::vector<uint8_t>& seed) {
+ int status = 0;
+
+ // we pass the seed value via environment variable in the forked process
+ setenv("SEED_VALUE", toHexString(seed).c_str(), 1);
+ int rc = executor.run({"/system/bin/vdc", "cryptfs", "bindkeys"}, &status);
+ unsetenv("SEED_VALUE");
+ LOG(INFO) << "rc: " << rc;
+ LOG(INFO) << "status: " << status;
+ if (rc != 0 || status != 0) {
+ LOG(ERROR) << "Error running vdc: " << rc << ", " << status;
+ return BindingStatus::ERROR;
+ }
+ return BindingStatus::OK;
+}
+
+} // namespace
+
+bool DefaultCsrng::fill(void* buffer, size_t size) const {
+ int fd = TEMP_FAILURE_RETRY(open("/dev/urandom", O_RDONLY | O_CLOEXEC | O_NOFOLLOW));
+ if (fd == -1) {
+ LOG(ERROR) << "Error opening urandom: " << errno;
+ return false;
+ }
+
+ ssize_t bytes_read;
+ uint8_t* bufptr = static_cast<uint8_t*>(buffer);
+ while ((bytes_read = TEMP_FAILURE_RETRY(read(fd, bufptr, size))) > 0) {
+ size -= bytes_read;
+ bufptr += bytes_read;
+ }
+
+ close(fd);
+
+ if (size != 0) {
+ LOG(ERROR) << "Unable to read " << size << " bytes from urandom";
+ return false;
+ }
+ return true;
+}
+
+int DefaultExecutor::run(const std::vector<std::string>& cmd_args, int* exit_code) const {
+ std::vector<const char*> argv;
+ argv.reserve(cmd_args.size());
+ for (auto& arg : cmd_args) {
+ argv.push_back(arg.c_str());
+ }
+ int status = 0;
+ return logwrap_fork_execvp(argv.size(), argv.data(), exit_code, false /*forward_signals*/,
+ LOG_KLOG, true /*abbreviated*/, nullptr /*file_path*/);
+}
+
+BindingStatus setVehicleBindingSeed(sp<IVehicle> vehicle, const Executor& executor,
+ const Csrng& csrng) {
+ if (!isSeedVhalPropertySupported(vehicle)) {
+ LOG(WARNING) << "Vehicle binding seed is not supported by the VHAL.";
+ return BindingStatus::NOT_SUPPORTED;
+ }
+
+ std::vector<uint8_t> seed;
+ BindingStatus status = getSeedVhalProperty(vehicle, &seed);
+ if (status != BindingStatus::OK) {
+ LOG(ERROR) << "Unable to read the seed from the VHAL: " << static_cast<int>(status);
+ return status;
+ }
+
+ if (seed.empty()) {
+ seed = std::vector<uint8_t>(SEED_BYTE_SIZE);
+ if (!csrng.fill(seed.data(), seed.size())) {
+ LOG(ERROR) << "Error getting random seed: " << static_cast<int>(status);
+ return BindingStatus::ERROR;
+ }
+
+ status = setSeedVhalProperty(vehicle, seed);
+ if (status != BindingStatus::OK) {
+ LOG(ERROR) << "Error storing the seed in the VHAL: " << static_cast<int>(status);
+ return status;
+ }
+ }
+
+ status = sendSeedToVold(executor, seed);
+ if (status == BindingStatus::OK) {
+ LOG(INFO) << "Successfully bound vehicle storage to seed.";
+ }
+ return status;
+}
+
+} // namespace security
+} // namespace automotive
+} // namespace android
diff --git a/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.h b/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.h
new file mode 100644
index 0000000..3fa89ec
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/src/VehicleBindingUtil.h
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+#ifndef CPP_SECURITY_VEHICLE_BINDING_UTIL_SRC_VEHICLEBINDINGUTIL_H_
+#define CPP_SECURITY_VEHICLE_BINDING_UTIL_SRC_VEHICLEBINDINGUTIL_H_
+
+#include "android/hardware/automotive/vehicle/2.0/types.h"
+
+#include <android/hardware/automotive/vehicle/2.0/IVehicle.h>
+#include <utils/StrongPointer.h>
+
+#include <cstdint>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace security {
+
+constexpr size_t SEED_BYTE_SIZE = 16;
+
+// Possible results of attempting to set the vehicle binding seed.
+enum class BindingStatus {
+ OK,
+ NOT_SUPPORTED,
+ ERROR,
+};
+
+template <typename EnumT>
+constexpr auto toInt(const EnumT value) {
+ return static_cast<typename std::underlying_type<EnumT>::type>(value);
+}
+
+// Interface for getting cryptographically secure random byte strings
+class Csrng {
+public:
+ virtual ~Csrng() = default;
+
+ // Fill the given buffer with random bytes. Returns false if there is
+ // an unrecoverable error getting bits.
+ virtual bool fill(void* buffer, size_t size) const = 0;
+};
+
+// Csrng that relies on `/dev/urandom` to supply bits. We have to rely on
+// urandom so that we don't block boot-up. Devices that wish to supply very
+// high-quality random bits at boot should seed the linux PRNG at boot with
+// entropy.
+class DefaultCsrng : public Csrng {
+public:
+ bool fill(void* buffer, size_t size) const override;
+};
+
+// Interface for forking and executing a child process.
+class Executor {
+public:
+ virtual ~Executor() = default;
+
+ // Run the given command line and its arguments. Returns 0 on success, -1
+ // if an internal error occurred, and -ECHILD if the child process did not
+ // exit properly.
+ //
+ // On exit, `exit_code` is set to the child's exit status.
+ virtual int run(const std::vector<std::string>& cmd_args, int* exit_code) const = 0;
+};
+
+// Default Executor which forks, execs, and logs output from the child process.
+class DefaultExecutor : public Executor {
+ int run(const std::vector<std::string>& cmd_args, int* exit_code) const override;
+};
+
+// Set the seed in vold that is used to bind the encryption keys to the vehicle.
+// This is used to guard against headunit removal and subsequent scraping of
+// the filesystem for sensitive data (e.g. PII).
+//
+// The seed is read from the VHAL property STORAGE_ENCRYPTION_BINDING_SEED. If
+// the property has not yet been set, a random byte value is generated and
+// saved in the VHAL for reuse on future boots.
+BindingStatus setVehicleBindingSeed(
+ sp<::android::hardware::automotive::vehicle::V2_0::IVehicle> vehicle,
+ const Executor& executor, const Csrng& csrng);
+
+} // namespace security
+} // namespace automotive
+} // namespace android
+
+#endif // CPP_SECURITY_VEHICLE_BINDING_UTIL_SRC_VEHICLEBINDINGUTIL_H_
diff --git a/cpp/security/vehicle_binding_util/src/main.cpp b/cpp/security/vehicle_binding_util/src/main.cpp
new file mode 100644
index 0000000..cd82017
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/src/main.cpp
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#include "VehicleBindingUtil.h"
+
+#include <android-base/logging.h>
+#include <android/hardware/automotive/vehicle/2.0/IVehicle.h>
+#include <binder/IServiceManager.h>
+#include <binder/Status.h>
+
+#include <iostream>
+#include <map>
+#include <string>
+
+namespace {
+
+using android::defaultServiceManager;
+using android::automotive::security::BindingStatus;
+using android::automotive::security::DefaultCsrng;
+using android::automotive::security::DefaultExecutor;
+using android::hardware::automotive::vehicle::V2_0::IVehicle;
+
+static int printHelp(int argc, char* argv[]);
+static int setBinding(int /*argc*/, char*[] /*argv*/);
+
+// Avoid calling complex destructor on cleanup.
+const auto& subcommandTable = *new std::map<std::string, std::function<int(int, char*[])>>{
+ {"help", printHelp},
+ {"set_binding", setBinding},
+};
+
+static int setBinding(int /*argc*/, char*[] /*argv*/) {
+ auto status = setVehicleBindingSeed(IVehicle::getService(), DefaultExecutor{}, DefaultCsrng{});
+ if (status != BindingStatus::OK) {
+ LOG(ERROR) << "Unable to set the binding seed. Encryption keys are not "
+ << "bound to the platform.";
+ return static_cast<int>(status);
+ }
+
+ return 0;
+}
+
+static int printHelp(int /*argc*/, char* argv[]) {
+ std::cout << "Usage: " << argv[0] << " <subcommand> [args]" << std::endl
+ << "Valid subcommands: " << std::endl;
+ for (const auto& i : subcommandTable) {
+ std::cout << " " << i.first << std::endl;
+ }
+ return 0;
+}
+
+} // namespace
+
+int main(int argc, char* argv[]) {
+ setenv("ANDROID_LOG_TAGS", "*:v", 1);
+ android::base::InitLogging(argv,
+ (getppid() == 1) ? &android::base::KernelLogger
+ : &android::base::StderrLogger);
+ if (argc < 2) {
+ LOG(ERROR) << "Please specify a subcommand.";
+ printHelp(argc, argv);
+ return -1;
+ }
+
+ auto subcommand = subcommandTable.find(argv[1]);
+ if (subcommand == subcommandTable.end()) {
+ LOG(ERROR) << "Invalid subcommand: " << argv[1];
+ printHelp(argc, argv);
+ return -1;
+ }
+
+ return subcommand->second(argc, argv);
+}
diff --git a/cpp/security/vehicle_binding_util/tests/Android.bp b/cpp/security/vehicle_binding_util/tests/Android.bp
new file mode 100644
index 0000000..69a61cf
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/tests/Android.bp
@@ -0,0 +1,67 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_test {
+ name: "libvehicle_binding_util_test",
+ defaults: [
+ "vehicle_binding_util_defaults",
+ ],
+ test_suites: ["general-tests"],
+ srcs: [
+ "VehicleBindingUtilTests.cpp",
+ ],
+ static_libs: [
+ "libbase",
+ "libgmock",
+ "libgtest",
+ "libvehicle_binding_util",
+ ],
+}
+
+cc_test {
+ name: "vehicle_binding_integration_test",
+ test_suites: [
+ "device-tests",
+ ],
+ require_root: true,
+ defaults: ["vehicle_binding_util_defaults"],
+ tidy: false,
+ srcs: [
+ "VehicleBindingIntegrationTedt.cpp"
+ ],
+ shared_libs: [
+ "android.hardware.automotive.vehicle@2.0",
+ "libbase",
+ "libbinder",
+ "libhidlbase",
+ "libutils",
+ ],
+ compile_multilib: "both",
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
+ sanitize: {
+ address: false,
+ recover: [ "all" ],
+ },
+}
diff --git a/cpp/security/vehicle_binding_util/tests/VehicleBindingIntegrationTedt.cpp b/cpp/security/vehicle_binding_util/tests/VehicleBindingIntegrationTedt.cpp
new file mode 100644
index 0000000..50f358e
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/tests/VehicleBindingIntegrationTedt.cpp
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#include <android-base/properties.h>
+#include <android/hardware/automotive/vehicle/2.0/IVehicle.h>
+#include <gtest/gtest.h>
+
+namespace android {
+namespace automotive {
+namespace security {
+namespace {
+
+using android::hardware::automotive::vehicle::V2_0::IVehicle;
+using android::hardware::automotive::vehicle::V2_0::StatusCode;
+using android::hardware::automotive::vehicle::V2_0::VehiclePropConfig;
+using android::hardware::automotive::vehicle::V2_0::VehicleProperty;
+
+template <typename T>
+using hidl_vec = android::hardware::hidl_vec<T>;
+
+bool isSeedVhalPropertySupported(sp<IVehicle> vehicle) {
+ bool is_supported = false;
+
+ hidl_vec<int32_t> props = {
+ static_cast<int32_t>(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED)};
+ vehicle->getPropConfigs(props,
+ [&is_supported](StatusCode status,
+ hidl_vec<VehiclePropConfig> /*propConfigs*/) {
+ is_supported = (status == StatusCode::OK);
+ });
+ return is_supported;
+}
+
+// Verify that vold got the binding seed if VHAL reports a seed
+TEST(VehicleBindingIntegrationTedt, TestVehicleBindingSeedSet) {
+ std::string expected_value = "1";
+ if (!isSeedVhalPropertySupported(IVehicle::getService())) {
+ GTEST_LOG_(INFO) << "Device does not support vehicle binding seed "
+ "(STORAGE_ENCRYPTION_BINDING_SEED).";
+ expected_value = "";
+ }
+
+ ASSERT_EQ(expected_value, android::base::GetProperty("vold.storage_seed_bound", ""));
+}
+
+} // namespace
+} // namespace security
+} // namespace automotive
+} // namespace android
diff --git a/cpp/security/vehicle_binding_util/tests/VehicleBindingUtilTests.cpp b/cpp/security/vehicle_binding_util/tests/VehicleBindingUtilTests.cpp
new file mode 100644
index 0000000..dc3ed6e
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/tests/VehicleBindingUtilTests.cpp
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#include "VehicleBindingUtil.h"
+
+#include <android/hardware/automotive/vehicle/2.0/IVehicle.h>
+#include <android/hardware/automotive/vehicle/2.0/types.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <hidl/Status.h>
+#include <utils/SystemClock.h>
+
+#include <iterator>
+
+namespace android {
+namespace automotive {
+namespace security {
+namespace {
+
+using android::hardware::Void;
+using android::hardware::automotive::vehicle::V2_0::IVehicle;
+using android::hardware::automotive::vehicle::V2_0::IVehicleCallback;
+using android::hardware::automotive::vehicle::V2_0::StatusCode;
+using android::hardware::automotive::vehicle::V2_0::SubscribeOptions;
+using android::hardware::automotive::vehicle::V2_0::VehicleProperty;
+using android::hardware::automotive::vehicle::V2_0::VehiclePropValue;
+
+template <typename T>
+using hidl_vec = android::hardware::hidl_vec<T>;
+template <typename T>
+using VhalReturn = android::hardware::Return<T>;
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::ElementsAreArray;
+using ::testing::NotNull;
+using ::testing::Return;
+using ::testing::SetArgPointee;
+using ::testing::Test;
+
+class MockVehicle : public IVehicle {
+public:
+ MOCK_METHOD(VhalReturn<void>, getAllPropConfigs, (getAllPropConfigs_cb), (override));
+
+ MOCK_METHOD(VhalReturn<void>, getPropConfigs, (const hidl_vec<int32_t>&, getPropConfigs_cb),
+ (override));
+
+ MOCK_METHOD(VhalReturn<void>, get, (const VehiclePropValue&, get_cb), (override));
+
+ MOCK_METHOD(VhalReturn<StatusCode>, set, (const VehiclePropValue&), (override));
+
+ MOCK_METHOD(VhalReturn<StatusCode>, subscribe,
+ (const sp<IVehicleCallback>&, const hidl_vec<SubscribeOptions>&), (override));
+
+ MOCK_METHOD(VhalReturn<StatusCode>, unsubscribe, (const sp<IVehicleCallback>&, int32_t),
+ (override));
+
+ MOCK_METHOD(VhalReturn<void>, debugDump, (debugDump_cb), (override));
+};
+
+class MockCsrng : public Csrng {
+public:
+ MOCK_METHOD(bool, fill, (void*, size_t), (const override));
+};
+
+class MockExecutor : public Executor {
+public:
+ MOCK_METHOD(int, run, (const std::vector<std::string>&, int*), (const override));
+};
+
+class VehicleBindingUtilTests : public Test {
+protected:
+ void setMockVhalPropertySupported() {
+ hidl_vec<int32_t> expectedProps = {toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED)};
+ EXPECT_CALL(*mMockVehicle, getPropConfigs(expectedProps, _))
+ .WillOnce([](const hidl_vec<int32_t>&, IVehicle::getPropConfigs_cb callback) {
+ callback(StatusCode::OK, {});
+ return Void();
+ });
+ }
+
+ void setMockVhalPropertyValue(const std::vector<uint8_t>& seed) {
+ EXPECT_CALL(*mMockVehicle, get(_, _))
+ .WillOnce([seed](const VehiclePropValue& propValue, IVehicle::get_cb callback) {
+ EXPECT_EQ(propValue.prop,
+ toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED));
+ VehiclePropValue value;
+ value.prop = propValue.prop;
+ value.value.bytes = hidl_vec<uint8_t>{seed.begin(), seed.end()};
+ callback(StatusCode::OK, value);
+ return Void();
+ });
+ }
+
+ void setTestRandomness(const char seed[SEED_BYTE_SIZE]) {
+ EXPECT_CALL(mMockCsrng, fill(NotNull(), SEED_BYTE_SIZE))
+ .WillOnce([seed](void* buf, size_t) {
+ memcpy(buf, seed, SEED_BYTE_SIZE);
+ return true;
+ });
+ }
+
+ static std::vector<uint8_t> toVector(const char seed[SEED_BYTE_SIZE]) {
+ return {seed, seed + SEED_BYTE_SIZE};
+ }
+
+ static std::vector<std::string> makeVdcArgs() {
+ return {"/system/bin/vdc", "cryptfs", "bindkeys"};
+ }
+
+ sp<MockVehicle> mMockVehicle{new MockVehicle};
+ MockExecutor mMockExecutor;
+ MockCsrng mMockCsrng;
+};
+
+// Verify that we fail as expected if the VHAL property is not supported. This
+// is not necessarily an error, and is expected on platforms that don't
+// implement the feature.
+TEST_F(VehicleBindingUtilTests, VhalPropertyUnsupported) {
+ hidl_vec<int32_t> expectedProps = {toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED)};
+ EXPECT_CALL(*mMockVehicle, getPropConfigs(expectedProps, _))
+ .WillOnce([](const hidl_vec<int32_t>&, IVehicle::getPropConfigs_cb callback) {
+ callback(StatusCode::INVALID_ARG, {});
+ return Void();
+ });
+
+ EXPECT_EQ(BindingStatus::NOT_SUPPORTED,
+ setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+// Verify that we properly handle an attempt to generate a random seed.
+TEST_F(VehicleBindingUtilTests, GetRandomnessFails) {
+ setMockVhalPropertySupported();
+ setMockVhalPropertyValue({});
+ EXPECT_CALL(mMockCsrng, fill(_, SEED_BYTE_SIZE)).WillOnce(Return(false));
+ EXPECT_EQ(BindingStatus::ERROR, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+// Verify that we properly handle an attempt to generate a random seed.
+TEST_F(VehicleBindingUtilTests, GetSeedVhalPropertyFails) {
+ setMockVhalPropertySupported();
+ EXPECT_CALL(*mMockVehicle, get(_, _))
+ .WillOnce([&](const VehiclePropValue& propValue, IVehicle::get_cb callback) {
+ EXPECT_EQ(propValue.prop, toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED));
+ callback(StatusCode::NOT_AVAILABLE, {});
+ return Void();
+ });
+ EXPECT_EQ(BindingStatus::ERROR, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+TEST_F(VehicleBindingUtilTests, SetSeedVhalPropertyFails) {
+ setMockVhalPropertySupported();
+ setMockVhalPropertyValue({});
+ setTestRandomness("I am not random");
+
+ EXPECT_CALL(*mMockVehicle, set(_)).WillOnce([](const VehiclePropValue&) {
+ return StatusCode::NOT_AVAILABLE;
+ });
+
+ EXPECT_EQ(BindingStatus::ERROR, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+TEST_F(VehicleBindingUtilTests, SetSeedWithNewRandomSeed) {
+ setMockVhalPropertySupported();
+ setMockVhalPropertyValue({});
+ constexpr char SEED[SEED_BYTE_SIZE] = "Seed Value Here";
+ setTestRandomness(SEED);
+
+ EXPECT_CALL(*mMockVehicle, set(_)).WillOnce([&](const VehiclePropValue& value) {
+ EXPECT_EQ(value.prop, toInt(VehicleProperty::STORAGE_ENCRYPTION_BINDING_SEED));
+ EXPECT_THAT(value.value.bytes, testing::ElementsAreArray(SEED));
+ return StatusCode::OK;
+ });
+
+ EXPECT_CALL(mMockExecutor, run(ElementsAreArray(makeVdcArgs()), _)).WillOnce(Return(0));
+
+ EXPECT_EQ(BindingStatus::OK, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+TEST_F(VehicleBindingUtilTests, SetSeedWithExistingProperty) {
+ setMockVhalPropertySupported();
+ const auto SEED = toVector("16 bytes of seed");
+ setMockVhalPropertyValue(SEED);
+ EXPECT_CALL(mMockExecutor, run(ElementsAreArray(makeVdcArgs()), _)).WillOnce(Return(0));
+ EXPECT_EQ(BindingStatus::OK, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+TEST_F(VehicleBindingUtilTests, SetSeedVdcExecFails) {
+ setMockVhalPropertySupported();
+ const auto SEED = toVector("abcdefghijklmnop");
+ setMockVhalPropertyValue(SEED);
+ EXPECT_CALL(mMockExecutor, run(ElementsAreArray(makeVdcArgs()), _)).WillOnce(Return(-1));
+ EXPECT_EQ(BindingStatus::ERROR, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+TEST_F(VehicleBindingUtilTests, SetSeedVdcExitsWithNonZeroStatus) {
+ setMockVhalPropertySupported();
+ const auto SEED = toVector("1123581321345589");
+ setMockVhalPropertyValue(SEED);
+ EXPECT_CALL(mMockExecutor, run(ElementsAreArray(makeVdcArgs()), _))
+ .WillOnce(DoAll(SetArgPointee<1>(-1), Return(0)));
+ EXPECT_EQ(BindingStatus::ERROR, setVehicleBindingSeed(mMockVehicle, mMockExecutor, mMockCsrng));
+}
+
+} // namespace
+} // namespace security
+} // namespace automotive
+} // namespace android
diff --git a/cpp/security/vehicle_binding_util/vehicle_binding_util.rc b/cpp/security/vehicle_binding_util/vehicle_binding_util.rc
new file mode 100644
index 0000000..cf73a23
--- /dev/null
+++ b/cpp/security/vehicle_binding_util/vehicle_binding_util.rc
@@ -0,0 +1,4 @@
+service vold_seed_binding /system/bin/vehicle_binding_util set_binding
+ oneshot
+ user root
+ group root
diff --git a/cpp/telemetry/proto/Android.bp b/cpp/telemetry/proto/Android.bp
index 862a537..53be1a4 100644
--- a/cpp/telemetry/proto/Android.bp
+++ b/cpp/telemetry/proto/Android.bp
@@ -24,3 +24,9 @@
"evs.proto",
],
}
+
+filegroup {
+ name: "cartelemetry-cardata-proto-srcs",
+ srcs: ["*.proto"],
+}
+
diff --git a/cpp/telemetry/proto/CarData.proto b/cpp/telemetry/proto/CarData.proto
index 96e8c2e..e5e9b38 100644
--- a/cpp/telemetry/proto/CarData.proto
+++ b/cpp/telemetry/proto/CarData.proto
@@ -31,13 +31,18 @@
import "packages/services/Car/cpp/telemetry/proto/evs.proto";
+// Contains all the CarData messages to declare all the messages.
+// Unique protobuf number is used as an ID.
+// A message will be sent from writer clients to the cartelemetryd
+// wrapped in
+// frameworks/hardware/interfaces/automotive/telemetry/aidl/android/frameworks/automotive/telemetry/CarData.aidl
message CarData {
oneof pushed {
EvsFirstFrameLatency evs_first_frame_latency = 1;
}
- // DO NOT USE field numbers above 100,000 in AOSP.
- // Field numbers 100,000 - 199,999 are reserved for non-AOSP (e.g. OEMs) to
- // use. Field numbers 200,000 and above are reserved for future use; do not
+ // DO NOT USE field numbers above 10,000 in AOSP.
+ // Field numbers 10,000 - 19,999 are reserved for non-AOSP (e.g. OEMs) to
+ // use. Field numbers 20,000 and above are reserved for future use; do not
// use them at all.
}
diff --git a/cpp/telemetry/script_executor/Android.bp b/cpp/telemetry/script_executor/Android.bp
deleted file mode 100644
index 6eaec62..0000000
--- a/cpp/telemetry/script_executor/Android.bp
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright (C) 2021 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 {
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-cc_defaults {
- name: "scriptexecutor_defaults",
- cflags: [
- "-Wno-unused-parameter",
- ],
- static_libs: [
- "libbase",
- "liblog",
- "liblua",
- ],
-}
-
-cc_library {
- name: "libscriptexecutor",
- defaults: [
- "scriptexecutor_defaults",
- ],
- srcs: [
- "src/JniUtils.cpp",
- "src/LuaEngine.cpp",
- "src/ScriptExecutorListener.cpp",
- ],
- shared_libs: [
- "libandroid_runtime",
- "libnativehelper",
- ],
- // Allow dependents to use the header files.
- export_include_dirs: [
- "src",
- ],
-}
-
-cc_library_shared {
- name: "libscriptexecutorjniutils-test",
- defaults: [
- "scriptexecutor_defaults",
- ],
- srcs: [
- "src/tests/JniUtilsTestHelper.cpp",
- ],
- shared_libs: [
- "libnativehelper",
- "libscriptexecutor",
- ],
-}
-
-cc_library {
- name: "libscriptexecutorjni",
- defaults: [
- "scriptexecutor_defaults",
- ],
- srcs: [
- "src/ScriptExecutorJni.cpp",
- ],
- shared_libs: [
- "libnativehelper",
- "libscriptexecutor",
- ],
-}
diff --git a/cpp/telemetry/script_executor/src/JniUtils.cpp b/cpp/telemetry/script_executor/src/JniUtils.cpp
deleted file mode 100644
index 93c1af8..0000000
--- a/cpp/telemetry/script_executor/src/JniUtils.cpp
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (c) 2021, 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.
- */
-
-#include "JniUtils.h"
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-void PushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle) {
- lua_newtable(luaEngine->GetLuaState());
- // null bundle object is allowed. We will treat it as an empty table.
- if (bundle == nullptr) {
- return;
- }
-
- // TODO(b/188832769): Consider caching some of these JNI references for
- // performance reasons.
- jclass bundleClass = env->FindClass("android/os/Bundle");
- jmethodID getKeySetMethod = env->GetMethodID(bundleClass, "keySet", "()Ljava/util/Set;");
- jobject keys = env->CallObjectMethod(bundle, getKeySetMethod);
- jclass setClass = env->FindClass("java/util/Set");
- jmethodID iteratorMethod = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;");
- jobject keySetIteratorObject = env->CallObjectMethod(keys, iteratorMethod);
-
- jclass iteratorClass = env->FindClass("java/util/Iterator");
- jmethodID hasNextMethod = env->GetMethodID(iteratorClass, "hasNext", "()Z");
- jmethodID nextMethod = env->GetMethodID(iteratorClass, "next", "()Ljava/lang/Object;");
-
- jclass booleanClass = env->FindClass("java/lang/Boolean");
- jclass integerClass = env->FindClass("java/lang/Integer");
- jclass numberClass = env->FindClass("java/lang/Number");
- jclass stringClass = env->FindClass("java/lang/String");
- // TODO(b/188816922): Handle more types such as float and integer arrays,
- // and perhaps nested Bundles.
-
- jmethodID getMethod =
- env->GetMethodID(bundleClass, "get", "(Ljava/lang/String;)Ljava/lang/Object;");
-
- // Iterate over key set of the bundle one key at a time.
- while (env->CallBooleanMethod(keySetIteratorObject, hasNextMethod)) {
- // Read the value object that corresponds to this key.
- jstring key = (jstring)env->CallObjectMethod(keySetIteratorObject, nextMethod);
- jobject value = env->CallObjectMethod(bundle, getMethod, key);
-
- // Get the value of the type, extract it accordingly from the bundle and
- // push the extracted value and the key to the Lua table.
- if (env->IsInstanceOf(value, booleanClass)) {
- jmethodID boolMethod = env->GetMethodID(booleanClass, "booleanValue", "()Z");
- bool boolValue = static_cast<bool>(env->CallBooleanMethod(value, boolMethod));
- lua_pushboolean(luaEngine->GetLuaState(), boolValue);
- } else if (env->IsInstanceOf(value, integerClass)) {
- jmethodID intMethod = env->GetMethodID(integerClass, "intValue", "()I");
- lua_pushinteger(luaEngine->GetLuaState(), env->CallIntMethod(value, intMethod));
- } else if (env->IsInstanceOf(value, numberClass)) {
- // Condense other numeric types using one class. Because lua supports only
- // integer or double, and we handled integer in previous if clause.
- jmethodID numberMethod = env->GetMethodID(numberClass, "doubleValue", "()D");
- /* Pushes a double onto the stack */
- lua_pushnumber(luaEngine->GetLuaState(), env->CallDoubleMethod(value, numberMethod));
- } else if (env->IsInstanceOf(value, stringClass)) {
- const char* rawStringValue = env->GetStringUTFChars((jstring)value, nullptr);
- lua_pushstring(luaEngine->GetLuaState(), rawStringValue);
- env->ReleaseStringUTFChars((jstring)value, rawStringValue);
- } else {
- // Other types are not implemented yet, skipping.
- continue;
- }
-
- const char* rawKey = env->GetStringUTFChars(key, nullptr);
- // table[rawKey] = value, where value is on top of the stack,
- // and the table is the next element in the stack.
- lua_setfield(luaEngine->GetLuaState(), /* idx= */ -2, rawKey);
- env->ReleaseStringUTFChars(key, rawKey);
- }
-}
-
-} // namespace script_executor
-} // namespace telemetry
-} // namespace automotive
-} // namespace android
diff --git a/cpp/telemetry/script_executor/src/JniUtils.h b/cpp/telemetry/script_executor/src/JniUtils.h
deleted file mode 100644
index c3ef677..0000000
--- a/cpp/telemetry/script_executor/src/JniUtils.h
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (c) 2021, 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.
- */
-#ifndef CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_JNIUTILS_H_
-#define CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_JNIUTILS_H_
-
-#include "LuaEngine.h"
-#include "jni.h"
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-// Helper function which takes android.os.Bundle object in "bundle" argument
-// and converts it to Lua table on top of Lua stack. All key-value pairs are
-// converted to the corresponding key-value pairs of the Lua table as long as
-// the Bundle value types are supported. At this point, we support boolean,
-// integer, double and String types in Java.
-void PushBundleToLuaTable(JNIEnv* env, LuaEngine* luaEngine, jobject bundle);
-
-} // namespace script_executor
-} // namespace telemetry
-} // namespace automotive
-} // namespace android
-
-#endif // CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_JNIUTILS_H_
diff --git a/cpp/telemetry/script_executor/src/LuaEngine.cpp b/cpp/telemetry/script_executor/src/LuaEngine.cpp
deleted file mode 100644
index 1a074f2..0000000
--- a/cpp/telemetry/script_executor/src/LuaEngine.cpp
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (c) 2021, 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.
- */
-
-#include "LuaEngine.h"
-
-#include <utility>
-
-extern "C" {
-#include "lauxlib.h"
-#include "lualib.h"
-}
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-LuaEngine::LuaEngine() {
- // Instantiate Lua environment
- mLuaState = luaL_newstate();
- luaL_openlibs(mLuaState);
-}
-
-LuaEngine::~LuaEngine() {
- lua_close(mLuaState);
-}
-
-lua_State* LuaEngine::GetLuaState() {
- return mLuaState;
-}
-
-void LuaEngine::ResetListener(ScriptExecutorListener* listener) {
- mListener.reset(listener);
-}
-
-int LuaEngine::LoadScript(const char* scriptBody) {
- // As the first step in Lua script execution we want to load
- // the body of the script into Lua stack and have it processed by Lua
- // to catch any errors.
- // More on luaL_dostring: https://www.lua.org/manual/5.3/manual.html#lual_dostring
- // If error, pushes the error object into the stack.
- const auto status = luaL_dostring(mLuaState, scriptBody);
- if (status) {
- // Removes error object from the stack.
- // Lua stack must be properly maintained due to its limited size,
- // ~20 elements and its critical function because all interaction with
- // Lua happens via the stack.
- // Starting read about Lua stack: https://www.lua.org/pil/24.2.html
- // TODO(b/192284232): add test case to trigger this.
- lua_pop(mLuaState, 1);
- }
- return status;
-}
-
-bool LuaEngine::PushFunction(const char* functionName) {
- // Interaction between native code and Lua happens via Lua stack.
- // In such model, a caller first pushes the name of the function
- // that needs to be called, followed by the function's input
- // arguments, one input value pushed at a time.
- // More info: https://www.lua.org/pil/24.2.html
- lua_getglobal(mLuaState, functionName);
- const auto status = lua_isfunction(mLuaState, /*idx= */ -1);
- // TODO(b/192284785): add test case for wrong function name in Lua.
- if (status == 0) lua_pop(mLuaState, 1);
- return status;
-}
-
-int LuaEngine::Run() {
- // Performs blocking call of the provided Lua function. Assumes all
- // input arguments are in the Lua stack as well in proper order.
- // On how to call Lua functions: https://www.lua.org/pil/25.2.html
- // Doc on lua_pcall: https://www.lua.org/manual/5.3/manual.html#lua_pcall
- // TODO(b/189241508): Once we implement publishedData parsing, nargs should
- // change from 1 to 2.
- // TODO(b/192284612): add test case for failed call.
- return lua_pcall(mLuaState, /* nargs= */ 1, /* nresults= */ 0, /*errfunc= */ 0);
-}
-
-} // namespace script_executor
-} // namespace telemetry
-} // namespace automotive
-} // namespace android
diff --git a/cpp/telemetry/script_executor/src/LuaEngine.h b/cpp/telemetry/script_executor/src/LuaEngine.h
deleted file mode 100644
index a1d6e48..0000000
--- a/cpp/telemetry/script_executor/src/LuaEngine.h
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (c) 2021, 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.
- */
-
-#ifndef CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_LUAENGINE_H_
-#define CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_LUAENGINE_H_
-
-#include "ScriptExecutorListener.h"
-
-#include <memory>
-
-extern "C" {
-#include "lua.h"
-}
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-// Encapsulates Lua script execution environment.
-class LuaEngine {
-public:
- LuaEngine();
-
- virtual ~LuaEngine();
-
- // Returns pointer to Lua state object.
- lua_State* GetLuaState();
-
- // Loads Lua script provided as scriptBody string.
- // Returns 0 if successful. Otherwise returns non-zero Lua error code.
- int LoadScript(const char* scriptBody);
-
- // Pushes a Lua function under provided name into the stack.
- // Returns true if successful.
- bool PushFunction(const char* functionName);
-
- // Invokes function with the inputs provided in the stack.
- // Assumes that the script body has been already loaded and successully
- // compiled and run, and all input arguments, and the function have been
- // pushed to the stack.
- // Returns 0 if successful. Otherwise returns non-zero Lua error code.
- int Run();
-
- // Updates stored listener and destroys the previous one.
- void ResetListener(ScriptExecutorListener* listener);
-
-private:
- lua_State* mLuaState; // owned
-
- std::unique_ptr<ScriptExecutorListener> mListener;
-};
-
-} // namespace script_executor
-} // namespace telemetry
-} // namespace automotive
-} // namespace android
-
-#endif // CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_LUAENGINE_H_
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp b/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
deleted file mode 100644
index 500b8e2..0000000
--- a/cpp/telemetry/script_executor/src/ScriptExecutorJni.cpp
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (c) 2021, 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.
- */
-
-#include "JniUtils.h"
-#include "LuaEngine.h"
-#include "ScriptExecutorListener.h"
-#include "jni.h"
-
-#include <android-base/logging.h>
-
-#include <cstdint>
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-extern "C" {
-
-JNIEXPORT jlong JNICALL
-Java_com_android_car_telemetry_ScriptExecutor_nativeInitLuaEngine(JNIEnv* env, jobject object) {
- // Cast first to intptr_t to ensure int can hold the pointer without loss.
- return static_cast<jlong>(reinterpret_cast<intptr_t>(new LuaEngine()));
-}
-
-JNIEXPORT void JNICALL Java_com_android_car_telemetry_ScriptExecutor_nativeDestroyLuaEngine(
- JNIEnv* env, jobject object, jlong luaEnginePtr) {
- delete reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-}
-
-// Parses the inputs and loads them to Lua one at a time.
-// Loading of data into Lua also triggers checks on Lua side to verify the
-// inputs are valid. For example, pushing "functionName" into Lua stack verifies
-// that the function name actually exists in the previously loaded body of the
-// script.
-//
-// The steps are:
-// Step 1: Parse the inputs for obvious programming errors.
-// Step 2: Parse and load the body of the script.
-// Step 3: Parse and push function name we want to execute in the provided
-// script body to Lua stack. If the function name doesn't exist, we exit.
-// Step 4: Parse publishedData, convert it into Lua table and push it to the
-// stack.
-// Step 5: Parse savedState Bundle object, convert it into Lua table and push it
-// to the stack.
-// Any errors that occur at the stage above result in quick exit or crash.
-//
-// All interaction with Lua happens via Lua stack. Therefore, order of how the
-// inputs are parsed and processed is critical because Lua API methods such as
-// lua_pcall assume specific order between function name and the input arguments
-// on the stack.
-// More information about how to work with Lua stack: https://www.lua.org/pil/24.2.html
-// and how Lua functions are called via Lua API: https://www.lua.org/pil/25.2.html
-//
-// Finally, once parsing and pushing to Lua stack is complete, we do
-//
-// Step 6: attempt to run the provided function.
-JNIEXPORT void JNICALL Java_com_android_car_telemetry_ScriptExecutor_nativeInvokeScript(
- JNIEnv* env, jobject object, jlong luaEnginePtr, jstring scriptBody, jstring functionName,
- jobject publishedData, jobject savedState, jobject listener) {
- if (!luaEnginePtr) {
- env->FatalError("luaEnginePtr parameter cannot be nil");
- }
- if (scriptBody == nullptr) {
- env->FatalError("scriptBody parameter cannot be null");
- }
- if (functionName == nullptr) {
- env->FatalError("functionName parameter cannot be null");
- }
- if (listener == nullptr) {
- env->FatalError("listener parameter cannot be null");
- }
-
- LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-
- // Load and parse the script
- const char* scriptStr = env->GetStringUTFChars(scriptBody, nullptr);
- auto status = engine->LoadScript(scriptStr);
- env->ReleaseStringUTFChars(scriptBody, scriptStr);
- // status == 0 if the script loads successfully.
- if (status) {
- env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"),
- "Failed to load the script.");
- return;
- }
- engine->ResetListener(new ScriptExecutorListener(env, listener));
-
- // Push the function name we want to invoke to Lua stack
- const char* functionNameStr = env->GetStringUTFChars(functionName, nullptr);
- status = engine->PushFunction(functionNameStr);
- env->ReleaseStringUTFChars(functionName, functionNameStr);
- // status == 1 if the name is indeed a function.
- if (!status) {
- env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"),
- "symbol functionName does not correspond to a function.");
- return;
- }
-
- // TODO(b/189241508): Provide implementation to parse publishedData input,
- // convert it into Lua table and push into Lua stack.
- if (publishedData) {
- env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
- "Parsing of publishedData is not implemented yet.");
- return;
- }
-
- // Unpack bundle in savedState, convert to Lua table and push it to Lua
- // stack.
- PushBundleToLuaTable(env, engine, savedState);
-
- // Execute the function. This will block until complete or error.
- if (engine->Run()) {
- env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
- "Runtime error occurred while running the function.");
- return;
- }
-}
-
-} // extern "C"
-
-} // namespace script_executor
-} // namespace telemetry
-} // namespace automotive
-} // namespace android
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp b/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
deleted file mode 100644
index 8c10aa4..0000000
--- a/cpp/telemetry/script_executor/src/ScriptExecutorListener.cpp
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (c) 2021, 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.
- */
-
-#include "ScriptExecutorListener.h"
-
-#include <android-base/logging.h>
-#include <android_runtime/AndroidRuntime.h>
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-ScriptExecutorListener::~ScriptExecutorListener() {
- if (mScriptExecutorListener != NULL) {
- JNIEnv* env = AndroidRuntime::getJNIEnv();
- env->DeleteGlobalRef(mScriptExecutorListener);
- }
-}
-
-ScriptExecutorListener::ScriptExecutorListener(JNIEnv* env, jobject script_executor_listener) {
- mScriptExecutorListener = env->NewGlobalRef(script_executor_listener);
-}
-
-void ScriptExecutorListener::onError(const int errorType, const std::string& message,
- const std::string& stackTrace) {
- LOG(INFO) << "errorType: " << errorType << ", message: " << message
- << ", stackTrace: " << stackTrace;
-}
-
-} // namespace script_executor
-} // namespace telemetry
-} // namespace automotive
-} // namespace android
diff --git a/cpp/telemetry/script_executor/src/ScriptExecutorListener.h b/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
deleted file mode 100644
index 1e5c7d7..0000000
--- a/cpp/telemetry/script_executor/src/ScriptExecutorListener.h
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (c) 2021, 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.
- */
-
-#ifndef CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
-#define CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
-
-#include "jni.h"
-
-#include <string>
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-
-// Wrapper class for IScriptExecutorListener.aidl.
-class ScriptExecutorListener {
-public:
- ScriptExecutorListener(JNIEnv* jni, jobject script_executor_listener);
-
- virtual ~ScriptExecutorListener();
-
- void onScriptFinished() {}
-
- void onSuccess() {}
-
- void onError(const int errorType, const std::string& message, const std::string& stackTrace);
-
-private:
- // Stores a jni global reference to Java Script Executor listener object.
- jobject mScriptExecutorListener;
-};
-
-} // namespace script_executor
-} // namespace telemetry
-} // namespace automotive
-} // namespace android
-
-#endif // CPP_TELEMETRY_SCRIPT_EXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
diff --git a/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp b/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
deleted file mode 100644
index 9e2c43a..0000000
--- a/cpp/telemetry/script_executor/src/tests/JniUtilsTestHelper.cpp
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (c) 2021, 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.
- */
-
-#include "JniUtils.h"
-#include "LuaEngine.h"
-#include "jni.h"
-
-#include <cstdint>
-#include <cstring>
-
-namespace android {
-namespace automotive {
-namespace telemetry {
-namespace script_executor {
-namespace {
-
-extern "C" {
-
-#include "lua.h"
-
-JNIEXPORT jlong JNICALL
-Java_com_android_car_telemetry_JniUtilsTest_nativeCreateLuaEngine(JNIEnv* env, jobject object) {
- // Cast first to intptr_t to ensure int can hold the pointer without loss.
- return static_cast<jlong>(reinterpret_cast<intptr_t>(new LuaEngine()));
-}
-
-JNIEXPORT void JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeDestroyLuaEngine(
- JNIEnv* env, jobject object, jlong luaEnginePtr) {
- delete reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
-}
-
-JNIEXPORT void JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativePushBundleToLuaTableCaller(
- JNIEnv* env, jobject object, jlong luaEnginePtr, jobject bundle) {
- PushBundleToLuaTable(env, reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr)),
- bundle);
-}
-
-JNIEXPORT jint JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeGetObjectSize(
- JNIEnv* env, jobject object, jlong luaEnginePtr, jint index) {
- LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
- return lua_rawlen(engine->GetLuaState(), static_cast<int>(index));
-}
-
-JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasBooleanValue(
- JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jboolean value) {
- const char* rawKey = env->GetStringUTFChars(key, nullptr);
- LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
- auto* luaState = engine->GetLuaState();
- lua_pushstring(luaState, rawKey);
- env->ReleaseStringUTFChars(key, rawKey);
- lua_gettable(luaState, -2);
- bool result = false;
- if (!lua_isboolean(luaState, -1))
- result = false;
- else
- result = static_cast<bool>(lua_toboolean(luaState, -1)) == static_cast<bool>(value);
- lua_pop(luaState, 1);
- return result;
-}
-
-JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasIntValue(
- JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jint value) {
- const char* rawKey = env->GetStringUTFChars(key, nullptr);
- LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
- // Assumes the table is on top of the stack.
- auto* luaState = engine->GetLuaState();
- lua_pushstring(luaState, rawKey);
- env->ReleaseStringUTFChars(key, rawKey);
- lua_gettable(luaState, -2);
- bool result = false;
- if (!lua_isinteger(luaState, -1))
- result = false;
- else
- result = lua_tointeger(luaState, -1) == static_cast<int>(value);
- lua_pop(luaState, 1);
- return result;
-}
-
-JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasDoubleValue(
- JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jdouble value) {
- const char* rawKey = env->GetStringUTFChars(key, nullptr);
- LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
- // Assumes the table is on top of the stack.
- auto* luaState = engine->GetLuaState();
- lua_pushstring(luaState, rawKey);
- env->ReleaseStringUTFChars(key, rawKey);
- lua_gettable(luaState, -2);
- bool result = false;
- if (!lua_isnumber(luaState, -1))
- result = false;
- else
- result = static_cast<double>(lua_tonumber(luaState, -1)) == static_cast<double>(value);
- lua_pop(luaState, 1);
- return result;
-}
-
-JNIEXPORT bool JNICALL Java_com_android_car_telemetry_JniUtilsTest_nativeHasStringValue(
- JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jstring value) {
- const char* rawKey = env->GetStringUTFChars(key, nullptr);
- LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
- // Assumes the table is on top of the stack.
- auto* luaState = engine->GetLuaState();
- lua_pushstring(luaState, rawKey);
- env->ReleaseStringUTFChars(key, rawKey);
- lua_gettable(luaState, -2);
- bool result = false;
- if (!lua_isstring(luaState, -1)) {
- result = false;
- } else {
- std::string s = lua_tostring(luaState, -1);
- const char* rawValue = env->GetStringUTFChars(value, nullptr);
- result = strcmp(lua_tostring(luaState, -1), rawValue) == 0;
- env->ReleaseStringUTFChars(value, rawValue);
- }
- lua_pop(luaState, 1);
- return result;
-}
-
-} // extern "C"
-
-} // namespace
-} // namespace script_executor
-} // namespace telemetry
-} // namespace automotive
-} // namespace android
diff --git a/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdog.aidl b/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdog.aidl
index 57d82ca..c9ece21 100644
--- a/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdog.aidl
+++ b/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdog.aidl
@@ -128,4 +128,12 @@
* @param actions List of actions take on resource overusing packages.
*/
void actionTakenOnResourceOveruse(in List<PackageResourceOveruseAction> actions);
+
+ /**
+ * Enable/disable the internal client health check process.
+ * Disabling would stop the ANR killing process.
+ *
+ * @param isEnabled New enabled state.
+ */
+ void controlProcessHealthCheck(in boolean disable);
}
diff --git a/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdogServiceForSystem.aidl b/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdogServiceForSystem.aidl
index c0cfd23..8f36fad 100644
--- a/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdogServiceForSystem.aidl
+++ b/cpp/watchdog/aidl/android/automotive/watchdog/internal/ICarWatchdogServiceForSystem.aidl
@@ -19,6 +19,7 @@
import android.automotive.watchdog.internal.PackageInfo;
import android.automotive.watchdog.internal.PackageIoOveruseStats;
import android.automotive.watchdog.internal.TimeoutLength;
+import android.automotive.watchdog.internal.UserPackageIoUsageStats;
/**
* ICarWatchdogServiceForSystem interface used by the watchdog server to communicate with the
@@ -69,4 +70,10 @@
* @param packageNames Package names for which to reset the stats.
*/
oneway void resetResourceOveruseStats(in @utf8InCpp List<String> packageNames);
+
+ /**
+ * Fetches today's I/O usage stats for all packages collected during the
+ * previous boot.
+ */
+ List<UserPackageIoUsageStats> getTodayIoUsageStats();
}
diff --git a/cpp/watchdog/aidl/android/automotive/watchdog/internal/IoUsageStats.aidl b/cpp/watchdog/aidl/android/automotive/watchdog/internal/IoUsageStats.aidl
new file mode 100644
index 0000000..468ca99
--- /dev/null
+++ b/cpp/watchdog/aidl/android/automotive/watchdog/internal/IoUsageStats.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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 android.automotive.watchdog.internal;
+
+import android.automotive.watchdog.PerStateBytes;
+
+/**
+ * Structure that describes the I/O usage stats.
+ */
+parcelable IoUsageStats {
+ /**
+ * Total number of bytes written to disk.
+ */
+ PerStateBytes writtenBytes;
+
+ /**
+ * Number of bytes written to disk but forgiven.
+ */
+ PerStateBytes forgivenWriteBytes;
+
+ /**
+ * Total number of overuses.
+ */
+ int totalOveruses;
+}
diff --git a/cpp/watchdog/aidl/android/automotive/watchdog/internal/PackageIdentifier.aidl b/cpp/watchdog/aidl/android/automotive/watchdog/internal/PackageIdentifier.aidl
index c0dd86e..b4ff4c1 100644
--- a/cpp/watchdog/aidl/android/automotive/watchdog/internal/PackageIdentifier.aidl
+++ b/cpp/watchdog/aidl/android/automotive/watchdog/internal/PackageIdentifier.aidl
@@ -21,7 +21,7 @@
*/
parcelable PackageIdentifier {
/**
- * Name of the package.
+ * Generic name of the package.
*/
@utf8InCpp String name;
diff --git a/cpp/watchdog/aidl/android/automotive/watchdog/internal/PackageIoOveruseStats.aidl b/cpp/watchdog/aidl/android/automotive/watchdog/internal/PackageIoOveruseStats.aidl
index 8f3a143..ac9e140 100644
--- a/cpp/watchdog/aidl/android/automotive/watchdog/internal/PackageIoOveruseStats.aidl
+++ b/cpp/watchdog/aidl/android/automotive/watchdog/internal/PackageIoOveruseStats.aidl
@@ -17,6 +17,7 @@
package android.automotive.watchdog.internal;
import android.automotive.watchdog.IoOveruseStats;
+import android.automotive.watchdog.PerStateBytes;
/**
* Structure that describes the I/O overuse stats for a package.
@@ -32,8 +33,13 @@
*/
boolean shouldNotify;
- /**
- * I/O overuse stats for the package.
- */
+ /**
+ * Written bytes that are forgiven as they are accounted towards overuse.
+ */
+ PerStateBytes forgivenWriteBytes;
+
+ /**
+ * I/O overuse stats for the package.
+ */
IoOveruseStats ioOveruseStats;
}
diff --git a/cpp/watchdog/aidl/android/automotive/watchdog/internal/UserPackageIoUsageStats.aidl b/cpp/watchdog/aidl/android/automotive/watchdog/internal/UserPackageIoUsageStats.aidl
new file mode 100644
index 0000000..fb867e6
--- /dev/null
+++ b/cpp/watchdog/aidl/android/automotive/watchdog/internal/UserPackageIoUsageStats.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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 android.automotive.watchdog.internal;
+
+import android.automotive.watchdog.internal.IoUsageStats;
+
+/**
+ * Structure that describes the I/O usage stats for a user package.
+ */
+parcelable UserPackageIoUsageStats {
+ /**
+ * User ID for the package.
+ */
+ int userId;
+
+ /**
+ * Generic name of the package whose stats are recorded in this parcelable.
+ */
+ @utf8InCpp String packageName;
+
+ /**
+ * I/O overuse stats for the package.
+ */
+ IoUsageStats ioUsageStats;
+}
diff --git a/cpp/watchdog/car-watchdog-lib/src/android/car/watchdoglib/CarWatchdogDaemonHelper.java b/cpp/watchdog/car-watchdog-lib/src/android/car/watchdoglib/CarWatchdogDaemonHelper.java
index 1810c92..e7229ba 100644
--- a/cpp/watchdog/car-watchdog-lib/src/android/car/watchdoglib/CarWatchdogDaemonHelper.java
+++ b/cpp/watchdog/car-watchdog-lib/src/android/car/watchdoglib/CarWatchdogDaemonHelper.java
@@ -291,6 +291,16 @@
invokeDaemonMethod((daemon) -> daemon.actionTakenOnResourceOveruse(actions));
}
+ /**
+ * Enable/disable the internal client health check process.
+ * Disabling would stop the ANR killing process.
+ *
+ * @param disable True to disable watchdog's health check process.
+ */
+ public void controlProcessHealthCheck(boolean disable) throws RemoteException {
+ invokeDaemonMethod((daemon) -> daemon.controlProcessHealthCheck(disable));
+ }
+
private void invokeDaemonMethod(Invokable r) throws RemoteException {
ICarWatchdog daemon;
synchronized (mLock) {
diff --git a/cpp/watchdog/sepolicy/private/carwatchdog.te b/cpp/watchdog/sepolicy/private/carwatchdog.te
index 91620f5..5b18ebf 100644
--- a/cpp/watchdog/sepolicy/private/carwatchdog.te
+++ b/cpp/watchdog/sepolicy/private/carwatchdog.te
@@ -1,4 +1,4 @@
-# Car watchdog server
+# Car watchdog server.
typeattribute carwatchdogd coredomain;
typeattribute carwatchdogd mlstrustedsubject;
@@ -9,22 +9,26 @@
binder_use(carwatchdogd)
binder_service(carwatchdogd)
-# Configration to communicate with VHAL
+# Configration to communicate with VHAL.
hwbinder_use(carwatchdogd)
get_prop(carwatchdogd, hwservicemanager_prop)
hal_client_domain(carwatchdogd, hal_vehicle)
-# Scan through /proc/pid for all processes
+# Scan through /proc/pid for all processes.
r_dir_file(carwatchdogd, domain)
-# Read /proc/uid_io/stats
+# Read /proc/uid_io/stats.
allow carwatchdogd proc_uid_io_stats:file r_file_perms;
-# Read /proc/stat file
+# Read /proc/stat file.
allow carwatchdogd proc_stat:file r_file_perms;
-# Read /proc/diskstats file
+# Read /proc/diskstats file.
allow carwatchdogd proc_diskstats:file r_file_perms;
# List HALs to get pid of vehicle HAL.
allow carwatchdogd hwservicemanager:hwservice_manager list;
+
+# R/W /data/system/car for resource overuse configurations.
+allow carwatchdogd system_car_data_file:dir create_dir_perms;
+allow carwatchdogd system_car_data_file:{ file lnk_file } create_file_perms;
diff --git a/cpp/watchdog/sepolicy/public/carwatchdog.te b/cpp/watchdog/sepolicy/public/carwatchdog.te
index 2cb9c5a..fd7ab3b 100644
--- a/cpp/watchdog/sepolicy/public/carwatchdog.te
+++ b/cpp/watchdog/sepolicy/public/carwatchdog.te
@@ -1,9 +1,9 @@
-# Car watchdog server
+# Car watchdog server.
type carwatchdogd, domain;
binder_call(carwatchdogd, carwatchdogclient_domain)
binder_call(carwatchdogclient_domain, carwatchdogd)
-# Configuration for system_server
+# Configuration for system_server.
allow system_server carwatchdogd_service:service_manager find;
binder_call(carwatchdogd, system_server)
diff --git a/cpp/watchdog/server/Android.bp b/cpp/watchdog/server/Android.bp
index 8ce4c04..5aeebb2 100644
--- a/cpp/watchdog/server/Android.bp
+++ b/cpp/watchdog/server/Android.bp
@@ -30,16 +30,15 @@
"system/libbase/include",
],
shared_libs: [
- "android.automotive.watchdog-V3-cpp",
- "android.automotive.watchdog.internal-cpp",
- "android.automotive.watchdog.internal-cpp",
- "android.automotive.watchdog-V3-cpp",
- "android.hardware.automotive.vehicle@2.0",
"libbase",
"libbinder",
"liblog",
"libutils",
],
+ static_libs: [
+ "android.automotive.watchdog.internal-cpp",
+ "android.automotive.watchdog-V3-cpp",
+ ],
header_libs: [
"libgtest_prod_headers",
],
@@ -54,6 +53,7 @@
"carwatchdogd_defaults",
],
shared_libs: [
+ "android.hardware.automotive.vehicle@2.0",
"libcutils",
],
export_include_dirs: [
@@ -67,7 +67,6 @@
"libcutils",
"libprocessgroup",
"libtinyxml2",
- "libwatchdog_package_info_resolver",
],
}
@@ -85,12 +84,17 @@
"src/LooperWrapper.cpp",
"src/OveruseConfigurationXmlHelper.cpp",
"src/ProcDiskStats.cpp",
- "src/ProcPidStat.cpp",
"src/ProcStat.cpp",
- "src/UidIoStats.cpp",
+ "src/UidIoStatsCollector.cpp",
+ "src/UidProcStatsCollector.cpp",
+ "src/UidStatsCollector.cpp",
+ ],
+ shared_libs: [
+ "android.hardware.automotive.vehicle@2.0",
],
whole_static_libs: [
"libwatchdog_properties",
+ "libwatchdog_package_info_resolver",
],
export_include_dirs: [
"src",
@@ -120,11 +124,13 @@
"tests/OveruseConfigurationTestUtils.cpp",
"tests/OveruseConfigurationXmlHelperTest.cpp",
"tests/PackageInfoResolverTest.cpp",
+ "tests/PackageInfoTestUtils.cpp",
"tests/ProcDiskStatsTest.cpp",
"tests/ProcPidDir.cpp",
- "tests/ProcPidStatTest.cpp",
"tests/ProcStatTest.cpp",
- "tests/UidIoStatsTest.cpp",
+ "tests/UidIoStatsCollectorTest.cpp",
+ "tests/UidProcStatsCollectorTest.cpp",
+ "tests/UidStatsCollectorTest.cpp",
"tests/WatchdogBinderMediatorTest.cpp",
"tests/WatchdogInternalHandlerTest.cpp",
"tests/WatchdogPerfServiceTest.cpp",
@@ -137,7 +143,10 @@
"libwatchdog_binder_mediator",
"libwatchdog_perf_service",
"libwatchdog_process_service",
+ ],
+ whole_static_libs: [
"libwatchdog_package_info_resolver",
+ "android.hardware.automotive.vehicle@2.0",
],
data: [":watchdog_test_xml_files"],
}
@@ -158,6 +167,9 @@
"carwatchdogd_defaults",
"libwatchdog_process_service_defaults"
],
+ shared_libs: [
+ "android.hardware.automotive.vehicle@2.0",
+ ],
}
cc_library {
@@ -173,6 +185,9 @@
"src/WatchdogServiceHelper.cpp",
],
shared_libs: [
+ "android.hardware.automotive.vehicle@2.0",
+ ],
+ static_libs: [
"libwatchdog_perf_service",
"libwatchdog_process_service",
],
@@ -194,10 +209,15 @@
],
init_rc: ["carwatchdogd.rc"],
shared_libs: [
- "libwatchdog_binder_mediator",
- "libwatchdog_perf_service",
- "libwatchdog_process_service",
- "libwatchdog_package_info_resolver",
+ "android.hardware.automotive.vehicle@2.0",
+ ],
+ static_libs: [
+ "libwatchdog_binder_mediator",
+ "libwatchdog_perf_service",
+ "libwatchdog_process_service",
+ ],
+ whole_static_libs: [
+ "libwatchdog_package_info_resolver",
],
vintf_fragments: ["carwatchdogd.xml"],
required: [
diff --git a/cpp/watchdog/server/src/IoOveruseConfigs.cpp b/cpp/watchdog/server/src/IoOveruseConfigs.cpp
index 6fb6633..0237e7d 100644
--- a/cpp/watchdog/server/src/IoOveruseConfigs.cpp
+++ b/cpp/watchdog/server/src/IoOveruseConfigs.cpp
@@ -83,12 +83,6 @@
return output;
}
-bool isZeroValueThresholds(const PerStateIoOveruseThreshold& thresholds) {
- return thresholds.perStateWriteBytes.foregroundBytes == 0 &&
- thresholds.perStateWriteBytes.backgroundBytes == 0 &&
- thresholds.perStateWriteBytes.garageModeBytes == 0;
-}
-
std::string toString(const PerStateIoOveruseThreshold& thresholds) {
return StringPrintf("name=%s, foregroundBytes=%" PRId64 ", backgroundBytes=%" PRId64
", garageModeBytes=%" PRId64,
@@ -102,23 +96,20 @@
return Error() << "Doesn't contain threshold name";
}
- if (isZeroValueThresholds(thresholds)) {
- return Error() << "Zero value thresholds for " << thresholds.name;
- }
-
- if (thresholds.perStateWriteBytes.foregroundBytes == 0 ||
- thresholds.perStateWriteBytes.backgroundBytes == 0 ||
- thresholds.perStateWriteBytes.garageModeBytes == 0) {
- return Error() << "Some thresholds are zero: " << toString(thresholds);
+ if (thresholds.perStateWriteBytes.foregroundBytes <= 0 ||
+ thresholds.perStateWriteBytes.backgroundBytes <= 0 ||
+ thresholds.perStateWriteBytes.garageModeBytes <= 0) {
+ return Error() << "Some thresholds are less than or equal to zero: "
+ << toString(thresholds);
}
return {};
}
Result<void> containsValidThreshold(const IoOveruseAlertThreshold& threshold) {
- if (threshold.durationInSeconds == 0) {
+ if (threshold.durationInSeconds <= 0) {
return Error() << "Duration must be greater than zero";
}
- if (threshold.writtenBytesPerSecond == 0) {
+ if (threshold.writtenBytesPerSecond <= 0) {
return Error() << "Written bytes/second must be greater than zero";
}
return {};
@@ -238,6 +229,16 @@
return {};
}
+bool isSafeToKillAnyPackage(const std::vector<std::string>& packages,
+ const std::unordered_set<std::string>& safeToKillPackages) {
+ for (const auto& packageName : packages) {
+ if (safeToKillPackages.find(packageName) != safeToKillPackages.end()) {
+ return true;
+ }
+ }
+ return false;
+}
+
} // namespace
IoOveruseConfigs::ParseXmlFileFunction IoOveruseConfigs::sParseXmlFile =
@@ -579,7 +580,8 @@
return {};
}
-void IoOveruseConfigs::get(std::vector<ResourceOveruseConfiguration>* resourceOveruseConfigs) {
+void IoOveruseConfigs::get(
+ std::vector<ResourceOveruseConfiguration>* resourceOveruseConfigs) const {
auto systemConfig = get(mSystemConfig, kSystemComponentUpdatableConfigs);
if (systemConfig.has_value()) {
systemConfig->componentType = ComponentType::SYSTEM;
@@ -600,7 +602,8 @@
}
std::optional<ResourceOveruseConfiguration> IoOveruseConfigs::get(
- const ComponentSpecificConfig& componentSpecificConfig, const int32_t componentFilter) {
+ const ComponentSpecificConfig& componentSpecificConfig,
+ const int32_t componentFilter) const {
if (componentSpecificConfig.mGeneric.name == kDefaultThresholdName) {
return {};
}
@@ -724,11 +727,26 @@
}
switch (packageInfo.componentType) {
case ComponentType::SYSTEM:
- return mSystemConfig.mSafeToKillPackages.find(packageInfo.packageIdentifier.name) !=
- mSystemConfig.mSafeToKillPackages.end();
+ if (mSystemConfig.mSafeToKillPackages.find(packageInfo.packageIdentifier.name) !=
+ mSystemConfig.mSafeToKillPackages.end()) {
+ return true;
+ }
+ return isSafeToKillAnyPackage(packageInfo.sharedUidPackages,
+ mSystemConfig.mSafeToKillPackages);
case ComponentType::VENDOR:
- return mVendorConfig.mSafeToKillPackages.find(packageInfo.packageIdentifier.name) !=
- mVendorConfig.mSafeToKillPackages.end();
+ if (mVendorConfig.mSafeToKillPackages.find(packageInfo.packageIdentifier.name) !=
+ mVendorConfig.mSafeToKillPackages.end()) {
+ return true;
+ }
+ /*
+ * Packages under the vendor shared UID may contain system packages because when
+ * CarWatchdogService derives the shared component type it attributes system packages
+ * as vendor packages when there is at least one vendor package.
+ */
+ return isSafeToKillAnyPackage(packageInfo.sharedUidPackages,
+ mSystemConfig.mSafeToKillPackages) ||
+ isSafeToKillAnyPackage(packageInfo.sharedUidPackages,
+ mVendorConfig.mSafeToKillPackages);
default:
return true;
}
diff --git a/cpp/watchdog/server/src/IoOveruseConfigs.h b/cpp/watchdog/server/src/IoOveruseConfigs.h
index 53ee9dd..7ae758b 100644
--- a/cpp/watchdog/server/src/IoOveruseConfigs.h
+++ b/cpp/watchdog/server/src/IoOveruseConfigs.h
@@ -68,7 +68,7 @@
} // namespace internal
-/*
+/**
* Defines the methods that the I/O overuse configs module should implement.
*/
class IIoOveruseConfigs : public android::RefBase {
@@ -80,18 +80,18 @@
// Returns the existing configurations.
virtual void get(
std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*
- resourceOveruseConfigs) = 0;
+ resourceOveruseConfigs) const = 0;
// Writes the cached configs to disk.
virtual android::base::Result<void> writeToDisk() = 0;
- /*
+ /**
* Returns the list of vendor package prefixes. Any pre-installed package matching one of these
* prefixes should be classified as a vendor package.
*/
virtual const std::unordered_set<std::string>& vendorPackagePrefixes() = 0;
- /*
+ /**
* Returns the package names to application category mappings.
*/
virtual const std::unordered_map<
@@ -129,7 +129,7 @@
class IoOveruseConfigs;
-/*
+/**
* ComponentSpecificConfig represents the I/O overuse config defined per component.
*/
class ComponentSpecificConfig final {
@@ -141,32 +141,32 @@
mSafeToKillPackages.clear();
}
- /*
+ /**
* Updates |mPerPackageThresholds|.
*/
android::base::Result<void> updatePerPackageThresholds(
const std::vector<android::automotive::watchdog::internal::PerStateIoOveruseThreshold>&
thresholds,
const std::function<void(const std::string&)>& maybeAppendVendorPackagePrefixes);
- /*
+ /**
* Updates |mSafeToKillPackages|.
*/
android::base::Result<void> updateSafeToKillPackages(
const std::vector<std::string>& packages,
const std::function<void(const std::string&)>& maybeAppendVendorPackagePrefixes);
- /*
+ /**
* I/O overuse configurations for all packages under the component that are not covered by
* |mPerPackageThresholds| or |IoOveruseConfigs.mPerCategoryThresholds|.
*/
android::automotive::watchdog::internal::PerStateIoOveruseThreshold mGeneric;
- /*
+ /**
* I/O overuse configurations for specific packages under the component.
*/
std::unordered_map<std::string,
android::automotive::watchdog::internal::PerStateIoOveruseThreshold>
mPerPackageThresholds;
- /*
+ /**
* List of safe to kill packages under the component in the event of I/O overuse.
*/
std::unordered_set<std::string> mSafeToKillPackages;
@@ -175,7 +175,7 @@
friend class IoOveruseConfigs;
};
-/*
+/**
* IoOveruseConfigs represents the I/O overuse configuration defined by system and vendor
* applications. This class is not thread safe for performance purposes. The caller is responsible
* for calling the methods in a thread safe manner.
@@ -194,7 +194,7 @@
configs) override;
void get(std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*
- resourceOveruseConfigs) override;
+ resourceOveruseConfigs) const override;
android::base::Result<void> writeToDisk();
@@ -245,7 +245,8 @@
thresholds);
std::optional<android::automotive::watchdog::internal::ResourceOveruseConfiguration> get(
- const ComponentSpecificConfig& componentSpecificConfig, const int32_t componentFilter);
+ const ComponentSpecificConfig& componentSpecificConfig,
+ const int32_t componentFilter) const;
// System component specific configuration.
ComponentSpecificConfig mSystemConfig;
diff --git a/cpp/watchdog/server/src/IoOveruseMonitor.cpp b/cpp/watchdog/server/src/IoOveruseMonitor.cpp
index b97dd98..1164a2a 100644
--- a/cpp/watchdog/server/src/IoOveruseMonitor.cpp
+++ b/cpp/watchdog/server/src/IoOveruseMonitor.cpp
@@ -40,21 +40,27 @@
namespace automotive {
namespace watchdog {
+namespace {
+
using ::android::IPCThreadState;
using ::android::sp;
using ::android::automotive::watchdog::internal::ComponentType;
using ::android::automotive::watchdog::internal::IoOveruseConfiguration;
+using ::android::automotive::watchdog::internal::IoUsageStats;
using ::android::automotive::watchdog::internal::PackageIdentifier;
using ::android::automotive::watchdog::internal::PackageInfo;
using ::android::automotive::watchdog::internal::PackageIoOveruseStats;
using ::android::automotive::watchdog::internal::PackageResourceOveruseAction;
using ::android::automotive::watchdog::internal::ResourceOveruseConfiguration;
using ::android::automotive::watchdog::internal::UidType;
+using ::android::automotive::watchdog::internal::UserPackageIoUsageStats;
using ::android::base::Error;
using ::android::base::Result;
using ::android::base::WriteStringToFd;
using ::android::binder::Status;
+constexpr int64_t kMaxInt32 = std::numeric_limits<int32_t>::max();
+constexpr int64_t kMaxInt64 = std::numeric_limits<int64_t>::max();
// Minimum written bytes to sync the stats with the Watchdog service.
constexpr int64_t kMinSyncWrittenBytes = 100 * 1024;
// Minimum percentage of threshold to warn killable applications.
@@ -66,10 +72,23 @@
"%s <package name>, <package name>,...: Reset resource overuse stats for the given package "
"names. Value for this flag is a comma-separated value containing package names.\n";
-namespace {
+std::string uniquePackageIdStr(const std::string& name, userid_t userId) {
+ return StringPrintf("%s:%" PRId32, name.c_str(), userId);
+}
std::string uniquePackageIdStr(const PackageIdentifier& id) {
- return StringPrintf("%s:%" PRId32, id.name.c_str(), multiuser_get_user_id(id.uid));
+ return uniquePackageIdStr(id.name, multiuser_get_user_id(id.uid));
+}
+
+PerStateBytes sum(const PerStateBytes& lhs, const PerStateBytes& rhs) {
+ const auto sum = [](const int64_t& l, const int64_t& r) -> int64_t {
+ return (kMaxInt64 - l) > r ? (l + r) : kMaxInt64;
+ };
+ PerStateBytes result;
+ result.foregroundBytes = sum(lhs.foregroundBytes, rhs.foregroundBytes);
+ result.backgroundBytes = sum(lhs.backgroundBytes, rhs.backgroundBytes);
+ result.garageModeBytes = sum(lhs.garageModeBytes, rhs.garageModeBytes);
+ return result;
}
PerStateBytes diff(const PerStateBytes& lhs, const PerStateBytes& rhs) {
@@ -97,13 +116,39 @@
int64_t totalPerStateBytes(PerStateBytes perStateBytes) {
const auto sum = [](const int64_t& l, const int64_t& r) -> int64_t {
- return std::numeric_limits<int64_t>::max() - l > r ? (l + r)
- : std::numeric_limits<int64_t>::max();
+ return kMaxInt64 - l > r ? (l + r) : kMaxInt64;
};
return sum(perStateBytes.foregroundBytes,
sum(perStateBytes.backgroundBytes, perStateBytes.garageModeBytes));
}
+std::tuple<int32_t, PerStateBytes> calculateOveruseAndForgivenBytes(PerStateBytes writtenBytes,
+ PerStateBytes threshold) {
+ const auto div = [](const int64_t& l, const int64_t& r) -> int32_t {
+ return r > 0 ? (l / r) : 1;
+ };
+ const auto mul = [](const int32_t& l, const int32_t& r) -> int32_t {
+ if (l == 0 || r == 0) {
+ return 0;
+ }
+ return (kMaxInt32 / r) > l ? (l * r) : kMaxInt32;
+ };
+ const auto sum = [](const int32_t& l, const int32_t& r) -> int32_t {
+ return (kMaxInt32 - l) > r ? (l + r) : kMaxInt32;
+ };
+ int32_t foregroundOveruses = div(writtenBytes.foregroundBytes, threshold.foregroundBytes);
+ int32_t backgroundOveruses = div(writtenBytes.backgroundBytes, threshold.backgroundBytes);
+ int32_t garageModeOveruses = div(writtenBytes.garageModeBytes, threshold.garageModeBytes);
+ int32_t totalOveruses = sum(foregroundOveruses, sum(backgroundOveruses, garageModeOveruses));
+
+ PerStateBytes forgivenWriteBytes;
+ forgivenWriteBytes.foregroundBytes = mul(foregroundOveruses, threshold.foregroundBytes);
+ forgivenWriteBytes.backgroundBytes = mul(backgroundOveruses, threshold.backgroundBytes);
+ forgivenWriteBytes.garageModeBytes = mul(garageModeOveruses, threshold.garageModeBytes);
+
+ return std::make_tuple(totalOveruses, forgivenWriteBytes);
+}
+
} // namespace
std::tuple<int64_t, int64_t> calculateStartAndDuration(const time_t& currentTime) {
@@ -116,6 +161,7 @@
const android::sp<IWatchdogServiceHelper>& watchdogServiceHelper) :
mMinSyncWrittenBytes(kMinSyncWrittenBytes),
mWatchdogServiceHelper(watchdogServiceHelper),
+ mDidReadTodayPrevBootStats(false),
mSystemWideWrittenBytes({}),
mPeriodicMonitorBufferSize(0),
mLastSystemWideIoMonitorTime(0),
@@ -170,14 +216,18 @@
}
Result<void> IoOveruseMonitor::onPeriodicCollection(
- time_t time, SystemState systemState, const android::wp<UidIoStats>& uidIoStats,
- [[maybe_unused]] const android::wp<ProcStat>& procStat,
- [[maybe_unused]] const android::wp<ProcPidStat>& procPidStat) {
- if (uidIoStats == nullptr) {
+ time_t time, SystemState systemState,
+ const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+ [[maybe_unused]] const android::wp<ProcStat>& procStat) {
+ android::sp<UidStatsCollectorInterface> uidStatsCollectorSp = uidStatsCollector.promote();
+ if (uidStatsCollectorSp == nullptr) {
return Error() << "Per-UID I/O stats collector must not be null";
}
std::unique_lock writeLock(mRwMutex);
+ if (!mDidReadTodayPrevBootStats) {
+ syncTodayIoUsageStatsLocked();
+ }
struct tm prevGmt, curGmt;
gmtime_r(&mLastUserPackageIoMonitorTime, &prevGmt);
gmtime_r(&time, &curGmt);
@@ -191,42 +241,35 @@
mLastUserPackageIoMonitorTime = time;
const auto [startTime, durationInSeconds] = calculateStartAndDuration(curGmt);
- auto perUidIoUsage = uidIoStats.promote()->deltaStats();
- /*
- * TODO(b/185849350): Maybe move the packageInfo fetching logic into UidIoStats module.
- * This will also help avoid fetching package names in IoPerfCollection module.
- */
- std::vector<uid_t> seenUids;
- for (auto it = perUidIoUsage.begin(); it != perUidIoUsage.end();) {
- /*
- * UidIoStats::deltaStats returns entries with zero write bytes because other metrics
- * in these entries are non-zero.
- */
- if (it->second.ios.sumWriteBytes() == 0) {
- it = perUidIoUsage.erase(it);
- continue;
- }
- seenUids.push_back(it->first);
- ++it;
- }
- if (perUidIoUsage.empty()) {
+ auto uidStats = uidStatsCollectorSp->deltaStats();
+ if (uidStats.empty()) {
return {};
}
- const auto packageInfosByUid = mPackageInfoResolver->getPackageInfosForUids(seenUids);
std::unordered_map<uid_t, IoOveruseStats> overusingNativeStats;
bool isGarageModeActive = systemState == SystemState::GARAGE_MODE;
- for (const auto& [uid, uidIoStats] : perUidIoUsage) {
- const auto& packageInfo = packageInfosByUid.find(uid);
- if (packageInfo == packageInfosByUid.end()) {
+ for (const auto& curUidStats : uidStats) {
+ if (curUidStats.ioStats.sumWriteBytes() == 0 || !curUidStats.hasPackageInfo()) {
+ /* 1. Ignore UIDs with zero written bytes since the last collection because they are
+ * either already accounted for or no writes made since system start.
+ *
+ * 2. UID stats without package info is not useful because the stats isn't attributed to
+ * any package/service.
+ */
continue;
}
- UserPackageIoUsage curUsage(packageInfo->second, uidIoStats.ios, isGarageModeActive);
+ UserPackageIoUsage curUsage(curUidStats.packageInfo, curUidStats.ioStats,
+ isGarageModeActive);
UserPackageIoUsage* dailyIoUsage;
if (auto cachedUsage = mUserPackageDailyIoUsageById.find(curUsage.id());
cachedUsage != mUserPackageDailyIoUsageById.end()) {
cachedUsage->second += curUsage;
dailyIoUsage = &cachedUsage->second;
} else {
+ if (auto prevBootStats = mPrevBootIoUsageStatsById.find(curUsage.id());
+ prevBootStats != mPrevBootIoUsageStatsById.end()) {
+ curUsage += prevBootStats->second;
+ mPrevBootIoUsageStatsById.erase(prevBootStats);
+ }
const auto& [it, wasInserted] = mUserPackageDailyIoUsageById.insert(
std::pair(curUsage.id(), std::move(curUsage)));
dailyIoUsage = &it->second;
@@ -234,15 +277,23 @@
const auto threshold = mIoOveruseConfigs->fetchThreshold(dailyIoUsage->packageInfo);
+ const auto deltaWrittenBytes =
+ diff(dailyIoUsage->writtenBytes, dailyIoUsage->forgivenWriteBytes);
+ const auto [currentOveruses, forgivenWriteBytes] =
+ calculateOveruseAndForgivenBytes(deltaWrittenBytes, threshold);
+ dailyIoUsage->totalOveruses += currentOveruses;
+ dailyIoUsage->forgivenWriteBytes =
+ sum(dailyIoUsage->forgivenWriteBytes, forgivenWriteBytes);
+
PackageIoOveruseStats stats;
- stats.uid = uid;
+ stats.uid = curUidStats.packageInfo.packageIdentifier.uid;
stats.shouldNotify = false;
+ stats.forgivenWriteBytes = dailyIoUsage->forgivenWriteBytes;
stats.ioOveruseStats.startTime = startTime;
stats.ioOveruseStats.durationInSeconds = durationInSeconds;
stats.ioOveruseStats.writtenBytes = dailyIoUsage->writtenBytes;
stats.ioOveruseStats.totalOveruses = dailyIoUsage->totalOveruses;
- stats.ioOveruseStats.remainingWriteBytes =
- diff(threshold, diff(dailyIoUsage->writtenBytes, dailyIoUsage->forgivenWriteBytes));
+ stats.ioOveruseStats.remainingWriteBytes = diff(threshold, deltaWrittenBytes);
stats.ioOveruseStats.killableOnOveruse =
mIoOveruseConfigs->isSafeToKill(dailyIoUsage->packageInfo);
@@ -257,14 +308,7 @@
bool shouldSyncWatchdogService =
(totalPerStateBytes(dailyIoUsage->writtenBytes) -
dailyIoUsage->lastSyncedWrittenBytes) >= mMinSyncWrittenBytes;
- if (remainingWriteBytes.foregroundBytes == 0 || remainingWriteBytes.backgroundBytes == 0 ||
- remainingWriteBytes.garageModeBytes == 0) {
- stats.ioOveruseStats.totalOveruses = ++dailyIoUsage->totalOveruses;
- /*
- * Reset counters as the package may be disabled/killed by the watchdog service.
- * NOTE: If this logic is updated, update watchdog service side logic as well.
- */
- dailyIoUsage->forgivenWriteBytes = dailyIoUsage->writtenBytes;
+ if (currentOveruses > 0) {
dailyIoUsage->isPackageWarned = false;
/*
* Send notifications for native service I/O overuses as well because system listeners
@@ -272,7 +316,7 @@
*/
stats.shouldNotify = true;
if (dailyIoUsage->packageInfo.uidType == UidType::NATIVE) {
- overusingNativeStats[uid] = stats.ioOveruseStats;
+ overusingNativeStats[stats.uid] = stats.ioOveruseStats;
}
shouldSyncWatchdogService = true;
} else if (dailyIoUsage->packageInfo.uidType != UidType::NATIVE &&
@@ -320,10 +364,10 @@
Result<void> IoOveruseMonitor::onCustomCollection(
time_t time, SystemState systemState,
[[maybe_unused]] const std::unordered_set<std::string>& filterPackages,
- const android::wp<UidIoStats>& uidIoStats, const android::wp<ProcStat>& procStat,
- const android::wp<ProcPidStat>& procPidStat) {
+ const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+ const android::wp<ProcStat>& procStat) {
// Nothing special for custom collection.
- return onPeriodicCollection(time, systemState, uidIoStats, procStat, procPidStat);
+ return onPeriodicCollection(time, systemState, uidStatsCollector, procStat);
}
Result<void> IoOveruseMonitor::onPeriodicMonitor(
@@ -380,17 +424,38 @@
return {};
}
-Result<void> IoOveruseMonitor::onDump([[maybe_unused]] int fd) {
+Result<void> IoOveruseMonitor::onDump([[maybe_unused]] int fd) const {
// TODO(b/183436216): Dump the list of killed/disabled packages. Dump the list of packages that
// exceed xx% of their threshold.
return {};
}
-bool IoOveruseMonitor::dumpHelpText(int fd) {
+bool IoOveruseMonitor::dumpHelpText(int fd) const {
return WriteStringToFd(StringPrintf(kHelpText, name().c_str(), kResetResourceOveruseStatsFlag),
fd);
}
+void IoOveruseMonitor::syncTodayIoUsageStatsLocked() {
+ std::vector<UserPackageIoUsageStats> userPackageIoUsageStats;
+ if (const auto status = mWatchdogServiceHelper->getTodayIoUsageStats(&userPackageIoUsageStats);
+ !status.isOk()) {
+ ALOGE("Failed to fetch today I/O usage stats collected during previous boot: %s",
+ status.exceptionMessage().c_str());
+ return;
+ }
+ for (const auto& statsEntry : userPackageIoUsageStats) {
+ std::string uniqueId = uniquePackageIdStr(statsEntry.packageName,
+ static_cast<userid_t>(statsEntry.userId));
+ if (auto it = mUserPackageDailyIoUsageById.find(uniqueId);
+ it != mUserPackageDailyIoUsageById.end()) {
+ it->second += statsEntry.ioUsageStats;
+ continue;
+ }
+ mPrevBootIoUsageStatsById.insert(std::pair(uniqueId, statsEntry.ioUsageStats));
+ }
+ mDidReadTodayPrevBootStats = true;
+}
+
void IoOveruseMonitor::notifyNativePackagesLocked(
const std::unordered_map<uid_t, IoOveruseStats>& statsByUid) {
for (const auto& [uid, ioOveruseStats] : statsByUid) {
@@ -438,7 +503,7 @@
}
Result<void> IoOveruseMonitor::getResourceOveruseConfigurations(
- std::vector<ResourceOveruseConfiguration>* configs) {
+ std::vector<ResourceOveruseConfiguration>* configs) const {
std::shared_lock readLock(mRwMutex);
if (!isInitializedLocked()) {
return Error(Status::EX_ILLEGAL_STATE) << name() << " is not initialized";
@@ -496,7 +561,7 @@
return {};
}
-Result<void> IoOveruseMonitor::getIoOveruseStats(IoOveruseStats* ioOveruseStats) {
+Result<void> IoOveruseMonitor::getIoOveruseStats(IoOveruseStats* ioOveruseStats) const {
if (!isInitialized()) {
return Error(Status::EX_ILLEGAL_STATE) << "I/O overuse monitor is not initialized";
}
@@ -580,14 +645,14 @@
}
IoOveruseMonitor::UserPackageIoUsage::UserPackageIoUsage(const PackageInfo& pkgInfo,
- const IoUsage& ioUsage,
+ const UidIoStats& uidIoStats,
const bool isGarageModeActive) {
packageInfo = pkgInfo;
if (isGarageModeActive) {
- writtenBytes.garageModeBytes = ioUsage.sumWriteBytes();
+ writtenBytes.garageModeBytes = uidIoStats.sumWriteBytes();
} else {
- writtenBytes.foregroundBytes = ioUsage.metrics[WRITE_BYTES][FOREGROUND];
- writtenBytes.backgroundBytes = ioUsage.metrics[WRITE_BYTES][BACKGROUND];
+ writtenBytes.foregroundBytes = uidIoStats.metrics[WRITE_BYTES][FOREGROUND];
+ writtenBytes.backgroundBytes = uidIoStats.metrics[WRITE_BYTES][BACKGROUND];
}
}
@@ -596,17 +661,15 @@
if (id() == r.id()) {
packageInfo = r.packageInfo;
}
- const auto sum = [](const int64_t& l, const int64_t& r) -> int64_t {
- return (std::numeric_limits<int64_t>::max() - l) > r ? (l + r)
- : std::numeric_limits<int64_t>::max();
- };
- writtenBytes.foregroundBytes =
- sum(writtenBytes.foregroundBytes, r.writtenBytes.foregroundBytes);
- writtenBytes.backgroundBytes =
- sum(writtenBytes.backgroundBytes, r.writtenBytes.backgroundBytes);
- writtenBytes.garageModeBytes =
- sum(writtenBytes.garageModeBytes, r.writtenBytes.garageModeBytes);
+ writtenBytes = sum(writtenBytes, r.writtenBytes);
+ return *this;
+}
+IoOveruseMonitor::UserPackageIoUsage& IoOveruseMonitor::UserPackageIoUsage::operator+=(
+ const IoUsageStats& ioUsageStats) {
+ writtenBytes = sum(writtenBytes, ioUsageStats.writtenBytes);
+ forgivenWriteBytes = sum(forgivenWriteBytes, ioUsageStats.forgivenWriteBytes);
+ totalOveruses += ioUsageStats.totalOveruses;
return *this;
}
diff --git a/cpp/watchdog/server/src/IoOveruseMonitor.h b/cpp/watchdog/server/src/IoOveruseMonitor.h
index 27e179f..57c01c0 100644
--- a/cpp/watchdog/server/src/IoOveruseMonitor.h
+++ b/cpp/watchdog/server/src/IoOveruseMonitor.h
@@ -19,9 +19,8 @@
#include "IoOveruseConfigs.h"
#include "PackageInfoResolver.h"
-#include "ProcPidStat.h"
#include "ProcStat.h"
-#include "UidIoStats.h"
+#include "UidStatsCollector.h"
#include "WatchdogPerfService.h"
#include <android-base/result.h>
@@ -62,17 +61,17 @@
// Used only in tests.
std::tuple<int64_t, int64_t> calculateStartAndDuration(const time_t& currentTime);
-/*
+/**
* IIoOveruseMonitor interface defines the methods that the I/O overuse monitoring module
* should implement.
*/
class IIoOveruseMonitor : virtual public IDataProcessorInterface {
public:
// Returns whether or not the monitor is initialized.
- virtual bool isInitialized() = 0;
+ virtual bool isInitialized() const = 0;
// Dumps the help text.
- virtual bool dumpHelpText(int fd) = 0;
+ virtual bool dumpHelpText(int fd) const = 0;
// Below API is from internal/ICarWatchdog.aidl. Please refer to the AIDL for description.
virtual android::base::Result<void> updateResourceOveruseConfigurations(
@@ -81,7 +80,7 @@
configs) = 0;
virtual android::base::Result<void> getResourceOveruseConfigurations(
std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*
- configs) = 0;
+ configs) const = 0;
virtual android::base::Result<void> actionTakenOnIoOveruse(
const std::vector<
android::automotive::watchdog::internal::PackageResourceOveruseAction>&
@@ -94,7 +93,7 @@
virtual android::base::Result<void> removeIoOveruseListener(
const sp<IResourceOveruseListener>& listener) = 0;
- virtual android::base::Result<void> getIoOveruseStats(IoOveruseStats* ioOveruseStats) = 0;
+ virtual android::base::Result<void> getIoOveruseStats(IoOveruseStats* ioOveruseStats) const = 0;
virtual android::base::Result<void> resetIoOveruseStats(
const std::vector<std::string>& packageNames) = 0;
@@ -106,42 +105,42 @@
~IoOveruseMonitor() { terminate(); }
- bool isInitialized() {
+ bool isInitialized() const override {
std::shared_lock readLock(mRwMutex);
return isInitializedLocked();
}
// Below methods implement IDataProcessorInterface.
- std::string name() { return "IoOveruseMonitor"; }
+ std::string name() const override { return "IoOveruseMonitor"; }
friend std::ostream& operator<<(std::ostream& os, const IoOveruseMonitor& monitor);
android::base::Result<void> onBoottimeCollection(
- time_t /*time*/, const android::wp<UidIoStats>& /*uidIoStats*/,
- const android::wp<ProcStat>& /*procStat*/,
- const android::wp<ProcPidStat>& /*procPidStat*/) {
+ [[maybe_unused]] time_t time,
+ [[maybe_unused]] const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+ [[maybe_unused]] const android::wp<ProcStat>& procStat) override {
// No I/O overuse monitoring during boot-time.
return {};
}
- android::base::Result<void> onPeriodicCollection(time_t time, SystemState systemState,
- const android::wp<UidIoStats>& uidIoStats,
- const android::wp<ProcStat>& procStat,
- const android::wp<ProcPidStat>& procPidStat);
+ android::base::Result<void> onPeriodicCollection(
+ time_t time, SystemState systemState,
+ const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+ const android::wp<ProcStat>& procStat) override;
android::base::Result<void> onCustomCollection(
time_t time, SystemState systemState,
const std::unordered_set<std::string>& filterPackages,
- const android::wp<UidIoStats>& uidIoStats, const android::wp<ProcStat>& procStat,
- const android::wp<ProcPidStat>& procPidStat);
+ const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+ const android::wp<ProcStat>& procStat) override;
android::base::Result<void> onPeriodicMonitor(
time_t time, const android::wp<IProcDiskStatsInterface>& procDiskStats,
- const std::function<void()>& alertHandler);
+ const std::function<void()>& alertHandler) override;
- android::base::Result<void> onDump(int fd);
+ android::base::Result<void> onDump(int fd) const override;
- bool dumpHelpText(int fd);
+ bool dumpHelpText(int fd) const override;
- android::base::Result<void> onCustomCollectionDump(int /*fd*/) {
+ android::base::Result<void> onCustomCollectionDump([[maybe_unused]] int fd) override {
// No special processing for custom collection. Thus no custom collection dump.
return {};
}
@@ -149,26 +148,28 @@
// Below methods implement AIDL interfaces.
android::base::Result<void> updateResourceOveruseConfigurations(
const std::vector<
- android::automotive::watchdog::internal::ResourceOveruseConfiguration>&
- configs);
+ android::automotive::watchdog::internal::ResourceOveruseConfiguration>& configs)
+ override;
android::base::Result<void> getResourceOveruseConfigurations(
std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*
- configs);
+ configs) const override;
android::base::Result<void> actionTakenOnIoOveruse(
const std::vector<
- android::automotive::watchdog::internal::PackageResourceOveruseAction>&
- actions);
+ android::automotive::watchdog::internal::PackageResourceOveruseAction>& actions)
+ override;
- android::base::Result<void> addIoOveruseListener(const sp<IResourceOveruseListener>& listener);
+ android::base::Result<void> addIoOveruseListener(
+ const sp<IResourceOveruseListener>& listener) override;
android::base::Result<void> removeIoOveruseListener(
- const sp<IResourceOveruseListener>& listener);
+ const sp<IResourceOveruseListener>& listener) override;
- android::base::Result<void> getIoOveruseStats(IoOveruseStats* ioOveruseStats);
+ android::base::Result<void> getIoOveruseStats(IoOveruseStats* ioOveruseStats) const override;
- android::base::Result<void> resetIoOveruseStats(const std::vector<std::string>& packageName);
+ android::base::Result<void> resetIoOveruseStats(
+ const std::vector<std::string>& packageName) override;
protected:
android::base::Result<void> init();
@@ -183,7 +184,7 @@
struct UserPackageIoUsage {
UserPackageIoUsage(const android::automotive::watchdog::internal::PackageInfo& packageInfo,
- const IoUsage& IoUsage, const bool isGarageModeActive);
+ const UidIoStats& uidIoStats, const bool isGarageModeActive);
android::automotive::watchdog::internal::PackageInfo packageInfo = {};
PerStateBytes writtenBytes = {};
PerStateBytes forgivenWriteBytes = {};
@@ -192,6 +193,8 @@
uint64_t lastSyncedWrittenBytes = 0;
UserPackageIoUsage& operator+=(const UserPackageIoUsage& r);
+ UserPackageIoUsage& operator+=(
+ const android::automotive::watchdog::internal::IoUsageStats& r);
const std::string id() const;
void resetStats();
@@ -211,7 +214,9 @@
};
private:
- bool isInitializedLocked() { return mIoOveruseConfigs != nullptr; }
+ bool isInitializedLocked() const { return mIoOveruseConfigs != nullptr; }
+
+ void syncTodayIoUsageStatsLocked();
void notifyNativePackagesLocked(const std::unordered_map<uid_t, IoOveruseStats>& statsByUid);
@@ -221,7 +226,7 @@
using Processor = std::function<void(ListenersByUidMap&, ListenersByUidMap::const_iterator)>;
bool findListenerAndProcessLocked(const sp<IBinder>& binder, const Processor& processor);
- /*
+ /**
* Writes in-memory configs to disk asynchronously if configs are not written after latest
* update.
*/
@@ -236,10 +241,14 @@
// Makes sure only one collection is running at any given time.
mutable std::shared_mutex mRwMutex;
+ // Indicates whether or not today's I/O usage stats, that were collected during previous boot,
+ // are read from CarService because CarService persists these stats in database across reboot.
+ bool mDidReadTodayPrevBootStats GUARDED_BY(mRwMutex);
+
// Summary of configs available for all the components and system-wide overuse alert thresholds.
sp<IIoOveruseConfigs> mIoOveruseConfigs GUARDED_BY(mRwMutex);
- /*
+ /**
* Delta of system-wide written kib across all disks from the last |mPeriodicMonitorBufferSize|
* polls along with the polling duration.
*/
@@ -247,6 +256,11 @@
size_t mPeriodicMonitorBufferSize GUARDED_BY(mRwMutex);
time_t mLastSystemWideIoMonitorTime GUARDED_BY(mRwMutex);
+ // Cache of I/O usage stats from previous boot that happened today. Key is a unique ID with
+ // the format `packageName:userId`.
+ std::unordered_map<std::string, android::automotive::watchdog::internal::IoUsageStats>
+ mPrevBootIoUsageStatsById GUARDED_BY(mRwMutex);
+
// Cache of per user package I/O usage. Key is a unique ID with the format `packageName:userId`.
std::unordered_map<std::string, UserPackageIoUsage> mUserPackageDailyIoUsageById
GUARDED_BY(mRwMutex);
diff --git a/cpp/watchdog/server/src/IoPerfCollection.cpp b/cpp/watchdog/server/src/IoPerfCollection.cpp
index 5537caf..d9f5e02 100644
--- a/cpp/watchdog/server/src/IoPerfCollection.cpp
+++ b/cpp/watchdog/server/src/IoPerfCollection.cpp
@@ -45,229 +45,281 @@
namespace {
-const int32_t kDefaultTopNStatsPerCategory = 10;
-const int32_t kDefaultTopNStatsPerSubcategory = 5;
+constexpr int32_t kDefaultTopNStatsPerCategory = 10;
+constexpr int32_t kDefaultTopNStatsPerSubcategory = 5;
+constexpr const char kBootTimeCollectionTitle[] = "%s\nBoot-time I/O performance report:\n%s\n";
+constexpr const char kPeriodicCollectionTitle[] =
+ "%s\nLast N minutes I/O performance report:\n%s\n";
+constexpr const char kCustomCollectionTitle[] = "%s\nCustom I/O performance data report:\n%s\n";
+constexpr const char kCollectionTitle[] =
+ "Collection duration: %.f seconds\nNumber of collections: %zu\n";
+constexpr const char kRecordTitle[] = "\nCollection %zu: <%s>\n%s\n%s";
+constexpr const char kIoReadsTitle[] = "\nTop N Reads:\n%s\n";
+constexpr const char kIoWritesTitle[] = "\nTop N Writes:\n%s\n";
+constexpr const char kIoStatsHeader[] =
+ "Android User ID, Package Name, Foreground Bytes, Foreground Bytes %%, Foreground Fsync, "
+ "Foreground Fsync %%, Background Bytes, Background Bytes %%, Background Fsync, "
+ "Background Fsync %%\n";
+constexpr const char kIoBlockedTitle[] = "\nTop N I/O waiting UIDs:\n%s\n";
+constexpr const char kIoBlockedHeader[] =
+ "Android User ID, Package Name, Number of owned tasks waiting for I/O, Percentage of owned "
+ "tasks waiting for I/O\n\tCommand, Number of I/O waiting tasks, Percentage of UID's tasks "
+ "waiting for I/O\n";
+constexpr const char kMajorPageFaultsTitle[] = "\nTop N major page faults:\n%s\n";
+constexpr const char kMajorFaultsHeader[] =
+ "Android User ID, Package Name, Number of major page faults, Percentage of total major "
+ "page faults\n\tCommand, Number of major page faults, Percentage of UID's major page "
+ "faults\n";
+constexpr const char kMajorFaultsSummary[] =
+ "Number of major page faults since last collection: %" PRIu64 "\n"
+ "Percentage of change in major page faults since last collection: %.2f%%\n";
double percentage(uint64_t numer, uint64_t denom) {
return denom == 0 ? 0.0 : (static_cast<double>(numer) / static_cast<double>(denom)) * 100.0;
}
-struct UidProcessStats {
- struct ProcessInfo {
- std::string comm = "";
- uint64_t count = 0;
+void addUidIoStats(const int64_t entry[][UID_STATES], int64_t total[][UID_STATES]) {
+ const auto sum = [](int64_t lhs, int64_t rhs) -> int64_t {
+ return std::numeric_limits<int64_t>::max() - lhs > rhs
+ ? lhs + rhs
+ : std::numeric_limits<int64_t>::max();
};
- uint64_t uid = 0;
- uint32_t ioBlockedTasksCnt = 0;
- uint32_t totalTasksCnt = 0;
- uint64_t majorFaults = 0;
- std::vector<ProcessInfo> topNIoBlockedProcesses = {};
- std::vector<ProcessInfo> topNMajorFaultProcesses = {};
-};
-
-std::unique_ptr<std::unordered_map<uid_t, UidProcessStats>> getUidProcessStats(
- const std::vector<ProcessStats>& processStats, int topNStatsPerSubCategory) {
- std::unique_ptr<std::unordered_map<uid_t, UidProcessStats>> uidProcessStats(
- new std::unordered_map<uid_t, UidProcessStats>());
- for (const auto& stats : processStats) {
- if (stats.uid < 0) {
- continue;
- }
- uid_t uid = static_cast<uid_t>(stats.uid);
- if (uidProcessStats->find(uid) == uidProcessStats->end()) {
- (*uidProcessStats)[uid] = UidProcessStats{
- .uid = uid,
- .topNIoBlockedProcesses = std::vector<
- UidProcessStats::ProcessInfo>(topNStatsPerSubCategory,
- UidProcessStats::ProcessInfo{}),
- .topNMajorFaultProcesses = std::vector<
- UidProcessStats::ProcessInfo>(topNStatsPerSubCategory,
- UidProcessStats::ProcessInfo{}),
- };
- }
- auto& curUidProcessStats = (*uidProcessStats)[uid];
- // Top-level process stats has the aggregated major page faults count and this should be
- // persistent across thread creation/termination. Thus use the value from this field.
- curUidProcessStats.majorFaults += stats.process.majorFaults;
- curUidProcessStats.totalTasksCnt += stats.threads.size();
- // The process state is the same as the main thread state. Thus to avoid double counting
- // ignore the process state.
- uint32_t ioBlockedTasksCnt = 0;
- for (const auto& threadStat : stats.threads) {
- ioBlockedTasksCnt += threadStat.second.state == "D" ? 1 : 0;
- }
- curUidProcessStats.ioBlockedTasksCnt += ioBlockedTasksCnt;
- for (auto it = curUidProcessStats.topNIoBlockedProcesses.begin();
- it != curUidProcessStats.topNIoBlockedProcesses.end(); ++it) {
- if (it->count < ioBlockedTasksCnt) {
- curUidProcessStats.topNIoBlockedProcesses
- .emplace(it,
- UidProcessStats::ProcessInfo{
- .comm = stats.process.comm,
- .count = ioBlockedTasksCnt,
- });
- curUidProcessStats.topNIoBlockedProcesses.pop_back();
- break;
- }
- }
- for (auto it = curUidProcessStats.topNMajorFaultProcesses.begin();
- it != curUidProcessStats.topNMajorFaultProcesses.end(); ++it) {
- if (it->count < stats.process.majorFaults) {
- curUidProcessStats.topNMajorFaultProcesses
- .emplace(it,
- UidProcessStats::ProcessInfo{
- .comm = stats.process.comm,
- .count = stats.process.majorFaults,
- });
- curUidProcessStats.topNMajorFaultProcesses.pop_back();
- break;
- }
- }
- }
- return uidProcessStats;
+ total[READ_BYTES][FOREGROUND] =
+ sum(total[READ_BYTES][FOREGROUND], entry[READ_BYTES][FOREGROUND]);
+ total[READ_BYTES][BACKGROUND] =
+ sum(total[READ_BYTES][BACKGROUND], entry[READ_BYTES][BACKGROUND]);
+ total[WRITE_BYTES][FOREGROUND] =
+ sum(total[WRITE_BYTES][FOREGROUND], entry[WRITE_BYTES][FOREGROUND]);
+ total[WRITE_BYTES][BACKGROUND] =
+ sum(total[WRITE_BYTES][BACKGROUND], entry[WRITE_BYTES][BACKGROUND]);
+ total[FSYNC_COUNT][FOREGROUND] =
+ sum(total[FSYNC_COUNT][FOREGROUND], entry[FSYNC_COUNT][FOREGROUND]);
+ total[FSYNC_COUNT][BACKGROUND] =
+ sum(total[FSYNC_COUNT][BACKGROUND], entry[FSYNC_COUNT][BACKGROUND]);
+ return;
}
-Result<void> checkDataCollectors(const wp<UidIoStats>& uidIoStats, const wp<ProcStat>& procStat,
- const wp<ProcPidStat>& procPidStat) {
- if (uidIoStats != nullptr && procStat != nullptr && procPidStat != nullptr) {
+UserPackageStats toUserPackageStats(MetricType metricType, const UidStats& uidStats) {
+ const UidIoStats& ioStats = uidStats.ioStats;
+ return UserPackageStats{
+ .uid = uidStats.uid(),
+ .genericPackageName = uidStats.genericPackageName(),
+ .stats = UserPackageStats::
+ IoStats{.bytes = {ioStats.metrics[metricType][UidState::FOREGROUND],
+ ioStats.metrics[metricType][UidState::BACKGROUND]},
+ .fsync =
+ {ioStats.metrics[MetricType::FSYNC_COUNT][UidState::FOREGROUND],
+ ioStats.metrics[MetricType::FSYNC_COUNT]
+ [UidState::BACKGROUND]}},
+ };
+}
+
+void cacheTopNIoStats(MetricType metricType, const UidStats& uidStats,
+ std::vector<UserPackageStats>* topNIoStats) {
+ if (metricType != MetricType::READ_BYTES && metricType != MetricType::WRITE_BYTES) {
+ return;
+ }
+ int64_t totalBytes = metricType == MetricType::READ_BYTES ? uidStats.ioStats.sumReadBytes()
+ : uidStats.ioStats.sumWriteBytes();
+ if (totalBytes == 0) {
+ return;
+ }
+ for (auto it = topNIoStats->begin(); it != topNIoStats->end(); ++it) {
+ if (const auto* ioStats = std::get_if<UserPackageStats::IoStats>(&it->stats);
+ ioStats == nullptr || totalBytes > ioStats->totalBytes()) {
+ topNIoStats->emplace(it, toUserPackageStats(metricType, uidStats));
+ topNIoStats->pop_back();
+ break;
+ }
+ }
+ return;
+}
+
+enum ProcStatType {
+ IO_BLOCKED_TASKS_COUNT = 0,
+ MAJOR_FAULTS,
+ PROC_STAT_TYPES,
+};
+
+bool cacheTopNProcessStats(ProcStatType procStatType, const ProcessStats& processStats,
+ std::vector<UserPackageStats::ProcStats::ProcessCount>* topNProcesses) {
+ uint64_t count = procStatType == IO_BLOCKED_TASKS_COUNT ? processStats.ioBlockedTasksCount
+ : processStats.totalMajorFaults;
+ if (count == 0) {
+ return false;
+ }
+ for (auto it = topNProcesses->begin(); it != topNProcesses->end(); ++it) {
+ if (count > it->count) {
+ topNProcesses->emplace(it,
+ UserPackageStats::ProcStats::ProcessCount{
+ .comm = processStats.comm,
+ .count = count,
+ });
+ topNProcesses->pop_back();
+ return true;
+ }
+ }
+ return false;
+}
+
+UserPackageStats toUserPackageStats(ProcStatType procStatType, const UidStats& uidStats,
+ int topNProcessCount) {
+ uint64_t count = procStatType == IO_BLOCKED_TASKS_COUNT ? uidStats.procStats.ioBlockedTasksCount
+ : uidStats.procStats.totalMajorFaults;
+ UserPackageStats userPackageStats = {
+ .uid = uidStats.uid(),
+ .genericPackageName = uidStats.genericPackageName(),
+ .stats = UserPackageStats::ProcStats{.count = count},
+ };
+ auto& procStats = std::get<UserPackageStats::ProcStats>(userPackageStats.stats);
+ procStats.topNProcesses.resize(topNProcessCount);
+ int cachedProcessCount = 0;
+ for (const auto& [_, processStats] : uidStats.procStats.processStatsByPid) {
+ if (cacheTopNProcessStats(procStatType, processStats, &procStats.topNProcesses)) {
+ ++cachedProcessCount;
+ }
+ }
+ if (cachedProcessCount < topNProcessCount) {
+ procStats.topNProcesses.erase(procStats.topNProcesses.begin() + cachedProcessCount,
+ procStats.topNProcesses.end());
+ }
+ return userPackageStats;
+}
+
+bool cacheTopNProcStats(ProcStatType procStatType, const UidStats& uidStats, int topNProcessCount,
+ std::vector<UserPackageStats>* topNProcStats) {
+ uint64_t count = procStatType == IO_BLOCKED_TASKS_COUNT ? uidStats.procStats.ioBlockedTasksCount
+ : uidStats.procStats.totalMajorFaults;
+ if (count == 0) {
+ return false;
+ }
+ for (auto it = topNProcStats->begin(); it != topNProcStats->end(); ++it) {
+ if (const auto* procStats = std::get_if<UserPackageStats::ProcStats>(&it->stats);
+ procStats == nullptr || count > procStats->count) {
+ topNProcStats->emplace(it,
+ toUserPackageStats(procStatType, uidStats, topNProcessCount));
+ topNProcStats->pop_back();
+ return true;
+ }
+ }
+ return false;
+}
+
+Result<void> checkDataCollectors(const sp<UidStatsCollectorInterface>& uidStatsCollector,
+ const sp<ProcStat>& procStat) {
+ if (uidStatsCollector != nullptr && procStat != nullptr) {
return {};
}
std::string error;
- if (uidIoStats == nullptr) {
- error = "Per-UID I/O stats collector must not be empty";
+ if (uidStatsCollector == nullptr) {
+ error = "Per-UID stats collector must not be null";
}
if (procStat == nullptr) {
StringAppendF(&error, "%s%s", error.empty() ? "" : ", ",
- "Proc stats collector must not be empty");
+ "Proc stats collector must not be null");
}
- if (procPidStat == nullptr) {
- StringAppendF(&error, "%s%s", error.empty() ? "" : ", ",
- "Per-process stats collector must not be empty");
- }
-
return Error() << "Invalid data collectors: " << error;
}
} // namespace
-std::string toString(const UidIoPerfData& data) {
+std::string UserPackageStats::toString(MetricType metricsType,
+ const int64_t totalIoStats[][UID_STATES]) const {
std::string buffer;
- if (data.topNReads.size() > 0) {
- StringAppendF(&buffer, "\nTop N Reads:\n%s\n", std::string(12, '-').c_str());
- StringAppendF(&buffer,
- "Android User ID, Package Name, Foreground Bytes, Foreground Bytes %%, "
- "Foreground Fsync, Foreground Fsync %%, Background Bytes, "
- "Background Bytes %%, Background Fsync, Background Fsync %%\n");
+ StringAppendF(&buffer, "%" PRIu32 ", %s", multiuser_get_user_id(uid),
+ genericPackageName.c_str());
+ const auto& ioStats = std::get<UserPackageStats::IoStats>(stats);
+ for (int i = 0; i < UID_STATES; ++i) {
+ StringAppendF(&buffer, ", %" PRIi64 ", %.2f%%, %" PRIi64 ", %.2f%%", ioStats.bytes[i],
+ percentage(ioStats.bytes[i], totalIoStats[metricsType][i]), ioStats.fsync[i],
+ percentage(ioStats.fsync[i], totalIoStats[FSYNC_COUNT][i]));
}
- for (const auto& stat : data.topNReads) {
- StringAppendF(&buffer, "%" PRIu32 ", %s", stat.userId, stat.packageName.c_str());
- for (int i = 0; i < UID_STATES; ++i) {
- StringAppendF(&buffer, ", %" PRIi64 ", %.2f%%, %" PRIi64 ", %.2f%%", stat.bytes[i],
- percentage(stat.bytes[i], data.total[READ_BYTES][i]), stat.fsync[i],
- percentage(stat.fsync[i], data.total[FSYNC_COUNT][i]));
- }
- StringAppendF(&buffer, "\n");
- }
- if (data.topNWrites.size() > 0) {
- StringAppendF(&buffer, "\nTop N Writes:\n%s\n", std::string(13, '-').c_str());
- StringAppendF(&buffer,
- "Android User ID, Package Name, Foreground Bytes, Foreground Bytes %%, "
- "Foreground Fsync, Foreground Fsync %%, Background Bytes, "
- "Background Bytes %%, Background Fsync, Background Fsync %%\n");
- }
- for (const auto& stat : data.topNWrites) {
- StringAppendF(&buffer, "%" PRIu32 ", %s", stat.userId, stat.packageName.c_str());
- for (int i = 0; i < UID_STATES; ++i) {
- StringAppendF(&buffer, ", %" PRIi64 ", %.2f%%, %" PRIi64 ", %.2f%%", stat.bytes[i],
- percentage(stat.bytes[i], data.total[WRITE_BYTES][i]), stat.fsync[i],
- percentage(stat.fsync[i], data.total[FSYNC_COUNT][i]));
- }
- StringAppendF(&buffer, "\n");
+ StringAppendF(&buffer, "\n");
+ return buffer;
+}
+
+std::string UserPackageStats::toString(int64_t totalCount) const {
+ std::string buffer;
+ const auto& procStats = std::get<UserPackageStats::ProcStats>(stats);
+ StringAppendF(&buffer, "%" PRIu32 ", %s, %" PRIu64 ", %.2f%%\n", multiuser_get_user_id(uid),
+ genericPackageName.c_str(), procStats.count,
+ percentage(procStats.count, totalCount));
+ for (const auto& processCount : procStats.topNProcesses) {
+ StringAppendF(&buffer, "\t%s, %" PRIu64 ", %.2f%%\n", processCount.comm.c_str(),
+ processCount.count, percentage(processCount.count, procStats.count));
}
return buffer;
}
-std::string toString(const SystemIoPerfData& data) {
+std::string UserPackageSummaryStats::toString() const {
std::string buffer;
- StringAppendF(&buffer, "CPU I/O wait time/percent: %" PRIu64 " / %.2f%%\n", data.cpuIoWaitTime,
- percentage(data.cpuIoWaitTime, data.totalCpuTime));
+ if (!topNIoReads.empty()) {
+ StringAppendF(&buffer, kIoReadsTitle, std::string(12, '-').c_str());
+ StringAppendF(&buffer, kIoStatsHeader);
+ for (const auto& stats : topNIoReads) {
+ StringAppendF(&buffer, "%s",
+ stats.toString(MetricType::READ_BYTES, totalIoStats).c_str());
+ }
+ }
+ if (!topNIoWrites.empty()) {
+ StringAppendF(&buffer, kIoWritesTitle, std::string(13, '-').c_str());
+ StringAppendF(&buffer, kIoStatsHeader);
+ for (const auto& stats : topNIoWrites) {
+ StringAppendF(&buffer, "%s",
+ stats.toString(MetricType::WRITE_BYTES, totalIoStats).c_str());
+ }
+ }
+ if (!topNIoBlocked.empty()) {
+ StringAppendF(&buffer, kIoBlockedTitle, std::string(23, '-').c_str());
+ StringAppendF(&buffer, kIoBlockedHeader);
+ for (const auto& stats : topNIoBlocked) {
+ const auto it = taskCountByUid.find(stats.uid);
+ if (it == taskCountByUid.end()) {
+ continue;
+ }
+ StringAppendF(&buffer, "%s", stats.toString(it->second).c_str());
+ }
+ }
+ if (!topNMajorFaults.empty()) {
+ StringAppendF(&buffer, kMajorPageFaultsTitle, std::string(24, '-').c_str());
+ StringAppendF(&buffer, kMajorFaultsHeader);
+ for (const auto& stats : topNMajorFaults) {
+ StringAppendF(&buffer, "%s", stats.toString(totalMajorFaults).c_str());
+ }
+ StringAppendF(&buffer, kMajorFaultsSummary, totalMajorFaults, majorFaultsPercentChange);
+ }
+ return buffer;
+}
+
+std::string SystemSummaryStats::toString() const {
+ std::string buffer;
+ StringAppendF(&buffer, "CPU I/O wait time/percent: %" PRIu64 " / %.2f%%\n", cpuIoWaitTime,
+ percentage(cpuIoWaitTime, totalCpuTime));
StringAppendF(&buffer, "Number of I/O blocked processes/percent: %" PRIu32 " / %.2f%%\n",
- data.ioBlockedProcessesCnt,
- percentage(data.ioBlockedProcessesCnt, data.totalProcessesCnt));
+ ioBlockedProcessCount, percentage(ioBlockedProcessCount, totalProcessCount));
return buffer;
}
-std::string toString(const ProcessIoPerfData& data) {
+std::string PerfStatsRecord::toString() const {
std::string buffer;
- StringAppendF(&buffer, "Number of major page faults since last collection: %" PRIu64 "\n",
- data.totalMajorFaults);
- StringAppendF(&buffer,
- "Percentage of change in major page faults since last collection: %.2f%%\n",
- data.majorFaultsPercentChange);
- if (data.topNMajorFaultUids.size() > 0) {
- StringAppendF(&buffer, "\nTop N major page faults:\n%s\n", std::string(24, '-').c_str());
- StringAppendF(&buffer,
- "Android User ID, Package Name, Number of major page faults, "
- "Percentage of total major page faults\n");
- StringAppendF(&buffer,
- "\tCommand, Number of major page faults, Percentage of UID's major page "
- "faults\n");
- }
- for (const auto& uidStats : data.topNMajorFaultUids) {
- StringAppendF(&buffer, "%" PRIu32 ", %s, %" PRIu64 ", %.2f%%\n", uidStats.userId,
- uidStats.packageName.c_str(), uidStats.count,
- percentage(uidStats.count, data.totalMajorFaults));
- for (const auto& procStats : uidStats.topNProcesses) {
- StringAppendF(&buffer, "\t%s, %" PRIu64 ", %.2f%%\n", procStats.comm.c_str(),
- procStats.count, percentage(procStats.count, uidStats.count));
- }
- }
- if (data.topNIoBlockedUids.size() > 0) {
- StringAppendF(&buffer, "\nTop N I/O waiting UIDs:\n%s\n", std::string(23, '-').c_str());
- StringAppendF(&buffer,
- "Android User ID, Package Name, Number of owned tasks waiting for I/O, "
- "Percentage of owned tasks waiting for I/O\n");
- StringAppendF(&buffer,
- "\tCommand, Number of I/O waiting tasks, Percentage of UID's tasks waiting "
- "for I/O\n");
- }
- for (size_t i = 0; i < data.topNIoBlockedUids.size(); ++i) {
- const auto& uidStats = data.topNIoBlockedUids[i];
- StringAppendF(&buffer, "%" PRIu32 ", %s, %" PRIu64 ", %.2f%%\n", uidStats.userId,
- uidStats.packageName.c_str(), uidStats.count,
- percentage(uidStats.count, data.topNIoBlockedUidsTotalTaskCnt[i]));
- for (const auto& procStats : uidStats.topNProcesses) {
- StringAppendF(&buffer, "\t%s, %" PRIu64 ", %.2f%%\n", procStats.comm.c_str(),
- procStats.count, percentage(procStats.count, uidStats.count));
- }
- }
+ StringAppendF(&buffer, "%s%s", systemSummaryStats.toString().c_str(),
+ userPackageSummaryStats.toString().c_str());
return buffer;
}
-std::string toString(const IoPerfRecord& record) {
- std::string buffer;
- StringAppendF(&buffer, "%s%s%s", toString(record.systemIoPerfData).c_str(),
- toString(record.processIoPerfData).c_str(),
- toString(record.uidIoPerfData).c_str());
- return buffer;
-}
-
-std::string toString(const CollectionInfo& collectionInfo) {
- if (collectionInfo.records.empty()) {
+std::string CollectionInfo::toString() const {
+ if (records.empty()) {
return kEmptyCollectionMessage;
}
std::string buffer;
- double duration =
- difftime(collectionInfo.records.back().time, collectionInfo.records.front().time);
- StringAppendF(&buffer, "Collection duration: %.f seconds\nNumber of collections: %zu\n",
- duration, collectionInfo.records.size());
-
- for (size_t i = 0; i < collectionInfo.records.size(); ++i) {
- const auto& record = collectionInfo.records[i];
+ double duration = difftime(records.back().time, records.front().time);
+ StringAppendF(&buffer, kCollectionTitle, duration, records.size());
+ for (size_t i = 0; i < records.size(); ++i) {
+ const auto& record = records[i];
std::stringstream timestamp;
timestamp << std::put_time(std::localtime(&record.time), "%c %Z");
- StringAppendF(&buffer, "\nCollection %zu: <%s>\n%s\n%s", i, timestamp.str().c_str(),
- std::string(45, '=').c_str(), toString(record).c_str());
+ StringAppendF(&buffer, kRecordTitle, i, timestamp.str().c_str(),
+ std::string(45, '=').c_str(), record.toString().c_str());
}
return buffer;
}
@@ -313,16 +365,16 @@
mCustomCollection = {};
}
-Result<void> IoPerfCollection::onDump(int fd) {
+Result<void> IoPerfCollection::onDump(int fd) const {
Mutex::Autolock lock(mMutex);
- if (!WriteStringToFd(StringPrintf("%s\nBoot-time I/O performance report:\n%s\n",
- std::string(75, '-').c_str(), std::string(33, '=').c_str()),
+ if (!WriteStringToFd(StringPrintf(kBootTimeCollectionTitle, std::string(75, '-').c_str(),
+ std::string(33, '=').c_str()),
fd) ||
- !WriteStringToFd(toString(mBoottimeCollection), fd) ||
- !WriteStringToFd(StringPrintf("%s\nLast N minutes I/O performance report:\n%s\n",
- std::string(75, '-').c_str(), std::string(38, '=').c_str()),
+ !WriteStringToFd(mBoottimeCollection.toString(), fd) ||
+ !WriteStringToFd(StringPrintf(kPeriodicCollectionTitle, std::string(75, '-').c_str(),
+ std::string(38, '=').c_str()),
fd) ||
- !WriteStringToFd(toString(mPeriodicCollection), fd)) {
+ !WriteStringToFd(mPeriodicCollection.toString(), fd)) {
return Error(FAILED_TRANSACTION)
<< "Failed to dump the boot-time and periodic collection reports.";
}
@@ -340,70 +392,70 @@
return {};
}
- if (!WriteStringToFd(StringPrintf("%s\nCustom I/O performance data report:\n%s\n",
- std::string(75, '-').c_str(), std::string(75, '-').c_str()),
+ if (!WriteStringToFd(StringPrintf(kCustomCollectionTitle, std::string(75, '-').c_str(),
+ std::string(75, '-').c_str()),
fd) ||
- !WriteStringToFd(toString(mCustomCollection), fd)) {
+ !WriteStringToFd(mCustomCollection.toString(), fd)) {
return Error(FAILED_TRANSACTION) << "Failed to write custom I/O collection report.";
}
return {};
}
-Result<void> IoPerfCollection::onBoottimeCollection(time_t time, const wp<UidIoStats>& uidIoStats,
- const wp<ProcStat>& procStat,
- const wp<ProcPidStat>& procPidStat) {
- auto result = checkDataCollectors(uidIoStats, procStat, procPidStat);
+Result<void> IoPerfCollection::onBoottimeCollection(
+ time_t time, const wp<UidStatsCollectorInterface>& uidStatsCollector,
+ const wp<ProcStat>& procStat) {
+ const sp<UidStatsCollectorInterface> uidStatsCollectorSp = uidStatsCollector.promote();
+ const sp<ProcStat> procStatSp = procStat.promote();
+ auto result = checkDataCollectors(uidStatsCollectorSp, procStatSp);
if (!result.ok()) {
return result;
}
Mutex::Autolock lock(mMutex);
- return processLocked(time, std::unordered_set<std::string>(), uidIoStats, procStat, procPidStat,
+ return processLocked(time, std::unordered_set<std::string>(), uidStatsCollectorSp, procStatSp,
&mBoottimeCollection);
}
-Result<void> IoPerfCollection::onPeriodicCollection(time_t time,
- [[maybe_unused]] SystemState systemState,
- const wp<UidIoStats>& uidIoStats,
- const wp<ProcStat>& procStat,
- const wp<ProcPidStat>& procPidStat) {
- auto result = checkDataCollectors(uidIoStats, procStat, procPidStat);
+Result<void> IoPerfCollection::onPeriodicCollection(
+ time_t time, [[maybe_unused]] SystemState systemState,
+ const wp<UidStatsCollectorInterface>& uidStatsCollector, const wp<ProcStat>& procStat) {
+ const sp<UidStatsCollectorInterface> uidStatsCollectorSp = uidStatsCollector.promote();
+ const sp<ProcStat> procStatSp = procStat.promote();
+ auto result = checkDataCollectors(uidStatsCollectorSp, procStatSp);
if (!result.ok()) {
return result;
}
Mutex::Autolock lock(mMutex);
- return processLocked(time, std::unordered_set<std::string>(), uidIoStats, procStat, procPidStat,
+ return processLocked(time, std::unordered_set<std::string>(), uidStatsCollectorSp, procStatSp,
&mPeriodicCollection);
}
Result<void> IoPerfCollection::onCustomCollection(
time_t time, [[maybe_unused]] SystemState systemState,
- const std::unordered_set<std::string>& filterPackages, const wp<UidIoStats>& uidIoStats,
- const wp<ProcStat>& procStat, const wp<ProcPidStat>& procPidStat) {
- auto result = checkDataCollectors(uidIoStats, procStat, procPidStat);
+ const std::unordered_set<std::string>& filterPackages,
+ const wp<UidStatsCollectorInterface>& uidStatsCollector, const wp<ProcStat>& procStat) {
+ const sp<UidStatsCollectorInterface> uidStatsCollectorSp = uidStatsCollector.promote();
+ const sp<ProcStat> procStatSp = procStat.promote();
+ auto result = checkDataCollectors(uidStatsCollectorSp, procStatSp);
if (!result.ok()) {
return result;
}
Mutex::Autolock lock(mMutex);
- return processLocked(time, filterPackages, uidIoStats, procStat, procPidStat,
- &mCustomCollection);
+ return processLocked(time, filterPackages, uidStatsCollectorSp, procStatSp, &mCustomCollection);
}
-Result<void> IoPerfCollection::processLocked(time_t time,
- const std::unordered_set<std::string>& filterPackages,
- const wp<UidIoStats>& uidIoStats,
- const wp<ProcStat>& procStat,
- const wp<ProcPidStat>& procPidStat,
- CollectionInfo* collectionInfo) {
+Result<void> IoPerfCollection::processLocked(
+ time_t time, const std::unordered_set<std::string>& filterPackages,
+ const sp<UidStatsCollectorInterface>& uidStatsCollector, const sp<ProcStat>& procStat,
+ CollectionInfo* collectionInfo) {
if (collectionInfo->maxCacheSize == 0) {
return Error() << "Maximum cache size cannot be 0";
}
- IoPerfRecord record{
+ PerfStatsRecord record{
.time = time,
};
- processSystemIoPerfData(procStat, &record.systemIoPerfData);
- processProcessIoPerfDataLocked(filterPackages, procPidStat, &record.processIoPerfData);
- processUidIoPerfData(filterPackages, uidIoStats, &record.uidIoPerfData);
+ processUidStatsLocked(filterPackages, uidStatsCollector, &record.userPackageSummaryStats);
+ processProcStatLocked(procStat, &record.systemSummaryStats);
if (collectionInfo->records.size() > collectionInfo->maxCacheSize) {
collectionInfo->records.erase(collectionInfo->records.begin()); // Erase the oldest record.
}
@@ -411,220 +463,82 @@
return {};
}
-void IoPerfCollection::processUidIoPerfData(const std::unordered_set<std::string>& filterPackages,
- const wp<UidIoStats>& uidIoStats,
- UidIoPerfData* uidIoPerfData) const {
- const std::unordered_map<uid_t, UidIoUsage>& usages = uidIoStats.promote()->deltaStats();
-
- // Fetch only the top N reads and writes from the usage records.
- UidIoUsage tempUsage = {};
- std::vector<const UidIoUsage*> topNReads(mTopNStatsPerCategory, &tempUsage);
- std::vector<const UidIoUsage*> topNWrites(mTopNStatsPerCategory, &tempUsage);
- std::vector<uid_t> uids;
-
- for (const auto& uIt : usages) {
- const UidIoUsage& curUsage = uIt.second;
- uids.push_back(curUsage.uid);
- uidIoPerfData->total[READ_BYTES][FOREGROUND] +=
- curUsage.ios.metrics[READ_BYTES][FOREGROUND];
- uidIoPerfData->total[READ_BYTES][BACKGROUND] +=
- curUsage.ios.metrics[READ_BYTES][BACKGROUND];
- uidIoPerfData->total[WRITE_BYTES][FOREGROUND] +=
- curUsage.ios.metrics[WRITE_BYTES][FOREGROUND];
- uidIoPerfData->total[WRITE_BYTES][BACKGROUND] +=
- curUsage.ios.metrics[WRITE_BYTES][BACKGROUND];
- uidIoPerfData->total[FSYNC_COUNT][FOREGROUND] +=
- curUsage.ios.metrics[FSYNC_COUNT][FOREGROUND];
- uidIoPerfData->total[FSYNC_COUNT][BACKGROUND] +=
- curUsage.ios.metrics[FSYNC_COUNT][BACKGROUND];
-
- for (auto it = topNReads.begin(); it != topNReads.end(); ++it) {
- const UidIoUsage* curRead = *it;
- if (curRead->ios.sumReadBytes() < curUsage.ios.sumReadBytes()) {
- topNReads.emplace(it, &curUsage);
- if (filterPackages.empty()) {
- topNReads.pop_back();
- }
- break;
- }
- }
- for (auto it = topNWrites.begin(); it != topNWrites.end(); ++it) {
- const UidIoUsage* curWrite = *it;
- if (curWrite->ios.sumWriteBytes() < curUsage.ios.sumWriteBytes()) {
- topNWrites.emplace(it, &curUsage);
- if (filterPackages.empty()) {
- topNWrites.pop_back();
- }
- break;
- }
- }
+void IoPerfCollection::processUidStatsLocked(
+ const std::unordered_set<std::string>& filterPackages,
+ const sp<UidStatsCollectorInterface>& uidStatsCollector,
+ UserPackageSummaryStats* userPackageSummaryStats) {
+ const std::vector<UidStats>& uidStats = uidStatsCollector->deltaStats();
+ if (uidStats.empty()) {
+ return;
}
-
- const auto& uidToPackageNameMapping = mPackageInfoResolver->getPackageNamesForUids(uids);
-
- // Convert the top N I/O usage to UidIoPerfData.
- for (const auto& usage : topNReads) {
- if (usage->ios.isZero()) {
- // End of non-zero usage records. This case occurs when the number of UIDs with active
- // I/O operations is < |ro.carwatchdog.top_n_stats_per_category|.
- break;
- }
- UidIoPerfData::Stats stats = {
- .userId = multiuser_get_user_id(usage->uid),
- .packageName = std::to_string(usage->uid),
- .bytes = {usage->ios.metrics[READ_BYTES][FOREGROUND],
- usage->ios.metrics[READ_BYTES][BACKGROUND]},
- .fsync = {usage->ios.metrics[FSYNC_COUNT][FOREGROUND],
- usage->ios.metrics[FSYNC_COUNT][BACKGROUND]},
- };
- if (uidToPackageNameMapping.find(usage->uid) != uidToPackageNameMapping.end()) {
- stats.packageName = uidToPackageNameMapping.at(usage->uid);
- }
- if (!filterPackages.empty() &&
- filterPackages.find(stats.packageName) == filterPackages.end()) {
+ if (filterPackages.empty()) {
+ userPackageSummaryStats->topNIoReads.resize(mTopNStatsPerCategory);
+ userPackageSummaryStats->topNIoWrites.resize(mTopNStatsPerCategory);
+ userPackageSummaryStats->topNIoBlocked.resize(mTopNStatsPerCategory);
+ userPackageSummaryStats->topNMajorFaults.resize(mTopNStatsPerCategory);
+ }
+ for (const auto& curUidStats : uidStats) {
+ uid_t uid = curUidStats.uid();
+ addUidIoStats(curUidStats.ioStats.metrics, userPackageSummaryStats->totalIoStats);
+ userPackageSummaryStats->totalMajorFaults += curUidStats.procStats.totalMajorFaults;
+ if (filterPackages.empty()) {
+ cacheTopNIoStats(MetricType::READ_BYTES, curUidStats,
+ &userPackageSummaryStats->topNIoReads);
+ cacheTopNIoStats(MetricType::WRITE_BYTES, curUidStats,
+ &userPackageSummaryStats->topNIoWrites);
+ if (cacheTopNProcStats(IO_BLOCKED_TASKS_COUNT, curUidStats, mTopNStatsPerSubcategory,
+ &userPackageSummaryStats->topNIoBlocked)) {
+ userPackageSummaryStats->taskCountByUid[uid] =
+ curUidStats.procStats.totalTasksCount;
+ }
+ cacheTopNProcStats(MAJOR_FAULTS, curUidStats, mTopNStatsPerSubcategory,
+ &userPackageSummaryStats->topNMajorFaults);
continue;
}
- uidIoPerfData->topNReads.emplace_back(stats);
- }
-
- for (const auto& usage : topNWrites) {
- if (usage->ios.isZero()) {
- // End of non-zero usage records. This case occurs when the number of UIDs with active
- // I/O operations is < |ro.carwatchdog.top_n_stats_per_category|.
- break;
- }
- UidIoPerfData::Stats stats = {
- .userId = multiuser_get_user_id(usage->uid),
- .packageName = std::to_string(usage->uid),
- .bytes = {usage->ios.metrics[WRITE_BYTES][FOREGROUND],
- usage->ios.metrics[WRITE_BYTES][BACKGROUND]},
- .fsync = {usage->ios.metrics[FSYNC_COUNT][FOREGROUND],
- usage->ios.metrics[FSYNC_COUNT][BACKGROUND]},
- };
- if (uidToPackageNameMapping.find(usage->uid) != uidToPackageNameMapping.end()) {
- stats.packageName = uidToPackageNameMapping.at(usage->uid);
- }
- if (!filterPackages.empty() &&
- filterPackages.find(stats.packageName) == filterPackages.end()) {
- continue;
- }
- uidIoPerfData->topNWrites.emplace_back(stats);
- }
-}
-
-void IoPerfCollection::processSystemIoPerfData(const wp<ProcStat>& procStat,
- SystemIoPerfData* systemIoPerfData) const {
- const ProcStatInfo& procStatInfo = procStat.promote()->deltaStats();
- systemIoPerfData->cpuIoWaitTime = procStatInfo.cpuStats.ioWaitTime;
- systemIoPerfData->totalCpuTime = procStatInfo.totalCpuTime();
- systemIoPerfData->ioBlockedProcessesCnt = procStatInfo.ioBlockedProcessesCnt;
- systemIoPerfData->totalProcessesCnt = procStatInfo.totalProcessesCnt();
-}
-
-void IoPerfCollection::processProcessIoPerfDataLocked(
- const std::unordered_set<std::string>& filterPackages, const wp<ProcPidStat>& procPidStat,
- ProcessIoPerfData* processIoPerfData) {
- const std::vector<ProcessStats>& processStats = procPidStat.promote()->deltaStats();
-
- const auto& uidProcessStats = getUidProcessStats(processStats, mTopNStatsPerSubcategory);
- std::vector<uid_t> uids;
- // Fetch only the top N I/O blocked UIDs and UIDs with most major page faults.
- UidProcessStats temp = {};
- std::vector<const UidProcessStats*> topNIoBlockedUids(mTopNStatsPerCategory, &temp);
- std::vector<const UidProcessStats*> topNMajorFaultUids(mTopNStatsPerCategory, &temp);
- processIoPerfData->totalMajorFaults = 0;
- for (const auto& it : *uidProcessStats) {
- const UidProcessStats& curStats = it.second;
- uids.push_back(curStats.uid);
- processIoPerfData->totalMajorFaults += curStats.majorFaults;
- for (auto it = topNIoBlockedUids.begin(); it != topNIoBlockedUids.end(); ++it) {
- const UidProcessStats* topStats = *it;
- if (topStats->ioBlockedTasksCnt < curStats.ioBlockedTasksCnt) {
- topNIoBlockedUids.emplace(it, &curStats);
- if (filterPackages.empty()) {
- topNIoBlockedUids.pop_back();
- }
- break;
- }
- }
- for (auto it = topNMajorFaultUids.begin(); it != topNMajorFaultUids.end(); ++it) {
- const UidProcessStats* topStats = *it;
- if (topStats->majorFaults < curStats.majorFaults) {
- topNMajorFaultUids.emplace(it, &curStats);
- if (filterPackages.empty()) {
- topNMajorFaultUids.pop_back();
- }
- break;
- }
+ if (filterPackages.count(curUidStats.genericPackageName()) != 0) {
+ userPackageSummaryStats->topNIoReads.emplace_back(
+ toUserPackageStats(MetricType::READ_BYTES, curUidStats));
+ userPackageSummaryStats->topNIoWrites.emplace_back(
+ toUserPackageStats(MetricType::WRITE_BYTES, curUidStats));
+ userPackageSummaryStats->topNIoBlocked.emplace_back(
+ toUserPackageStats(IO_BLOCKED_TASKS_COUNT, curUidStats,
+ mTopNStatsPerSubcategory));
+ userPackageSummaryStats->topNMajorFaults.emplace_back(
+ toUserPackageStats(MAJOR_FAULTS, curUidStats, mTopNStatsPerSubcategory));
+ userPackageSummaryStats->taskCountByUid[uid] = curUidStats.procStats.totalTasksCount;
}
}
-
- const auto& uidToPackageNameMapping = mPackageInfoResolver->getPackageNamesForUids(uids);
-
- // Convert the top N uid process stats to ProcessIoPerfData.
- for (const auto& it : topNIoBlockedUids) {
- if (it->ioBlockedTasksCnt == 0) {
- // End of non-zero elements. This case occurs when the number of UIDs with I/O blocked
- // processes is < |ro.carwatchdog.top_n_stats_per_category|.
- break;
- }
- ProcessIoPerfData::UidStats stats = {
- .userId = multiuser_get_user_id(it->uid),
- .packageName = std::to_string(it->uid),
- .count = it->ioBlockedTasksCnt,
- };
- if (uidToPackageNameMapping.find(it->uid) != uidToPackageNameMapping.end()) {
- stats.packageName = uidToPackageNameMapping.at(it->uid);
- }
- if (!filterPackages.empty() &&
- filterPackages.find(stats.packageName) == filterPackages.end()) {
- continue;
- }
- for (const auto& pIt : it->topNIoBlockedProcesses) {
- if (pIt.count == 0) {
- break;
- }
- stats.topNProcesses.emplace_back(
- ProcessIoPerfData::UidStats::ProcessStats{pIt.comm, pIt.count});
- }
- processIoPerfData->topNIoBlockedUids.emplace_back(stats);
- processIoPerfData->topNIoBlockedUidsTotalTaskCnt.emplace_back(it->totalTasksCnt);
- }
- for (const auto& it : topNMajorFaultUids) {
- if (it->majorFaults == 0) {
- // End of non-zero elements. This case occurs when the number of UIDs with major faults
- // is < |ro.carwatchdog.top_n_stats_per_category|.
- break;
- }
- ProcessIoPerfData::UidStats stats = {
- .userId = multiuser_get_user_id(it->uid),
- .packageName = std::to_string(it->uid),
- .count = it->majorFaults,
- };
- if (uidToPackageNameMapping.find(it->uid) != uidToPackageNameMapping.end()) {
- stats.packageName = uidToPackageNameMapping.at(it->uid);
- }
- if (!filterPackages.empty() &&
- filterPackages.find(stats.packageName) == filterPackages.end()) {
- continue;
- }
- for (const auto& pIt : it->topNMajorFaultProcesses) {
- if (pIt.count == 0) {
- break;
- }
- stats.topNProcesses.emplace_back(
- ProcessIoPerfData::UidStats::ProcessStats{pIt.comm, pIt.count});
- }
- processIoPerfData->topNMajorFaultUids.emplace_back(stats);
- }
- if (mLastMajorFaults == 0) {
- processIoPerfData->majorFaultsPercentChange = 0;
- } else {
- int64_t increase = processIoPerfData->totalMajorFaults - mLastMajorFaults;
- processIoPerfData->majorFaultsPercentChange =
+ if (mLastMajorFaults != 0) {
+ int64_t increase = userPackageSummaryStats->totalMajorFaults - mLastMajorFaults;
+ userPackageSummaryStats->majorFaultsPercentChange =
(static_cast<double>(increase) / static_cast<double>(mLastMajorFaults)) * 100.0;
}
- mLastMajorFaults = processIoPerfData->totalMajorFaults;
+ mLastMajorFaults = userPackageSummaryStats->totalMajorFaults;
+
+ const auto removeEmptyStats = [](std::vector<UserPackageStats>& userPackageStats) {
+ for (auto it = userPackageStats.begin(); it != userPackageStats.end(); ++it) {
+ /* std::monostate is the first alternative in the variant. When the variant is
+ * uninitialized, the index points to this alternative.
+ */
+ if (it->stats.index() == 0) {
+ userPackageStats.erase(it, userPackageStats.end());
+ break;
+ }
+ }
+ };
+ removeEmptyStats(userPackageSummaryStats->topNIoReads);
+ removeEmptyStats(userPackageSummaryStats->topNIoWrites);
+ removeEmptyStats(userPackageSummaryStats->topNIoBlocked);
+ removeEmptyStats(userPackageSummaryStats->topNMajorFaults);
+}
+
+void IoPerfCollection::processProcStatLocked(const sp<ProcStat>& procStat,
+ SystemSummaryStats* systemSummaryStats) const {
+ const ProcStatInfo& procStatInfo = procStat->deltaStats();
+ systemSummaryStats->cpuIoWaitTime = procStatInfo.cpuStats.ioWaitTime;
+ systemSummaryStats->totalCpuTime = procStatInfo.totalCpuTime();
+ systemSummaryStats->ioBlockedProcessCount = procStatInfo.ioBlockedProcessCount;
+ systemSummaryStats->totalProcessCount = procStatInfo.totalProcessCount();
}
} // namespace watchdog
diff --git a/cpp/watchdog/server/src/IoPerfCollection.h b/cpp/watchdog/server/src/IoPerfCollection.h
index 2e976da..265cb55 100644
--- a/cpp/watchdog/server/src/IoPerfCollection.h
+++ b/cpp/watchdog/server/src/IoPerfCollection.h
@@ -17,11 +17,9 @@
#ifndef CPP_WATCHDOG_SERVER_SRC_IOPERFCOLLECTION_H_
#define CPP_WATCHDOG_SERVER_SRC_IOPERFCOLLECTION_H_
-#include "PackageInfoResolver.h"
#include "ProcDiskStats.h"
-#include "ProcPidStat.h"
#include "ProcStat.h"
-#include "UidIoStats.h"
+#include "UidStatsCollector.h"
#include "WatchdogPerfService.h"
#include <android-base/result.h>
@@ -34,79 +32,16 @@
#include <ctime>
#include <string>
#include <unordered_set>
+#include <variant>
#include <vector>
namespace android {
namespace automotive {
namespace watchdog {
-// Number of periodic collection perf data snapshots to cache in memory.
-const int32_t kDefaultPeriodicCollectionBufferSize = 180;
-constexpr const char* kEmptyCollectionMessage = "No collection recorded\n";
-
-// Performance data collected from the `/proc/uid_io/stats` file.
-struct UidIoPerfData {
- struct Stats {
- userid_t userId = 0;
- std::string packageName;
- int64_t bytes[UID_STATES];
- int64_t fsync[UID_STATES];
- };
- std::vector<Stats> topNReads = {};
- std::vector<Stats> topNWrites = {};
- int64_t total[METRIC_TYPES][UID_STATES] = {{0}};
-};
-
-std::string toString(const UidIoPerfData& perfData);
-
-// Performance data collected from the `/proc/stats` file.
-struct SystemIoPerfData {
- uint64_t cpuIoWaitTime = 0;
- uint64_t totalCpuTime = 0;
- uint32_t ioBlockedProcessesCnt = 0;
- uint32_t totalProcessesCnt = 0;
-};
-
-std::string toString(const SystemIoPerfData& perfData);
-
-// Performance data collected from the `/proc/[pid]/stat` and `/proc/[pid]/task/[tid]/stat` files.
-struct ProcessIoPerfData {
- struct UidStats {
- userid_t userId = 0;
- std::string packageName;
- uint64_t count = 0;
- struct ProcessStats {
- std::string comm = "";
- uint64_t count = 0;
- };
- std::vector<ProcessStats> topNProcesses = {};
- };
- std::vector<UidStats> topNIoBlockedUids = {};
- // Total # of tasks owned by each UID in |topNIoBlockedUids|.
- std::vector<uint64_t> topNIoBlockedUidsTotalTaskCnt = {};
- std::vector<UidStats> topNMajorFaultUids = {};
- uint64_t totalMajorFaults = 0;
- // Percentage of increase/decrease in the major page faults since last collection.
- double majorFaultsPercentChange = 0.0;
-};
-
-std::string toString(const ProcessIoPerfData& data);
-
-struct IoPerfRecord {
- time_t time; // Collection time.
- UidIoPerfData uidIoPerfData;
- SystemIoPerfData systemIoPerfData;
- ProcessIoPerfData processIoPerfData;
-};
-
-std::string toString(const IoPerfRecord& record);
-
-struct CollectionInfo {
- size_t maxCacheSize = 0; // Maximum cache size for the collection.
- std::vector<IoPerfRecord> records; // Cache of collected performance records.
-};
-
-std::string toString(const CollectionInfo& collectionInfo);
+// Number of periodic collection records to cache in memory.
+constexpr int32_t kDefaultPeriodicCollectionBufferSize = 180;
+constexpr const char kEmptyCollectionMessage[] = "No collection recorded\n";
// Forward declaration for testing use only.
namespace internal {
@@ -115,13 +50,84 @@
} // namespace internal
+// Below structs should be used only by the implementation and unit tests.
+/**
+ * Struct to represent user package performance stats.
+ */
+struct UserPackageStats {
+ struct IoStats {
+ int64_t bytes[UID_STATES] = {0};
+ int64_t fsync[UID_STATES] = {0};
+
+ int64_t totalBytes() const {
+ return std::numeric_limits<int64_t>::max() - bytes[UidState::FOREGROUND] >
+ bytes[UidState::BACKGROUND]
+ ? bytes[UidState::FOREGROUND] + bytes[UidState::BACKGROUND]
+ : std::numeric_limits<int64_t>::max();
+ }
+ };
+ struct ProcStats {
+ uint64_t count = 0;
+ struct ProcessCount {
+ std::string comm = "";
+ uint64_t count = 0;
+ };
+ std::vector<ProcessCount> topNProcesses = {};
+ };
+ uid_t uid = 0;
+ std::string genericPackageName = "";
+ std::variant<std::monostate, IoStats, ProcStats> stats;
+ std::string toString(MetricType metricsType, const int64_t totalIoStats[][UID_STATES]) const;
+ std::string toString(int64_t count) const;
+};
+
+/**
+ * User package summary performance stats collected from the `/proc/uid_io/stats`,
+ * `/proc/[pid]/stat`, `/proc/[pid]/task/[tid]/stat`, and /proc/[pid]/status` files.
+ */
+struct UserPackageSummaryStats {
+ std::vector<UserPackageStats> topNIoReads = {};
+ std::vector<UserPackageStats> topNIoWrites = {};
+ std::vector<UserPackageStats> topNIoBlocked = {};
+ std::vector<UserPackageStats> topNMajorFaults = {};
+ int64_t totalIoStats[METRIC_TYPES][UID_STATES] = {{0}};
+ std::unordered_map<uid_t, uint64_t> taskCountByUid = {};
+ uint64_t totalMajorFaults = 0;
+ // Percentage of increase/decrease in the major page faults since last collection.
+ double majorFaultsPercentChange = 0.0;
+ std::string toString() const;
+};
+
+// System performance stats collected from the `/proc/stats` file.
+struct SystemSummaryStats {
+ uint64_t cpuIoWaitTime = 0;
+ uint64_t totalCpuTime = 0;
+ uint32_t ioBlockedProcessCount = 0;
+ uint32_t totalProcessCount = 0;
+ std::string toString() const;
+};
+
+// Performance record collected during a sampling/collection period.
+struct PerfStatsRecord {
+ time_t time; // Collection time.
+ SystemSummaryStats systemSummaryStats;
+ UserPackageSummaryStats userPackageSummaryStats;
+ std::string toString() const;
+};
+
+// Group of performance records collected for a collection event.
+struct CollectionInfo {
+ size_t maxCacheSize = 0; // Maximum cache size for the collection.
+ std::vector<PerfStatsRecord> records; // Cache of collected performance records.
+ std::string toString() const;
+};
+
// IoPerfCollection implements the I/O performance data collection module.
class IoPerfCollection : public IDataProcessorInterface {
public:
IoPerfCollection() :
mTopNStatsPerCategory(0),
mTopNStatsPerSubcategory(0),
- mPackageInfoResolver(PackageInfoResolver::getInstance()),
mBoottimeCollection({}),
mPeriodicCollection({}),
mCustomCollection({}),
@@ -129,36 +135,35 @@
~IoPerfCollection() { terminate(); }
- std::string name() { return "IoPerfCollection"; }
+ std::string name() const override { return "IoPerfCollection"; }
// Implements IDataProcessorInterface.
- android::base::Result<void> onBoottimeCollection(time_t time,
- const android::wp<UidIoStats>& uidIoStats,
- const android::wp<ProcStat>& procStat,
- const android::wp<ProcPidStat>& procPidStat);
+ android::base::Result<void> onBoottimeCollection(
+ time_t time, const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+ const android::wp<ProcStat>& procStat) override;
- android::base::Result<void> onPeriodicCollection(time_t time, SystemState systemState,
- const android::wp<UidIoStats>& uidIoStats,
- const android::wp<ProcStat>& procStat,
- const android::wp<ProcPidStat>& procPidStat);
+ android::base::Result<void> onPeriodicCollection(
+ time_t time, SystemState systemState,
+ const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+ const android::wp<ProcStat>& procStat) override;
android::base::Result<void> onCustomCollection(
time_t time, SystemState systemState,
const std::unordered_set<std::string>& filterPackages,
- const android::wp<UidIoStats>& uidIoStats, const android::wp<ProcStat>& procStat,
- const android::wp<ProcPidStat>& procPidStat);
+ const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+ const android::wp<ProcStat>& procStat) override;
android::base::Result<void> onPeriodicMonitor(
[[maybe_unused]] time_t time,
[[maybe_unused]] const android::wp<IProcDiskStatsInterface>& procDiskStats,
- [[maybe_unused]] const std::function<void()>& alertHandler) {
+ [[maybe_unused]] const std::function<void()>& alertHandler) override {
// No monitoring done here as this DataProcessor only collects I/O performance records.
return {};
}
- android::base::Result<void> onDump(int fd);
+ android::base::Result<void> onDump(int fd) const override;
- android::base::Result<void> onCustomCollectionDump(int fd);
+ android::base::Result<void> onCustomCollectionDump(int fd) override;
protected:
android::base::Result<void> init();
@@ -168,27 +173,19 @@
private:
// Processes the collected data.
- android::base::Result<void> processLocked(time_t time,
- const std::unordered_set<std::string>& filterPackages,
- const android::wp<UidIoStats>& uidIoStats,
- const android::wp<ProcStat>& procStat,
- const android::wp<ProcPidStat>& procPidStat,
- CollectionInfo* collectionInfo);
+ android::base::Result<void> processLocked(
+ time_t time, const std::unordered_set<std::string>& filterPackages,
+ const android::sp<UidStatsCollectorInterface>& uidStatsCollector,
+ const android::sp<ProcStat>& procStat, CollectionInfo* collectionInfo);
- // Processes performance data from the `/proc/uid_io/stats` file.
- void processUidIoPerfData(const std::unordered_set<std::string>& filterPackages,
- const android::wp<UidIoStats>& uidIoStats,
- UidIoPerfData* uidIoPerfData) const;
+ // Processes per-UID performance data.
+ void processUidStatsLocked(const std::unordered_set<std::string>& filterPackages,
+ const android::sp<UidStatsCollectorInterface>& uidStatsCollector,
+ UserPackageSummaryStats* userPackageSummaryStats);
- // Processes performance data from the `/proc/stats` file.
- void processSystemIoPerfData(const android::wp<ProcStat>& procStat,
- SystemIoPerfData* systemIoPerfData) const;
-
- // Processes performance data from the `/proc/[pid]/stat` and `/proc/[pid]/task/[tid]/stat`
- // files.
- void processProcessIoPerfDataLocked(const std::unordered_set<std::string>& filterPackages,
- const android::wp<ProcPidStat>& procPidStat,
- ProcessIoPerfData* processIoPerfData);
+ // Processes system performance data from the `/proc/stats` file.
+ void processProcStatLocked(const android::sp<ProcStat>& procStat,
+ SystemSummaryStats* systemSummaryStats) const;
// Top N per-UID stats per category.
int mTopNStatsPerCategory;
@@ -196,36 +193,34 @@
// Top N per-process stats per subcategory.
int mTopNStatsPerSubcategory;
- // Local IPackageInfoResolver instance. Useful to mock in tests.
- sp<IPackageInfoResolver> mPackageInfoResolver;
-
// Makes sure only one collection is running at any given time.
- Mutex mMutex;
+ mutable Mutex mMutex;
// Info for the boot-time collection event. The cache is persisted until system shutdown/reboot.
CollectionInfo mBoottimeCollection GUARDED_BY(mMutex);
- // Info for the periodic collection event. The cache size is limited by
- // |ro.carwatchdog.periodic_collection_buffer_size|.
+ /**
+ * Info for the periodic collection event. The cache size is limited by
+ * |ro.carwatchdog.periodic_collection_buffer_size|.
+ */
CollectionInfo mPeriodicCollection GUARDED_BY(mMutex);
- // Info for the custom collection event. The info is cleared at the end of every custom
- // collection.
+ /**
+ * Info for the custom collection event. The info is cleared at the end of every custom
+ * collection.
+ */
CollectionInfo mCustomCollection GUARDED_BY(mMutex);
- // Major faults delta from last collection. Useful when calculating the percentage change in
- // major faults since last collection.
+ /**
+ * Major faults delta from last collection. Useful when calculating the percentage change in
+ * major faults since last collection.
+ */
uint64_t mLastMajorFaults GUARDED_BY(mMutex);
friend class WatchdogPerfService;
// For unit tests.
friend class internal::IoPerfCollectionPeer;
- FRIEND_TEST(IoPerfCollectionTest, TestUidIoStatsGreaterThanTopNStatsLimit);
- FRIEND_TEST(IoPerfCollectionTest, TestUidIOStatsLessThanTopNStatsLimit);
- FRIEND_TEST(IoPerfCollectionTest, TestProcessSystemIoPerfData);
- FRIEND_TEST(IoPerfCollectionTest, TestProcPidContentsGreaterThanTopNStatsLimit);
- FRIEND_TEST(IoPerfCollectionTest, TestProcPidContentsLessThanTopNStatsLimit);
};
} // namespace watchdog
diff --git a/cpp/watchdog/server/src/PackageInfoResolver.cpp b/cpp/watchdog/server/src/PackageInfoResolver.cpp
index 2b30600..84d7c67 100644
--- a/cpp/watchdog/server/src/PackageInfoResolver.cpp
+++ b/cpp/watchdog/server/src/PackageInfoResolver.cpp
@@ -179,9 +179,23 @@
if (id.name.empty()) {
continue;
}
- if (const auto it = mPackagesToAppCategories.find(id.name);
- packageInfo.uidType == UidType::APPLICATION && it != mPackagesToAppCategories.end()) {
- packageInfo.appCategoryType = it->second;
+ if (packageInfo.uidType == UidType::APPLICATION) {
+ if (const auto it = mPackagesToAppCategories.find(id.name);
+ it != mPackagesToAppCategories.end()) {
+ packageInfo.appCategoryType = it->second;
+ } else if (!packageInfo.sharedUidPackages.empty()) {
+ /* The recommendation for the OEMs is to define the application category mapping
+ * by the shared package names. However, this a fallback to catch if any mapping is
+ * defined by the individual package name.
+ */
+ for (const auto& packageName : packageInfo.sharedUidPackages) {
+ if (const auto it = mPackagesToAppCategories.find(packageName);
+ it != mPackagesToAppCategories.end()) {
+ packageInfo.appCategoryType = it->second;
+ break;
+ }
+ }
+ }
}
mUidToPackageInfoMapping[id.uid] = packageInfo;
}
diff --git a/cpp/watchdog/server/src/ProcPidStat.cpp b/cpp/watchdog/server/src/ProcPidStat.cpp
deleted file mode 100644
index 8bf9cf0..0000000
--- a/cpp/watchdog/server/src/ProcPidStat.cpp
+++ /dev/null
@@ -1,348 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#define LOG_TAG "carwatchdogd"
-
-#include "ProcPidStat.h"
-
-#include <android-base/file.h>
-#include <android-base/parseint.h>
-#include <android-base/strings.h>
-#include <dirent.h>
-#include <log/log.h>
-
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-using ::android::base::EndsWith;
-using ::android::base::Error;
-using ::android::base::ParseInt;
-using ::android::base::ParseUint;
-using ::android::base::ReadFileToString;
-using ::android::base::Result;
-using ::android::base::Split;
-using ::android::base::Trim;
-
-namespace {
-
-enum ReadError {
- ERR_INVALID_FILE = 0,
- ERR_FILE_OPEN_READ = 1,
- NUM_ERRORS = 2,
-};
-
-// /proc/PID/stat or /proc/PID/task/TID/stat format:
-// <pid> <comm> <state> <ppid> <pgrp ID> <session ID> <tty_nr> <tpgid> <flags> <minor faults>
-// <children minor faults> <major faults> <children major faults> <user mode time>
-// <system mode time> <children user mode time> <children kernel mode time> <priority> <nice value>
-// <num threads> <start time since boot> <virtual memory size> <resident set size> <rss soft limit>
-// <start code addr> <end code addr> <start stack addr> <ESP value> <EIP> <bitmap of pending sigs>
-// <bitmap of blocked sigs> <bitmap of ignored sigs> <waiting channel> <num pages swapped>
-// <cumulative pages swapped> <exit signal> <processor #> <real-time prio> <agg block I/O delays>
-// <guest time> <children guest time> <start data addr> <end data addr> <start break addr>
-// <cmd line args start addr> <amd line args end addr> <env start addr> <env end addr> <exit code>
-// Example line: 1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 2 0 0 ...etc...
-bool parsePidStatLine(const std::string& line, PidStat* pidStat) {
- std::vector<std::string> fields = Split(line, " ");
-
- // Note: Regex parsing for the below logic increased the time taken to run the
- // ProcPidStatTest#TestProcPidStatContentsFromDevice from 151.7ms to 1.3 seconds.
-
- // Comm string is enclosed with ( ) brackets and may contain space(s). Thus calculate the
- // commEndOffset based on the field that contains the closing bracket.
- size_t commEndOffset = 0;
- for (size_t i = 1; i < fields.size(); ++i) {
- pidStat->comm += fields[i];
- if (EndsWith(fields[i], ")")) {
- commEndOffset = i - 1;
- break;
- }
- pidStat->comm += " ";
- }
-
- if (pidStat->comm.front() != '(' || pidStat->comm.back() != ')') {
- ALOGW("Comm string `%s` not enclosed in brackets", pidStat->comm.c_str());
- return false;
- }
- pidStat->comm.erase(pidStat->comm.begin());
- pidStat->comm.erase(pidStat->comm.end() - 1);
-
- // The required data is in the first 22 + |commEndOffset| fields so make sure there are at least
- // these many fields in the file.
- if (fields.size() < 22 + commEndOffset || !ParseInt(fields[0], &pidStat->pid) ||
- !ParseInt(fields[3 + commEndOffset], &pidStat->ppid) ||
- !ParseUint(fields[11 + commEndOffset], &pidStat->majorFaults) ||
- !ParseUint(fields[19 + commEndOffset], &pidStat->numThreads) ||
- !ParseUint(fields[21 + commEndOffset], &pidStat->startTime)) {
- ALOGW("Invalid proc pid stat contents: \"%s\"", line.c_str());
- return false;
- }
- pidStat->state = fields[2 + commEndOffset];
- return true;
-}
-
-Result<void> readPidStatFile(const std::string& path, PidStat* pidStat) {
- std::string buffer;
- if (!ReadFileToString(path, &buffer)) {
- return Error(ERR_FILE_OPEN_READ) << "ReadFileToString failed for " << path;
- }
- std::vector<std::string> lines = Split(std::move(buffer), "\n");
- if (lines.size() != 1 && (lines.size() != 2 || !lines[1].empty())) {
- return Error(ERR_INVALID_FILE) << path << " contains " << lines.size() << " lines != 1";
- }
- if (!parsePidStatLine(std::move(lines[0]), pidStat)) {
- return Error(ERR_INVALID_FILE) << "Failed to parse the contents of " << path;
- }
- return {};
-}
-
-Result<std::unordered_map<std::string, std::string>> readKeyValueFile(
- const std::string& path, const std::string& delimiter) {
- std::string buffer;
- if (!ReadFileToString(path, &buffer)) {
- return Error(ERR_FILE_OPEN_READ) << "ReadFileToString failed for " << path;
- }
- std::unordered_map<std::string, std::string> contents;
- std::vector<std::string> lines = Split(std::move(buffer), "\n");
- for (size_t i = 0; i < lines.size(); ++i) {
- if (lines[i].empty()) {
- continue;
- }
- std::vector<std::string> elements = Split(lines[i], delimiter);
- if (elements.size() < 2) {
- return Error(ERR_INVALID_FILE)
- << "Line \"" << lines[i] << "\" doesn't contain the delimiter \"" << delimiter
- << "\" in file " << path;
- }
- std::string key = elements[0];
- std::string value = Trim(lines[i].substr(key.length() + delimiter.length()));
- if (contents.find(key) != contents.end()) {
- return Error(ERR_INVALID_FILE)
- << "Duplicate " << key << " line: \"" << lines[i] << "\" in file " << path;
- }
- contents[key] = value;
- }
- return contents;
-}
-
-// /proc/PID/status file format(*):
-// Tgid: <Thread group ID of the process>
-// Uid: <Read UID> <Effective UID> <Saved set UID> <Filesystem UID>
-// VmPeak: <Peak virtual memory size> kB
-// VmSize: <Virtual memory size> kB
-// VmHWM: <Peak resident set size> kB
-// VmRSS: <Resident set size> kB
-//
-// (*) - Included only the fields that are parsed from the file.
-Result<void> readPidStatusFile(const std::string& path, ProcessStats* processStats) {
- auto ret = readKeyValueFile(path, ":\t");
- if (!ret.ok()) {
- return Error(ret.error().code()) << ret.error();
- }
- auto contents = ret.value();
- if (contents.empty()) {
- return Error(ERR_INVALID_FILE) << "Empty file " << path;
- }
- if (contents.find("Uid") == contents.end() ||
- !ParseInt(Split(contents["Uid"], "\t")[0], &processStats->uid)) {
- return Error(ERR_INVALID_FILE) << "Failed to read 'UIDs' from file " << path;
- }
- if (contents.find("Tgid") == contents.end() ||
- !ParseInt(contents["Tgid"], &processStats->tgid)) {
- return Error(ERR_INVALID_FILE) << "Failed to read 'Tgid' from file " << path;
- }
- // Below Vm* fields may not be present for some processes so don't fail when they are missing.
- if (contents.find("VmPeak") != contents.end() &&
- !ParseUint(Split(contents["VmPeak"], " ")[0], &processStats->vmPeakKb)) {
- return Error(ERR_INVALID_FILE) << "Failed to parse 'VmPeak' from file " << path;
- }
- if (contents.find("VmSize") != contents.end() &&
- !ParseUint(Split(contents["VmSize"], " ")[0], &processStats->vmSizeKb)) {
- return Error(ERR_INVALID_FILE) << "Failed to parse 'VmSize' from file " << path;
- }
- if (contents.find("VmHWM") != contents.end() &&
- !ParseUint(Split(contents["VmHWM"], " ")[0], &processStats->vmHwmKb)) {
- return Error(ERR_INVALID_FILE) << "Failed to parse 'VmHWM' from file " << path;
- }
- if (contents.find("VmRSS") != contents.end() &&
- !ParseUint(Split(contents["VmRSS"], " ")[0], &processStats->vmRssKb)) {
- return Error(ERR_INVALID_FILE) << "Failed to parse 'VmRSS' from file " << path;
- }
- return {};
-}
-
-} // namespace
-
-Result<void> ProcPidStat::collect() {
- if (!mEnabled) {
- return Error() << "Can not access PID stat files under " << kProcDirPath;
- }
-
- Mutex::Autolock lock(mMutex);
- const auto& processStats = getProcessStatsLocked();
- if (!processStats.ok()) {
- return Error() << processStats.error();
- }
-
- mDeltaProcessStats.clear();
- for (const auto& it : *processStats) {
- const ProcessStats& curStats = it.second;
- const auto& cachedIt = mLatestProcessStats.find(it.first);
- if (cachedIt == mLatestProcessStats.end() ||
- cachedIt->second.process.startTime != curStats.process.startTime) {
- // New/reused PID so don't calculate the delta.
- mDeltaProcessStats.emplace_back(curStats);
- continue;
- }
-
- ProcessStats deltaStats = curStats;
- const ProcessStats& cachedStats = cachedIt->second;
- deltaStats.process.majorFaults -= cachedStats.process.majorFaults;
- for (auto& deltaThread : deltaStats.threads) {
- const auto& cachedThread = cachedStats.threads.find(deltaThread.first);
- if (cachedThread == cachedStats.threads.end() ||
- cachedThread->second.startTime != deltaThread.second.startTime) {
- // New TID or TID reused by the same PID so don't calculate the delta.
- continue;
- }
- deltaThread.second.majorFaults -= cachedThread->second.majorFaults;
- }
- mDeltaProcessStats.emplace_back(deltaStats);
- }
- mLatestProcessStats = *processStats;
- return {};
-}
-
-Result<std::unordered_map<pid_t, ProcessStats>> ProcPidStat::getProcessStatsLocked() const {
- std::unordered_map<pid_t, ProcessStats> processStats;
- auto procDirp = std::unique_ptr<DIR, int (*)(DIR*)>(opendir(mPath.c_str()), closedir);
- if (!procDirp) {
- return Error() << "Failed to open " << mPath << " directory";
- }
- dirent* pidDir = nullptr;
- while ((pidDir = readdir(procDirp.get())) != nullptr) {
- // 1. Read top-level pid stats.
- pid_t pid = 0;
- if (pidDir->d_type != DT_DIR || !ParseInt(pidDir->d_name, &pid)) {
- continue;
- }
- ProcessStats curStats;
- std::string path = StringPrintf((mPath + kStatFileFormat).c_str(), pid);
- auto ret = readPidStatFile(path, &curStats.process);
- if (!ret.ok()) {
- // PID may disappear between scanning the directory and parsing the stat file.
- // Thus treat ERR_FILE_OPEN_READ errors as soft errors.
- if (ret.error().code() != ERR_FILE_OPEN_READ) {
- return Error() << "Failed to read top-level per-process stat file: "
- << ret.error().message().c_str();
- }
- ALOGW("Failed to read top-level per-process stat file %s: %s", path.c_str(),
- ret.error().message().c_str());
- continue;
- }
-
- // 2. Read aggregated process status.
- path = StringPrintf((mPath + kStatusFileFormat).c_str(), curStats.process.pid);
- ret = readPidStatusFile(path, &curStats);
- if (!ret.ok()) {
- if (ret.error().code() != ERR_FILE_OPEN_READ) {
- return Error() << "Failed to read pid status for pid " << curStats.process.pid
- << ": " << ret.error().message().c_str();
- }
- ALOGW("Failed to read pid status for pid %" PRIu32 ": %s", curStats.process.pid,
- ret.error().message().c_str());
- }
-
- // 3. When failed to read tgid or uid, copy these from the previous collection.
- if (curStats.tgid == -1 || curStats.uid == -1) {
- const auto& it = mLatestProcessStats.find(curStats.process.pid);
- if (it != mLatestProcessStats.end() &&
- it->second.process.startTime == curStats.process.startTime) {
- curStats.tgid = it->second.tgid;
- curStats.uid = it->second.uid;
- }
- }
-
- if (curStats.tgid != -1 && curStats.tgid != curStats.process.pid) {
- ALOGW("Skipping non-process (i.e., Tgid != PID) entry for PID %" PRIu32,
- curStats.process.pid);
- continue;
- }
-
- // 3. Fetch per-thread stats.
- std::string taskDir = StringPrintf((mPath + kTaskDirFormat).c_str(), pid);
- auto taskDirp = std::unique_ptr<DIR, int (*)(DIR*)>(opendir(taskDir.c_str()), closedir);
- if (!taskDirp) {
- // Treat this as a soft error so at least the process stats will be collected.
- ALOGW("Failed to open %s directory", taskDir.c_str());
- }
- dirent* tidDir = nullptr;
- bool didReadMainThread = false;
- while (taskDirp != nullptr && (tidDir = readdir(taskDirp.get())) != nullptr) {
- pid_t tid = 0;
- if (tidDir->d_type != DT_DIR || !ParseInt(tidDir->d_name, &tid)) {
- continue;
- }
- if (processStats.find(tid) != processStats.end()) {
- return Error() << "Process stats already exists for TID " << tid
- << ". Stats will be double counted";
- }
-
- PidStat curThreadStat = {};
- path = StringPrintf((taskDir + kStatFileFormat).c_str(), tid);
- const auto& ret = readPidStatFile(path, &curThreadStat);
- if (!ret.ok()) {
- if (ret.error().code() != ERR_FILE_OPEN_READ) {
- return Error() << "Failed to read per-thread stat file: "
- << ret.error().message().c_str();
- }
- // Maybe the thread terminated before reading the file so skip this thread and
- // continue with scanning the next thread's stat.
- ALOGW("Failed to read per-thread stat file %s: %s", path.c_str(),
- ret.error().message().c_str());
- continue;
- }
- if (curThreadStat.pid == curStats.process.pid) {
- didReadMainThread = true;
- }
- curStats.threads[curThreadStat.pid] = curThreadStat;
- }
- if (!didReadMainThread) {
- // In the event of failure to read main-thread info (mostly because the process
- // terminated during scanning/parsing), fill out the stat that are common between main
- // thread and the process.
- curStats.threads[curStats.process.pid] = PidStat{
- .pid = curStats.process.pid,
- .comm = curStats.process.comm,
- .state = curStats.process.state,
- .ppid = curStats.process.ppid,
- .numThreads = curStats.process.numThreads,
- .startTime = curStats.process.startTime,
- };
- }
- processStats[curStats.process.pid] = curStats;
- }
- return processStats;
-}
-
-} // namespace watchdog
-} // namespace automotive
-} // namespace android
diff --git a/cpp/watchdog/server/src/ProcPidStat.h b/cpp/watchdog/server/src/ProcPidStat.h
deleted file mode 100644
index de1988a..0000000
--- a/cpp/watchdog/server/src/ProcPidStat.h
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#ifndef CPP_WATCHDOG_SERVER_SRC_PROCPIDSTAT_H_
-#define CPP_WATCHDOG_SERVER_SRC_PROCPIDSTAT_H_
-
-#include <android-base/result.h>
-#include <android-base/stringprintf.h>
-#include <gtest/gtest_prod.h>
-#include <inttypes.h>
-#include <stdint.h>
-#include <utils/Mutex.h>
-#include <utils/RefBase.h>
-
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-using ::android::base::StringPrintf;
-
-#define PID_FOR_INIT 1
-
-constexpr const char* kProcDirPath = "/proc";
-constexpr const char* kStatFileFormat = "/%" PRIu32 "/stat";
-constexpr const char* kTaskDirFormat = "/%" PRIu32 "/task";
-constexpr const char* kStatusFileFormat = "/%" PRIu32 "/status";
-
-struct PidStat {
- pid_t pid = 0;
- std::string comm = "";
- std::string state = "";
- pid_t ppid = 0;
- uint64_t majorFaults = 0;
- uint32_t numThreads = 0;
- uint64_t startTime = 0; // Useful when identifying PID/TID reuse
-};
-
-struct ProcessStats {
- int64_t tgid = -1; // -1 indicates a failure to read this value
- int64_t uid = -1; // -1 indicates a failure to read this value
- uint64_t vmPeakKb = 0;
- uint64_t vmSizeKb = 0;
- uint64_t vmHwmKb = 0;
- uint64_t vmRssKb = 0;
- PidStat process = {}; // Aggregated stats across all the threads
- std::unordered_map<pid_t, PidStat> threads; // Per-thread stat including the main thread
-};
-
-// Collector/parser for `/proc/[pid]/stat`, `/proc/[pid]/task/[tid]/stat` and /proc/[pid]/status`
-// files.
-class ProcPidStat : public RefBase {
-public:
- explicit ProcPidStat(const std::string& path = kProcDirPath) :
- mLatestProcessStats({}),
- mPath(path) {
- std::string pidStatPath = StringPrintf((mPath + kStatFileFormat).c_str(), PID_FOR_INIT);
- std::string tidStatPath = StringPrintf((mPath + kTaskDirFormat + kStatFileFormat).c_str(),
- PID_FOR_INIT, PID_FOR_INIT);
- std::string pidStatusPath = StringPrintf((mPath + kStatusFileFormat).c_str(), PID_FOR_INIT);
-
- mEnabled = !access(pidStatPath.c_str(), R_OK) && !access(tidStatPath.c_str(), R_OK) &&
- !access(pidStatusPath.c_str(), R_OK);
- }
-
- virtual ~ProcPidStat() {}
-
- // Collects per-process stats.
- virtual android::base::Result<void> collect();
-
- // Returns the latest per-process stats collected.
- virtual const std::unordered_map<pid_t, ProcessStats> latestStats() const {
- Mutex::Autolock lock(mMutex);
- return mLatestProcessStats;
- }
-
- // Returns the delta of per-process stats since the last before collection.
- virtual const std::vector<ProcessStats> deltaStats() const {
- Mutex::Autolock lock(mMutex);
- return mDeltaProcessStats;
- }
-
- // Called by WatchdogPerfService and tests.
- virtual bool enabled() { return mEnabled; }
-
- virtual std::string dirPath() { return mPath; }
-
-private:
- // Reads the contents of the below files:
- // 1. Pid stat file at |mPath| + |kStatFileFormat|
- // 2. Aggregated per-process status at |mPath| + |kStatusFileFormat|
- // 3. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
- android::base::Result<std::unordered_map<pid_t, ProcessStats>> getProcessStatsLocked() const;
-
- // Makes sure only one collection is running at any given time.
- mutable Mutex mMutex;
-
- // Latest dump of per-process stats. Useful for calculating the delta and identifying PID/TID
- // reuse.
- std::unordered_map<pid_t, ProcessStats> mLatestProcessStats GUARDED_BY(mMutex);
-
- // Latest delta of per-process stats.
- std::vector<ProcessStats> mDeltaProcessStats GUARDED_BY(mMutex);
-
- // True if the below files are accessible:
- // 1. Pid stat file at |mPath| + |kTaskStatFileFormat|
- // 2. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
- // 3. Pid status file at |mPath| + |kStatusFileFormat|
- // Otherwise, set to false.
- bool mEnabled;
-
- // Proc directory path. Default value is |kProcDirPath|.
- // Updated by tests to point to a different location when needed.
- std::string mPath;
-
- FRIEND_TEST(IoPerfCollectionTest, TestValidProcPidContents);
- FRIEND_TEST(ProcPidStatTest, TestValidStatFiles);
- FRIEND_TEST(ProcPidStatTest, TestHandlesProcessTerminationBetweenScanningAndParsing);
- FRIEND_TEST(ProcPidStatTest, TestHandlesPidTidReuse);
-};
-
-} // namespace watchdog
-} // namespace automotive
-} // namespace android
-
-#endif // CPP_WATCHDOG_SERVER_SRC_PROCPIDSTAT_H_
diff --git a/cpp/watchdog/server/src/ProcStat.cpp b/cpp/watchdog/server/src/ProcStat.cpp
index 0a78dcd..7c5337b 100644
--- a/cpp/watchdog/server/src/ProcStat.cpp
+++ b/cpp/watchdog/server/src/ProcStat.cpp
@@ -42,8 +42,9 @@
bool parseCpuStats(const std::string& data, CpuStats* cpuStats) {
std::vector<std::string> fields = Split(data, " ");
if (fields.size() == 12 && fields[1].empty()) {
- // The first cpu line will have an extra space after the first word. This will generate an
- // empty element when the line is split on " ". Erase the extra element.
+ /* The first cpu line will have an extra space after the first word. This will generate an
+ * empty element when the line is split on " ". Erase the extra element.
+ */
fields.erase(fields.begin() + 1);
}
if (fields.size() != 11 || fields[0] != "cpu" || !ParseUint(fields[1], &cpuStats->userTime) ||
@@ -115,7 +116,7 @@
if (didReadProcsRunning) {
return Error() << "Duplicate `procs_running .*` line in " << kPath;
}
- if (!parseProcsCount(std::move(lines[i]), &info.runnableProcessesCnt)) {
+ if (!parseProcsCount(std::move(lines[i]), &info.runnableProcessCount)) {
return Error() << "Failed to parse `procs_running .*` line in " << kPath;
}
didReadProcsRunning = true;
@@ -124,7 +125,7 @@
if (didReadProcsBlocked) {
return Error() << "Duplicate `procs_blocked .*` line in " << kPath;
}
- if (!parseProcsCount(std::move(lines[i]), &info.ioBlockedProcessesCnt)) {
+ if (!parseProcsCount(std::move(lines[i]), &info.ioBlockedProcessCount)) {
return Error() << "Failed to parse `procs_blocked .*` line in " << kPath;
}
didReadProcsBlocked = true;
diff --git a/cpp/watchdog/server/src/ProcStat.h b/cpp/watchdog/server/src/ProcStat.h
index 2e660a1..3bb03b4 100644
--- a/cpp/watchdog/server/src/ProcStat.h
+++ b/cpp/watchdog/server/src/ProcStat.h
@@ -18,10 +18,11 @@
#define CPP_WATCHDOG_SERVER_SRC_PROCSTAT_H_
#include <android-base/result.h>
-#include <stdint.h>
#include <utils/Mutex.h>
#include <utils/RefBase.h>
+#include <stdint.h>
+
namespace android {
namespace automotive {
namespace watchdog {
@@ -57,28 +58,31 @@
class ProcStatInfo {
public:
- ProcStatInfo() : cpuStats({}), runnableProcessesCnt(0), ioBlockedProcessesCnt(0) {}
+ ProcStatInfo() : cpuStats({}), runnableProcessCount(0), ioBlockedProcessCount(0) {}
ProcStatInfo(CpuStats stats, uint32_t runnableCnt, uint32_t ioBlockedCnt) :
- cpuStats(stats), runnableProcessesCnt(runnableCnt), ioBlockedProcessesCnt(ioBlockedCnt) {}
+ cpuStats(stats),
+ runnableProcessCount(runnableCnt),
+ ioBlockedProcessCount(ioBlockedCnt) {}
CpuStats cpuStats;
- uint32_t runnableProcessesCnt;
- uint32_t ioBlockedProcessesCnt;
+ uint32_t runnableProcessCount;
+ uint32_t ioBlockedProcessCount;
uint64_t totalCpuTime() const {
return cpuStats.userTime + cpuStats.niceTime + cpuStats.sysTime + cpuStats.idleTime +
cpuStats.ioWaitTime + cpuStats.irqTime + cpuStats.softIrqTime + cpuStats.stealTime +
cpuStats.guestTime + cpuStats.guestNiceTime;
}
- uint32_t totalProcessesCnt() const { return runnableProcessesCnt + ioBlockedProcessesCnt; }
+ uint32_t totalProcessCount() const { return runnableProcessCount + ioBlockedProcessCount; }
bool operator==(const ProcStatInfo& info) const {
return memcmp(&cpuStats, &info.cpuStats, sizeof(cpuStats)) == 0 &&
- runnableProcessesCnt == info.runnableProcessesCnt &&
- ioBlockedProcessesCnt == info.ioBlockedProcessesCnt;
+ runnableProcessCount == info.runnableProcessCount &&
+ ioBlockedProcessCount == info.ioBlockedProcessCount;
}
ProcStatInfo& operator-=(const ProcStatInfo& rhs) {
cpuStats -= rhs.cpuStats;
- // Don't diff *ProcessesCnt as they are real-time values unlike |cpuStats|, which are
- // aggregated values since system startup.
+ /* Don't diff *ProcessCount as they are real-time values unlike |cpuStats|, which are
+ * aggregated values since system startup.
+ */
return *this;
}
};
@@ -96,8 +100,9 @@
// Collects proc stat delta since the last collection.
virtual android::base::Result<void> collect();
- // Returns true when the proc stat file is accessible. Otherwise, returns false.
- // Called by WatchdogPerfService and tests.
+ /* Returns true when the proc stat file is accessible. Otherwise, returns false.
+ * Called by WatchdogPerfService and tests.
+ */
virtual bool enabled() { return kEnabled; }
virtual std::string filePath() { return kProcStatPath; }
diff --git a/cpp/watchdog/server/src/UidIoStats.cpp b/cpp/watchdog/server/src/UidIoStats.cpp
deleted file mode 100644
index e88693a..0000000
--- a/cpp/watchdog/server/src/UidIoStats.cpp
+++ /dev/null
@@ -1,157 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#define LOG_TAG "carwatchdogd"
-
-#include "UidIoStats.h"
-
-#include <android-base/file.h>
-#include <android-base/parseint.h>
-#include <android-base/stringprintf.h>
-#include <android-base/strings.h>
-#include <log/log.h>
-
-#include <inttypes.h>
-
-#include <string>
-#include <unordered_map>
-#include <utility>
-#include <vector>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-using ::android::base::Error;
-using ::android::base::ParseInt;
-using ::android::base::ParseUint;
-using ::android::base::ReadFileToString;
-using ::android::base::Result;
-using ::android::base::Split;
-using ::android::base::StringPrintf;
-
-namespace {
-
-bool parseUidIoStats(const std::string& data, UidIoUsage* usage) {
- std::vector<std::string> fields = Split(data, " ");
- if (fields.size() < 11 || !ParseUint(fields[0], &usage->uid) ||
- !ParseInt(fields[3], &usage->ios.metrics[READ_BYTES][FOREGROUND]) ||
- !ParseInt(fields[4], &usage->ios.metrics[WRITE_BYTES][FOREGROUND]) ||
- !ParseInt(fields[7], &usage->ios.metrics[READ_BYTES][BACKGROUND]) ||
- !ParseInt(fields[8], &usage->ios.metrics[WRITE_BYTES][BACKGROUND]) ||
- !ParseInt(fields[9], &usage->ios.metrics[FSYNC_COUNT][FOREGROUND]) ||
- !ParseInt(fields[10], &usage->ios.metrics[FSYNC_COUNT][BACKGROUND])) {
- ALOGW("Invalid uid I/O stats: \"%s\"", data.c_str());
- return false;
- }
- return true;
-}
-
-int64_t maybeDiff(int64_t lhs, int64_t rhs) {
- return lhs > rhs ? lhs - rhs : 0;
-}
-
-} // namespace
-
-IoUsage& IoUsage::operator-=(const IoUsage& rhs) {
- metrics[READ_BYTES][FOREGROUND] =
- maybeDiff(metrics[READ_BYTES][FOREGROUND], rhs.metrics[READ_BYTES][FOREGROUND]);
- metrics[READ_BYTES][BACKGROUND] =
- maybeDiff(metrics[READ_BYTES][BACKGROUND], rhs.metrics[READ_BYTES][BACKGROUND]);
- metrics[WRITE_BYTES][FOREGROUND] =
- maybeDiff(metrics[WRITE_BYTES][FOREGROUND], rhs.metrics[WRITE_BYTES][FOREGROUND]);
- metrics[WRITE_BYTES][BACKGROUND] =
- maybeDiff(metrics[WRITE_BYTES][BACKGROUND], rhs.metrics[WRITE_BYTES][BACKGROUND]);
- metrics[FSYNC_COUNT][FOREGROUND] =
- maybeDiff(metrics[FSYNC_COUNT][FOREGROUND], rhs.metrics[FSYNC_COUNT][FOREGROUND]);
- metrics[FSYNC_COUNT][BACKGROUND] =
- maybeDiff(metrics[FSYNC_COUNT][BACKGROUND], rhs.metrics[FSYNC_COUNT][BACKGROUND]);
- return *this;
-}
-
-bool IoUsage::isZero() const {
- for (int i = 0; i < METRIC_TYPES; i++) {
- for (int j = 0; j < UID_STATES; j++) {
- if (metrics[i][j]) {
- return false;
- }
- }
- }
- return true;
-}
-
-std::string IoUsage::toString() const {
- return StringPrintf("FgRdBytes:%" PRIi64 " BgRdBytes:%" PRIi64 " FgWrBytes:%" PRIi64
- " BgWrBytes:%" PRIi64 " FgFsync:%" PRIi64 " BgFsync:%" PRIi64,
- metrics[READ_BYTES][FOREGROUND], metrics[READ_BYTES][BACKGROUND],
- metrics[WRITE_BYTES][FOREGROUND], metrics[WRITE_BYTES][BACKGROUND],
- metrics[FSYNC_COUNT][FOREGROUND], metrics[FSYNC_COUNT][BACKGROUND]);
-}
-
-Result<void> UidIoStats::collect() {
- if (!kEnabled) {
- return Error() << "Can not access " << kPath;
- }
-
- Mutex::Autolock lock(mMutex);
- const auto& uidIoUsages = getUidIoUsagesLocked();
- if (!uidIoUsages.ok() || uidIoUsages->empty()) {
- return Error() << "Failed to get UID IO stats: " << uidIoUsages.error();
- }
-
- mDeltaUidIoUsages.clear();
- for (const auto& it : *uidIoUsages) {
- UidIoUsage curUsage = it.second;
- if (curUsage.ios.isZero()) {
- continue;
- }
- if (mLatestUidIoUsages.find(it.first) != mLatestUidIoUsages.end()) {
- if (curUsage -= mLatestUidIoUsages[it.first]; curUsage.ios.isZero()) {
- continue;
- }
- }
- mDeltaUidIoUsages[it.first] = curUsage;
- }
- mLatestUidIoUsages = *uidIoUsages;
- return {};
-}
-
-Result<std::unordered_map<uid_t, UidIoUsage>> UidIoStats::getUidIoUsagesLocked() const {
- std::string buffer;
- if (!ReadFileToString(kPath, &buffer)) {
- return Error() << "ReadFileToString failed for " << kPath;
- }
-
- std::vector<std::string> ioStats = Split(std::move(buffer), "\n");
- std::unordered_map<uid_t, UidIoUsage> uidIoUsages;
- UidIoUsage usage;
- for (size_t i = 0; i < ioStats.size(); i++) {
- if (ioStats[i].empty() || !ioStats[i].compare(0, 4, "task")) {
- // Skip per-task stats as CONFIG_UID_SYS_STATS_DEBUG is not set in the kernel and
- // the collected data is aggregated only per-UID.
- continue;
- }
- if (!parseUidIoStats(std::move(ioStats[i]), &usage)) {
- return Error() << "Failed to parse the contents of " << kPath;
- }
- uidIoUsages[usage.uid] = usage;
- }
- return uidIoUsages;
-}
-
-} // namespace watchdog
-} // namespace automotive
-} // namespace android
diff --git a/cpp/watchdog/server/src/UidIoStats.h b/cpp/watchdog/server/src/UidIoStats.h
deleted file mode 100644
index b3257f8..0000000
--- a/cpp/watchdog/server/src/UidIoStats.h
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#ifndef CPP_WATCHDOG_SERVER_SRC_UIDIOSTATS_H_
-#define CPP_WATCHDOG_SERVER_SRC_UIDIOSTATS_H_
-
-#include <android-base/result.h>
-#include <android-base/stringprintf.h>
-#include <utils/Mutex.h>
-#include <utils/RefBase.h>
-
-#include <stdint.h>
-
-#include <string>
-#include <unordered_map>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-constexpr const char* kUidIoStatsPath = "/proc/uid_io/stats";
-
-enum UidState {
- FOREGROUND = 0,
- BACKGROUND,
- UID_STATES,
-};
-
-enum MetricType {
- READ_BYTES = 0, // bytes read (from storage layer)
- WRITE_BYTES, // bytes written (to storage layer)
- FSYNC_COUNT, // number of fsync syscalls
- METRIC_TYPES,
-};
-
-class IoUsage {
-public:
- IoUsage() : metrics{{0}} {};
- IoUsage(int64_t fgRdBytes, int64_t bgRdBytes, int64_t fgWrBytes, int64_t bgWrBytes,
- int64_t fgFsync, int64_t bgFsync) {
- metrics[READ_BYTES][FOREGROUND] = fgRdBytes;
- metrics[READ_BYTES][BACKGROUND] = bgRdBytes;
- metrics[WRITE_BYTES][FOREGROUND] = fgWrBytes;
- metrics[WRITE_BYTES][BACKGROUND] = bgWrBytes;
- metrics[FSYNC_COUNT][FOREGROUND] = fgFsync;
- metrics[FSYNC_COUNT][BACKGROUND] = bgFsync;
- }
- IoUsage& operator-=(const IoUsage& rhs);
- bool operator==(const IoUsage& usage) const {
- return memcmp(&metrics, &usage.metrics, sizeof(metrics)) == 0;
- }
- int64_t sumReadBytes() const {
- const auto& [fgBytes, bgBytes] =
- std::tuple(metrics[READ_BYTES][FOREGROUND], metrics[READ_BYTES][BACKGROUND]);
- return (std::numeric_limits<int64_t>::max() - fgBytes) > bgBytes
- ? (fgBytes + bgBytes)
- : std::numeric_limits<int64_t>::max();
- }
- int64_t sumWriteBytes() const {
- const auto& [fgBytes, bgBytes] =
- std::tuple(metrics[WRITE_BYTES][FOREGROUND], metrics[WRITE_BYTES][BACKGROUND]);
- return (std::numeric_limits<int64_t>::max() - fgBytes) > bgBytes
- ? (fgBytes + bgBytes)
- : std::numeric_limits<int64_t>::max();
- }
- bool isZero() const;
- std::string toString() const;
- int64_t metrics[METRIC_TYPES][UID_STATES];
-};
-
-struct UidIoUsage {
- uid_t uid = 0; // Linux user id.
- IoUsage ios = {};
- UidIoUsage& operator-=(const UidIoUsage& rhs) {
- ios -= rhs.ios;
- return *this;
- }
- bool operator==(const UidIoUsage& rhs) const { return uid == rhs.uid && ios == rhs.ios; }
- std::string toString() const {
- return android::base::StringPrintf("Uid: %d, Usage: {%s}", uid, ios.toString().c_str());
- }
-};
-
-class UidIoStats : public RefBase {
-public:
- explicit UidIoStats(const std::string& path = kUidIoStatsPath) :
- kEnabled(!access(path.c_str(), R_OK)), kPath(path) {}
-
- virtual ~UidIoStats() {}
-
- // Collects the per-UID I/O usage.
- virtual android::base::Result<void> collect();
-
- virtual const std::unordered_map<uid_t, UidIoUsage> latestStats() const {
- Mutex::Autolock lock(mMutex);
- return mLatestUidIoUsages;
- }
-
- virtual const std::unordered_map<uid_t, UidIoUsage> deltaStats() const {
- Mutex::Autolock lock(mMutex);
- return mDeltaUidIoUsages;
- }
-
- // Returns true when the uid_io stats file is accessible. Otherwise, returns false.
- // Called by IoPerfCollection and tests.
- virtual bool enabled() { return kEnabled; }
-
- virtual std::string filePath() { return kPath; }
-
-private:
- // Reads the contents of |kPath|.
- android::base::Result<std::unordered_map<uid_t, UidIoUsage>> getUidIoUsagesLocked() const;
-
- // Makes sure only one collection is running at any given time.
- mutable Mutex mMutex;
-
- // Latest dump from the file at |kPath|.
- std::unordered_map<uid_t, UidIoUsage> mLatestUidIoUsages GUARDED_BY(mMutex);
-
- // Delta of per-UID I/O usage since last before collection.
- std::unordered_map<uid_t, UidIoUsage> mDeltaUidIoUsages GUARDED_BY(mMutex);
-
- // True if kPath is accessible.
- const bool kEnabled;
-
- // Path to uid_io stats file. Default path is |kUidIoStatsPath|.
- const std::string kPath;
-};
-
-} // namespace watchdog
-} // namespace automotive
-} // namespace android
-
-#endif // CPP_WATCHDOG_SERVER_SRC_UIDIOSTATS_H_
diff --git a/cpp/watchdog/server/src/UidIoStatsCollector.cpp b/cpp/watchdog/server/src/UidIoStatsCollector.cpp
new file mode 100644
index 0000000..9e8bc32
--- /dev/null
+++ b/cpp/watchdog/server/src/UidIoStatsCollector.cpp
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#define LOG_TAG "carwatchdogd"
+
+#include "UidIoStatsCollector.h"
+
+#include <android-base/file.h>
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <log/log.h>
+
+#include <inttypes.h>
+
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::base::Error;
+using ::android::base::ParseInt;
+using ::android::base::ParseUint;
+using ::android::base::ReadFileToString;
+using ::android::base::Result;
+using ::android::base::Split;
+using ::android::base::StringPrintf;
+
+namespace {
+
+bool parseUidIoStats(const std::string& data, UidIoStats* stats, uid_t* uid) {
+ std::vector<std::string> fields = Split(data, " ");
+ if (fields.size() < 11 || !ParseUint(fields[0], uid) ||
+ !ParseInt(fields[3], &stats->metrics[READ_BYTES][FOREGROUND]) ||
+ !ParseInt(fields[4], &stats->metrics[WRITE_BYTES][FOREGROUND]) ||
+ !ParseInt(fields[7], &stats->metrics[READ_BYTES][BACKGROUND]) ||
+ !ParseInt(fields[8], &stats->metrics[WRITE_BYTES][BACKGROUND]) ||
+ !ParseInt(fields[9], &stats->metrics[FSYNC_COUNT][FOREGROUND]) ||
+ !ParseInt(fields[10], &stats->metrics[FSYNC_COUNT][BACKGROUND])) {
+ ALOGW("Invalid uid I/O stats: \"%s\"", data.c_str());
+ return false;
+ }
+ return true;
+}
+
+} // namespace
+
+UidIoStats& UidIoStats::operator-=(const UidIoStats& rhs) {
+ const auto diff = [](int64_t lhs, int64_t rhs) -> int64_t { return lhs > rhs ? lhs - rhs : 0; };
+ metrics[READ_BYTES][FOREGROUND] =
+ diff(metrics[READ_BYTES][FOREGROUND], rhs.metrics[READ_BYTES][FOREGROUND]);
+ metrics[READ_BYTES][BACKGROUND] =
+ diff(metrics[READ_BYTES][BACKGROUND], rhs.metrics[READ_BYTES][BACKGROUND]);
+ metrics[WRITE_BYTES][FOREGROUND] =
+ diff(metrics[WRITE_BYTES][FOREGROUND], rhs.metrics[WRITE_BYTES][FOREGROUND]);
+ metrics[WRITE_BYTES][BACKGROUND] =
+ diff(metrics[WRITE_BYTES][BACKGROUND], rhs.metrics[WRITE_BYTES][BACKGROUND]);
+ metrics[FSYNC_COUNT][FOREGROUND] =
+ diff(metrics[FSYNC_COUNT][FOREGROUND], rhs.metrics[FSYNC_COUNT][FOREGROUND]);
+ metrics[FSYNC_COUNT][BACKGROUND] =
+ diff(metrics[FSYNC_COUNT][BACKGROUND], rhs.metrics[FSYNC_COUNT][BACKGROUND]);
+ return *this;
+}
+
+bool UidIoStats::isZero() const {
+ for (int i = 0; i < METRIC_TYPES; i++) {
+ for (int j = 0; j < UID_STATES; j++) {
+ if (metrics[i][j]) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+std::string UidIoStats::toString() const {
+ return StringPrintf("FgRdBytes:%" PRIi64 " BgRdBytes:%" PRIi64 " FgWrBytes:%" PRIi64
+ " BgWrBytes:%" PRIi64 " FgFsync:%" PRIi64 " BgFsync:%" PRIi64,
+ metrics[READ_BYTES][FOREGROUND], metrics[READ_BYTES][BACKGROUND],
+ metrics[WRITE_BYTES][FOREGROUND], metrics[WRITE_BYTES][BACKGROUND],
+ metrics[FSYNC_COUNT][FOREGROUND], metrics[FSYNC_COUNT][BACKGROUND]);
+}
+
+Result<void> UidIoStatsCollector::collect() {
+ if (!kEnabled) {
+ return Error() << "Can not access " << kPath;
+ }
+
+ Mutex::Autolock lock(mMutex);
+ const auto& uidIoStatsByUid = readUidIoStatsLocked();
+ if (!uidIoStatsByUid.ok() || uidIoStatsByUid->empty()) {
+ return Error() << "Failed to get UID IO stats: " << uidIoStatsByUid.error();
+ }
+
+ mDeltaStats.clear();
+ for (const auto& [uid, uidIoStats] : *uidIoStatsByUid) {
+ if (uidIoStats.isZero()) {
+ continue;
+ }
+ UidIoStats deltaStats = uidIoStats;
+ if (const auto it = mLatestStats.find(uid); it != mLatestStats.end()) {
+ if (deltaStats -= it->second; deltaStats.isZero()) {
+ continue;
+ }
+ }
+ mDeltaStats[uid] = deltaStats;
+ }
+ mLatestStats = *uidIoStatsByUid;
+ return {};
+}
+
+Result<std::unordered_map<uid_t, UidIoStats>> UidIoStatsCollector::readUidIoStatsLocked() const {
+ std::string buffer;
+ if (!ReadFileToString(kPath, &buffer)) {
+ return Error() << "ReadFileToString failed for " << kPath;
+ }
+ std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid;
+ std::vector<std::string> lines = Split(std::move(buffer), "\n");
+ for (const auto& line : lines) {
+ if (line.empty() || !line.compare(0, 4, "task")) {
+ /* Skip per-task stats as CONFIG_UID_SYS_STATS_DEBUG is not set in the kernel and
+ * the collected data is aggregated only per-UID.
+ */
+ continue;
+ }
+ uid_t uid;
+ UidIoStats uidIoStats;
+ if (!parseUidIoStats(line, &uidIoStats, &uid)) {
+ return Error() << "Failed to parse the contents of " << kPath;
+ }
+ uidIoStatsByUid[uid] = uidIoStats;
+ }
+ return uidIoStatsByUid;
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
diff --git a/cpp/watchdog/server/src/UidIoStatsCollector.h b/cpp/watchdog/server/src/UidIoStatsCollector.h
new file mode 100644
index 0000000..b5f4494
--- /dev/null
+++ b/cpp/watchdog/server/src/UidIoStatsCollector.h
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_SRC_UIDIOSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_SRC_UIDIOSTATSCOLLECTOR_H_
+
+#include <android-base/result.h>
+#include <android-base/stringprintf.h>
+#include <utils/Mutex.h>
+#include <utils/RefBase.h>
+
+#include <stdint.h>
+
+#include <string>
+#include <unordered_map>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+constexpr const char* kUidIoStatsPath = "/proc/uid_io/stats";
+
+enum UidState {
+ FOREGROUND = 0,
+ BACKGROUND,
+ UID_STATES,
+};
+
+enum MetricType {
+ READ_BYTES = 0, // bytes read (from storage layer)
+ WRITE_BYTES, // bytes written (to storage layer)
+ FSYNC_COUNT, // number of fsync syscalls
+ METRIC_TYPES,
+};
+
+// Defines the per-UID I/O stats.
+class UidIoStats {
+public:
+ UidIoStats() : metrics{{0}} {};
+ UidIoStats(int64_t fgRdBytes, int64_t bgRdBytes, int64_t fgWrBytes, int64_t bgWrBytes,
+ int64_t fgFsync, int64_t bgFsync) {
+ metrics[READ_BYTES][FOREGROUND] = fgRdBytes;
+ metrics[READ_BYTES][BACKGROUND] = bgRdBytes;
+ metrics[WRITE_BYTES][FOREGROUND] = fgWrBytes;
+ metrics[WRITE_BYTES][BACKGROUND] = bgWrBytes;
+ metrics[FSYNC_COUNT][FOREGROUND] = fgFsync;
+ metrics[FSYNC_COUNT][BACKGROUND] = bgFsync;
+ }
+ UidIoStats& operator-=(const UidIoStats& rhs);
+ bool operator==(const UidIoStats& stats) const {
+ return memcmp(&metrics, &stats.metrics, sizeof(metrics)) == 0;
+ }
+ int64_t sumReadBytes() const {
+ const auto& [fgBytes, bgBytes] =
+ std::tuple(metrics[READ_BYTES][FOREGROUND], metrics[READ_BYTES][BACKGROUND]);
+ return (std::numeric_limits<int64_t>::max() - fgBytes) > bgBytes
+ ? (fgBytes + bgBytes)
+ : std::numeric_limits<int64_t>::max();
+ }
+ int64_t sumWriteBytes() const {
+ const auto& [fgBytes, bgBytes] =
+ std::tuple(metrics[WRITE_BYTES][FOREGROUND], metrics[WRITE_BYTES][BACKGROUND]);
+ return (std::numeric_limits<int64_t>::max() - fgBytes) > bgBytes
+ ? (fgBytes + bgBytes)
+ : std::numeric_limits<int64_t>::max();
+ }
+ bool isZero() const;
+ std::string toString() const;
+ int64_t metrics[METRIC_TYPES][UID_STATES];
+};
+
+// Collector/Parser for `/proc/uid_io/stats`.
+class UidIoStatsCollectorInterface : public RefBase {
+public:
+ // Collects the per-UID I/O stats.
+ virtual android::base::Result<void> collect() = 0;
+ // Returns the latest per-uid I/O stats.
+ virtual const std::unordered_map<uid_t, UidIoStats> latestStats() const = 0;
+ // Returns the delta of per-uid I/O stats since the last before collection.
+ virtual const std::unordered_map<uid_t, UidIoStats> deltaStats() const = 0;
+ // Returns true only when the per-UID I/O stats file is accessible.
+ virtual bool enabled() const = 0;
+ // Returns the path for the per-UID I/O stats file.
+ virtual const std::string filePath() const = 0;
+};
+
+class UidIoStatsCollector final : public UidIoStatsCollectorInterface {
+public:
+ explicit UidIoStatsCollector(const std::string& path = kUidIoStatsPath) :
+ kEnabled(!access(path.c_str(), R_OK)), kPath(path) {}
+
+ ~UidIoStatsCollector() {}
+
+ android::base::Result<void> collect() override;
+
+ const std::unordered_map<uid_t, UidIoStats> latestStats() const override {
+ Mutex::Autolock lock(mMutex);
+ return mLatestStats;
+ }
+
+ const std::unordered_map<uid_t, UidIoStats> deltaStats() const override {
+ Mutex::Autolock lock(mMutex);
+ return mDeltaStats;
+ }
+
+ bool enabled() const override { return kEnabled; }
+
+ const std::string filePath() const override { return kPath; }
+
+private:
+ // Reads the contents of |kPath|.
+ android::base::Result<std::unordered_map<uid_t, UidIoStats>> readUidIoStatsLocked() const;
+
+ // Makes sure only one collection is running at any given time.
+ mutable Mutex mMutex;
+
+ // Latest dump from the file at |kPath|.
+ std::unordered_map<uid_t, UidIoStats> mLatestStats GUARDED_BY(mMutex);
+
+ // Delta of per-UID I/O stats since last before collection.
+ std::unordered_map<uid_t, UidIoStats> mDeltaStats GUARDED_BY(mMutex);
+
+ // True if kPath is accessible.
+ const bool kEnabled;
+
+ // Path to uid_io stats file. Default path is |kUidIoStatsPath|.
+ const std::string kPath;
+};
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
+
+#endif // CPP_WATCHDOG_SERVER_SRC_UIDIOSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/src/UidProcStatsCollector.cpp b/cpp/watchdog/server/src/UidProcStatsCollector.cpp
new file mode 100644
index 0000000..da3bffc
--- /dev/null
+++ b/cpp/watchdog/server/src/UidProcStatsCollector.cpp
@@ -0,0 +1,368 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#define LOG_TAG "carwatchdogd"
+#define DEBUG false // STOPSHIP if true.
+
+#include "UidProcStatsCollector.h"
+
+#include <android-base/file.h>
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <log/log.h>
+
+#include <dirent.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::base::EndsWith;
+using ::android::base::Error;
+using ::android::base::ParseInt;
+using ::android::base::ParseUint;
+using ::android::base::ReadFileToString;
+using ::android::base::Result;
+using ::android::base::Split;
+using ::android::base::StringAppendF;
+using ::android::base::Trim;
+
+namespace {
+
+enum ReadError {
+ ERR_INVALID_FILE = 0,
+ ERR_FILE_OPEN_READ = 1,
+ NUM_ERRORS = 2,
+};
+
+// Per-pid/tid stats.
+struct PidStat {
+ std::string comm = "";
+ std::string state = "";
+ uint64_t startTime = 0;
+ uint64_t majorFaults = 0;
+};
+
+/**
+ * /proc/PID/stat or /proc/PID/task/TID/stat format:
+ * <pid> <comm> <state> <ppid> <pgrp ID> <session ID> <tty_nr> <tpgid> <flags> <minor faults>
+ * <children minor faults> <major faults> <children major faults> <user mode time>
+ * <system mode time> <children user mode time> <children kernel mode time> <priority> <nice value>
+ * <num threads> <start time since boot> <virtual memory size> <resident set size> <rss soft limit>
+ * <start code addr> <end code addr> <start stack addr> <ESP value> <EIP> <bitmap of pending sigs>
+ * <bitmap of blocked sigs> <bitmap of ignored sigs> <waiting channel> <num pages swapped>
+ * <cumulative pages swapped> <exit signal> <processor #> <real-time prio> <agg block I/O delays>
+ * <guest time> <children guest time> <start data addr> <end data addr> <start break addr>
+ * <cmd line args start addr> <amd line args end addr> <env start addr> <env end addr> <exit code>
+ * Example line: 1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 2 0 0 ...etc...
+ */
+bool parsePidStatLine(const std::string& line, PidStat* pidStat) {
+ std::vector<std::string> fields = Split(line, " ");
+
+ /* Note: Regex parsing for the below logic increased the time taken to run the
+ * UidProcStatsCollectorTest#TestProcPidStatContentsFromDevice from 151.7ms to 1.3 seconds.
+ *
+ * Comm string is enclosed with ( ) brackets and may contain space(s). Thus calculate the
+ * commEndOffset based on the field that contains the closing bracket.
+ */
+ size_t commEndOffset = 0;
+ for (size_t i = 1; i < fields.size(); ++i) {
+ pidStat->comm += fields[i];
+ if (EndsWith(fields[i], ")")) {
+ commEndOffset = i - 1;
+ break;
+ }
+ pidStat->comm += " ";
+ }
+
+ if (pidStat->comm.front() != '(' || pidStat->comm.back() != ')') {
+ ALOGD("Comm string `%s` not enclosed in brackets", pidStat->comm.c_str());
+ return false;
+ }
+ pidStat->comm.erase(pidStat->comm.begin());
+ pidStat->comm.erase(pidStat->comm.end() - 1);
+
+ if (fields.size() < 22 + commEndOffset ||
+ !ParseUint(fields[11 + commEndOffset], &pidStat->majorFaults) ||
+ !ParseUint(fields[21 + commEndOffset], &pidStat->startTime)) {
+ ALOGD("Invalid proc pid stat contents: \"%s\"", line.c_str());
+ return false;
+ }
+ pidStat->state = fields[2 + commEndOffset];
+ return true;
+}
+
+Result<void> readPidStatFile(const std::string& path, PidStat* pidStat) {
+ std::string buffer;
+ if (!ReadFileToString(path, &buffer)) {
+ return Error(ERR_FILE_OPEN_READ) << "ReadFileToString failed for " << path;
+ }
+ std::vector<std::string> lines = Split(std::move(buffer), "\n");
+ if (lines.size() != 1 && (lines.size() != 2 || !lines[1].empty())) {
+ return Error(ERR_INVALID_FILE) << path << " contains " << lines.size() << " lines != 1";
+ }
+ if (!parsePidStatLine(std::move(lines[0]), pidStat)) {
+ return Error(ERR_INVALID_FILE) << "Failed to parse the contents of " << path;
+ }
+ return {};
+}
+
+Result<std::unordered_map<std::string, std::string>> readKeyValueFile(
+ const std::string& path, const std::string& delimiter) {
+ std::string buffer;
+ if (!ReadFileToString(path, &buffer)) {
+ return Error(ERR_FILE_OPEN_READ) << "ReadFileToString failed for " << path;
+ }
+ std::unordered_map<std::string, std::string> contents;
+ std::vector<std::string> lines = Split(std::move(buffer), "\n");
+ for (size_t i = 0; i < lines.size(); ++i) {
+ if (lines[i].empty()) {
+ continue;
+ }
+ std::vector<std::string> elements = Split(lines[i], delimiter);
+ if (elements.size() < 2) {
+ return Error(ERR_INVALID_FILE)
+ << "Line \"" << lines[i] << "\" doesn't contain the delimiter \"" << delimiter
+ << "\" in file " << path;
+ }
+ std::string key = elements[0];
+ std::string value = Trim(lines[i].substr(key.length() + delimiter.length()));
+ if (contents.find(key) != contents.end()) {
+ return Error(ERR_INVALID_FILE)
+ << "Duplicate " << key << " line: \"" << lines[i] << "\" in file " << path;
+ }
+ contents[key] = value;
+ }
+ return contents;
+}
+
+/**
+ * /proc/PID/status file format:
+ * Tgid: <Thread group ID of the process>
+ * Uid: <Read UID> <Effective UID> <Saved set UID> <Filesystem UID>
+ *
+ * Note: Included only the fields that are parsed from the file.
+ */
+Result<std::tuple<uid_t, pid_t>> readPidStatusFile(const std::string& path) {
+ auto result = readKeyValueFile(path, ":\t");
+ if (!result.ok()) {
+ return Error(result.error().code()) << result.error();
+ }
+ auto contents = result.value();
+ if (contents.empty()) {
+ return Error(ERR_INVALID_FILE) << "Empty file " << path;
+ }
+ int64_t uid = 0;
+ int64_t tgid = 0;
+ if (contents.find("Uid") == contents.end() ||
+ !ParseInt(Split(contents["Uid"], "\t")[0], &uid)) {
+ return Error(ERR_INVALID_FILE) << "Failed to read 'UID' from file " << path;
+ }
+ if (contents.find("Tgid") == contents.end() || !ParseInt(contents["Tgid"], &tgid)) {
+ return Error(ERR_INVALID_FILE) << "Failed to read 'Tgid' from file " << path;
+ }
+ return std::make_tuple(uid, tgid);
+}
+
+} // namespace
+
+std::string ProcessStats::toString() const {
+ return StringPrintf("{comm: %s, startTime: %" PRIu64 ", totalMajorFaults: %" PRIu64
+ ", totalTasksCount: %d, ioBlockedTasksCount: %d}",
+ comm.c_str(), startTime, totalMajorFaults, totalTasksCount,
+ ioBlockedTasksCount);
+}
+
+std::string UidProcStats::toString() const {
+ std::string buffer;
+ StringAppendF(&buffer,
+ "UidProcStats{totalMajorFaults: %" PRIu64 ", totalTasksCount: %d,"
+ "ioBlockedTasksCount: %d, processStatsByPid: {",
+ totalMajorFaults, totalTasksCount, ioBlockedTasksCount);
+ for (const auto& [pid, processStats] : processStatsByPid) {
+ StringAppendF(&buffer, "{pid: %" PRIi32 ", processStats: %s},", pid,
+ processStats.toString().c_str());
+ }
+ StringAppendF(&buffer, "}");
+ return buffer;
+}
+
+Result<void> UidProcStatsCollector::collect() {
+ if (!mEnabled) {
+ return Error() << "Can not access PID stat files under " << kProcDirPath;
+ }
+
+ Mutex::Autolock lock(mMutex);
+ auto uidProcStatsByUid = readUidProcStatsLocked();
+ if (!uidProcStatsByUid.ok()) {
+ return Error() << uidProcStatsByUid.error();
+ }
+
+ mDeltaStats.clear();
+ for (const auto& [uid, currStats] : *uidProcStatsByUid) {
+ if (const auto& it = mLatestStats.find(uid); it == mLatestStats.end()) {
+ mDeltaStats[uid] = currStats;
+ continue;
+ }
+ const auto& prevStats = mLatestStats[uid];
+ UidProcStats deltaStats = {
+ .totalTasksCount = currStats.totalTasksCount,
+ .ioBlockedTasksCount = currStats.ioBlockedTasksCount,
+ };
+ for (const auto& [pid, processStats] : currStats.processStatsByPid) {
+ ProcessStats deltaProcessStats = processStats;
+ if (const auto& it = prevStats.processStatsByPid.find(pid);
+ it != prevStats.processStatsByPid.end() &&
+ it->second.startTime == processStats.startTime &&
+ it->second.totalMajorFaults <= deltaProcessStats.totalMajorFaults) {
+ deltaProcessStats.totalMajorFaults =
+ deltaProcessStats.totalMajorFaults - it->second.totalMajorFaults;
+ }
+ deltaStats.totalMajorFaults += deltaProcessStats.totalMajorFaults;
+ deltaStats.processStatsByPid[pid] = deltaProcessStats;
+ }
+ mDeltaStats[uid] = std::move(deltaStats);
+ }
+ mLatestStats = std::move(*uidProcStatsByUid);
+ return {};
+}
+
+Result<std::unordered_map<uid_t, UidProcStats>> UidProcStatsCollector::readUidProcStatsLocked()
+ const {
+ std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid;
+ auto procDirp = std::unique_ptr<DIR, int (*)(DIR*)>(opendir(mPath.c_str()), closedir);
+ if (!procDirp) {
+ return Error() << "Failed to open " << mPath << " directory";
+ }
+ for (dirent* pidDir = nullptr; (pidDir = readdir(procDirp.get())) != nullptr;) {
+ pid_t pid = 0;
+ if (pidDir->d_type != DT_DIR || !ParseInt(pidDir->d_name, &pid)) {
+ continue;
+ }
+ auto result = readProcessStatsLocked(pid);
+ if (!result.ok()) {
+ if (result.error().code() != ERR_FILE_OPEN_READ) {
+ return Error() << result.error();
+ }
+ /* |ERR_FILE_OPEN_READ| is a soft-error because PID may disappear between scanning and
+ * reading directory/files.
+ */
+ if (DEBUG) {
+ ALOGD("%s", result.error().message().c_str());
+ }
+ continue;
+ }
+ uid_t uid = std::get<0>(*result);
+ ProcessStats processStats = std::get<ProcessStats>(*result);
+ if (uidProcStatsByUid.find(uid) == uidProcStatsByUid.end()) {
+ uidProcStatsByUid[uid] = {};
+ }
+ UidProcStats* uidProcStats = &uidProcStatsByUid[uid];
+ uidProcStats->totalMajorFaults += processStats.totalMajorFaults;
+ uidProcStats->totalTasksCount += processStats.totalTasksCount;
+ uidProcStats->ioBlockedTasksCount += processStats.ioBlockedTasksCount;
+ uidProcStats->processStatsByPid[pid] = std::move(processStats);
+ }
+ return uidProcStatsByUid;
+}
+
+Result<std::tuple<uid_t, ProcessStats>> UidProcStatsCollector::readProcessStatsLocked(
+ pid_t pid) const {
+ // 1. Read top-level pid stats.
+ PidStat pidStat = {};
+ std::string path = StringPrintf((mPath + kStatFileFormat).c_str(), pid);
+ if (auto result = readPidStatFile(path, &pidStat); !result.ok()) {
+ return Error(result.error().code())
+ << "Failed to read top-level per-process stat file '%s': %s"
+ << result.error().message().c_str();
+ }
+
+ // 2. Read aggregated process status.
+ pid_t tgid = -1;
+ uid_t uid = -1;
+ path = StringPrintf((mPath + kStatusFileFormat).c_str(), pid);
+ if (auto result = readPidStatusFile(path); !result.ok()) {
+ if (result.error().code() != ERR_FILE_OPEN_READ) {
+ return Error() << "Failed to read pid status for pid " << pid << ": "
+ << result.error().message().c_str();
+ }
+ for (const auto& [curUid, uidProcStats] : mLatestStats) {
+ if (const auto it = uidProcStats.processStatsByPid.find(pid);
+ it != uidProcStats.processStatsByPid.end() &&
+ it->second.startTime == pidStat.startTime) {
+ tgid = pid;
+ uid = curUid;
+ break;
+ }
+ }
+ } else {
+ uid = std::get<0>(*result);
+ tgid = std::get<1>(*result);
+ }
+
+ if (uid == -1 || tgid != pid) {
+ return Error(ERR_FILE_OPEN_READ)
+ << "Skipping PID '" << pid << "' because either Tgid != PID or invalid UID";
+ }
+
+ ProcessStats processStats = {
+ .comm = std::move(pidStat.comm),
+ .startTime = pidStat.startTime,
+ .totalTasksCount = 1,
+ /* Top-level process stats has the aggregated major page faults count and this should be
+ * persistent across thread creation/termination. Thus use the value from this field.
+ */
+ .totalMajorFaults = pidStat.majorFaults,
+ .ioBlockedTasksCount = pidStat.state == "D" ? 1 : 0,
+ };
+
+ // 3. Read per-thread stats.
+ std::string taskDir = StringPrintf((mPath + kTaskDirFormat).c_str(), pid);
+ bool didReadMainThread = false;
+ auto taskDirp = std::unique_ptr<DIR, int (*)(DIR*)>(opendir(taskDir.c_str()), closedir);
+ for (dirent* tidDir = nullptr;
+ taskDirp != nullptr && (tidDir = readdir(taskDirp.get())) != nullptr;) {
+ pid_t tid = 0;
+ if (tidDir->d_type != DT_DIR || !ParseInt(tidDir->d_name, &tid) || tid == pid) {
+ continue;
+ }
+
+ PidStat tidStat = {};
+ path = StringPrintf((taskDir + kStatFileFormat).c_str(), tid);
+ if (const auto& result = readPidStatFile(path, &tidStat); !result.ok()) {
+ if (result.error().code() != ERR_FILE_OPEN_READ) {
+ return Error() << "Failed to read per-thread stat file: "
+ << result.error().message().c_str();
+ }
+ /* Maybe the thread terminated before reading the file so skip this thread and
+ * continue with scanning the next thread's stat.
+ */
+ continue;
+ }
+ processStats.ioBlockedTasksCount += tidStat.state == "D" ? 1 : 0;
+ processStats.totalTasksCount += 1;
+ }
+ return std::make_tuple(uid, processStats);
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
diff --git a/cpp/watchdog/server/src/UidProcStatsCollector.h b/cpp/watchdog/server/src/UidProcStatsCollector.h
new file mode 100644
index 0000000..d0ec3c0
--- /dev/null
+++ b/cpp/watchdog/server/src/UidProcStatsCollector.h
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_SRC_UIDPROCSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_SRC_UIDPROCSTATSCOLLECTOR_H_
+
+#include <android-base/result.h>
+#include <android-base/stringprintf.h>
+#include <gtest/gtest_prod.h>
+#include <utils/Mutex.h>
+#include <utils/RefBase.h>
+
+#include <inttypes.h>
+#include <stdint.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::base::StringPrintf;
+
+#define PID_FOR_INIT 1
+
+constexpr const char kProcDirPath[] = "/proc";
+constexpr const char kStatFileFormat[] = "/%" PRIu32 "/stat";
+constexpr const char kTaskDirFormat[] = "/%" PRIu32 "/task";
+constexpr const char kStatusFileFormat[] = "/%" PRIu32 "/status";
+
+// Per-process stats.
+struct ProcessStats {
+ std::string comm = "";
+ uint64_t startTime = 0; // Useful when identifying PID reuse
+ uint64_t totalMajorFaults = 0;
+ int totalTasksCount = 0;
+ int ioBlockedTasksCount = 0;
+ std::string toString() const;
+};
+
+// Per-UID stats.
+struct UidProcStats {
+ uint64_t totalMajorFaults = 0;
+ int totalTasksCount = 0;
+ int ioBlockedTasksCount = 0;
+ std::unordered_map<pid_t, ProcessStats> processStatsByPid = {};
+ std::string toString() const;
+};
+
+/**
+ * Collector/parser for `/proc/[pid]/stat`, `/proc/[pid]/task/[tid]/stat` and /proc/[pid]/status`
+ * files.
+ */
+class UidProcStatsCollectorInterface : public RefBase {
+public:
+ // Collects the per-uid stats from /proc directory.
+ virtual android::base::Result<void> collect() = 0;
+ // Returns the latest per-uid process stats.
+ virtual const std::unordered_map<uid_t, UidProcStats> latestStats() const = 0;
+ // Returns the delta of per-uid process stats since the last before collection.
+ virtual const std::unordered_map<uid_t, UidProcStats> deltaStats() const = 0;
+ // Returns true only when the /proc files for the init process are accessible.
+ virtual bool enabled() const = 0;
+ // Returns the /proc files common ancestor directory path.
+ virtual const std::string dirPath() const = 0;
+};
+
+class UidProcStatsCollector final : public UidProcStatsCollectorInterface {
+public:
+ explicit UidProcStatsCollector(const std::string& path = kProcDirPath) :
+ mLatestStats({}),
+ mPath(path) {
+ std::string pidStatPath = StringPrintf((mPath + kStatFileFormat).c_str(), PID_FOR_INIT);
+ std::string tidStatPath = StringPrintf((mPath + kTaskDirFormat + kStatFileFormat).c_str(),
+ PID_FOR_INIT, PID_FOR_INIT);
+ std::string pidStatusPath = StringPrintf((mPath + kStatusFileFormat).c_str(), PID_FOR_INIT);
+
+ mEnabled = !access(pidStatPath.c_str(), R_OK) && !access(tidStatPath.c_str(), R_OK) &&
+ !access(pidStatusPath.c_str(), R_OK);
+ }
+
+ ~UidProcStatsCollector() {}
+
+ android::base::Result<void> collect() override;
+
+ const std::unordered_map<uid_t, UidProcStats> latestStats() const {
+ Mutex::Autolock lock(mMutex);
+ return mLatestStats;
+ }
+
+ const std::unordered_map<uid_t, UidProcStats> deltaStats() const {
+ Mutex::Autolock lock(mMutex);
+ return mDeltaStats;
+ }
+
+ bool enabled() const { return mEnabled; }
+
+ const std::string dirPath() const { return mPath; }
+
+private:
+ android::base::Result<std::unordered_map<uid_t, UidProcStats>> readUidProcStatsLocked() const;
+
+ /**
+ * Reads the contents of the below files:
+ * 1. Pid stat file at |mPath| + |kStatFileFormat|
+ * 2. Aggregated per-process status at |mPath| + |kStatusFileFormat|
+ * 3. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
+ */
+ android::base::Result<std::tuple<uid_t, ProcessStats>> readProcessStatsLocked(pid_t pid) const;
+
+ // Makes sure only one collection is running at any given time.
+ mutable Mutex mMutex;
+
+ // Latest dump of per-UID stats.
+ std::unordered_map<uid_t, UidProcStats> mLatestStats GUARDED_BY(mMutex);
+
+ // Latest delta of per-uid stats.
+ std::unordered_map<uid_t, UidProcStats> mDeltaStats GUARDED_BY(mMutex);
+
+ /**
+ * True if the below files are accessible:
+ * 1. Pid stat file at |mPath| + |kTaskStatFileFormat|
+ * 2. Tid stat file at |mPath| + |kTaskDirFormat| + |kStatFileFormat|
+ * 3. Pid status file at |mPath| + |kStatusFileFormat|
+ * Otherwise, set to false.
+ */
+ bool mEnabled;
+
+ /**
+ * Proc directory path. Default value is |kProcDirPath|.
+ * Updated by tests to point to a different location when needed.
+ */
+ std::string mPath;
+
+ FRIEND_TEST(IoPerfCollectionTest, TestValidProcPidContents);
+ FRIEND_TEST(UidProcStatsCollectorTest, TestValidStatFiles);
+ FRIEND_TEST(UidProcStatsCollectorTest, TestHandlesProcessTerminationBetweenScanningAndParsing);
+ FRIEND_TEST(UidProcStatsCollectorTest, TestHandlesPidTidReuse);
+};
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
+
+#endif // CPP_WATCHDOG_SERVER_SRC_UIDPROCSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/src/UidStatsCollector.cpp b/cpp/watchdog/server/src/UidStatsCollector.cpp
new file mode 100644
index 0000000..19d0fc9
--- /dev/null
+++ b/cpp/watchdog/server/src/UidStatsCollector.cpp
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#define LOG_TAG "carwatchdogd"
+
+#include "UidStatsCollector.h"
+
+#include <algorithm>
+#include <unordered_map>
+#include <unordered_set>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::sp;
+using ::android::base::Error;
+using ::android::base::Result;
+
+bool UidStats::hasPackageInfo() const {
+ return !packageInfo.packageIdentifier.name.empty();
+}
+
+uid_t UidStats::uid() const {
+ return static_cast<uid_t>(packageInfo.packageIdentifier.uid);
+}
+
+std::string UidStats::genericPackageName() const {
+ if (hasPackageInfo()) {
+ return packageInfo.packageIdentifier.name;
+ }
+ return std::to_string(packageInfo.packageIdentifier.uid);
+}
+
+Result<void> UidStatsCollector::collect() {
+ if (mUidProcStatsCollector->enabled()) {
+ if (const auto& result = mUidIoStatsCollector->collect(); !result.ok()) {
+ return Error() << "Failed to collect per-uid I/O stats: " << result.error();
+ }
+ }
+ if (mUidProcStatsCollector->enabled()) {
+ if (const auto& result = mUidProcStatsCollector->collect(); !result.ok()) {
+ return Error() << "Failed to collect per-uid process stats: " << result.error();
+ }
+ }
+ mLatestStats =
+ process(mUidIoStatsCollector->latestStats(), mUidProcStatsCollector->latestStats());
+ mDeltaStats = process(mUidIoStatsCollector->deltaStats(), mUidProcStatsCollector->deltaStats());
+ return {};
+}
+
+std::vector<UidStats> UidStatsCollector::process(
+ const std::unordered_map<uid_t, UidIoStats>& uidIoStatsByUid,
+ const std::unordered_map<uid_t, UidProcStats>& uidProcStatsByUid) const {
+ if (uidIoStatsByUid.empty() && uidProcStatsByUid.empty()) {
+ return std::vector<UidStats>();
+ }
+ std::unordered_set<uid_t> uidSet;
+ for (const auto& [uid, _] : uidIoStatsByUid) {
+ uidSet.insert(uid);
+ }
+ for (const auto& [uid, _] : uidProcStatsByUid) {
+ uidSet.insert(uid);
+ }
+ std::vector<uid_t> uids;
+ for (const auto& uid : uidSet) {
+ uids.push_back(uid);
+ }
+ const auto packageInfoByUid = mPackageInfoResolver->getPackageInfosForUids(uids);
+ std::vector<UidStats> uidStats;
+ for (const auto& uid : uids) {
+ UidStats curUidStats;
+ if (const auto it = packageInfoByUid.find(uid); it != packageInfoByUid.end()) {
+ curUidStats.packageInfo = it->second;
+ } else {
+ curUidStats.packageInfo.packageIdentifier.uid = uid;
+ }
+ if (const auto it = uidIoStatsByUid.find(uid); it != uidIoStatsByUid.end()) {
+ curUidStats.ioStats = it->second;
+ }
+ if (const auto it = uidProcStatsByUid.find(uid); it != uidProcStatsByUid.end()) {
+ curUidStats.procStats = it->second;
+ }
+ uidStats.emplace_back(std::move(curUidStats));
+ }
+ return uidStats;
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
diff --git a/cpp/watchdog/server/src/UidStatsCollector.h b/cpp/watchdog/server/src/UidStatsCollector.h
new file mode 100644
index 0000000..9a81abe
--- /dev/null
+++ b/cpp/watchdog/server/src/UidStatsCollector.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_SRC_UIDSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_SRC_UIDSTATSCOLLECTOR_H_
+
+#include "PackageInfoResolver.h"
+#include "UidIoStatsCollector.h"
+#include "UidProcStatsCollector.h"
+
+#include <android-base/result.h>
+#include <android/automotive/watchdog/internal/PackageInfo.h>
+#include <utils/Mutex.h>
+#include <utils/RefBase.h>
+#include <utils/StrongPointer.h>
+
+#include <string>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+// Forward declaration for testing use only.
+namespace internal {
+
+class UidStatsCollectorPeer;
+
+} // namespace internal
+
+struct UidStats {
+ android::automotive::watchdog::internal::PackageInfo packageInfo;
+ UidIoStats ioStats = {};
+ UidProcStats procStats = {};
+ // Returns true when package info is available.
+ bool hasPackageInfo() const;
+ // Returns package name if the |packageInfo| is available. Otherwise, returns the |uid|.
+ std::string genericPackageName() const;
+ // Returns the uid for the stats;
+ uid_t uid() const;
+};
+
+// Collector/Aggregator for per-UID I/O and proc stats.
+class UidStatsCollectorInterface : public RefBase {
+public:
+ // Collects the per-UID I/O and proc stats.
+ virtual android::base::Result<void> collect() = 0;
+ // Returns the latest per-uid I/O and proc stats.
+ virtual const std::vector<UidStats> latestStats() const = 0;
+ // Returns the delta of per-uid I/O and proc stats since the last before collection.
+ virtual const std::vector<UidStats> deltaStats() const = 0;
+ // Returns true only when the per-UID I/O or proc stats files are accessible.
+ virtual bool enabled() const = 0;
+};
+
+class UidStatsCollector final : public UidStatsCollectorInterface {
+public:
+ UidStatsCollector() :
+ mPackageInfoResolver(PackageInfoResolver::getInstance()),
+ mUidIoStatsCollector(android::sp<UidIoStatsCollector>::make()),
+ mUidProcStatsCollector(android::sp<UidProcStatsCollector>::make()) {}
+
+ android::base::Result<void> collect() override;
+
+ const std::vector<UidStats> latestStats() const override {
+ Mutex::Autolock lock(mMutex);
+ return mLatestStats;
+ }
+
+ const std::vector<UidStats> deltaStats() const override {
+ Mutex::Autolock lock(mMutex);
+ return mDeltaStats;
+ }
+
+ bool enabled() const override {
+ return mUidIoStatsCollector->enabled() || mUidProcStatsCollector->enabled();
+ }
+
+private:
+ std::vector<UidStats> process(
+ const std::unordered_map<uid_t, UidIoStats>& uidIoStatsByUid,
+ const std::unordered_map<uid_t, UidProcStats>& uidProcStatsByUid) const;
+ // Local IPackageInfoResolver instance. Useful to mock in tests.
+ sp<IPackageInfoResolver> mPackageInfoResolver;
+
+ mutable Mutex mMutex;
+
+ android::sp<UidIoStatsCollectorInterface> mUidIoStatsCollector;
+
+ android::sp<UidProcStatsCollectorInterface> mUidProcStatsCollector;
+
+ std::vector<UidStats> mLatestStats;
+
+ std::vector<UidStats> mDeltaStats;
+
+ // For unit tests.
+ friend class internal::UidStatsCollectorPeer;
+};
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
+
+#endif // CPP_WATCHDOG_SERVER_SRC_UIDSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/src/WatchdogInternalHandler.cpp b/cpp/watchdog/server/src/WatchdogInternalHandler.cpp
index 6da5fb5..be49ff9 100644
--- a/cpp/watchdog/server/src/WatchdogInternalHandler.cpp
+++ b/cpp/watchdog/server/src/WatchdogInternalHandler.cpp
@@ -268,6 +268,15 @@
return Status::ok();
}
+Status WatchdogInternalHandler::controlProcessHealthCheck(bool disable) {
+ Status status = checkSystemUser();
+ if (!status.isOk()) {
+ return status;
+ }
+ mWatchdogProcessService->setEnabled(!disable);
+ return Status::ok();
+}
+
} // namespace watchdog
} // namespace automotive
} // namespace android
diff --git a/cpp/watchdog/server/src/WatchdogInternalHandler.h b/cpp/watchdog/server/src/WatchdogInternalHandler.h
index a2b4892..e23620e 100644
--- a/cpp/watchdog/server/src/WatchdogInternalHandler.h
+++ b/cpp/watchdog/server/src/WatchdogInternalHandler.h
@@ -91,8 +91,9 @@
configs) override;
android::binder::Status actionTakenOnResourceOveruse(
const std::vector<
- android::automotive::watchdog::internal::PackageResourceOveruseAction>&
- actions);
+ android::automotive::watchdog::internal::PackageResourceOveruseAction>& actions)
+ override;
+ android::binder::Status controlProcessHealthCheck(bool disable) override;
protected:
void terminate() {
diff --git a/cpp/watchdog/server/src/WatchdogPerfService.cpp b/cpp/watchdog/server/src/WatchdogPerfService.cpp
index bf7480f..2761b34 100644
--- a/cpp/watchdog/server/src/WatchdogPerfService.cpp
+++ b/cpp/watchdog/server/src/WatchdogPerfService.cpp
@@ -370,7 +370,7 @@
<< kEndCustomCollectionFlag << " flags";
}
-Result<void> WatchdogPerfService::onDump(int fd) {
+Result<void> WatchdogPerfService::onDump(int fd) const {
Mutex::Autolock lock(mMutex);
if (mCurrCollectionEvent == EventType::TERMINATED) {
ALOGW("%s not active. Dumping cached data", kServiceName);
@@ -407,7 +407,7 @@
return {};
}
-bool WatchdogPerfService::dumpHelpText(int fd) {
+bool WatchdogPerfService::dumpHelpText(int fd) const {
return WriteStringToFd(StringPrintf(kHelpText, kServiceName, kStartCustomCollectionFlag,
kIntervalFlag,
std::chrono::duration_cast<std::chrono::seconds>(
@@ -421,12 +421,11 @@
fd);
}
-Result<void> WatchdogPerfService::dumpCollectorsStatusLocked(int fd) {
- if (!mUidIoStats->enabled() &&
- !WriteStringToFd(StringPrintf("UidIoStats collector failed to access the file %s",
- mUidIoStats->filePath().c_str()),
+Result<void> WatchdogPerfService::dumpCollectorsStatusLocked(int fd) const {
+ if (!mUidStatsCollector->enabled() &&
+ !WriteStringToFd(StringPrintf("UidStatsCollector failed to access proc and I/O files"),
fd)) {
- return Error() << "Failed to write UidIoStats collector status";
+ return Error() << "Failed to write UidStatsCollector status";
}
if (!mProcStat->enabled() &&
!WriteStringToFd(StringPrintf("ProcStat collector failed to access the file %s",
@@ -434,12 +433,6 @@
fd)) {
return Error() << "Failed to write ProcStat collector status";
}
- if (!mProcPidStat->enabled() &&
- !WriteStringToFd(StringPrintf("ProcPidStat collector failed to access the directory %s",
- mProcPidStat->dirPath().c_str()),
- fd)) {
- return Error() << "Failed to write ProcPidStat collector status";
- }
return {};
}
@@ -620,15 +613,15 @@
}
Result<void> WatchdogPerfService::collectLocked(WatchdogPerfService::EventMetadata* metadata) {
- if (!mUidIoStats->enabled() && !mProcStat->enabled() && !mProcPidStat->enabled()) {
+ if (!mUidStatsCollector->enabled() && !mProcStat->enabled()) {
return Error() << "No collectors enabled";
}
time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
- if (mUidIoStats->enabled()) {
- if (const auto result = mUidIoStats->collect(); !result.ok()) {
- return Error() << "Failed to collect per-uid I/O usage: " << result.error();
+ if (mUidStatsCollector->enabled()) {
+ if (const auto result = mUidStatsCollector->collect(); !result.ok()) {
+ return Error() << "Failed to collect per-uid proc and I/O stats: " << result.error();
}
}
@@ -638,25 +631,19 @@
}
}
- if (mProcPidStat->enabled()) {
- if (const auto result = mProcPidStat->collect(); !result.ok()) {
- return Error() << "Failed to collect process stats: " << result.error();
- }
- }
-
for (const auto& processor : mDataProcessors) {
Result<void> result;
switch (mCurrCollectionEvent) {
case EventType::BOOT_TIME_COLLECTION:
- result = processor->onBoottimeCollection(now, mUidIoStats, mProcStat, mProcPidStat);
+ result = processor->onBoottimeCollection(now, mUidStatsCollector, mProcStat);
break;
case EventType::PERIODIC_COLLECTION:
- result = processor->onPeriodicCollection(now, mSystemState, mUidIoStats, mProcStat,
- mProcPidStat);
+ result = processor->onPeriodicCollection(now, mSystemState, mUidStatsCollector,
+ mProcStat);
break;
case EventType::CUSTOM_COLLECTION:
result = processor->onCustomCollection(now, mSystemState, metadata->filterPackages,
- mUidIoStats, mProcStat, mProcPidStat);
+ mUidStatsCollector, mProcStat);
break;
default:
result = Error() << "Invalid collection event " << toString(mCurrCollectionEvent);
diff --git a/cpp/watchdog/server/src/WatchdogPerfService.h b/cpp/watchdog/server/src/WatchdogPerfService.h
index 7ff9e56..8fdf303 100644
--- a/cpp/watchdog/server/src/WatchdogPerfService.h
+++ b/cpp/watchdog/server/src/WatchdogPerfService.h
@@ -19,9 +19,8 @@
#include "LooperWrapper.h"
#include "ProcDiskStats.h"
-#include "ProcPidStat.h"
#include "ProcStat.h"
-#include "UidIoStats.h"
+#include "UidStatsCollector.h"
#include <android-base/chrono_utils.h>
#include <android-base/result.h>
@@ -64,7 +63,7 @@
GARAGE_MODE = 1,
};
-/*
+/**
* DataProcessor defines methods that must be implemented in order to process the data collected
* by |WatchdogPerfService|.
*/
@@ -73,29 +72,30 @@
IDataProcessorInterface() {}
virtual ~IDataProcessorInterface() {}
// Returns the name of the data processor.
- virtual std::string name() = 0;
+ virtual std::string name() const = 0;
// Callback to initialize the data processor.
virtual android::base::Result<void> init() = 0;
// Callback to terminate the data processor.
virtual void terminate() = 0;
// Callback to process the data collected during boot-time.
virtual android::base::Result<void> onBoottimeCollection(
- time_t time, const android::wp<UidIoStats>& uidIoStats,
- const android::wp<ProcStat>& procStat, const android::wp<ProcPidStat>& procPidStat) = 0;
+ time_t time, const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+ const android::wp<ProcStat>& procStat) = 0;
// Callback to process the data collected periodically post boot complete.
virtual android::base::Result<void> onPeriodicCollection(
- time_t time, SystemState systemState, const android::wp<UidIoStats>& uidIoStats,
- const android::wp<ProcStat>& procStat, const android::wp<ProcPidStat>& procPidStat) = 0;
- /*
+ time_t time, SystemState systemState,
+ const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+ const android::wp<ProcStat>& procStat) = 0;
+ /**
* Callback to process the data collected on custom collection and filter the results only to
* the specified |filterPackages|.
*/
virtual android::base::Result<void> onCustomCollection(
time_t time, SystemState systemState,
const std::unordered_set<std::string>& filterPackages,
- const android::wp<UidIoStats>& uidIoStats, const android::wp<ProcStat>& procStat,
- const android::wp<ProcPidStat>& procPidStat) = 0;
- /*
+ const android::wp<UidStatsCollectorInterface>& uidStatsCollector,
+ const android::wp<ProcStat>& procStat) = 0;
+ /**
* Callback to periodically monitor the collected data and trigger the given |alertHandler|
* on detecting resource overuse.
*/
@@ -103,8 +103,8 @@
time_t time, const android::wp<IProcDiskStatsInterface>& procDiskStats,
const std::function<void()>& alertHandler) = 0;
// Callback to dump the boot-time collected and periodically collected data.
- virtual android::base::Result<void> onDump(int fd) = 0;
- /*
+ virtual android::base::Result<void> onDump(int fd) const = 0;
+ /**
* Callback to dump the custom collected data. When fd == -1, clear the custom collection cache.
*/
virtual android::base::Result<void> onCustomCollectionDump(int fd) = 0;
@@ -127,20 +127,20 @@
};
enum SwitchMessage {
- /*
+ /**
* On receiving this message, collect the last boot-time record and start periodic collection
* and monitor.
*/
END_BOOTTIME_COLLECTION = EventType::LAST_EVENT + 1,
- /*
+ /**
* On receiving this message, ends custom collection, discard collected data and start periodic
* collection and monitor.
*/
END_CUSTOM_COLLECTION,
};
-/*
+/**
* WatchdogPerfServiceInterface collects performance data during boot-time and periodically post
* boot complete. It exposes APIs that the main thread and binder service can call to start a
* collection, switch the collection type, and generate collection dumps.
@@ -150,7 +150,7 @@
// Register a data processor to process the data collected by |WatchdogPerfService|.
virtual android::base::Result<void> registerDataProcessor(
android::sp<IDataProcessorInterface> processor) = 0;
- /*
+ /**
* Starts the boot-time collection in the looper handler on a new thread and returns
* immediately. Must be called only once. Otherwise, returns an error.
*/
@@ -161,7 +161,7 @@
virtual void setSystemState(SystemState systemState) = 0;
// Ends the boot-time collection by switching to periodic collection and returns immediately.
virtual android::base::Result<void> onBootFinished() = 0;
- /*
+ /**
* Depending on the arguments, it either:
* 1. Starts a custom collection.
* 2. Or ends the current custom collection and dumps the collected data.
@@ -170,9 +170,9 @@
virtual android::base::Result<void> onCustomCollection(
int fd, const Vector<android::String16>& args) = 0;
// Generates a dump from the boot-time and periodic collection events.
- virtual android::base::Result<void> onDump(int fd) = 0;
+ virtual android::base::Result<void> onDump(int fd) const = 0;
// Dumps the help text.
- virtual bool dumpHelpText(int fd) = 0;
+ virtual bool dumpHelpText(int fd) const = 0;
};
class WatchdogPerfService final : public WatchdogPerfServiceInterface {
@@ -185,9 +185,8 @@
mCustomCollection({}),
mPeriodicMonitor({}),
mCurrCollectionEvent(EventType::INIT),
- mUidIoStats(android::sp<UidIoStats>::make()),
+ mUidStatsCollector(android::sp<UidStatsCollector>::make()),
mProcStat(android::sp<ProcStat>::make()),
- mProcPidStat(android::sp<ProcPidStat>::make()),
mProcDiskStats(android::sp<ProcDiskStats>::make()),
mDataProcessors({}) {}
@@ -207,9 +206,9 @@
android::base::Result<void> onCustomCollection(int fd,
const Vector<android::String16>& args) override;
- android::base::Result<void> onDump(int fd) override;
+ android::base::Result<void> onDump(int fd) const override;
- bool dumpHelpText(int fd) override;
+ bool dumpHelpText(int fd) const override;
private:
struct EventMetadata {
@@ -226,9 +225,9 @@
};
// Dumps the collectors' status when they are disabled.
- android::base::Result<void> dumpCollectorsStatusLocked(int fd);
+ android::base::Result<void> dumpCollectorsStatusLocked(int fd) const;
- /*
+ /**
* Starts a custom collection on the looper handler, temporarily stops the periodic collection
* (won't discard the collected data), and returns immediately. Returns any error observed
* during this process.
@@ -243,7 +242,7 @@
std::chrono::nanoseconds interval, std::chrono::nanoseconds maxDuration,
const std::unordered_set<std::string>& filterPackages);
- /*
+ /**
* Ends the current custom collection, generates a dump, sends a looper message to start the
* periodic collection, and returns immediately. Returns an error when there is no custom
* collection running or when a dump couldn't be generated from the custom collection.
@@ -262,7 +261,7 @@
// Processes the monitor events received by |handleMessage|.
android::base::Result<void> processMonitorEvent(EventMetadata* metadata);
- /*
+ /**
* Returns the metadata for the current collection based on |mCurrCollectionEvent|. Returns
* nullptr on invalid collection event.
*/
@@ -272,7 +271,7 @@
std::thread mCollectionThread;
// Makes sure only one collection is running at any given time.
- Mutex mMutex;
+ mutable Mutex mMutex;
// Handler lopper to execute different collection events on the collection thread.
android::sp<LooperWrapper> mHandlerLooper GUARDED_BY(mMutex);
@@ -295,21 +294,18 @@
// Info for the |EventType::PERIODIC| monitor event.
EventMetadata mPeriodicMonitor GUARDED_BY(mMutex);
- /*
+ /**
* Tracks either the WatchdogPerfService's state or current collection event. Updated on
* |start|, |onBootComplete|, |startCustomCollection|, |endCustomCollection|, and |terminate|.
*/
EventType mCurrCollectionEvent GUARDED_BY(mMutex);
- // Collector/parser for `/proc/uid_io/stats`.
- android::sp<UidIoStats> mUidIoStats GUARDED_BY(mMutex);
+ // Collector for UID process and I/O stats.
+ android::sp<UidStatsCollectorInterface> mUidStatsCollector GUARDED_BY(mMutex);
// Collector/parser for `/proc/stat`.
android::sp<ProcStat> mProcStat GUARDED_BY(mMutex);
- // Collector/parser for `/proc/PID/*` stat files.
- android::sp<ProcPidStat> mProcPidStat GUARDED_BY(mMutex);
-
// Collector/parser for `/proc/diskstats` file.
android::sp<IProcDiskStatsInterface> mProcDiskStats GUARDED_BY(mMutex);
diff --git a/cpp/watchdog/server/src/WatchdogProcessService.cpp b/cpp/watchdog/server/src/WatchdogProcessService.cpp
index 97a4047..891c6e2 100644
--- a/cpp/watchdog/server/src/WatchdogProcessService.cpp
+++ b/cpp/watchdog/server/src/WatchdogProcessService.cpp
@@ -21,7 +21,6 @@
#include "WatchdogServiceHelper.h"
-#include <android-base/chrono_utils.h>
#include <android-base/file.h>
#include <android-base/macros.h>
#include <android-base/properties.h>
@@ -50,6 +49,7 @@
using ::android::sp;
using ::android::String16;
using ::android::base::Error;
+using ::android::base::GetIntProperty;
using ::android::base::GetProperty;
using ::android::base::ReadFileToString;
using ::android::base::Result;
@@ -83,11 +83,14 @@
const int32_t MSG_VHAL_WATCHDOG_ALIVE = static_cast<int>(TimeoutLength::TIMEOUT_NORMAL) + 1;
const int32_t MSG_VHAL_HEALTH_CHECK = MSG_VHAL_WATCHDOG_ALIVE + 1;
-// VHAL sends heart beat every 3s. Car watchdog checks if there is the latest heart beat from VHAL
-// with 1s marginal time.
-constexpr std::chrono::nanoseconds kVhalHealthCheckDelayNs = 4s;
-constexpr int64_t kVhalHeartBeatIntervalMs = 3000;
+// VHAL is supposed to send heart beat every 3s. Car watchdog checks if there is the latest heart
+// beat from VHAL within 3s, allowing 1s marginal time.
+// If {@code ro.carwatchdog.vhal_healthcheck.interval} is set, car watchdog checks VHAL health at
+// the given interval. The lower bound of the interval is 3s.
+constexpr int32_t kDefaultVhalCheckIntervalSec = 3;
+constexpr std::chrono::milliseconds kHealthCheckDelayMs = 1s;
+constexpr const char kPropertyVhalCheckInterval[] = "ro.carwatchdog.vhal_healthcheck.interval";
constexpr const char kServiceName[] = "WatchdogProcessService";
constexpr const char kVhalInterfaceName[] = "android.hardware.automotive.vehicle@2.0::IVehicle";
@@ -139,6 +142,10 @@
mClients.insert(std::make_pair(timeout, std::vector<ClientInfo>()));
mPingedClients.insert(std::make_pair(timeout, PingedClientMap()));
}
+ int32_t vhalHealthCheckIntervalSec =
+ GetIntProperty(kPropertyVhalCheckInterval, kDefaultVhalCheckIntervalSec);
+ vhalHealthCheckIntervalSec = std::max(vhalHealthCheckIntervalSec, kDefaultVhalCheckIntervalSec);
+ mVhalHealthCheckWindowMs = std::chrono::seconds(vhalHealthCheckIntervalSec);
}
Result<void> WatchdogProcessService::registerWatchdogServiceHelper(
const sp<IWatchdogServiceHelper>& helper) {
@@ -338,6 +345,9 @@
}
}
WriteStringToFd(StringPrintf("%sStopped users: %s\n", indent, buffer.c_str()), fd);
+ WriteStringToFd(StringPrintf("%sVHAL health check interval: %lldms\n", indent,
+ mVhalHealthCheckWindowMs.count()),
+ fd);
return {};
}
@@ -763,7 +773,8 @@
ALOGW("Failed to subscribe to VHAL_HEARTBEAT. Checking VHAL health is disabled.");
return;
}
- mHandlerLooper->sendMessageDelayed(kVhalHealthCheckDelayNs.count(), mMessageHandler,
+ std::chrono::nanoseconds intervalNs = mVhalHealthCheckWindowMs + kHealthCheckDelayMs;
+ mHandlerLooper->sendMessageDelayed(intervalNs.count(), mMessageHandler,
Message(MSG_VHAL_HEALTH_CHECK));
}
@@ -788,7 +799,8 @@
terminateVhal();
return;
}
- mHandlerLooper->sendMessageDelayed(kVhalHealthCheckDelayNs.count(), mMessageHandler,
+ std::chrono::nanoseconds intervalNs = mVhalHealthCheckWindowMs + kHealthCheckDelayMs;
+ mHandlerLooper->sendMessageDelayed(intervalNs.count(), mMessageHandler,
Message(MSG_VHAL_HEALTH_CHECK));
}
@@ -799,7 +811,7 @@
Mutex::Autolock lock(mMutex);
lastEventTime = mVhalHeartBeat.eventTime;
}
- if (currentUptime > lastEventTime + kVhalHeartBeatIntervalMs) {
+ if (currentUptime > lastEventTime + mVhalHealthCheckWindowMs.count()) {
ALOGW("VHAL failed to update heart beat within timeout. Terminating VHAL...");
terminateVhal();
}
diff --git a/cpp/watchdog/server/src/WatchdogProcessService.h b/cpp/watchdog/server/src/WatchdogProcessService.h
index 7fab6cb..46d3ef4 100644
--- a/cpp/watchdog/server/src/WatchdogProcessService.h
+++ b/cpp/watchdog/server/src/WatchdogProcessService.h
@@ -17,6 +17,7 @@
#ifndef CPP_WATCHDOG_SERVER_SRC_WATCHDOGPROCESSSERVICE_H_
#define CPP_WATCHDOG_SERVER_SRC_WATCHDOGPROCESSSERVICE_H_
+#include <android-base/chrono_utils.h>
#include <android-base/result.h>
#include <android/automotive/watchdog/ICarWatchdogClient.h>
#include <android/automotive/watchdog/internal/ICarWatchdogMonitor.h>
@@ -244,6 +245,7 @@
mNotSupportedVhalProperties;
android::sp<PropertyChangeListener> mPropertyChangeListener;
HeartBeat mVhalHeartBeat GUARDED_BY(mMutex);
+ std::chrono::milliseconds mVhalHealthCheckWindowMs;
android::sp<IWatchdogServiceHelper> mWatchdogServiceHelper GUARDED_BY(mMutex);
};
diff --git a/cpp/watchdog/server/src/WatchdogServiceHelper.cpp b/cpp/watchdog/server/src/WatchdogServiceHelper.cpp
index 4c1258f..702ad95 100644
--- a/cpp/watchdog/server/src/WatchdogServiceHelper.cpp
+++ b/cpp/watchdog/server/src/WatchdogServiceHelper.cpp
@@ -32,6 +32,7 @@
using aawi::ICarWatchdogServiceForSystem;
using aawi::PackageInfo;
using aawi::PackageIoOveruseStats;
+using aawi::UserPackageIoUsageStats;
using ::android::IBinder;
using ::android::sp;
using ::android::wp;
@@ -214,6 +215,17 @@
return service->resetResourceOveruseStats(packageNames);
}
+Status WatchdogServiceHelper::getTodayIoUsageStats(
+ std::vector<UserPackageIoUsageStats>* userPackageIoUsageStats) {
+ sp<ICarWatchdogServiceForSystem> service;
+ if (std::shared_lock readLock(mRWMutex); mService == nullptr) {
+ return fromExceptionCode(Status::EX_ILLEGAL_STATE, "Watchdog service is not initialized");
+ } else {
+ service = mService;
+ }
+ return service->getTodayIoUsageStats(userPackageIoUsageStats);
+}
+
} // namespace watchdog
} // namespace automotive
} // namespace android
diff --git a/cpp/watchdog/server/src/WatchdogServiceHelper.h b/cpp/watchdog/server/src/WatchdogServiceHelper.h
index 390013a..eb25c6d 100644
--- a/cpp/watchdog/server/src/WatchdogServiceHelper.h
+++ b/cpp/watchdog/server/src/WatchdogServiceHelper.h
@@ -70,6 +70,9 @@
packageIoOveruseStats) = 0;
virtual android::binder::Status resetResourceOveruseStats(
const std::vector<std::string>& packageNames) = 0;
+ virtual android::binder::Status getTodayIoUsageStats(
+ std::vector<android::automotive::watchdog::internal::UserPackageIoUsageStats>*
+ userPackageIoUsageStats) = 0;
protected:
virtual android::base::Result<void> init(
@@ -110,6 +113,9 @@
const std::vector<android::automotive::watchdog::internal::PackageIoOveruseStats>&
packageIoOveruseStats);
android::binder::Status resetResourceOveruseStats(const std::vector<std::string>& packageNames);
+ android::binder::Status getTodayIoUsageStats(
+ std::vector<android::automotive::watchdog::internal::UserPackageIoUsageStats>*
+ userPackageIoUsageStats);
protected:
android::base::Result<void> init(
diff --git a/cpp/watchdog/server/tests/IoOveruseConfigsTest.cpp b/cpp/watchdog/server/tests/IoOveruseConfigsTest.cpp
index 79b32fc..8eefeb4 100644
--- a/cpp/watchdog/server/tests/IoOveruseConfigsTest.cpp
+++ b/cpp/watchdog/server/tests/IoOveruseConfigsTest.cpp
@@ -17,6 +17,7 @@
#include "IoOveruseConfigs.h"
#include "OveruseConfigurationTestUtils.h"
#include "OveruseConfigurationXmlHelper.h"
+#include "PackageInfoTestUtils.h"
#include <android-base/strings.h>
#include <gmock/gmock.h>
@@ -73,17 +74,6 @@
return mappings;
}
-PackageInfo constructPackageInfo(
- const char* packageName, const ComponentType componentType,
- const ApplicationCategoryType appCategoryType = ApplicationCategoryType::OTHERS) {
- PackageInfo packageInfo;
- packageInfo.packageIdentifier.name = packageName;
- packageInfo.uidType = UidType::APPLICATION;
- packageInfo.componentType = componentType;
- packageInfo.appCategoryType = appCategoryType;
- return packageInfo;
-}
-
std::string toString(std::vector<ResourceOveruseConfiguration> configs) {
std::string buffer;
StringAppendF(&buffer, "[");
@@ -97,9 +87,9 @@
return buffer;
}
-std::vector<Matcher<const ResourceOveruseConfiguration>> ResourceOveruseConfigurationsMatchers(
+std::vector<Matcher<const ResourceOveruseConfiguration&>> ResourceOveruseConfigurationsMatchers(
const std::vector<ResourceOveruseConfiguration>& configs) {
- std::vector<Matcher<const ResourceOveruseConfiguration>> matchers;
+ std::vector<Matcher<const ResourceOveruseConfiguration&>> matchers;
for (const auto config : configs) {
matchers.push_back(ResourceOveruseConfigurationMatcher(config));
}
@@ -524,21 +514,21 @@
PerStateBytes defaultPerStateBytes = defaultThreshold().perStateWriteBytes;
IoOveruseConfigs ioOveruseConfigs;
- auto packageInfo = constructPackageInfo("systemPackage", ComponentType::SYSTEM);
+ auto packageInfo = constructAppPackageInfo("systemPackage", ComponentType::SYSTEM);
EXPECT_THAT(ioOveruseConfigs.fetchThreshold(packageInfo), defaultPerStateBytes)
<< "System package should have default threshold";
EXPECT_FALSE(ioOveruseConfigs.isSafeToKill(packageInfo))
<< "System package shouldn't be killed by default";
- packageInfo = constructPackageInfo("vendorPackage", ComponentType::VENDOR,
- ApplicationCategoryType::MEDIA);
+ packageInfo = constructAppPackageInfo("vendorPackage", ComponentType::VENDOR,
+ ApplicationCategoryType::MEDIA);
EXPECT_THAT(ioOveruseConfigs.fetchThreshold(packageInfo), defaultPerStateBytes)
<< "Vendor package should have default threshold";
EXPECT_FALSE(ioOveruseConfigs.isSafeToKill(packageInfo))
<< "Vendor package shouldn't be killed by default";
- packageInfo = constructPackageInfo("3pPackage", ComponentType::THIRD_PARTY,
- ApplicationCategoryType::MAPS);
+ packageInfo = constructAppPackageInfo("3pPackage", ComponentType::THIRD_PARTY,
+ ApplicationCategoryType::MAPS);
EXPECT_THAT(ioOveruseConfigs.fetchThreshold(packageInfo), defaultPerStateBytes)
<< "Third-party package should have default threshold";
EXPECT_TRUE(ioOveruseConfigs.isSafeToKill(packageInfo))
@@ -808,68 +798,112 @@
const auto ioOveruseConfigs = sampleIoOveruseConfigs();
auto actual = ioOveruseConfigs->fetchThreshold(
- constructPackageInfo("systemPackageGeneric", ComponentType::SYSTEM));
+ constructAppPackageInfo("systemPackageGeneric", ComponentType::SYSTEM));
EXPECT_THAT(actual, SYSTEM_COMPONENT_LEVEL_THRESHOLDS);
actual = ioOveruseConfigs->fetchThreshold(
- constructPackageInfo("systemPackageA", ComponentType::SYSTEM));
+ constructAppPackageInfo("systemPackageA", ComponentType::SYSTEM));
EXPECT_THAT(actual, SYSTEM_PACKAGE_A_THRESHOLDS);
- actual = ioOveruseConfigs->fetchThreshold(constructPackageInfo("systemPackageB",
- ComponentType::SYSTEM,
- ApplicationCategoryType::MEDIA));
+ actual = ioOveruseConfigs->fetchThreshold(
+ constructAppPackageInfo("systemPackageB", ComponentType::SYSTEM,
+ ApplicationCategoryType::MEDIA));
// Package specific thresholds get priority over media category thresholds.
EXPECT_THAT(actual, SYSTEM_PACKAGE_B_THRESHOLDS);
- actual = ioOveruseConfigs->fetchThreshold(constructPackageInfo("systemPackageC",
- ComponentType::SYSTEM,
- ApplicationCategoryType::MEDIA));
+ actual = ioOveruseConfigs->fetchThreshold(
+ constructAppPackageInfo("systemPackageC", ComponentType::SYSTEM,
+ ApplicationCategoryType::MEDIA));
// Media category thresholds as there is no package specific thresholds.
EXPECT_THAT(actual, MEDIA_THRESHOLDS);
}
+TEST_F(IoOveruseConfigsTest, TestFetchThresholdForSharedSystemPackages) {
+ const auto ioOveruseConfigs = sampleIoOveruseConfigs();
+ auto sampleSystemConfig = sampleUpdateSystemConfig();
+ auto& ioConfig = sampleSystemConfig.resourceSpecificConfigurations[0]
+ .get<ResourceSpecificConfiguration::ioOveruseConfiguration>();
+ ioConfig.packageSpecificThresholds.push_back(
+ toPerStateIoOveruseThreshold("shared:systemSharedPackage",
+ toPerStateBytes(100, 200, 300)));
+
+ ioOveruseConfigs->update({sampleSystemConfig});
+
+ auto actual = ioOveruseConfigs->fetchThreshold(
+ constructAppPackageInfo("shared:systemSharedPackage", ComponentType::SYSTEM));
+
+ EXPECT_THAT(actual, toPerStateBytes(100, 200, 300));
+
+ actual = ioOveruseConfigs->fetchThreshold(
+ constructAppPackageInfo("systemSharedPackage", ComponentType::SYSTEM));
+
+ EXPECT_THAT(actual, SYSTEM_COMPONENT_LEVEL_THRESHOLDS);
+}
+
TEST_F(IoOveruseConfigsTest, TestFetchThresholdForVendorPackages) {
const auto ioOveruseConfigs = sampleIoOveruseConfigs();
auto actual = ioOveruseConfigs->fetchThreshold(
- constructPackageInfo("vendorPackageGeneric", ComponentType::VENDOR));
+ constructAppPackageInfo("vendorPackageGeneric", ComponentType::VENDOR));
EXPECT_THAT(actual, VENDOR_COMPONENT_LEVEL_THRESHOLDS);
actual = ioOveruseConfigs->fetchThreshold(
- constructPackageInfo("vendorPkgB", ComponentType::VENDOR));
+ constructAppPackageInfo("vendorPkgB", ComponentType::VENDOR));
EXPECT_THAT(actual, VENDOR_PKG_B_THRESHOLDS);
- actual = ioOveruseConfigs->fetchThreshold(constructPackageInfo("vendorPackageC",
- ComponentType::VENDOR,
- ApplicationCategoryType::MAPS));
+ actual = ioOveruseConfigs->fetchThreshold(
+ constructAppPackageInfo("vendorPackageC", ComponentType::VENDOR,
+ ApplicationCategoryType::MAPS));
// Maps category thresholds as there is no package specific thresholds.
EXPECT_THAT(actual, MAPS_THRESHOLDS);
}
+TEST_F(IoOveruseConfigsTest, TestFetchThresholdForSharedVendorPackages) {
+ const auto ioOveruseConfigs = sampleIoOveruseConfigs();
+ auto sampleVendorConfig = sampleUpdateVendorConfig();
+ auto& ioConfig = sampleVendorConfig.resourceSpecificConfigurations[0]
+ .get<ResourceSpecificConfiguration::ioOveruseConfiguration>();
+ ioConfig.packageSpecificThresholds.push_back(
+ toPerStateIoOveruseThreshold("shared:vendorSharedPackage",
+ toPerStateBytes(100, 200, 300)));
+
+ ioOveruseConfigs->update({sampleVendorConfig});
+
+ auto actual = ioOveruseConfigs->fetchThreshold(
+ constructAppPackageInfo("shared:vendorSharedPackage", ComponentType::VENDOR));
+
+ EXPECT_THAT(actual, toPerStateBytes(100, 200, 300));
+
+ actual = ioOveruseConfigs->fetchThreshold(
+ constructAppPackageInfo("vendorSharedPackage", ComponentType::VENDOR));
+
+ EXPECT_THAT(actual, VENDOR_COMPONENT_LEVEL_THRESHOLDS);
+}
+
TEST_F(IoOveruseConfigsTest, TestFetchThresholdForThirdPartyPackages) {
const auto ioOveruseConfigs = sampleIoOveruseConfigs();
auto actual = ioOveruseConfigs->fetchThreshold(
- constructPackageInfo("vendorPackageGenericImpostor", ComponentType::THIRD_PARTY));
+ constructAppPackageInfo("vendorPackageGenericImpostor", ComponentType::THIRD_PARTY));
EXPECT_THAT(actual, THIRD_PARTY_COMPONENT_LEVEL_THRESHOLDS);
- actual = ioOveruseConfigs->fetchThreshold(constructPackageInfo("3pMapsPackage",
- ComponentType::THIRD_PARTY,
- ApplicationCategoryType::MAPS));
+ actual = ioOveruseConfigs->fetchThreshold(
+ constructAppPackageInfo("3pMapsPackage", ComponentType::THIRD_PARTY,
+ ApplicationCategoryType::MAPS));
EXPECT_THAT(actual, MAPS_THRESHOLDS);
- actual = ioOveruseConfigs->fetchThreshold(constructPackageInfo("3pMediaPackage",
- ComponentType::THIRD_PARTY,
- ApplicationCategoryType::MEDIA));
+ actual = ioOveruseConfigs->fetchThreshold(
+ constructAppPackageInfo("3pMediaPackage", ComponentType::THIRD_PARTY,
+ ApplicationCategoryType::MEDIA));
EXPECT_THAT(actual, MEDIA_THRESHOLDS);
}
@@ -877,29 +911,93 @@
TEST_F(IoOveruseConfigsTest, TestIsSafeToKillSystemPackages) {
const auto ioOveruseConfigs = sampleIoOveruseConfigs();
EXPECT_FALSE(ioOveruseConfigs->isSafeToKill(
- constructPackageInfo("systemPackageGeneric", ComponentType::SYSTEM)));
+ constructAppPackageInfo("systemPackageGeneric", ComponentType::SYSTEM)));
EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(
- constructPackageInfo("systemPackageA", ComponentType::SYSTEM)));
+ constructAppPackageInfo("systemPackageA", ComponentType::SYSTEM)));
+}
+
+TEST_F(IoOveruseConfigsTest, TestIsSafeToKillSharedSystemPackages) {
+ auto sampleSystemConfig = sampleUpdateSystemConfig();
+ sampleSystemConfig.safeToKillPackages.push_back("sharedUidSystemPackageC");
+ sampleSystemConfig.safeToKillPackages.push_back("shared:systemSharedPackageD");
+ sp<IoOveruseConfigs> ioOveruseConfigs = new IoOveruseConfigs();
+
+ EXPECT_RESULT_OK(ioOveruseConfigs->update({sampleSystemConfig}));
+
+ PackageInfo packageInfo =
+ constructAppPackageInfo("systemSharedPackage", ComponentType::SYSTEM,
+ ApplicationCategoryType::OTHERS,
+ {"sharedUidSystemPackageA", "sharedUidSystemPackageB",
+ "sharedUidSystemPackageC"});
+
+ EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(packageInfo))
+ << "Should be safe-to-kill when at least one package under shared UID is safe-to-kill";
+
+ packageInfo =
+ constructAppPackageInfo("shared:systemSharedPackageD", ComponentType::SYSTEM,
+ ApplicationCategoryType::OTHERS, {"sharedUidSystemPackageA"});
+ EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(packageInfo))
+ << "Should be safe-to-kill when shared package is safe-to-kill";
+
+ packageInfo =
+ constructAppPackageInfo("systemSharedPackageD", ComponentType::SYSTEM,
+ ApplicationCategoryType::OTHERS, {"sharedUidSystemPackageA"});
+ EXPECT_FALSE(ioOveruseConfigs->isSafeToKill(packageInfo))
+ << "Shouldn't be safe-to-kill when the 'shared:' prefix is missing";
}
TEST_F(IoOveruseConfigsTest, TestIsSafeToKillVendorPackages) {
const auto ioOveruseConfigs = sampleIoOveruseConfigs();
EXPECT_FALSE(ioOveruseConfigs->isSafeToKill(
- constructPackageInfo("vendorPackageGeneric", ComponentType::VENDOR)));
+ constructAppPackageInfo("vendorPackageGeneric", ComponentType::VENDOR)));
EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(
- constructPackageInfo("vendorPackageA", ComponentType::VENDOR)));
+ constructAppPackageInfo("vendorPackageA", ComponentType::VENDOR)));
+}
+
+TEST_F(IoOveruseConfigsTest, TestIsSafeToKillSharedVendorPackages) {
+ auto sampleVendorConfig = sampleUpdateVendorConfig();
+ sampleVendorConfig.safeToKillPackages.push_back("sharedUidVendorPackageC");
+ sampleVendorConfig.safeToKillPackages.push_back("shared:vendorSharedPackageD");
+
+ auto sampleSystemConfig = sampleUpdateSystemConfig();
+ sampleSystemConfig.safeToKillPackages.push_back("sharedUidSystemPackageC");
+
+ sp<IoOveruseConfigs> ioOveruseConfigs = new IoOveruseConfigs();
+
+ EXPECT_RESULT_OK(ioOveruseConfigs->update({sampleSystemConfig, sampleVendorConfig}));
+
+ PackageInfo packageInfo =
+ constructAppPackageInfo("vendorSharedPackage", ComponentType::VENDOR,
+ ApplicationCategoryType::OTHERS,
+ {"sharedUidVendorPackageA", "sharedUidVendorPackageB",
+ "sharedUidVendorPackageC"});
+
+ EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(packageInfo))
+ << "Should be safe-to-kill when at least one package under shared UID is safe-to-kill";
+
+ packageInfo =
+ constructAppPackageInfo("shared:vendorSharedPackageD", ComponentType::VENDOR,
+ ApplicationCategoryType::OTHERS, {"sharedUidVendorPackageA"});
+ EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(packageInfo))
+ << "Should be safe-to-kill when shared package is safe-to-kill";
+
+ packageInfo =
+ constructAppPackageInfo("shared:vendorSharedPackageE", ComponentType::VENDOR,
+ ApplicationCategoryType::OTHERS, {"sharedUidVendorPackageA"});
+ EXPECT_FALSE(ioOveruseConfigs->isSafeToKill(packageInfo))
+ << "Shouldn't be safe-to-kill when the 'shared:' prefix is missing";
}
TEST_F(IoOveruseConfigsTest, TestIsSafeToKillThirdPartyPackages) {
const auto ioOveruseConfigs = sampleIoOveruseConfigs();
EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(
- constructPackageInfo("vendorPackageGenericImpostor", ComponentType::THIRD_PARTY)));
+ constructAppPackageInfo("vendorPackageGenericImpostor", ComponentType::THIRD_PARTY)));
EXPECT_TRUE(ioOveruseConfigs->isSafeToKill(
- constructPackageInfo("3pMapsPackage", ComponentType::THIRD_PARTY,
- ApplicationCategoryType::MAPS)));
+ constructAppPackageInfo("3pMapsPackage", ComponentType::THIRD_PARTY,
+ ApplicationCategoryType::MAPS)));
}
TEST_F(IoOveruseConfigsTest, TestIsSafeToKillNativePackages) {
@@ -931,6 +1029,32 @@
UnorderedElementsAre("vendorPackage", "vendorPkgB"));
}
+TEST_F(IoOveruseConfigsTest, TestVendorPackagePrefixesWithSharedPackages) {
+ auto sampleVendorConfig = sampleUpdateVendorConfig();
+ sampleVendorConfig.vendorPackagePrefixes.push_back("shared:vendorSharedPackage");
+ sampleVendorConfig.safeToKillPackages.push_back("sharedUidVendorPackageD");
+ sampleVendorConfig.safeToKillPackages.push_back("shared:vendorSharedPackageE");
+ sampleVendorConfig.safeToKillPackages.push_back("shared:vndrSharedPkgF");
+
+ auto& ioConfig = sampleVendorConfig.resourceSpecificConfigurations[0]
+ .get<ResourceSpecificConfiguration::ioOveruseConfiguration>();
+
+ ioConfig.packageSpecificThresholds.push_back(
+ toPerStateIoOveruseThreshold("shared:vendorSharedPackageG",
+ VENDOR_PACKAGE_A_THRESHOLDS));
+ ioConfig.packageSpecificThresholds.push_back(
+ toPerStateIoOveruseThreshold("shared:vndrSharedPkgH", VENDOR_PACKAGE_A_THRESHOLDS));
+
+ sp<IoOveruseConfigs> ioOveruseConfigs = new IoOveruseConfigs();
+
+ EXPECT_RESULT_OK(ioOveruseConfigs->update({sampleVendorConfig}));
+
+ EXPECT_THAT(ioOveruseConfigs->vendorPackagePrefixes(),
+ UnorderedElementsAre("vendorPackage", "vendorPkgB", "shared:vendorSharedPackage",
+ "sharedUidVendorPackageD", "shared:vndrSharedPkgF",
+ "shared:vndrSharedPkgH"));
+}
+
TEST_F(IoOveruseConfigsTest, TestPackagesToAppCategoriesWithSystemConfig) {
IoOveruseConfigs ioOveruseConfigs;
const auto resourceOveruseConfig = sampleUpdateSystemConfig();
diff --git a/cpp/watchdog/server/tests/IoOveruseMonitorTest.cpp b/cpp/watchdog/server/tests/IoOveruseMonitorTest.cpp
index 7dcc2d4..c1dd4f7 100644
--- a/cpp/watchdog/server/tests/IoOveruseMonitorTest.cpp
+++ b/cpp/watchdog/server/tests/IoOveruseMonitorTest.cpp
@@ -19,14 +19,17 @@
#include "MockPackageInfoResolver.h"
#include "MockProcDiskStats.h"
#include "MockResourceOveruseListener.h"
-#include "MockUidIoStats.h"
+#include "MockUidStatsCollector.h"
#include "MockWatchdogServiceHelper.h"
+#include "PackageInfoTestUtils.h"
#include <binder/IPCThreadState.h>
#include <binder/Status.h>
#include <utils/RefBase.h>
#include <functional>
+#include <tuple>
+#include <unordered_map>
namespace android {
namespace automotive {
@@ -41,15 +44,18 @@
using ::android::automotive::watchdog::internal::PackageIoOveruseStats;
using ::android::automotive::watchdog::internal::ResourceOveruseConfiguration;
using ::android::automotive::watchdog::internal::UidType;
+using ::android::automotive::watchdog::internal::UserPackageIoUsageStats;
using ::android::base::Error;
using ::android::base::Result;
using ::android::base::StringAppendF;
using ::android::binder::Status;
using ::testing::_;
using ::testing::DoAll;
+using ::testing::Eq;
using ::testing::Return;
using ::testing::ReturnRef;
using ::testing::SaveArg;
+using ::testing::SetArgPointee;
using ::testing::UnorderedElementsAreArray;
namespace {
@@ -68,20 +74,11 @@
return threshold;
}
-PackageIdentifier constructPackageIdentifier(const char* packageName, const int32_t uid) {
- PackageIdentifier packageIdentifier;
- packageIdentifier.name = packageName;
- packageIdentifier.uid = uid;
- return packageIdentifier;
-}
-
-PackageInfo constructPackageInfo(const char* packageName, const int32_t uid,
- const UidType uidType) {
+struct PackageWrittenBytes {
PackageInfo packageInfo;
- packageInfo.packageIdentifier = constructPackageIdentifier(packageName, uid);
- packageInfo.uidType = uidType;
- return packageInfo;
-}
+ int32_t foregroundBytes;
+ int32_t backgroundBytes;
+};
PerStateBytes constructPerStateBytes(const int64_t fgBytes, const int64_t bgBytes,
const int64_t gmBytes) {
@@ -114,17 +111,32 @@
PackageIoOveruseStats constructPackageIoOveruseStats(
const int32_t uid, const bool shouldNotify, const bool isKillable,
- const PerStateBytes& remaining, const PerStateBytes& written, const int totalOveruses,
- const int64_t startTime, const int64_t durationInSeconds) {
+ const PerStateBytes& remaining, const PerStateBytes& written, const PerStateBytes& forgiven,
+ const int totalOveruses, const int64_t startTime, const int64_t durationInSeconds) {
PackageIoOveruseStats stats;
stats.uid = uid;
stats.shouldNotify = shouldNotify;
+ stats.forgivenWriteBytes = forgiven;
stats.ioOveruseStats = constructIoOveruseStats(isKillable, remaining, written, totalOveruses,
startTime, durationInSeconds);
return stats;
}
+UserPackageIoUsageStats constructUserPackageIoUsageStats(userid_t userId,
+ const std::string& packageName,
+ const PerStateBytes& writtenBytes,
+ const PerStateBytes& forgivenWriteBytes,
+ int32_t totalOveruses) {
+ UserPackageIoUsageStats stats;
+ stats.userId = userId;
+ stats.packageName = packageName;
+ stats.ioUsageStats.writtenBytes = writtenBytes;
+ stats.ioUsageStats.forgivenWriteBytes = forgivenWriteBytes;
+ stats.ioUsageStats.totalOveruses = totalOveruses;
+ return stats;
+}
+
class ScopedChangeCallingUid : public RefBase {
public:
explicit ScopedChangeCallingUid(uid_t uid) {
@@ -196,42 +208,25 @@
mMockWatchdogServiceHelper = sp<MockWatchdogServiceHelper>::make();
mMockIoOveruseConfigs = sp<MockIoOveruseConfigs>::make();
mMockPackageInfoResolver = sp<MockPackageInfoResolver>::make();
+ mMockUidStatsCollector = sp<MockUidStatsCollector>::make();
mIoOveruseMonitor = sp<IoOveruseMonitor>::make(mMockWatchdogServiceHelper);
mIoOveruseMonitorPeer = sp<internal::IoOveruseMonitorPeer>::make(mIoOveruseMonitor);
mIoOveruseMonitorPeer->init(mMockIoOveruseConfigs, mMockPackageInfoResolver);
+ setUpPackagesAndConfigurations();
}
virtual void TearDown() {
mMockWatchdogServiceHelper.clear();
mMockIoOveruseConfigs.clear();
mMockPackageInfoResolver.clear();
+ mMockUidStatsCollector.clear();
mIoOveruseMonitor.clear();
mIoOveruseMonitorPeer.clear();
}
void setUpPackagesAndConfigurations() {
- std::unordered_map<uid_t, PackageInfo> packageInfoMapping =
- {{1001000,
- constructPackageInfo(
- /*packageName=*/"system.daemon", /*uid=*/1001000, UidType::NATIVE)},
- {1112345,
- constructPackageInfo(
- /*packageName=*/"com.android.google.package", /*uid=*/1112345,
- UidType::APPLICATION)},
- {1113999,
- constructPackageInfo(
- /*packageName=*/"com.android.google.package", /*uid=*/1113999,
- UidType::APPLICATION)},
- {1212345,
- constructPackageInfo(
- /*packageName=*/"com.android.google.package", /*uid=*/1212345,
- UidType::APPLICATION)},
- {1312345,
- constructPackageInfo(
- /*packageName=*/"com.android.google.package", /*uid=*/1312345,
- UidType::APPLICATION)}};
ON_CALL(*mMockPackageInfoResolver, getPackageInfosForUids(_))
- .WillByDefault(Return(packageInfoMapping));
+ .WillByDefault(Return(kPackageInfosByUid));
mMockIoOveruseConfigs->injectPackageConfigs({
{"system.daemon",
{constructPerStateBytes(/*fgBytes=*/80'000, /*bgBytes=*/40'000,
@@ -241,9 +236,33 @@
{constructPerStateBytes(/*fgBytes=*/70'000, /*bgBytes=*/30'000,
/*gmBytes=*/100'000),
/*isSafeToKill=*/true}},
+ {"com.android.kitchensink",
+ {constructPerStateBytes(/*fgBytes=*/30'000, /*bgBytes=*/15'000,
+ /*gmBytes=*/10'000),
+ /*isSafeToKill=*/true}},
});
}
+ std::vector<UidStats> constructUidStats(
+ std::unordered_map<uid_t, std::tuple<int32_t, int32_t>> writtenBytesByUid) {
+ std::vector<UidStats> uidStats;
+ for (const auto& [uid, writtenBytes] : writtenBytesByUid) {
+ PackageInfo packageInfo;
+ if (kPackageInfosByUid.find(uid) != kPackageInfosByUid.end()) {
+ packageInfo = kPackageInfosByUid.at(uid);
+ } else {
+ packageInfo.packageIdentifier.uid = uid;
+ }
+ uidStats.push_back(UidStats{.packageInfo = packageInfo,
+ .ioStats = {/*fgRdBytes=*/989'000,
+ /*bgRdBytes=*/678'000,
+ /*fgWrBytes=*/std::get<0>(writtenBytes),
+ /*bgWrBytes=*/std::get<1>(writtenBytes),
+ /*fgFsync=*/10'000, /*bgFsync=*/50'000}});
+ }
+ return uidStats;
+ }
+
void executeAsUid(uid_t uid, std::function<void()> func) {
sp<ScopedChangeCallingUid> scopedChangeCallingUid = sp<ScopedChangeCallingUid>::make(uid);
ASSERT_NO_FATAL_FAILURE(func());
@@ -252,12 +271,40 @@
sp<MockWatchdogServiceHelper> mMockWatchdogServiceHelper;
sp<MockIoOveruseConfigs> mMockIoOveruseConfigs;
sp<MockPackageInfoResolver> mMockPackageInfoResolver;
+ sp<MockUidStatsCollector> mMockUidStatsCollector;
sp<IoOveruseMonitor> mIoOveruseMonitor;
sp<internal::IoOveruseMonitorPeer> mIoOveruseMonitorPeer;
+
+ static const std::unordered_map<uid_t, PackageInfo> kPackageInfosByUid;
};
+const std::unordered_map<uid_t, PackageInfo> IoOveruseMonitorTest::kPackageInfosByUid =
+ {{1001000,
+ constructPackageInfo(
+ /*packageName=*/"system.daemon",
+ /*uid=*/1001000, UidType::NATIVE)},
+ {1112345,
+ constructPackageInfo(
+ /*packageName=*/"com.android.google.package",
+ /*uid=*/1112345, UidType::APPLICATION)},
+ {1113999,
+ constructPackageInfo(
+ /*packageName=*/"com.android.google.package",
+ /*uid=*/1113999, UidType::APPLICATION)},
+ {1212345,
+ constructPackageInfo(
+ /*packageName=*/"com.android.google.package",
+ /*uid=*/1212345, UidType::APPLICATION)},
+ {1245678,
+ constructPackageInfo(
+ /*packageName=*/"com.android.kitchensink",
+ /*uid=*/1245678, UidType::APPLICATION)},
+ {1312345,
+ constructPackageInfo(
+ /*packageName=*/"com.android.google.package",
+ /*uid=*/1312345, UidType::APPLICATION)}};
+
TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollection) {
- setUpPackagesAndConfigurations();
sp<MockResourceOveruseListener> mockResourceOveruseListener =
sp<MockResourceOveruseListener>::make();
ASSERT_NO_FATAL_FAILURE(executeAsUid(1001000, [&]() {
@@ -268,11 +315,11 @@
* Package "system.daemon" (UID: 1001000) exceeds warn threshold percentage of 80% but no
* warning is issued as it is a native UID.
*/
- sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
- mockUidIoStats->expectDeltaStats(
- {{1001000, IoUsage(0, 0, /*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000, 0, 0)},
- {1112345, IoUsage(0, 0, /*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000, 0, 0)},
- {1212345, IoUsage(0, 0, /*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000, 0, 0)}});
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1001000, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000}},
+ {1112345, {/*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000}},
+ {1212345, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000}}})));
std::vector<PackageIoOveruseStats> actualIoOveruseStats;
EXPECT_CALL(*mMockWatchdogServiceHelper, latestIoOveruseStats(_))
@@ -282,25 +329,28 @@
const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
- mockUidIoStats, nullptr, nullptr));
+ mMockUidStatsCollector, nullptr));
std::vector<PackageIoOveruseStats> expectedIoOveruseStats =
- {constructPackageIoOveruseStats(/*uid*=*/1001000, /*shouldNotify=*/false,
+ {constructPackageIoOveruseStats(/*uid=*/1001000, /*shouldNotify=*/false,
/*isKillable=*/false, /*remaining=*/
constructPerStateBytes(10'000, 20'000, 100'000),
/*written=*/constructPerStateBytes(70'000, 20'000, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, startTime, durationInSeconds),
- constructPackageIoOveruseStats(/*uid*=*/1112345, /*shouldNotify=*/false,
+ constructPackageIoOveruseStats(/*uid=*/1112345, /*shouldNotify=*/false,
/*isKillable=*/true, /*remaining=*/
constructPerStateBytes(35'000, 15'000, 100'000),
/*written=*/constructPerStateBytes(35'000, 15'000, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, startTime, durationInSeconds),
// Exceeds threshold.
- constructPackageIoOveruseStats(/*uid*=*/1212345, /*shouldNotify=*/true,
+ constructPackageIoOveruseStats(/*uid=*/1212345, /*shouldNotify=*/true,
/*isKillable=*/true,
/*remaining=*/
constructPerStateBytes(0, 10'000, 100'000),
/*written=*/constructPerStateBytes(70'000, 20'000, 0),
+ /*forgiven=*/constructPerStateBytes(70'000, 0, 0),
/*totalOveruses=*/1, startTime, durationInSeconds)};
EXPECT_THAT(actualIoOveruseStats, UnorderedElementsAreArray(expectedIoOveruseStats))
<< "Expected: " << toString(expectedIoOveruseStats)
@@ -308,10 +358,11 @@
ResourceOveruseStats actualOverusingNativeStats;
// Package "com.android.google.package" for user 11 changed uid from 1112345 to 1113999.
- mockUidIoStats->expectDeltaStats(
- {{1001000, IoUsage(0, 0, /*fgWrBytes=*/30'000, /*bgWrBytes=*/0, 0, 0)},
- {1113999, IoUsage(0, 0, /*fgWrBytes=*/25'000, /*bgWrBytes=*/10'000, 0, 0)},
- {1212345, IoUsage(0, 0, /*fgWrBytes=*/20'000, /*bgWrBytes=*/30'000, 0, 0)}});
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1001000, {/*fgWrBytes=*/30'000, /*bgWrBytes=*/0}},
+ {1113999, {/*fgWrBytes=*/25'000, /*bgWrBytes=*/10'000}},
+ {1212345, {/*fgWrBytes=*/20'000, /*bgWrBytes=*/30'000}}})));
actualIoOveruseStats.clear();
EXPECT_CALL(*mockResourceOveruseListener, onOveruse(_))
.WillOnce(DoAll(SaveArg<0>(&actualOverusingNativeStats), Return(Status::ok())));
@@ -319,38 +370,41 @@
.WillOnce(DoAll(SaveArg<0>(&actualIoOveruseStats), Return(Status::ok())));
ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
- mockUidIoStats, nullptr, nullptr));
+ mMockUidStatsCollector, nullptr));
const auto expectedOverusingNativeStats = constructResourceOveruseStats(
constructIoOveruseStats(/*isKillable=*/false,
/*remaining=*/constructPerStateBytes(0, 20'000, 100'000),
/*written=*/constructPerStateBytes(100'000, 20'000, 0),
/*totalOveruses=*/1, startTime, durationInSeconds));
- EXPECT_THAT(actualOverusingNativeStats, expectedOverusingNativeStats)
+ EXPECT_THAT(actualOverusingNativeStats, Eq(expectedOverusingNativeStats))
<< "Expected: " << expectedOverusingNativeStats.toString()
<< "\nActual: " << actualOverusingNativeStats.toString();
expectedIoOveruseStats =
- {constructPackageIoOveruseStats(/*uid*=*/1001000, /*shouldNotify=*/true,
+ {constructPackageIoOveruseStats(/*uid=*/1001000, /*shouldNotify=*/true,
/*isKillable=*/false, /*remaining=*/
constructPerStateBytes(0, 20'000, 100'000),
/*written=*/constructPerStateBytes(100'000, 20'000, 0),
+ /*forgiven=*/constructPerStateBytes(80'000, 0, 0),
/*totalOveruses=*/1, startTime, durationInSeconds),
// Exceeds warn threshold percentage.
- constructPackageIoOveruseStats(/*uid*=*/1113999, /*shouldNotify=*/true,
+ constructPackageIoOveruseStats(/*uid=*/1113999, /*shouldNotify=*/true,
/*isKillable=*/true, /*remaining=*/
constructPerStateBytes(10'000, 5'000, 100'000),
/*written=*/constructPerStateBytes(60'000, 25'000, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, startTime, durationInSeconds),
/*
* Exceeds threshold.
* The package was forgiven on previous overuse so the remaining bytes should only
* reflect the bytes written after the forgiven bytes.
*/
- constructPackageIoOveruseStats(/*uid*=*/1212345, /*shouldNotify=*/true,
+ constructPackageIoOveruseStats(/*uid=*/1212345, /*shouldNotify=*/true,
/*isKillable=*/true, /*remaining=*/
constructPerStateBytes(50'000, 0, 100'000),
/*written=*/constructPerStateBytes(90'000, 50'000, 0),
+ /*forgiven=*/constructPerStateBytes(70'000, 30'000, 0),
/*totalOveruses=*/2, startTime, durationInSeconds)};
EXPECT_THAT(actualIoOveruseStats, UnorderedElementsAreArray(expectedIoOveruseStats))
<< "Expected: " << toString(expectedIoOveruseStats)
@@ -360,34 +414,38 @@
* Current date changed so the daily I/O usage stats should be reset and the latest I/O overuse
* stats should not aggregate with the previous day's stats.
*/
- mockUidIoStats->expectDeltaStats(
- {{1001000, IoUsage(0, 0, /*fgWrBytes=*/78'000, /*bgWrBytes=*/38'000, 0, 0)},
- {1113999, IoUsage(0, 0, /*fgWrBytes=*/55'000, /*bgWrBytes=*/23'000, 0, 0)},
- {1212345, IoUsage(0, 0, /*fgWrBytes=*/55'000, /*bgWrBytes=*/23'000, 0, 0)}});
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1001000, {/*fgWrBytes=*/78'000, /*bgWrBytes=*/38'000}},
+ {1113999, {/*fgWrBytes=*/55'000, /*bgWrBytes=*/23'000}},
+ {1212345, {/*fgWrBytes=*/55'000, /*bgWrBytes=*/23'000}}})));
actualIoOveruseStats.clear();
EXPECT_CALL(*mMockWatchdogServiceHelper, latestIoOveruseStats(_))
.WillOnce(DoAll(SaveArg<0>(&actualIoOveruseStats), Return(Status::ok())));
currentTime += (24 * 60 * 60); // Change collection time to next day.
ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
- mockUidIoStats, nullptr, nullptr));
+ mMockUidStatsCollector, nullptr));
const auto [nextDayStartTime, nextDayDuration] = calculateStartAndDuration(currentTime);
expectedIoOveruseStats =
- {constructPackageIoOveruseStats(/*uid*=*/1001000, /*shouldNotify=*/false,
+ {constructPackageIoOveruseStats(/*uid=*/1001000, /*shouldNotify=*/false,
/*isKillable=*/false, /*remaining=*/
constructPerStateBytes(2'000, 2'000, 100'000),
/*written=*/constructPerStateBytes(78'000, 38'000, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, nextDayStartTime, nextDayDuration),
- constructPackageIoOveruseStats(/*uid*=*/1113999, /*shouldNotify=*/false,
+ constructPackageIoOveruseStats(/*uid=*/1113999, /*shouldNotify=*/false,
/*isKillable=*/true, /*remaining=*/
constructPerStateBytes(15'000, 7'000, 100'000),
/*written=*/constructPerStateBytes(55'000, 23'000, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, nextDayStartTime, nextDayDuration),
- constructPackageIoOveruseStats(/*uid*=*/1212345, /*shouldNotify=*/false,
+ constructPackageIoOveruseStats(/*uid=*/1212345, /*shouldNotify=*/false,
/*isKillable=*/true, /*remaining=*/
constructPerStateBytes(15'000, 7'000, 100'000),
/*written=*/constructPerStateBytes(55'000, 23'000, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, nextDayStartTime,
nextDayDuration)};
@@ -397,7 +455,6 @@
}
TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithGarageMode) {
- setUpPackagesAndConfigurations();
sp<MockResourceOveruseListener> mockResourceOveruseListener =
sp<MockResourceOveruseListener>::make();
ASSERT_NO_FATAL_FAILURE(executeAsUid(1001000, [&]() {
@@ -408,11 +465,11 @@
* Package "system.daemon" (UID: 1001000) exceeds warn threshold percentage of 80% but no
* warning is issued as it is a native UID.
*/
- sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
- mockUidIoStats->expectDeltaStats(
- {{1001000, IoUsage(0, 0, /*fgWrBytes=*/70'000, /*bgWrBytes=*/60'000, 0, 0)},
- {1112345, IoUsage(0, 0, /*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000, 0, 0)},
- {1212345, IoUsage(0, 0, /*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000, 0, 0)}});
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1001000, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/60'000}},
+ {1112345, {/*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000}},
+ {1212345, {/*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000}}})));
ResourceOveruseStats actualOverusingNativeStats;
EXPECT_CALL(*mockResourceOveruseListener, onOveruse(_))
@@ -425,34 +482,37 @@
const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::GARAGE_MODE,
- mockUidIoStats, nullptr, nullptr));
+ mMockUidStatsCollector, nullptr));
const auto expectedOverusingNativeStats = constructResourceOveruseStats(
constructIoOveruseStats(/*isKillable=*/false,
/*remaining=*/constructPerStateBytes(80'000, 40'000, 0),
/*written=*/constructPerStateBytes(0, 0, 130'000),
/*totalOveruses=*/1, startTime, durationInSeconds));
- EXPECT_THAT(actualOverusingNativeStats, expectedOverusingNativeStats)
+ EXPECT_THAT(actualOverusingNativeStats, Eq(expectedOverusingNativeStats))
<< "Expected: " << expectedOverusingNativeStats.toString()
<< "\nActual: " << actualOverusingNativeStats.toString();
const std::vector<PackageIoOveruseStats> expectedIoOveruseStats =
- {constructPackageIoOveruseStats(/*uid*=*/1001000, /*shouldNotify=*/true,
+ {constructPackageIoOveruseStats(/*uid=*/1001000, /*shouldNotify=*/true,
/*isKillable=*/false, /*remaining=*/
constructPerStateBytes(80'000, 40'000, 0),
/*written=*/constructPerStateBytes(0, 0, 130'000),
+ /*forgiven=*/constructPerStateBytes(0, 0, 100'000),
/*totalOveruses=*/1, startTime, durationInSeconds),
- constructPackageIoOveruseStats(/*uid*=*/1112345, /*shouldNotify=*/false,
+ constructPackageIoOveruseStats(/*uid=*/1112345, /*shouldNotify=*/false,
/*isKillable=*/true, /*remaining=*/
constructPerStateBytes(70'000, 30'000, 50'000),
/*written=*/constructPerStateBytes(0, 0, 50'000),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, startTime, durationInSeconds),
// Exceeds threshold.
- constructPackageIoOveruseStats(/*uid*=*/1212345, /*shouldNotify=*/true,
+ constructPackageIoOveruseStats(/*uid=*/1212345, /*shouldNotify=*/true,
/*isKillable=*/true,
/*remaining=*/
constructPerStateBytes(70'000, 30'000, 0),
/*written=*/constructPerStateBytes(0, 0, 110'000),
+ /*forgiven=*/constructPerStateBytes(0, 0, 100'000),
/*totalOveruses=*/1, startTime, durationInSeconds)};
EXPECT_THAT(actualIoOveruseStats, UnorderedElementsAreArray(expectedIoOveruseStats))
<< "Expected: " << toString(expectedIoOveruseStats)
@@ -460,11 +520,10 @@
}
TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithZeroWriteBytes) {
- sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
- mockUidIoStats->expectDeltaStats(
- {{1001000, IoUsage(10, 0, /*fgWrBytes=*/0, /*bgWrBytes=*/0, 1, 0)},
- {1112345, IoUsage(0, 20, /*fgWrBytes=*/0, /*bgWrBytes=*/0, 0, 0)},
- {1212345, IoUsage(0, 00, /*fgWrBytes=*/0, /*bgWrBytes=*/0, 0, 1)}});
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(constructUidStats({{1001000, {/*fgWrBytes=*/0, /*bgWrBytes=*/0}},
+ {1112345, {/*fgWrBytes=*/0, /*bgWrBytes=*/0}},
+ {1212345, {/*fgWrBytes=*/0, /*bgWrBytes=*/0}}})));
EXPECT_CALL(*mMockPackageInfoResolver, getPackageInfosForUids(_)).Times(0);
EXPECT_CALL(*mMockIoOveruseConfigs, fetchThreshold(_)).Times(0);
@@ -474,22 +533,91 @@
ASSERT_RESULT_OK(
mIoOveruseMonitor->onPeriodicCollection(std::chrono::system_clock::to_time_t(
std::chrono::system_clock::now()),
- SystemState::NORMAL_MODE, mockUidIoStats,
- nullptr, nullptr));
+ SystemState::NORMAL_MODE,
+ mMockUidStatsCollector, nullptr));
+}
+
+TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithExtremeOveruse) {
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1001000, {/*fgWrBytes=*/190'000, /*bgWrBytes=*/42'000}},
+ {1212345, {/*fgWrBytes=*/90'000, /*bgWrBytes=*/90'000}}})));
+
+ time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+ const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
+
+ std::vector<PackageIoOveruseStats> actualPackageIoOveruseStats;
+ EXPECT_CALL(*mMockWatchdogServiceHelper, latestIoOveruseStats(_))
+ .WillOnce(DoAll(SaveArg<0>(&actualPackageIoOveruseStats), Return(Status::ok())));
+
+ ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
+ mMockUidStatsCollector, nullptr));
+
+ std::vector<PackageIoOveruseStats> expectedPackageIoOveruseStats =
+ {constructPackageIoOveruseStats(/*uid=*/1001000, /*shouldNotify=*/true,
+ /*isKillable=*/false, /*remaining=*/
+ constructPerStateBytes(0, 0, 100'000),
+ /*written=*/constructPerStateBytes(190'000, 42'000, 0),
+ /*forgiven=*/constructPerStateBytes(160'000, 40'000, 0),
+ /*totalOveruses=*/3, startTime, durationInSeconds),
+ constructPackageIoOveruseStats(/*uid=*/1212345, /*shouldNotify=*/true,
+ /*isKillable=*/true, /*remaining=*/
+ constructPerStateBytes(0, 0, 100'000),
+ /*written=*/constructPerStateBytes(90'000, 90'000, 0),
+ /*forgiven=*/constructPerStateBytes(70'000, 90'000, 0),
+ /*totalOveruses=*/4, startTime, durationInSeconds)};
+ EXPECT_THAT(actualPackageIoOveruseStats,
+ UnorderedElementsAreArray(expectedPackageIoOveruseStats))
+ << "Expected: " << toString(expectedPackageIoOveruseStats)
+ << "\nActual: " << toString(actualPackageIoOveruseStats);
+}
+
+TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithExtremeOveruseInGarageMode) {
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1001000, {/*fgWrBytes=*/190'000, /*bgWrBytes=*/42'000}},
+ {1212345, {/*fgWrBytes=*/90'000, /*bgWrBytes=*/90'000}}})));
+
+ time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+ const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
+
+ std::vector<PackageIoOveruseStats> actualPackageIoOveruseStats;
+ EXPECT_CALL(*mMockWatchdogServiceHelper, latestIoOveruseStats(_))
+ .WillOnce(DoAll(SaveArg<0>(&actualPackageIoOveruseStats), Return(Status::ok())));
+
+ ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::GARAGE_MODE,
+ mMockUidStatsCollector, nullptr));
+
+ std::vector<PackageIoOveruseStats> expectedPackageIoOveruseStats =
+ {constructPackageIoOveruseStats(/*uid=*/1001000, /*shouldNotify=*/true,
+ /*isKillable=*/false, /*remaining=*/
+ constructPerStateBytes(80'000, 40'000, 0),
+ /*written=*/constructPerStateBytes(0, 0, 232'000),
+ /*forgiven=*/constructPerStateBytes(0, 0, 200'000),
+ /*totalOveruses=*/2, startTime, durationInSeconds),
+ constructPackageIoOveruseStats(/*uid=*/1212345, /*shouldNotify=*/true,
+ /*isKillable=*/true, /*remaining=*/
+ constructPerStateBytes(70'000, 30'000, 0),
+ /*written=*/constructPerStateBytes(0, 0, 180'000),
+ /*forgiven=*/constructPerStateBytes(0, 0, 100'000),
+ /*totalOveruses=*/1, startTime, durationInSeconds)};
+ EXPECT_THAT(actualPackageIoOveruseStats,
+ UnorderedElementsAreArray(expectedPackageIoOveruseStats))
+ << "Expected: " << toString(expectedPackageIoOveruseStats)
+ << "\nActual: " << toString(actualPackageIoOveruseStats);
}
TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithSmallWrittenBytes) {
- setUpPackagesAndConfigurations();
- sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
/*
* UID 1212345 current written bytes < |KTestMinSyncWrittenBytes| so the UID's stats are not
* synced.
*/
- mockUidIoStats->expectDeltaStats(
- {{1001000, IoUsage(10, 0, /*fgWrBytes=*/59'200, /*bgWrBytes=*/0, 1, 0)},
- {1112345, IoUsage(0, 20, /*fgWrBytes=*/0, /*bgWrBytes=*/25'200, 0, 0)},
- {1212345, IoUsage(0, 00, /*fgWrBytes=*/300, /*bgWrBytes=*/600, 0, 1)},
- {1312345, IoUsage(0, 00, /*fgWrBytes=*/51'200, /*bgWrBytes=*/0, 0, 1)}});
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1001000, {/*fgWrBytes=*/59'200, /*bgWrBytes=*/0}},
+ {1112345, {/*fgWrBytes=*/0, /*bgWrBytes=*/25'200}},
+ {1212345, {/*fgWrBytes=*/300, /*bgWrBytes=*/600}},
+ {1312345, {/*fgWrBytes=*/51'200, /*bgWrBytes=*/0}}})));
std::vector<PackageIoOveruseStats> actualIoOveruseStats;
EXPECT_CALL(*mMockWatchdogServiceHelper, latestIoOveruseStats(_))
@@ -499,24 +627,27 @@
const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
- mockUidIoStats, nullptr, nullptr));
+ mMockUidStatsCollector, nullptr));
std::vector<PackageIoOveruseStats> expectedIoOveruseStats =
- {constructPackageIoOveruseStats(/*uid*=*/1001000, /*shouldNotify=*/false,
+ {constructPackageIoOveruseStats(/*uid=*/1001000, /*shouldNotify=*/false,
/*isKillable=*/false, /*remaining=*/
constructPerStateBytes(20'800, 40'000, 100'000),
/*written=*/
constructPerStateBytes(59'200, 0, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, startTime, durationInSeconds),
- constructPackageIoOveruseStats(/*uid*=*/1112345, /*shouldNotify=*/true,
+ constructPackageIoOveruseStats(/*uid=*/1112345, /*shouldNotify=*/true,
/*isKillable=*/true, /*remaining=*/
constructPerStateBytes(70'000, 4'800, 100'000),
/*written=*/constructPerStateBytes(0, 25'200, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, startTime, durationInSeconds),
- constructPackageIoOveruseStats(/*uid*=*/1312345, /*shouldNotify=*/false,
+ constructPackageIoOveruseStats(/*uid=*/1312345, /*shouldNotify=*/false,
/*isKillable=*/true, /*remaining=*/
constructPerStateBytes(18'800, 30'000, 100'000),
/*written=*/constructPerStateBytes(51'200, 0, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, startTime, durationInSeconds)};
EXPECT_THAT(actualIoOveruseStats, UnorderedElementsAreArray(expectedIoOveruseStats))
@@ -537,35 +668,35 @@
* UID 1312345 current written bytes is < |kTestMinSyncWrittenBytes| but exceeds warn threshold
* and killable so the UID's stat are synced.
*/
- mockUidIoStats->expectDeltaStats(
- {{1001000,
- IoUsage(10, 0, /*fgWrBytes=*/KTestMinSyncWrittenBytes - 100, /*bgWrBytes=*/0, 1, 0)},
- {1112345,
- IoUsage(0, 20, /*fgWrBytes=*/0, /*bgWrBytes=*/KTestMinSyncWrittenBytes - 100, 0, 0)},
- {1212345,
- IoUsage(0, 00, /*fgWrBytes=*/KTestMinSyncWrittenBytes - 300, /*bgWrBytes=*/0, 0, 1)},
- {1312345,
- IoUsage(0, 00, /*fgWrBytes=*/KTestMinSyncWrittenBytes - 100, /*bgWrBytes=*/0, 0,
- 1)}});
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(constructUidStats(
+ {{1001000, {/*fgWrBytes=*/KTestMinSyncWrittenBytes - 100, /*bgWrBytes=*/0}},
+ {1112345, {/*fgWrBytes=*/0, /*bgWrBytes=*/KTestMinSyncWrittenBytes - 100}},
+ {1212345, {/*fgWrBytes=*/KTestMinSyncWrittenBytes - 300, /*bgWrBytes=*/0}},
+ {1312345, {/*fgWrBytes=*/KTestMinSyncWrittenBytes - 100, /*bgWrBytes=*/0}}})));
ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
- mockUidIoStats, nullptr, nullptr));
+ mMockUidStatsCollector, nullptr));
expectedIoOveruseStats =
- {constructPackageIoOveruseStats(/*uid*=*/1112345, /*shouldNotify=*/true,
+ {constructPackageIoOveruseStats(/*uid=*/1112345, /*shouldNotify=*/true,
/*isKillable=*/true, /*remaining=*/
constructPerStateBytes(70'000, 0, 100'000),
/*written=*/constructPerStateBytes(0, 30'100, 0),
+ /*forgiven=*/
+ constructPerStateBytes(0, 30'000, 0),
/*totalOveruses=*/1, startTime, durationInSeconds),
- constructPackageIoOveruseStats(/*uid*=*/1212345, /*shouldNotify=*/false,
+ constructPackageIoOveruseStats(/*uid=*/1212345, /*shouldNotify=*/false,
/*isKillable=*/true, /*remaining=*/
constructPerStateBytes(65'000, 29'400, 100'000),
/*written=*/constructPerStateBytes(5'000, 600, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, startTime, durationInSeconds),
- constructPackageIoOveruseStats(/*uid*=*/1312345, /*shouldNotify=*/true,
+ constructPackageIoOveruseStats(/*uid=*/1312345, /*shouldNotify=*/true,
/*isKillable=*/true, /*remaining=*/
constructPerStateBytes(13'900, 30'000, 100'000),
/*written=*/constructPerStateBytes(56'100, 0, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
/*totalOveruses=*/0, startTime, durationInSeconds)};
EXPECT_THAT(actualIoOveruseStats, UnorderedElementsAreArray(expectedIoOveruseStats))
<< "Expected: " << toString(expectedIoOveruseStats)
@@ -573,14 +704,11 @@
}
TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithNoPackageInfo) {
- sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
- mockUidIoStats->expectDeltaStats(
- {{1001000, IoUsage(0, 0, /*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000, 0, 0)},
- {1112345, IoUsage(0, 0, /*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000, 0, 0)},
- {1212345, IoUsage(0, 0, /*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000, 0, 0)}});
-
- ON_CALL(*mMockPackageInfoResolver, getPackageInfosForUids(_))
- .WillByDefault(Return(std::unordered_map<uid_t, PackageInfo>{}));
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{2301000, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000}},
+ {2412345, {/*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000}},
+ {2512345, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000}}})));
EXPECT_CALL(*mMockIoOveruseConfigs, fetchThreshold(_)).Times(0);
EXPECT_CALL(*mMockIoOveruseConfigs, isSafeToKill(_)).Times(0);
@@ -589,8 +717,128 @@
ASSERT_RESULT_OK(
mIoOveruseMonitor->onPeriodicCollection(std::chrono::system_clock::to_time_t(
std::chrono::system_clock::now()),
- SystemState::NORMAL_MODE, mockUidIoStats,
- nullptr, nullptr));
+ SystemState::NORMAL_MODE,
+ mMockUidStatsCollector, nullptr));
+}
+
+TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithPrevBootStats) {
+ std::vector<UserPackageIoUsageStats> todayIoUsageStats =
+ {constructUserPackageIoUsageStats(
+ /*userId=*/11, "com.android.google.package",
+ /*writtenBytes=*/constructPerStateBytes(100'000, 85'000, 120'000),
+ /*forgivenWriteBytes=*/constructPerStateBytes(70'000, 60'000, 100'000),
+ /*totalOveruses=*/3),
+ constructUserPackageIoUsageStats(
+ /*userId=*/12, "com.android.kitchensink",
+ /*writtenBytes=*/constructPerStateBytes(50'000, 40'000, 35'000),
+ /*forgivenWriteBytes=*/constructPerStateBytes(30'000, 30'000, 30'000),
+ /*totalOveruses=*/6)};
+ EXPECT_CALL(*mMockWatchdogServiceHelper, getTodayIoUsageStats(_))
+ .WillOnce(DoAll(SetArgPointee<0>(todayIoUsageStats), Return(Status::ok())));
+
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1001000, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/20'000}},
+ {1112345, {/*fgWrBytes=*/35'000, /*bgWrBytes=*/15'000}}})));
+
+ std::vector<PackageIoOveruseStats> actualIoOveruseStats;
+ EXPECT_CALL(*mMockWatchdogServiceHelper, latestIoOveruseStats(_))
+ .WillOnce(DoAll(SaveArg<0>(&actualIoOveruseStats), Return(Status::ok())));
+
+ time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+ const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
+
+ ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
+ mMockUidStatsCollector, nullptr));
+
+ std::vector<PackageIoOveruseStats> expectedIoOveruseStats =
+ {constructPackageIoOveruseStats(
+ /*uid*=*/1001000, /*shouldNotify=*/false, /*isKillable=*/false,
+ /*remaining=*/constructPerStateBytes(10'000, 20'000, 100'000),
+ /*written=*/constructPerStateBytes(70'000, 20'000, 0),
+ /*forgiven=*/constructPerStateBytes(0, 0, 0),
+ /*totalOveruses=*/0, startTime, durationInSeconds),
+ constructPackageIoOveruseStats(
+ /*uid*=*/1112345, /*shouldNotify=*/true, /*isKillable=*/true,
+ /*remaining=*/constructPerStateBytes(5'000, 0, 80'000),
+ /*written=*/constructPerStateBytes(135'000, 100'000, 120'000),
+ /*forgiven=*/constructPerStateBytes(70'000, 90'000, 100'000),
+ /*totalOveruses=*/4, startTime, durationInSeconds)};
+ EXPECT_THAT(actualIoOveruseStats, UnorderedElementsAreArray(expectedIoOveruseStats))
+ << "Expected: " << toString(expectedIoOveruseStats)
+ << "\nActual: " << toString(actualIoOveruseStats);
+
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1112345, {/*fgWrBytes=*/70'000, /*bgWrBytes=*/40'000}},
+ {1245678, {/*fgWrBytes=*/30'000, /*bgWrBytes=*/10'000}}})));
+
+ actualIoOveruseStats.clear();
+ EXPECT_CALL(*mMockWatchdogServiceHelper, latestIoOveruseStats(_))
+ .WillOnce(DoAll(SaveArg<0>(&actualIoOveruseStats), Return(Status::ok())));
+
+ ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::GARAGE_MODE,
+ mMockUidStatsCollector, nullptr));
+
+ expectedIoOveruseStats = {constructPackageIoOveruseStats(
+ /*uid*=*/1112345, /*shouldNotify=*/true, /*isKillable=*/true,
+ /*remaining=*/constructPerStateBytes(5'000, 20'000, 0),
+ /*written=*/constructPerStateBytes(135'000, 100'000, 230'000),
+ /*forgiven=*/constructPerStateBytes(70'000, 90'000, 200'000),
+ /*totalOveruses=*/5, startTime, durationInSeconds),
+ constructPackageIoOveruseStats(
+ /*uid*=*/1245678, /*shouldNotify=*/true, /*isKillable=*/true,
+ /*remaining=*/constructPerStateBytes(10'000, 5'000, 0),
+ /*written=*/constructPerStateBytes(50'000, 40'000, 75'000),
+ /*forgiven=*/constructPerStateBytes(30'000, 30'000, 70'000),
+ /*totalOveruses=*/10, startTime, durationInSeconds)};
+ EXPECT_THAT(actualIoOveruseStats, UnorderedElementsAreArray(expectedIoOveruseStats))
+ << "Expected: " << toString(expectedIoOveruseStats)
+ << "\nActual: " << toString(actualIoOveruseStats);
+}
+
+TEST_F(IoOveruseMonitorTest, TestOnPeriodicCollectionWithErrorFetchingPrevBootStats) {
+ EXPECT_CALL(*mMockWatchdogServiceHelper, getTodayIoUsageStats(_))
+ .WillOnce(Return(Status::fromExceptionCode(Status::EX_ILLEGAL_STATE, "Illegal state")));
+
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1112345, {/*fgWrBytes=*/15'000, /*bgWrBytes=*/15'000}}})));
+
+ time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+ const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
+
+ ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
+ mMockUidStatsCollector, nullptr));
+
+ std::vector<UserPackageIoUsageStats> todayIoUsageStats = {constructUserPackageIoUsageStats(
+ /*userId=*/11, "com.android.google.package",
+ /*writtenBytes=*/constructPerStateBytes(100'000, 85'000, 120'000),
+ /*forgivenWriteBytes=*/constructPerStateBytes(70'000, 60'000, 100'000),
+ /*totalOveruses=*/3)};
+ EXPECT_CALL(*mMockWatchdogServiceHelper, getTodayIoUsageStats(_))
+ .WillOnce(DoAll(SetArgPointee<0>(todayIoUsageStats), Return(Status::ok())));
+
+ std::vector<PackageIoOveruseStats> actualIoOveruseStats;
+ EXPECT_CALL(*mMockWatchdogServiceHelper, latestIoOveruseStats(_))
+ .WillOnce(DoAll(SaveArg<0>(&actualIoOveruseStats), Return(Status::ok())));
+
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1112345, {/*fgWrBytes=*/20'000, /*bgWrBytes=*/40'000}}})));
+
+ ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
+ mMockUidStatsCollector, nullptr));
+
+ std::vector<PackageIoOveruseStats> expectedIoOveruseStats = {constructPackageIoOveruseStats(
+ /*uid*=*/1112345, /*shouldNotify=*/true, /*isKillable=*/true,
+ /*remaining=*/constructPerStateBytes(5'000, 0, 80'000),
+ /*written=*/constructPerStateBytes(135'000, 140'000, 120'000),
+ /*forgiven=*/constructPerStateBytes(70'000, 120'000, 100'000),
+ /*totalOveruses=*/5, startTime, durationInSeconds)};
+ EXPECT_THAT(actualIoOveruseStats, UnorderedElementsAreArray(expectedIoOveruseStats))
+ << "Expected: " << toString(expectedIoOveruseStats)
+ << "\nActual: " << toString(actualIoOveruseStats);
}
TEST_F(IoOveruseMonitorTest, TestOnPeriodicMonitor) {
@@ -712,21 +960,20 @@
}
TEST_F(IoOveruseMonitorTest, TestGetIoOveruseStats) {
- setUpPackagesAndConfigurations();
- sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
- mockUidIoStats->expectDeltaStats(
- {{1001000, IoUsage(0, 0, /*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000, 0, 0)}});
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1001000, {/*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000}}})));
time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
const auto [startTime, durationInSeconds] = calculateStartAndDuration(currentTime);
ASSERT_RESULT_OK(mIoOveruseMonitor->onPeriodicCollection(currentTime, SystemState::NORMAL_MODE,
- mockUidIoStats, nullptr, nullptr));
+ mMockUidStatsCollector, nullptr));
const auto expected =
constructIoOveruseStats(/*isKillable=*/false,
/*remaining=*/
- constructPerStateBytes(80'000, 40'000, 100'000),
+ constructPerStateBytes(70'000, 20'000, 100'000),
/*written=*/
constructPerStateBytes(90'000, 20'000, 0),
/*totalOveruses=*/1, startTime, durationInSeconds);
@@ -739,16 +986,15 @@
}
TEST_F(IoOveruseMonitorTest, TestResetIoOveruseStats) {
- setUpPackagesAndConfigurations();
- sp<MockUidIoStats> mockUidIoStats = sp<MockUidIoStats>::make();
- mockUidIoStats->expectDeltaStats(
- {{1001000, IoUsage(0, 0, /*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000, 0, 0)}});
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats())
+ .WillOnce(Return(
+ constructUidStats({{1001000, {/*fgWrBytes=*/90'000, /*bgWrBytes=*/20'000}}})));
ASSERT_RESULT_OK(
mIoOveruseMonitor->onPeriodicCollection(std::chrono::system_clock::to_time_t(
std::chrono::system_clock::now()),
- SystemState::NORMAL_MODE, mockUidIoStats,
- nullptr, nullptr));
+ SystemState::NORMAL_MODE,
+ mMockUidStatsCollector, nullptr));
IoOveruseStats actual;
ASSERT_NO_FATAL_FAILURE(executeAsUid(1001000, [&]() {
diff --git a/cpp/watchdog/server/tests/IoPerfCollectionTest.cpp b/cpp/watchdog/server/tests/IoPerfCollectionTest.cpp
index 85e049d..8da3b6f 100644
--- a/cpp/watchdog/server/tests/IoPerfCollectionTest.cpp
+++ b/cpp/watchdog/server/tests/IoPerfCollectionTest.cpp
@@ -15,101 +15,182 @@
*/
#include "IoPerfCollection.h"
-#include "MockPackageInfoResolver.h"
-#include "MockProcPidStat.h"
#include "MockProcStat.h"
-#include "MockUidIoStats.h"
+#include "MockUidStatsCollector.h"
#include "MockWatchdogServiceHelper.h"
-#include "PackageInfoResolver.h"
+#include "PackageInfoTestUtils.h"
#include <WatchdogProperties.sysprop.h>
#include <android-base/file.h>
#include <gmock/gmock.h>
+#include <utils/RefBase.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
+#include <type_traits>
#include <vector>
namespace android {
namespace automotive {
namespace watchdog {
+using ::android::RefBase;
using ::android::sp;
-using ::android::base::Error;
+using ::android::automotive::watchdog::internal::PackageInfo;
using ::android::base::ReadFdToString;
using ::android::base::Result;
using ::testing::_;
+using ::testing::AllOf;
+using ::testing::ElementsAreArray;
+using ::testing::Eq;
+using ::testing::ExplainMatchResult;
+using ::testing::Field;
+using ::testing::IsSubsetOf;
+using ::testing::Matcher;
using ::testing::Return;
+using ::testing::Test;
+using ::testing::UnorderedElementsAreArray;
+using ::testing::VariantWith;
namespace {
-bool isEqual(const UidIoPerfData& lhs, const UidIoPerfData& rhs) {
- if (lhs.topNReads.size() != rhs.topNReads.size() ||
- lhs.topNWrites.size() != rhs.topNWrites.size()) {
- return false;
+MATCHER_P(IoStatsEq, expected, "") {
+ return ExplainMatchResult(AllOf(Field("bytes", &UserPackageStats::IoStats::bytes,
+ ElementsAreArray(expected.bytes)),
+ Field("fsync", &UserPackageStats::IoStats::fsync,
+ ElementsAreArray(expected.fsync))),
+ arg, result_listener);
+}
+
+MATCHER_P(ProcessCountEq, expected, "") {
+ return ExplainMatchResult(AllOf(Field("comm", &UserPackageStats::ProcStats::ProcessCount::comm,
+ Eq(expected.comm)),
+ Field("count",
+ &UserPackageStats::ProcStats::ProcessCount::count,
+ Eq(expected.count))),
+ arg, result_listener);
+}
+
+MATCHER_P(ProcStatsEq, expected, "") {
+ std::vector<Matcher<const UserPackageStats::ProcStats::ProcessCount&>> processCountMatchers;
+ for (const auto& processCount : expected.topNProcesses) {
+ processCountMatchers.push_back(ProcessCountEq(processCount));
}
- for (int i = 0; i < METRIC_TYPES; ++i) {
- for (int j = 0; j < UID_STATES; ++j) {
- if (lhs.total[i][j] != rhs.total[i][j]) {
+ return ExplainMatchResult(AllOf(Field("count", &UserPackageStats::ProcStats::count,
+ Eq(expected.count)),
+ Field("topNProcesses",
+ &UserPackageStats::ProcStats::topNProcesses,
+ ElementsAreArray(processCountMatchers))),
+ arg, result_listener);
+}
+
+MATCHER_P(UserPackageStatsEq, expected, "") {
+ const auto uidMatcher = Field("uid", &UserPackageStats::uid, Eq(expected.uid));
+ const auto packageNameMatcher =
+ Field("genericPackageName", &UserPackageStats::genericPackageName,
+ Eq(expected.genericPackageName));
+ return std::visit(
+ [&](const auto& stats) -> bool {
+ using T = std::decay_t<decltype(stats)>;
+ if constexpr (std::is_same_v<T, UserPackageStats::IoStats>) {
+ return ExplainMatchResult(AllOf(uidMatcher, packageNameMatcher,
+ Field("stats:IoStats", &UserPackageStats::stats,
+ VariantWith<UserPackageStats::IoStats>(
+ IoStatsEq(stats)))),
+ arg, result_listener);
+ } else if constexpr (std::is_same_v<T, UserPackageStats::ProcStats>) {
+ return ExplainMatchResult(AllOf(uidMatcher, packageNameMatcher,
+ Field("stats:ProcStats",
+ &UserPackageStats::stats,
+ VariantWith<UserPackageStats::ProcStats>(
+ ProcStatsEq(stats)))),
+ arg, result_listener);
+ }
+ *result_listener << "Unexpected variant in UserPackageStats::stats";
return false;
- }
+ },
+ expected.stats);
+}
+
+MATCHER_P(UserPackageSummaryStatsEq, expected, "") {
+ const auto& userPackageStatsMatchers = [&](const std::vector<UserPackageStats>& stats) {
+ std::vector<Matcher<const UserPackageStats&>> matchers;
+ for (const auto& curStats : stats) {
+ matchers.push_back(UserPackageStatsEq(curStats));
}
- }
- auto comp = [&](const UidIoPerfData::Stats& l, const UidIoPerfData::Stats& r) -> bool {
- bool isEqual = l.userId == r.userId && l.packageName == r.packageName;
- for (int i = 0; i < UID_STATES; ++i) {
- isEqual &= l.bytes[i] == r.bytes[i] && l.fsync[i] == r.fsync[i];
+ return ElementsAreArray(matchers);
+ };
+ const auto& totalIoStatsArrayMatcher = [&](const int64_t expected[][UID_STATES]) {
+ std::vector<Matcher<const int64_t[UID_STATES]>> matchers;
+ for (int i = 0; i < METRIC_TYPES; ++i) {
+ matchers.push_back(ElementsAreArray(expected[i], UID_STATES));
}
- return isEqual;
+ return ElementsAreArray(matchers);
};
- return lhs.topNReads.size() == rhs.topNReads.size() &&
- std::equal(lhs.topNReads.begin(), lhs.topNReads.end(), rhs.topNReads.begin(), comp) &&
- lhs.topNWrites.size() == rhs.topNWrites.size() &&
- std::equal(lhs.topNWrites.begin(), lhs.topNWrites.end(), rhs.topNWrites.begin(), comp);
+ return ExplainMatchResult(AllOf(Field("topNIoReads", &UserPackageSummaryStats::topNIoReads,
+ userPackageStatsMatchers(expected.topNIoReads)),
+ Field("topNIoWrites", &UserPackageSummaryStats::topNIoWrites,
+ userPackageStatsMatchers(expected.topNIoWrites)),
+ Field("topNIoBlocked", &UserPackageSummaryStats::topNIoBlocked,
+ userPackageStatsMatchers(expected.topNIoBlocked)),
+ Field("topNMajorFaults",
+ &UserPackageSummaryStats::topNMajorFaults,
+ userPackageStatsMatchers(expected.topNMajorFaults)),
+ Field("totalIoStats", &UserPackageSummaryStats::totalIoStats,
+ totalIoStatsArrayMatcher(expected.totalIoStats)),
+ Field("taskCountByUid",
+ &UserPackageSummaryStats::taskCountByUid,
+ IsSubsetOf(expected.taskCountByUid)),
+ Field("totalMajorFaults",
+ &UserPackageSummaryStats::totalMajorFaults,
+ Eq(expected.totalMajorFaults)),
+ Field("majorFaultsPercentChange",
+ &UserPackageSummaryStats::majorFaultsPercentChange,
+ Eq(expected.majorFaultsPercentChange))),
+ arg, result_listener);
}
-bool isEqual(const SystemIoPerfData& lhs, const SystemIoPerfData& rhs) {
- return lhs.cpuIoWaitTime == rhs.cpuIoWaitTime && lhs.totalCpuTime == rhs.totalCpuTime &&
- lhs.ioBlockedProcessesCnt == rhs.ioBlockedProcessesCnt &&
- lhs.totalProcessesCnt == rhs.totalProcessesCnt;
+MATCHER_P(SystemSummaryStatsEq, expected, "") {
+ return ExplainMatchResult(AllOf(Field("cpuIoWaitTime", &SystemSummaryStats::cpuIoWaitTime,
+ Eq(expected.cpuIoWaitTime)),
+ Field("totalCpuTime", &SystemSummaryStats::totalCpuTime,
+ Eq(expected.totalCpuTime)),
+ Field("ioBlockedProcessCount",
+ &SystemSummaryStats::ioBlockedProcessCount,
+ Eq(expected.ioBlockedProcessCount)),
+ Field("totalProcessCount",
+ &SystemSummaryStats::totalProcessCount,
+ Eq(expected.totalProcessCount))),
+ arg, result_listener);
}
-bool isEqual(const ProcessIoPerfData& lhs, const ProcessIoPerfData& rhs) {
- if (lhs.topNIoBlockedUids.size() != rhs.topNIoBlockedUids.size() ||
- lhs.topNMajorFaultUids.size() != rhs.topNMajorFaultUids.size() ||
- lhs.totalMajorFaults != rhs.totalMajorFaults ||
- lhs.majorFaultsPercentChange != rhs.majorFaultsPercentChange) {
- return false;
+MATCHER_P(PerfStatsRecordEq, expected, "") {
+ return ExplainMatchResult(AllOf(Field(&PerfStatsRecord::systemSummaryStats,
+ SystemSummaryStatsEq(expected.systemSummaryStats)),
+ Field(&PerfStatsRecord::userPackageSummaryStats,
+ UserPackageSummaryStatsEq(
+ expected.userPackageSummaryStats))),
+ arg, result_listener);
+}
+
+const std::vector<Matcher<const PerfStatsRecord&>> constructPerfStatsRecordMatchers(
+ const std::vector<PerfStatsRecord>& records) {
+ std::vector<Matcher<const PerfStatsRecord&>> matchers;
+ for (const auto& record : records) {
+ matchers.push_back(PerfStatsRecordEq(record));
}
- auto comp = [&](const ProcessIoPerfData::UidStats& l,
- const ProcessIoPerfData::UidStats& r) -> bool {
- auto comp = [&](const ProcessIoPerfData::UidStats::ProcessStats& l,
- const ProcessIoPerfData::UidStats::ProcessStats& r) -> bool {
- return l.comm == r.comm && l.count == r.count;
- };
- return l.userId == r.userId && l.packageName == r.packageName && l.count == r.count &&
- l.topNProcesses.size() == r.topNProcesses.size() &&
- std::equal(l.topNProcesses.begin(), l.topNProcesses.end(), r.topNProcesses.begin(),
- comp);
- };
- return lhs.topNIoBlockedUids.size() == lhs.topNIoBlockedUids.size() &&
- std::equal(lhs.topNIoBlockedUids.begin(), lhs.topNIoBlockedUids.end(),
- rhs.topNIoBlockedUids.begin(), comp) &&
- lhs.topNIoBlockedUidsTotalTaskCnt.size() == rhs.topNIoBlockedUidsTotalTaskCnt.size() &&
- std::equal(lhs.topNIoBlockedUidsTotalTaskCnt.begin(),
- lhs.topNIoBlockedUidsTotalTaskCnt.end(),
- rhs.topNIoBlockedUidsTotalTaskCnt.begin()) &&
- lhs.topNMajorFaultUids.size() == rhs.topNMajorFaultUids.size() &&
- std::equal(lhs.topNMajorFaultUids.begin(), lhs.topNMajorFaultUids.end(),
- rhs.topNMajorFaultUids.begin(), comp);
+ return matchers;
}
-bool isEqual(const IoPerfRecord& lhs, const IoPerfRecord& rhs) {
- return isEqual(lhs.uidIoPerfData, rhs.uidIoPerfData) &&
- isEqual(lhs.systemIoPerfData, rhs.systemIoPerfData) &&
- isEqual(lhs.processIoPerfData, rhs.processIoPerfData);
+MATCHER_P(CollectionInfoEq, expected, "") {
+ return ExplainMatchResult(AllOf(Field("maxCacheSize", &CollectionInfo::maxCacheSize,
+ Eq(expected.maxCacheSize)),
+ Field("records", &CollectionInfo::records,
+ ElementsAreArray(constructPerfStatsRecordMatchers(
+ expected.records)))),
+ arg, result_listener);
}
int countOccurrences(std::string str, std::string subStr) {
@@ -122,23 +203,167 @@
return occurrences;
}
+std::tuple<std::vector<UidStats>, UserPackageSummaryStats> sampleUidStats(int multiplier = 1) {
+ /* The number of returned sample stats are less that the top N stats per category/sub-category.
+ * The top N stats per category/sub-category is set to % during test setup. Thus, the default
+ * testing behavior is # reported stats < top N stats.
+ */
+ const auto int64Multiplier = [&](int64_t bytes) -> int64_t {
+ return static_cast<int64_t>(bytes * multiplier);
+ };
+ const auto uint64Multiplier = [&](uint64_t count) -> uint64_t {
+ return static_cast<uint64_t>(count * multiplier);
+ };
+ std::vector<UidStats>
+ uidStats{{.packageInfo = constructPackageInfo("mount", 1009),
+ .ioStats = {/*fgRdBytes=*/0,
+ /*bgRdBytes=*/int64Multiplier(14'000),
+ /*fgWrBytes=*/0,
+ /*bgWrBytes=*/int64Multiplier(16'000),
+ /*fgFsync=*/0, /*bgFsync=*/int64Multiplier(100)},
+ .procStats = {.totalMajorFaults = uint64Multiplier(11'000),
+ .totalTasksCount = 1,
+ .ioBlockedTasksCount = 1,
+ .processStatsByPid =
+ {{/*pid=*/100,
+ {/*comm=*/"disk I/O", /*startTime=*/234,
+ /*totalMajorFaults=*/uint64Multiplier(11'000),
+ /*totalTasksCount=*/1,
+ /*ioBlockedTasksCount=*/1}}}}},
+ {.packageInfo =
+ constructPackageInfo("com.google.android.car.kitchensink", 1002001),
+ .ioStats = {/*fgRdBytes=*/0,
+ /*bgRdBytes=*/int64Multiplier(3'400),
+ /*fgWrBytes=*/0,
+ /*bgWrBytes=*/int64Multiplier(6'700),
+ /*fgFsync=*/0,
+ /*bgFsync=*/int64Multiplier(200)},
+ .procStats = {.totalMajorFaults = uint64Multiplier(22'445),
+ .totalTasksCount = 5,
+ .ioBlockedTasksCount = 3,
+ .processStatsByPid =
+ {{/*pid=*/1000,
+ {/*comm=*/"KitchenSinkApp", /*startTime=*/467,
+ /*totalMajorFaults=*/uint64Multiplier(12'345),
+ /*totalTasksCount=*/2,
+ /*ioBlockedTasksCount=*/1}},
+ {/*pid=*/1001,
+ {/*comm=*/"CTS", /*startTime=*/789,
+ /*totalMajorFaults=*/uint64Multiplier(10'100),
+ /*totalTasksCount=*/3,
+ /*ioBlockedTasksCount=*/2}}}}},
+ {.packageInfo = constructPackageInfo("", 1012345),
+ .ioStats = {/*fgRdBytes=*/int64Multiplier(1'000),
+ /*bgRdBytes=*/int64Multiplier(4'200),
+ /*fgWrBytes=*/int64Multiplier(300),
+ /*bgWrBytes=*/int64Multiplier(5'600),
+ /*fgFsync=*/int64Multiplier(600),
+ /*bgFsync=*/int64Multiplier(300)},
+ .procStats = {.totalMajorFaults = uint64Multiplier(50'900),
+ .totalTasksCount = 4,
+ .ioBlockedTasksCount = 2,
+ .processStatsByPid =
+ {{/*pid=*/2345,
+ {/*comm=*/"MapsApp", /*startTime=*/6789,
+ /*totalMajorFaults=*/uint64Multiplier(50'900),
+ /*totalTasksCount=*/4,
+ /*ioBlockedTasksCount=*/2}}}}},
+ {.packageInfo = constructPackageInfo("com.google.radio", 1015678),
+ .ioStats = {/*fgRdBytes=*/0,
+ /*bgRdBytes=*/0,
+ /*fgWrBytes=*/0,
+ /*bgWrBytes=*/0,
+ /*fgFsync=*/0, /*bgFsync=*/0},
+ .procStats = {.totalMajorFaults = 0,
+ .totalTasksCount = 4,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {
+ {/*pid=*/2345,
+ {/*comm=*/"RadioApp", /*startTime=*/19789,
+ /*totalMajorFaults=*/0,
+ /*totalTasksCount=*/4,
+ /*ioBlockedTasksCount=*/0}}}}}};
+
+ UserPackageSummaryStats userPackageSummaryStats{
+ .topNIoReads =
+ {{1009, "mount",
+ UserPackageStats::IoStats{{0, int64Multiplier(14'000)},
+ {0, int64Multiplier(100)}}},
+ {1012345, "1012345",
+ UserPackageStats::IoStats{{int64Multiplier(1'000), int64Multiplier(4'200)},
+ {int64Multiplier(600), int64Multiplier(300)}}},
+ {1002001, "com.google.android.car.kitchensink",
+ UserPackageStats::IoStats{{0, int64Multiplier(3'400)},
+ {0, int64Multiplier(200)}}}},
+ .topNIoWrites =
+ {{1009, "mount",
+ UserPackageStats::IoStats{{0, int64Multiplier(16'000)},
+ {0, int64Multiplier(100)}}},
+ {1002001, "com.google.android.car.kitchensink",
+ UserPackageStats::IoStats{{0, int64Multiplier(6'700)},
+ {0, int64Multiplier(200)}}},
+ {1012345, "1012345",
+ UserPackageStats::IoStats{{int64Multiplier(300), int64Multiplier(5'600)},
+ {int64Multiplier(600), int64Multiplier(300)}}}},
+ .topNIoBlocked = {{1002001, "com.google.android.car.kitchensink",
+ UserPackageStats::ProcStats{3, {{"CTS", 2}, {"KitchenSinkApp", 1}}}},
+ {1012345, "1012345",
+ UserPackageStats::ProcStats{2, {{"MapsApp", 2}}}},
+ {1009, "mount", UserPackageStats::ProcStats{1, {{"disk I/O", 1}}}}},
+ .topNMajorFaults =
+ {{1012345, "1012345",
+ UserPackageStats::ProcStats{uint64Multiplier(50'900),
+ {{"MapsApp", uint64Multiplier(50'900)}}}},
+ {1002001, "com.google.android.car.kitchensink",
+ UserPackageStats::ProcStats{uint64Multiplier(22'445),
+ {{"KitchenSinkApp", uint64Multiplier(12'345)},
+ {"CTS", uint64Multiplier(10'100)}}}},
+ {1009, "mount",
+ UserPackageStats::ProcStats{uint64Multiplier(11'000),
+ {{"disk I/O", uint64Multiplier(11'000)}}}}},
+ .totalIoStats = {{int64Multiplier(1'000), int64Multiplier(21'600)},
+ {int64Multiplier(300), int64Multiplier(28'300)},
+ {int64Multiplier(600), int64Multiplier(600)}},
+ .taskCountByUid = {{1009, 1}, {1002001, 5}, {1012345, 4}},
+ .totalMajorFaults = uint64Multiplier(84'345),
+ .majorFaultsPercentChange = 0.0,
+ };
+ return std::make_tuple(uidStats, userPackageSummaryStats);
+}
+
+std::tuple<ProcStatInfo, SystemSummaryStats> sampleProcStat(int multiplier = 1) {
+ const auto uint64Multiplier = [&](uint64_t bytes) -> uint64_t {
+ return static_cast<uint64_t>(bytes * multiplier);
+ };
+ const auto uint32Multiplier = [&](uint32_t bytes) -> uint32_t {
+ return static_cast<uint32_t>(bytes * multiplier);
+ };
+ ProcStatInfo procStatInfo{/*cpuStats=*/{uint64Multiplier(2'900), uint64Multiplier(7'900),
+ uint64Multiplier(4'900), uint64Multiplier(8'900),
+ /*ioWaitTime=*/uint64Multiplier(5'900),
+ uint64Multiplier(6'966), uint64Multiplier(7'980), 0, 0,
+ uint64Multiplier(2'930)},
+ /*runnableProcessCount=*/uint32Multiplier(100),
+ /*ioBlockedProcessCount=*/uint32Multiplier(57)};
+ SystemSummaryStats systemSummaryStats{/*cpuIoWaitTime=*/uint64Multiplier(5'900),
+ /*totalCpuTime=*/uint64Multiplier(48'376),
+ /*ioBlockedProcessCount=*/uint32Multiplier(57),
+ /*totalProcessCount=*/uint32Multiplier(157)};
+ return std::make_tuple(procStatInfo, systemSummaryStats);
+}
+
} // namespace
namespace internal {
-class IoPerfCollectionPeer {
+class IoPerfCollectionPeer : public RefBase {
public:
- explicit IoPerfCollectionPeer(sp<IoPerfCollection> collector) :
- mCollector(collector),
- mMockPackageInfoResolver(new MockPackageInfoResolver()) {
- mCollector->mPackageInfoResolver = mMockPackageInfoResolver;
- }
+ explicit IoPerfCollectionPeer(sp<IoPerfCollection> collector) : mCollector(collector) {}
IoPerfCollectionPeer() = delete;
~IoPerfCollectionPeer() {
mCollector->terminate();
mCollector.clear();
- mMockPackageInfoResolver.clear();
}
Result<void> init() { return mCollector->init(); }
@@ -147,11 +372,6 @@
void setTopNStatsPerSubcategory(int value) { mCollector->mTopNStatsPerSubcategory = value; }
- void injectUidToPackageNameMapping(std::unordered_map<uid_t, std::string> mapping) {
- EXPECT_CALL(*mMockPackageInfoResolver, getPackageNamesForUids(_))
- .WillRepeatedly(Return(mapping));
- }
-
const CollectionInfo& getBoottimeCollectionInfo() {
Mutex::Autolock lock(mCollector->mMutex);
return mCollector->mBoottimeCollection;
@@ -169,548 +389,319 @@
private:
sp<IoPerfCollection> mCollector;
- sp<MockPackageInfoResolver> mMockPackageInfoResolver;
};
} // namespace internal
-TEST(IoPerfCollectionTest, TestBoottimeCollection) {
- sp<MockUidIoStats> mockUidIoStats = new MockUidIoStats();
- sp<MockProcStat> mockProcStat = new MockProcStat();
- sp<MockProcPidStat> mockProcPidStat = new MockProcPidStat();
+class IoPerfCollectionTest : public Test {
+protected:
+ void SetUp() override {
+ mMockUidStatsCollector = sp<MockUidStatsCollector>::make();
+ mMockProcStat = sp<MockProcStat>::make();
+ mCollector = sp<IoPerfCollection>::make();
+ mCollectorPeer = sp<internal::IoPerfCollectionPeer>::make(mCollector);
+ ASSERT_RESULT_OK(mCollectorPeer->init());
+ mCollectorPeer->setTopNStatsPerCategory(5);
+ mCollectorPeer->setTopNStatsPerSubcategory(5);
+ }
- sp<IoPerfCollection> collector = new IoPerfCollection();
- internal::IoPerfCollectionPeer collectorPeer(collector);
+ void TearDown() override {
+ mMockUidStatsCollector.clear();
+ mMockProcStat.clear();
+ mCollector.clear();
+ mCollectorPeer.clear();
+ }
- ASSERT_RESULT_OK(collectorPeer.init());
+ void checkDumpContents(int wantedEmptyCollectionInstances) {
+ TemporaryFile dump;
+ ASSERT_RESULT_OK(mCollector->onDump(dump.fd));
- const std::unordered_map<uid_t, UidIoUsage> uidIoUsages({
- {1009, {.uid = 1009, .ios = {0, 14000, 0, 16000, 0, 100}}},
- });
- const ProcStatInfo procStatInfo{
- /*stats=*/{2900, 7900, 4900, 8900, /*ioWaitTime=*/5900, 6966, 7980, 0, 0, 2930},
- /*runnableCnt=*/100,
- /*ioBlockedCnt=*/57,
- };
- const std::vector<ProcessStats> processStats({
- {.tgid = 100,
- .uid = 1009,
- .process = {100, "disk I/O", "D", 1, 11000, 1, 234},
- .threads = {{100, {100, "mount", "D", 1, 11000, 1, 234}}}},
- });
+ checkDumpFd(wantedEmptyCollectionInstances, dump.fd);
+ }
- EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
- EXPECT_CALL(*mockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
- EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(processStats));
+ void checkCustomDumpContents() {
+ TemporaryFile dump;
+ ASSERT_RESULT_OK(mCollector->onCustomCollectionDump(dump.fd));
- const IoPerfRecord expected = {
- .uidIoPerfData = {.topNReads = {{0, "mount", {0, 14000}, {0, 100}}},
- .topNWrites = {{0, "mount", {0, 16000}, {0, 100}}},
- .total = {{0, 14000}, {0, 16000}, {0, 100}}},
- .systemIoPerfData = {5900, 48376, 57, 157},
- .processIoPerfData =
- {.topNIoBlockedUids = {{0, "mount", 1, {{"disk I/O", 1}}}},
- .topNIoBlockedUidsTotalTaskCnt = {1},
- .topNMajorFaultUids = {{0, "mount", 11000, {{"disk I/O", 11000}}}},
- .totalMajorFaults = 11000,
- .majorFaultsPercentChange = 0},
- };
- collectorPeer.injectUidToPackageNameMapping({{1009, "mount"}});
+ checkDumpFd(/*wantedEmptyCollectionInstances=*/0, dump.fd);
+ }
+
+private:
+ void checkDumpFd(int wantedEmptyCollectionInstances, int fd) {
+ lseek(fd, 0, SEEK_SET);
+ std::string dumpContents;
+ ASSERT_TRUE(ReadFdToString(fd, &dumpContents));
+ ASSERT_FALSE(dumpContents.empty());
+
+ ASSERT_EQ(countOccurrences(dumpContents, kEmptyCollectionMessage),
+ wantedEmptyCollectionInstances)
+ << "Dump contents: " << dumpContents;
+ }
+
+protected:
+ sp<MockUidStatsCollector> mMockUidStatsCollector;
+ sp<MockProcStat> mMockProcStat;
+ sp<IoPerfCollection> mCollector;
+ sp<internal::IoPerfCollectionPeer> mCollectorPeer;
+};
+
+TEST_F(IoPerfCollectionTest, TestOnBoottimeCollection) {
+ const auto [uidStats, userPackageSummaryStats] = sampleUidStats();
+ const auto [procStatInfo, systemSummaryStats] = sampleProcStat();
+
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(uidStats));
+ EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
- ASSERT_RESULT_OK(
- collector->onBoottimeCollection(now, mockUidIoStats, mockProcStat, mockProcPidStat));
+ ASSERT_RESULT_OK(mCollector->onBoottimeCollection(now, mMockUidStatsCollector, mMockProcStat));
- const CollectionInfo& collectionInfo = collectorPeer.getBoottimeCollectionInfo();
+ const auto actual = mCollectorPeer->getBoottimeCollectionInfo();
- ASSERT_EQ(collectionInfo.maxCacheSize, std::numeric_limits<std::size_t>::max());
- ASSERT_EQ(collectionInfo.records.size(), 1);
- ASSERT_TRUE(isEqual(collectionInfo.records[0], expected))
- << "Boottime collection record doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(collectionInfo.records[0]);
+ const CollectionInfo expected{
+ .maxCacheSize = std::numeric_limits<std::size_t>::max(),
+ .records = {{
+ .systemSummaryStats = systemSummaryStats,
+ .userPackageSummaryStats = userPackageSummaryStats,
+ }},
+ };
- TemporaryFile dump;
- ASSERT_RESULT_OK(collector->onDump(dump.fd));
+ EXPECT_THAT(actual, CollectionInfoEq(expected))
+ << "Boottime collection info doesn't match.\nExpected:\n"
+ << expected.toString() << "\nActual:\n"
+ << actual.toString();
- lseek(dump.fd, 0, SEEK_SET);
- std::string dumpContents;
- ASSERT_TRUE(ReadFdToString(dump.fd, &dumpContents));
- ASSERT_FALSE(dumpContents.empty());
-
- ASSERT_EQ(countOccurrences(dumpContents, kEmptyCollectionMessage), 1)
- << "Only periodic collection should be not collected. Dump contents: " << dumpContents;
+ ASSERT_NO_FATAL_FAILURE(checkDumpContents(/*wantedEmptyCollectionInstances=*/1))
+ << "Periodic collection shouldn't be reported";
}
-TEST(IoPerfCollectionTest, TestPeriodicCollection) {
- sp<MockUidIoStats> mockUidIoStats = new MockUidIoStats();
- sp<MockProcStat> mockProcStat = new MockProcStat();
- sp<MockProcPidStat> mockProcPidStat = new MockProcPidStat();
+TEST_F(IoPerfCollectionTest, TestOnPeriodicCollection) {
+ const auto [uidStats, userPackageSummaryStats] = sampleUidStats();
+ const auto [procStatInfo, systemSummaryStats] = sampleProcStat();
- sp<IoPerfCollection> collector = new IoPerfCollection();
- internal::IoPerfCollectionPeer collectorPeer(collector);
-
- ASSERT_RESULT_OK(collectorPeer.init());
-
- const std::unordered_map<uid_t, UidIoUsage> uidIoUsages({
- {1009, {.uid = 1009, .ios = {0, 14000, 0, 16000, 0, 100}}},
- });
- const ProcStatInfo procStatInfo{
- /*stats=*/{2900, 7900, 4900, 8900, /*ioWaitTime=*/5900, 6966, 7980, 0, 0, 2930},
- /*runnableCnt=*/100,
- /*ioBlockedCnt=*/57,
- };
- const std::vector<ProcessStats> processStats({
- {.tgid = 100,
- .uid = 1009,
- .process = {100, "disk I/O", "D", 1, 11000, 1, 234},
- .threads = {{100, {100, "mount", "D", 1, 11000, 1, 234}}}},
- });
-
- EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
- EXPECT_CALL(*mockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
- EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(processStats));
-
- const IoPerfRecord expected = {
- .uidIoPerfData = {.topNReads = {{0, "mount", {0, 14000}, {0, 100}}},
- .topNWrites = {{0, "mount", {0, 16000}, {0, 100}}},
- .total = {{0, 14000}, {0, 16000}, {0, 100}}},
- .systemIoPerfData = {5900, 48376, 57, 157},
- .processIoPerfData =
- {.topNIoBlockedUids = {{0, "mount", 1, {{"disk I/O", 1}}}},
- .topNIoBlockedUidsTotalTaskCnt = {1},
- .topNMajorFaultUids = {{0, "mount", 11000, {{"disk I/O", 11000}}}},
- .totalMajorFaults = 11000,
- .majorFaultsPercentChange = 0},
- };
-
- collectorPeer.injectUidToPackageNameMapping({{1009, "mount"}});
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(uidStats));
+ EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
- ASSERT_RESULT_OK(collector->onPeriodicCollection(now, SystemState::NORMAL_MODE, mockUidIoStats,
- mockProcStat, mockProcPidStat));
+ ASSERT_RESULT_OK(mCollector->onPeriodicCollection(now, SystemState::NORMAL_MODE,
+ mMockUidStatsCollector, mMockProcStat));
- const CollectionInfo& collectionInfo = collectorPeer.getPeriodicCollectionInfo();
+ const auto actual = mCollectorPeer->getPeriodicCollectionInfo();
- ASSERT_EQ(collectionInfo.maxCacheSize,
- static_cast<size_t>(sysprop::periodicCollectionBufferSize().value_or(
- kDefaultPeriodicCollectionBufferSize)));
- ASSERT_EQ(collectionInfo.records.size(), 1);
- ASSERT_TRUE(isEqual(collectionInfo.records[0], expected))
- << "Periodic collection record doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(collectionInfo.records[0]);
+ const CollectionInfo expected{
+ .maxCacheSize = static_cast<size_t>(sysprop::periodicCollectionBufferSize().value_or(
+ kDefaultPeriodicCollectionBufferSize)),
+ .records = {{
+ .systemSummaryStats = systemSummaryStats,
+ .userPackageSummaryStats = userPackageSummaryStats,
+ }},
+ };
- TemporaryFile dump;
- ASSERT_RESULT_OK(collector->onDump(dump.fd));
+ EXPECT_THAT(actual, CollectionInfoEq(expected))
+ << "Periodic collection info doesn't match.\nExpected:\n"
+ << expected.toString() << "\nActual:\n"
+ << actual.toString();
- lseek(dump.fd, 0, SEEK_SET);
- std::string dumpContents;
- ASSERT_TRUE(ReadFdToString(dump.fd, &dumpContents));
- ASSERT_FALSE(dumpContents.empty());
-
- ASSERT_EQ(countOccurrences(dumpContents, kEmptyCollectionMessage), 1)
- << "Only boot-time collection should be not collected. Dump contents: " << dumpContents;
+ ASSERT_NO_FATAL_FAILURE(checkDumpContents(/*wantedEmptyCollectionInstances=*/1))
+ << "Boot-time collection shouldn't be reported";
}
-TEST(IoPerfCollectionTest, TestCustomCollection) {
- sp<MockUidIoStats> mockUidIoStats = new MockUidIoStats();
- sp<MockProcStat> mockProcStat = new MockProcStat();
- sp<MockProcPidStat> mockProcPidStat = new MockProcPidStat();
+TEST_F(IoPerfCollectionTest, TestOnCustomCollectionWithoutPackageFilter) {
+ const auto [uidStats, userPackageSummaryStats] = sampleUidStats();
+ const auto [procStatInfo, systemSummaryStats] = sampleProcStat();
- sp<IoPerfCollection> collector = new IoPerfCollection();
- internal::IoPerfCollectionPeer collectorPeer(collector);
-
- ASSERT_RESULT_OK(collectorPeer.init());
-
- // Filter by package name should ignore this limit.
- collectorPeer.setTopNStatsPerCategory(1);
-
- const std::unordered_map<uid_t, UidIoUsage> uidIoUsages({
- {1009, {.uid = 1009, .ios = {0, 14000, 0, 16000, 0, 100}}},
- {2001, {.uid = 2001, .ios = {0, 3400, 0, 6700, 0, 200}}},
- {3456, {.uid = 3456, .ios = {0, 4200, 0, 5600, 0, 300}}},
- });
- const ProcStatInfo procStatInfo{
- /*stats=*/{2900, 7900, 4900, 8900, /*ioWaitTime=*/5900, 6966, 7980, 0, 0, 2930},
- /*runnableCnt=*/100,
- /*ioBlockedCnt=*/57,
- };
- const std::vector<ProcessStats> processStats({
- {.tgid = 100,
- .uid = 1009,
- .process = {100, "cts_test", "D", 1, 50900, 2, 234},
- .threads = {{100, {100, "cts_test", "D", 1, 50900, 1, 234}},
- {200, {200, "cts_test_2", "D", 1, 0, 1, 290}}}},
- {.tgid = 1000,
- .uid = 2001,
- .process = {1000, "system_server", "D", 1, 1234, 1, 345},
- .threads = {{1000, {1000, "system_server", "D", 1, 1234, 1, 345}}}},
- {.tgid = 4000,
- .uid = 3456,
- .process = {4000, "random_process", "D", 1, 3456, 1, 890},
- .threads = {{4000, {4000, "random_process", "D", 1, 50900, 1, 890}}}},
- });
-
- EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
- EXPECT_CALL(*mockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
- EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(processStats));
- const IoPerfRecord expected = {
- .uidIoPerfData = {.topNReads = {{.userId = 0,
- .packageName = "android.car.cts",
- .bytes = {0, 14000},
- .fsync = {0, 100}},
- {.userId = 0,
- .packageName = "system_server",
- .bytes = {0, 3400},
- .fsync = {0, 200}}},
- .topNWrites = {{.userId = 0,
- .packageName = "android.car.cts",
- .bytes = {0, 16000},
- .fsync = {0, 100}},
- {.userId = 0,
- .packageName = "system_server",
- .bytes = {0, 6700},
- .fsync = {0, 200}}},
- .total = {{0, 21600}, {0, 28300}, {0, 600}}},
- .systemIoPerfData = {.cpuIoWaitTime = 5900,
- .totalCpuTime = 48376,
- .ioBlockedProcessesCnt = 57,
- .totalProcessesCnt = 157},
- .processIoPerfData =
- {.topNIoBlockedUids = {{0, "android.car.cts", 2, {{"cts_test", 2}}},
- {0, "system_server", 1, {{"system_server", 1}}}},
- .topNIoBlockedUidsTotalTaskCnt = {2, 1},
- .topNMajorFaultUids = {{0, "android.car.cts", 50900, {{"cts_test", 50900}}},
- {0, "system_server", 1234, {{"system_server", 1234}}}},
- .totalMajorFaults = 55590,
- .majorFaultsPercentChange = 0},
- };
- collectorPeer.injectUidToPackageNameMapping({
- {1009, "android.car.cts"},
- {2001, "system_server"},
- {3456, "random_process"},
- });
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(uidStats));
+ EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
- ASSERT_RESULT_OK(collector->onCustomCollection(now, SystemState::NORMAL_MODE,
- {"android.car.cts", "system_server"},
- mockUidIoStats, mockProcStat, mockProcPidStat));
+ ASSERT_RESULT_OK(mCollector->onCustomCollection(now, SystemState::NORMAL_MODE, {},
+ mMockUidStatsCollector, mMockProcStat));
- const CollectionInfo& collectionInfo = collectorPeer.getCustomCollectionInfo();
+ const auto actual = mCollectorPeer->getCustomCollectionInfo();
- EXPECT_EQ(collectionInfo.maxCacheSize, std::numeric_limits<std::size_t>::max());
- ASSERT_EQ(collectionInfo.records.size(), 1);
- ASSERT_TRUE(isEqual(collectionInfo.records[0], expected))
- << "Custom collection record doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(collectionInfo.records[0]);
+ CollectionInfo expected{
+ .maxCacheSize = std::numeric_limits<std::size_t>::max(),
+ .records = {{
+ .systemSummaryStats = systemSummaryStats,
+ .userPackageSummaryStats = userPackageSummaryStats,
+ }},
+ };
+
+ EXPECT_THAT(actual, CollectionInfoEq(expected))
+ << "Custom collection info doesn't match.\nExpected:\n"
+ << expected.toString() << "\nActual:\n"
+ << actual.toString();
+
+ ASSERT_NO_FATAL_FAILURE(checkCustomDumpContents()) << "Custom collection should be reported";
TemporaryFile customDump;
- ASSERT_RESULT_OK(collector->onCustomCollectionDump(customDump.fd));
-
- lseek(customDump.fd, 0, SEEK_SET);
- std::string customDumpContents;
- ASSERT_TRUE(ReadFdToString(customDump.fd, &customDumpContents));
- ASSERT_FALSE(customDumpContents.empty());
- ASSERT_EQ(countOccurrences(customDumpContents, kEmptyCollectionMessage), 0)
- << "Custom collection should be reported. Dump contents: " << customDumpContents;
+ ASSERT_RESULT_OK(mCollector->onCustomCollectionDump(customDump.fd));
// Should clear the cache.
- ASSERT_RESULT_OK(collector->onCustomCollectionDump(-1));
+ ASSERT_RESULT_OK(mCollector->onCustomCollectionDump(-1));
- const CollectionInfo& emptyCollectionInfo = collectorPeer.getCustomCollectionInfo();
- EXPECT_TRUE(emptyCollectionInfo.records.empty());
- EXPECT_EQ(emptyCollectionInfo.maxCacheSize, std::numeric_limits<std::size_t>::max());
+ expected.records.clear();
+ const CollectionInfo& emptyCollectionInfo = mCollectorPeer->getCustomCollectionInfo();
+ EXPECT_THAT(emptyCollectionInfo, CollectionInfoEq(expected))
+ << "Custom collection should be cleared.";
}
-TEST(IoPerfCollectionTest, TestUidIoStatsGreaterThanTopNStatsLimit) {
- std::unordered_map<uid_t, UidIoUsage> uidIoUsages({
- {1001234, {.uid = 1001234, .ios = {3000, 0, 500, 0, 20, 0}}},
- {1005678, {.uid = 1005678, .ios = {30, 100, 50, 200, 45, 60}}},
- {1009, {.uid = 1009, .ios = {0, 20000, 0, 30000, 0, 300}}},
- {1001000, {.uid = 1001000, .ios = {2000, 200, 1000, 100, 50, 10}}},
- });
- sp<MockUidIoStats> mockUidIoStats = new MockUidIoStats();
- EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
+TEST_F(IoPerfCollectionTest, TestOnCustomCollectionWithPackageFilter) {
+ // Filter by package name should ignore this limit with package filter.
+ mCollectorPeer->setTopNStatsPerCategory(1);
- struct UidIoPerfData expectedUidIoPerfData = {
- .topNReads = {{.userId = 0, // uid: 1009
- .packageName = "mount",
- .bytes = {0, 20000},
- .fsync = {0, 300}},
- {.userId = 10, // uid: 1001234
- .packageName = "1001234",
- .bytes = {3000, 0},
- .fsync = {20, 0}}},
- .topNWrites = {{.userId = 0, // uid: 1009
- .packageName = "mount",
- .bytes = {0, 30000},
- .fsync = {0, 300}},
- {.userId = 10, // uid: 1001000
- .packageName = "shared:android.uid.system",
- .bytes = {1000, 100},
- .fsync = {50, 10}}},
- .total = {{5030, 20300}, {1550, 30300}, {115, 370}},
- };
+ const auto [uidStats, _] = sampleUidStats();
+ const auto [procStatInfo, systemSummaryStats] = sampleProcStat();
- IoPerfCollection collector;
- collector.mTopNStatsPerCategory = 2;
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(uidStats));
+ EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
- sp<MockPackageInfoResolver> mockPackageInfoResolver = new MockPackageInfoResolver();
- collector.mPackageInfoResolver = mockPackageInfoResolver;
- EXPECT_CALL(*mockPackageInfoResolver, getPackageNamesForUids(_))
- .WillRepeatedly(Return<std::unordered_map<uid_t, std::string>>(
- {{1009, "mount"}, {1001000, "shared:android.uid.system"}}));
+ time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+ ASSERT_RESULT_OK(mCollector->onCustomCollection(now, SystemState::NORMAL_MODE,
+ {"mount", "com.google.android.car.kitchensink"},
+ mMockUidStatsCollector, mMockProcStat));
- struct UidIoPerfData actualUidIoPerfData = {};
- collector.processUidIoPerfData({}, mockUidIoStats, &actualUidIoPerfData);
+ const auto actual = mCollectorPeer->getCustomCollectionInfo();
- EXPECT_TRUE(isEqual(expectedUidIoPerfData, actualUidIoPerfData))
- << "First snapshot doesn't match.\nExpected:\n"
- << toString(expectedUidIoPerfData) << "\nActual:\n"
- << toString(actualUidIoPerfData);
-
- uidIoUsages = {
- {1001234, {.uid = 1001234, .ios = {4000, 0, 450, 0, 25, 0}}},
- {1005678, {.uid = 1005678, .ios = {10, 900, 0, 400, 5, 10}}},
- {1003456, {.uid = 1003456, .ios = {200, 0, 300, 0, 50, 0}}},
- {1001000, {.uid = 1001000, .ios = {0, 0, 0, 0, 0, 0}}},
- };
- EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
-
- expectedUidIoPerfData = {
- .topNReads = {{.userId = 10, // uid: 1001234
- .packageName = "1001234",
- .bytes = {4000, 0},
- .fsync = {25, 0}},
- {.userId = 10, // uid: 1005678
- .packageName = "1005678",
- .bytes = {10, 900},
- .fsync = {5, 10}}},
- .topNWrites = {{.userId = 10, // uid: 1001234
- .packageName = "1001234",
- .bytes = {450, 0},
- .fsync = {25, 0}},
- {.userId = 10, // uid: 1005678
- .packageName = "1005678",
- .bytes = {0, 400},
- .fsync = {5, 10}}},
- .total = {{4210, 900}, {750, 400}, {80, 10}},
- };
- actualUidIoPerfData = {};
- collector.processUidIoPerfData({}, mockUidIoStats, &actualUidIoPerfData);
-
- EXPECT_TRUE(isEqual(expectedUidIoPerfData, actualUidIoPerfData))
- << "Second snapshot doesn't match.\nExpected:\n"
- << toString(expectedUidIoPerfData) << "\nActual:\n"
- << toString(actualUidIoPerfData);
-}
-
-TEST(IoPerfCollectionTest, TestUidIOStatsLessThanTopNStatsLimit) {
- const std::unordered_map<uid_t, UidIoUsage> uidIoUsages(
- {{1001234, {.uid = 1001234, .ios = {3000, 0, 500, 0, 20, 0}}}});
-
- const struct UidIoPerfData expectedUidIoPerfData = {
- .topNReads = {{.userId = 10,
- .packageName = "1001234",
- .bytes = {3000, 0},
- .fsync = {20, 0}}},
- .topNWrites =
- {{.userId = 10, .packageName = "1001234", .bytes = {500, 0}, .fsync = {20, 0}}},
- .total = {{3000, 0}, {500, 0}, {20, 0}},
- };
-
- sp<MockUidIoStats> mockUidIoStats = new MockUidIoStats();
- EXPECT_CALL(*mockUidIoStats, deltaStats()).WillOnce(Return(uidIoUsages));
-
- IoPerfCollection collector;
- collector.mTopNStatsPerCategory = 10;
-
- struct UidIoPerfData actualUidIoPerfData = {};
- collector.processUidIoPerfData({}, mockUidIoStats, &actualUidIoPerfData);
-
- EXPECT_TRUE(isEqual(expectedUidIoPerfData, actualUidIoPerfData))
- << "Collected data doesn't match.\nExpected:\n"
- << toString(expectedUidIoPerfData) << "\nActual:\n"
- << toString(actualUidIoPerfData);
-}
-
-TEST(IoPerfCollectionTest, TestProcessSystemIoPerfData) {
- const ProcStatInfo procStatInfo(
- /*stats=*/{6200, 5700, 1700, 3100, 1100, 5200, 3900, 0, 0, 0},
- /*runnableCnt=*/17,
- /*ioBlockedCnt=*/5);
- struct SystemIoPerfData expectedSystemIoPerfData = {
- .cpuIoWaitTime = 1100,
- .totalCpuTime = 26900,
- .ioBlockedProcessesCnt = 5,
- .totalProcessesCnt = 22,
- };
-
- sp<MockProcStat> mockProcStat = new MockProcStat();
- EXPECT_CALL(*mockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
-
- IoPerfCollection collector;
- struct SystemIoPerfData actualSystemIoPerfData = {};
- collector.processSystemIoPerfData(mockProcStat, &actualSystemIoPerfData);
-
- EXPECT_TRUE(isEqual(expectedSystemIoPerfData, actualSystemIoPerfData))
- << "Expected:\n"
- << toString(expectedSystemIoPerfData) << "\nActual:\n"
- << toString(actualSystemIoPerfData);
-}
-
-TEST(IoPerfCollectionTest, TestProcPidContentsGreaterThanTopNStatsLimit) {
- const std::vector<ProcessStats> firstProcessStats({
- {.tgid = 1,
- .uid = 0,
- .process = {1, "init", "S", 0, 220, 2, 0},
- .threads = {{1, {1, "init", "S", 0, 200, 2, 0}},
- {453, {453, "init", "S", 0, 20, 2, 275}}}},
- {.tgid = 2456,
- .uid = 1001000,
- .process = {2456, "system_server", "R", 1, 6000, 3, 1000},
- .threads = {{2456, {2456, "system_server", "R", 1, 1000, 3, 1000}},
- {3456, {3456, "system_server", "S", 1, 3000, 3, 2300}},
- {4789, {4789, "system_server", "D", 1, 2000, 3, 4500}}}},
- {.tgid = 7890,
- .uid = 1001000,
- .process = {7890, "logd", "D", 1, 15000, 3, 2345},
- .threads = {{7890, {7890, "logd", "D", 1, 10000, 3, 2345}},
- {8978, {8978, "logd", "D", 1, 1000, 3, 2500}},
- {12890, {12890, "logd", "D", 1, 500, 3, 2900}}}},
- {.tgid = 18902,
- .uid = 1009,
- .process = {18902, "disk I/O", "D", 1, 45678, 3, 897654},
- .threads = {{18902, {18902, "disk I/O", "D", 1, 30000, 3, 897654}},
- {21345, {21345, "disk I/O", "D", 1, 15000, 3, 904000}},
- {32452, {32452, "disk I/O", "D", 1, 678, 3, 1007000}}}},
- {.tgid = 28900,
- .uid = 1001234,
- .process = {28900, "tombstoned", "D", 1, 89765, 1, 2345671},
- .threads = {{28900, {28900, "tombstoned", "D", 1, 89765, 1, 2345671}}}},
- });
- sp<MockProcPidStat> mockProcPidStat = new MockProcPidStat();
- EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(firstProcessStats));
-
- struct ProcessIoPerfData expectedProcessIoPerfData = {
- .topNIoBlockedUids = {{.userId = 10, // uid: 1001000
- .packageName = "shared:android.uid.system",
- .count = 4,
- .topNProcesses = {{"logd", 3}, {"system_server", 1}}},
- {.userId = 0,
- .packageName = "mount",
- .count = 3,
- .topNProcesses = {{"disk I/O", 3}}}},
- .topNIoBlockedUidsTotalTaskCnt = {6, 3},
- .topNMajorFaultUids = {{.userId = 10, // uid: 1001234
- .packageName = "1001234",
- .count = 89765,
- .topNProcesses = {{"tombstoned", 89765}}},
- {.userId = 0, // uid: 1009
- .packageName = "mount",
- .count = 45678,
- .topNProcesses = {{"disk I/O", 45678}}}},
- .totalMajorFaults = 156663,
+ UserPackageSummaryStats userPackageSummaryStats{
+ .topNIoReads = {{1009, "mount", UserPackageStats::IoStats{{0, 14'000}, {0, 100}}},
+ {1002001, "com.google.android.car.kitchensink",
+ UserPackageStats::IoStats{{0, 3'400}, {0, 200}}}},
+ .topNIoWrites = {{1009, "mount", UserPackageStats::IoStats{{0, 16'000}, {0, 100}}},
+ {1002001, "com.google.android.car.kitchensink",
+ UserPackageStats::IoStats{{0, 6'700}, {0, 200}}}},
+ .topNIoBlocked = {{1009, "mount", UserPackageStats::ProcStats{1, {{"disk I/O", 1}}}},
+ {1002001, "com.google.android.car.kitchensink",
+ UserPackageStats::ProcStats{3,
+ {{"CTS", 2}, {"KitchenSinkApp", 1}}}}},
+ .topNMajorFaults =
+ {{1009, "mount", UserPackageStats::ProcStats{11'000, {{"disk I/O", 11'000}}}},
+ {1002001, "com.google.android.car.kitchensink",
+ UserPackageStats::ProcStats{22'445,
+ {{"KitchenSinkApp", 12'345}, {"CTS", 10'100}}}}},
+ .totalIoStats = {{1000, 21'600}, {300, 28'300}, {600, 600}},
+ .taskCountByUid = {{1009, 1}, {1002001, 5}},
+ .totalMajorFaults = 84'345,
.majorFaultsPercentChange = 0.0,
};
- IoPerfCollection collector;
- collector.mTopNStatsPerCategory = 2;
- collector.mTopNStatsPerSubcategory = 2;
-
- sp<MockPackageInfoResolver> mockPackageInfoResolver = new MockPackageInfoResolver();
- collector.mPackageInfoResolver = mockPackageInfoResolver;
- EXPECT_CALL(*mockPackageInfoResolver, getPackageNamesForUids(_))
- .WillRepeatedly(Return<std::unordered_map<uid_t, std::string>>(
- {{0, "root"}, {1009, "mount"}, {1001000, "shared:android.uid.system"}}));
-
- struct ProcessIoPerfData actualProcessIoPerfData = {};
- collector.processProcessIoPerfDataLocked({}, mockProcPidStat, &actualProcessIoPerfData);
-
- EXPECT_TRUE(isEqual(expectedProcessIoPerfData, actualProcessIoPerfData))
- << "First snapshot doesn't match.\nExpected:\n"
- << toString(expectedProcessIoPerfData) << "\nActual:\n"
- << toString(actualProcessIoPerfData);
-
- const std::vector<ProcessStats> secondProcessStats({
- {.tgid = 1,
- .uid = 0,
- .process = {1, "init", "S", 0, 660, 2, 0},
- .threads = {{1, {1, "init", "S", 0, 600, 2, 0}},
- {453, {453, "init", "S", 0, 60, 2, 275}}}},
- {.tgid = 2546,
- .uid = 1001000,
- .process = {2546, "system_server", "R", 1, 12000, 3, 1000},
- .threads = {{2456, {2456, "system_server", "R", 1, 2000, 3, 1000}},
- {3456, {3456, "system_server", "S", 1, 6000, 3, 2300}},
- {4789, {4789, "system_server", "D", 1, 4000, 3, 4500}}}},
- });
- EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(secondProcessStats));
- expectedProcessIoPerfData = {
- .topNIoBlockedUids = {{.userId = 10, // uid: 1001000
- .packageName = "shared:android.uid.system",
- .count = 1,
- .topNProcesses = {{"system_server", 1}}}},
- .topNIoBlockedUidsTotalTaskCnt = {3},
- .topNMajorFaultUids = {{.userId = 10, // uid: 1001000
- .packageName = "shared:android.uid.system",
- .count = 12000,
- .topNProcesses = {{"system_server", 12000}}},
- {.userId = 0, // uid: 0
- .packageName = "root",
- .count = 660,
- .topNProcesses = {{"init", 660}}}},
- .totalMajorFaults = 12660,
- .majorFaultsPercentChange = ((12660.0 - 156663.0) / 156663.0) * 100,
+ CollectionInfo expected{
+ .maxCacheSize = std::numeric_limits<std::size_t>::max(),
+ .records = {{
+ .systemSummaryStats = systemSummaryStats,
+ .userPackageSummaryStats = userPackageSummaryStats,
+ }},
};
- actualProcessIoPerfData = {};
- collector.processProcessIoPerfDataLocked({}, mockProcPidStat, &actualProcessIoPerfData);
+ EXPECT_THAT(actual, CollectionInfoEq(expected))
+ << "Custom collection info doesn't match.\nExpected:\n"
+ << expected.toString() << "\nActual:\n"
+ << actual.toString();
- EXPECT_TRUE(isEqual(expectedProcessIoPerfData, actualProcessIoPerfData))
- << "Second snapshot doesn't match.\nExpected:\n"
- << toString(expectedProcessIoPerfData) << "\nActual:\n"
- << toString(actualProcessIoPerfData);
+ ASSERT_NO_FATAL_FAILURE(checkCustomDumpContents()) << "Custom collection should be reported";
+
+ TemporaryFile customDump;
+ ASSERT_RESULT_OK(mCollector->onCustomCollectionDump(customDump.fd));
+
+ // Should clear the cache.
+ ASSERT_RESULT_OK(mCollector->onCustomCollectionDump(-1));
+
+ expected.records.clear();
+ const CollectionInfo& emptyCollectionInfo = mCollectorPeer->getCustomCollectionInfo();
+ EXPECT_THAT(emptyCollectionInfo, CollectionInfoEq(expected))
+ << "Custom collection should be cleared.";
}
-TEST(IoPerfCollectionTest, TestProcPidContentsLessThanTopNStatsLimit) {
- const std::vector<ProcessStats> processStats({
- {.tgid = 1,
- .uid = 0,
- .process = {1, "init", "S", 0, 880, 2, 0},
- .threads = {{1, {1, "init", "S", 0, 800, 2, 0}},
- {453, {453, "init", "S", 0, 80, 2, 275}}}},
- });
- sp<MockProcPidStat> mockProcPidStat = new MockProcPidStat();
- EXPECT_CALL(*mockProcPidStat, deltaStats()).WillOnce(Return(processStats));
+TEST_F(IoPerfCollectionTest, TestOnPeriodicCollectionWithTrimmingStatsAfterTopN) {
+ mCollectorPeer->setTopNStatsPerCategory(1);
+ mCollectorPeer->setTopNStatsPerSubcategory(1);
- struct ProcessIoPerfData expectedProcessIoPerfData = {
- .topNMajorFaultUids = {{.userId = 0, // uid: 0
- .packageName = "root",
- .count = 880,
- .topNProcesses = {{"init", 880}}}},
- .totalMajorFaults = 880,
+ const auto [uidStats, _] = sampleUidStats();
+ const auto [procStatInfo, systemSummaryStats] = sampleProcStat();
+
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(uidStats));
+ EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(procStatInfo));
+
+ time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+ ASSERT_RESULT_OK(mCollector->onPeriodicCollection(now, SystemState::NORMAL_MODE,
+ mMockUidStatsCollector, mMockProcStat));
+
+ const auto actual = mCollectorPeer->getPeriodicCollectionInfo();
+
+ UserPackageSummaryStats userPackageSummaryStats{
+ .topNIoReads = {{1009, "mount", UserPackageStats::IoStats{{0, 14'000}, {0, 100}}}},
+ .topNIoWrites = {{1009, "mount", UserPackageStats::IoStats{{0, 16'000}, {0, 100}}}},
+ .topNIoBlocked = {{1002001, "com.google.android.car.kitchensink",
+ UserPackageStats::ProcStats{3, {{"CTS", 2}}}}},
+ .topNMajorFaults = {{1012345, "1012345",
+ UserPackageStats::ProcStats{50'900, {{"MapsApp", 50'900}}}}},
+ .totalIoStats = {{1000, 21'600}, {300, 28'300}, {600, 600}},
+ .taskCountByUid = {{1009, 1}, {1002001, 5}, {1012345, 4}},
+ .totalMajorFaults = 84'345,
.majorFaultsPercentChange = 0.0,
};
- IoPerfCollection collector;
- collector.mTopNStatsPerCategory = 5;
- collector.mTopNStatsPerSubcategory = 3;
+ const CollectionInfo expected{
+ .maxCacheSize = static_cast<size_t>(sysprop::periodicCollectionBufferSize().value_or(
+ kDefaultPeriodicCollectionBufferSize)),
+ .records = {{
+ .systemSummaryStats = systemSummaryStats,
+ .userPackageSummaryStats = userPackageSummaryStats,
+ }},
+ };
- sp<MockPackageInfoResolver> mockPackageInfoResolver = new MockPackageInfoResolver();
- collector.mPackageInfoResolver = mockPackageInfoResolver;
- EXPECT_CALL(*mockPackageInfoResolver, getPackageNamesForUids(_))
- .WillRepeatedly(Return<std::unordered_map<uid_t, std::string>>({{0, "root"}}));
+ EXPECT_THAT(actual, CollectionInfoEq(expected))
+ << "Periodic collection info doesn't match.\nExpected:\n"
+ << expected.toString() << "\nActual:\n"
+ << actual.toString();
- struct ProcessIoPerfData actualProcessIoPerfData = {};
- collector.processProcessIoPerfDataLocked({}, mockProcPidStat, &actualProcessIoPerfData);
+ ASSERT_NO_FATAL_FAILURE(checkDumpContents(/*wantedEmptyCollectionInstances=*/1))
+ << "Boot-time collection shouldn't be reported";
+}
- EXPECT_TRUE(isEqual(expectedProcessIoPerfData, actualProcessIoPerfData))
- << "proc pid contents don't match.\nExpected:\n"
- << toString(expectedProcessIoPerfData) << "\nActual:\n"
- << toString(actualProcessIoPerfData);
+TEST_F(IoPerfCollectionTest, TestConsecutiveOnPeriodicCollection) {
+ const auto [firstUidStats, firstUserPackageSummaryStats] = sampleUidStats();
+ const auto [firstProcStatInfo, firstSystemSummaryStats] = sampleProcStat();
+
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(firstUidStats));
+ EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(firstProcStatInfo));
+
+ time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+ ASSERT_RESULT_OK(mCollector->onPeriodicCollection(now, SystemState::NORMAL_MODE,
+ mMockUidStatsCollector, mMockProcStat));
+
+ auto [secondUidStats, secondUserPackageSummaryStats] = sampleUidStats(/*multiplier=*/2);
+ const auto [secondProcStatInfo, secondSystemSummaryStats] = sampleProcStat(/*multiplier=*/2);
+
+ secondUserPackageSummaryStats.majorFaultsPercentChange =
+ (static_cast<double>(secondUserPackageSummaryStats.totalMajorFaults -
+ firstUserPackageSummaryStats.totalMajorFaults) /
+ static_cast<double>(firstUserPackageSummaryStats.totalMajorFaults)) *
+ 100.0;
+
+ EXPECT_CALL(*mMockUidStatsCollector, deltaStats()).WillOnce(Return(secondUidStats));
+ EXPECT_CALL(*mMockProcStat, deltaStats()).WillOnce(Return(secondProcStatInfo));
+
+ ASSERT_RESULT_OK(mCollector->onPeriodicCollection(now, SystemState::NORMAL_MODE,
+ mMockUidStatsCollector, mMockProcStat));
+
+ const auto actual = mCollectorPeer->getPeriodicCollectionInfo();
+
+ const CollectionInfo expected{
+ .maxCacheSize = static_cast<size_t>(sysprop::periodicCollectionBufferSize().value_or(
+ kDefaultPeriodicCollectionBufferSize)),
+ .records = {{.systemSummaryStats = firstSystemSummaryStats,
+ .userPackageSummaryStats = firstUserPackageSummaryStats},
+ {.systemSummaryStats = secondSystemSummaryStats,
+ .userPackageSummaryStats = secondUserPackageSummaryStats}},
+ };
+
+ EXPECT_THAT(actual, CollectionInfoEq(expected))
+ << "Periodic collection info doesn't match.\nExpected:\n"
+ << expected.toString() << "\nActual:\n"
+ << actual.toString();
+
+ ASSERT_NO_FATAL_FAILURE(checkDumpContents(/*wantedEmptyCollectionInstances=*/1))
+ << "Boot-time collection shouldn't be reported";
}
} // namespace watchdog
diff --git a/cpp/watchdog/server/tests/MockCarWatchdogServiceForSystem.h b/cpp/watchdog/server/tests/MockCarWatchdogServiceForSystem.h
index d484463..07997da 100644
--- a/cpp/watchdog/server/tests/MockCarWatchdogServiceForSystem.h
+++ b/cpp/watchdog/server/tests/MockCarWatchdogServiceForSystem.h
@@ -53,6 +53,9 @@
(override));
MOCK_METHOD(android::binder::Status, resetResourceOveruseStats,
(const std::vector<std::string>&), (override));
+ MOCK_METHOD(android::binder::Status, getTodayIoUsageStats,
+ (std::vector<android::automotive::watchdog::internal::UserPackageIoUsageStats>*),
+ (override));
private:
android::sp<MockBinder> mBinder;
diff --git a/cpp/watchdog/server/tests/MockDataProcessor.h b/cpp/watchdog/server/tests/MockDataProcessor.h
index d9a2600..b9ea48c 100644
--- a/cpp/watchdog/server/tests/MockDataProcessor.h
+++ b/cpp/watchdog/server/tests/MockDataProcessor.h
@@ -30,24 +30,22 @@
MockDataProcessor() {
EXPECT_CALL(*this, name()).WillRepeatedly(::testing::Return("MockedDataProcessor"));
}
- MOCK_METHOD(std::string, name, (), (override));
+ MOCK_METHOD(std::string, name, (), (const, override));
MOCK_METHOD(android::base::Result<void>, init, (), (override));
MOCK_METHOD(void, terminate, (), (override));
MOCK_METHOD(android::base::Result<void>, onBoottimeCollection,
- (time_t, const wp<UidIoStats>&, const wp<ProcStat>&, const wp<ProcPidStat>&),
- (override));
+ (time_t, const wp<UidStatsCollectorInterface>&, const wp<ProcStat>&), (override));
MOCK_METHOD(android::base::Result<void>, onPeriodicCollection,
- (time_t, SystemState, const wp<UidIoStats>&, const wp<ProcStat>&,
- const wp<ProcPidStat>&),
+ (time_t, SystemState, const wp<UidStatsCollectorInterface>&, const wp<ProcStat>&),
(override));
MOCK_METHOD(android::base::Result<void>, onCustomCollection,
- (time_t, SystemState, const std::unordered_set<std::string>&, const wp<UidIoStats>&,
- const wp<ProcStat>&, const wp<ProcPidStat>&),
+ (time_t, SystemState, const std::unordered_set<std::string>&,
+ const wp<UidStatsCollectorInterface>&, const wp<ProcStat>&),
(override));
MOCK_METHOD(android::base::Result<void>, onPeriodicMonitor,
(time_t, const android::wp<IProcDiskStatsInterface>&, const std::function<void()>&),
(override));
- MOCK_METHOD(android::base::Result<void>, onDump, (int), (override));
+ MOCK_METHOD(android::base::Result<void>, onDump, (int), (const, override));
MOCK_METHOD(android::base::Result<void>, onCustomCollectionDump, (int), (override));
};
diff --git a/cpp/watchdog/server/tests/MockIoOveruseConfigs.h b/cpp/watchdog/server/tests/MockIoOveruseConfigs.h
index 22c23f7..6859e18 100644
--- a/cpp/watchdog/server/tests/MockIoOveruseConfigs.h
+++ b/cpp/watchdog/server/tests/MockIoOveruseConfigs.h
@@ -43,7 +43,7 @@
MOCK_METHOD(
void, get,
(std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*),
- (override));
+ (const, override));
MOCK_METHOD(android::base::Result<void>, writeToDisk, (), (override));
diff --git a/cpp/watchdog/server/tests/MockIoOveruseMonitor.h b/cpp/watchdog/server/tests/MockIoOveruseMonitor.h
index 79ba932..a9d677e 100644
--- a/cpp/watchdog/server/tests/MockIoOveruseMonitor.h
+++ b/cpp/watchdog/server/tests/MockIoOveruseMonitor.h
@@ -35,8 +35,8 @@
ON_CALL(*this, name()).WillByDefault(::testing::Return("MockIoOveruseMonitor"));
}
~MockIoOveruseMonitor() {}
- MOCK_METHOD(bool, isInitialized, (), (override));
- MOCK_METHOD(bool, dumpHelpText, (int), (override));
+ MOCK_METHOD(bool, isInitialized, (), (const, override));
+ MOCK_METHOD(bool, dumpHelpText, (int), (const, override));
MOCK_METHOD(android::base::Result<void>, updateResourceOveruseConfigurations,
(const std::vector<
android::automotive::watchdog::internal::ResourceOveruseConfiguration>&),
@@ -44,7 +44,7 @@
MOCK_METHOD(
android::base::Result<void>, getResourceOveruseConfigurations,
(std::vector<android::automotive::watchdog::internal::ResourceOveruseConfiguration>*),
- (override));
+ (const, override));
MOCK_METHOD(android::base::Result<void>, actionTakenOnIoOveruse,
(const std::vector<
android::automotive::watchdog::internal::PackageResourceOveruseAction>&
@@ -54,7 +54,8 @@
(const sp<IResourceOveruseListener>&), (override));
MOCK_METHOD(android::base::Result<void>, removeIoOveruseListener,
(const sp<IResourceOveruseListener>&), (override));
- MOCK_METHOD(android::base::Result<void>, getIoOveruseStats, (IoOveruseStats*), (override));
+ MOCK_METHOD(android::base::Result<void>, getIoOveruseStats, (IoOveruseStats*),
+ (const, override));
MOCK_METHOD(android::base::Result<void>, resetIoOveruseStats, (const std::vector<std::string>&),
(override));
};
diff --git a/cpp/watchdog/server/tests/MockProcPidStat.h b/cpp/watchdog/server/tests/MockProcPidStat.h
deleted file mode 100644
index acc8b0c..0000000
--- a/cpp/watchdog/server/tests/MockProcPidStat.h
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#ifndef CPP_WATCHDOG_SERVER_TESTS_MOCKPROCPIDSTAT_H_
-#define CPP_WATCHDOG_SERVER_TESTS_MOCKPROCPIDSTAT_H_
-
-#include "ProcPidStat.h"
-
-#include <android-base/result.h>
-#include <gmock/gmock.h>
-
-#include <string>
-#include <unordered_map>
-#include <vector>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-class MockProcPidStat : public ProcPidStat {
-public:
- MockProcPidStat() { ON_CALL(*this, enabled()).WillByDefault(::testing::Return(true)); }
- MOCK_METHOD(bool, enabled, (), (override));
- MOCK_METHOD(android::base::Result<void>, collect, (), (override));
- MOCK_METHOD((const std::unordered_map<pid_t, ProcessStats>), latestStats, (),
- (const, override));
- MOCK_METHOD(const std::vector<ProcessStats>, deltaStats, (), (const, override));
- MOCK_METHOD(std::string, dirPath, (), (override));
-};
-
-} // namespace watchdog
-} // namespace automotive
-} // namespace android
-
-#endif // CPP_WATCHDOG_SERVER_TESTS_MOCKPROCPIDSTAT_H_
diff --git a/cpp/watchdog/server/tests/MockUidIoStats.h b/cpp/watchdog/server/tests/MockUidIoStats.h
deleted file mode 100644
index cf30d9f..0000000
--- a/cpp/watchdog/server/tests/MockUidIoStats.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2020, 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.
- */
-
-#ifndef CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATS_H_
-#define CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATS_H_
-
-#include "UidIoStats.h"
-
-#include <android-base/result.h>
-#include <gmock/gmock.h>
-
-#include <string>
-#include <unordered_map>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-class MockUidIoStats : public UidIoStats {
-public:
- MockUidIoStats() { ON_CALL(*this, enabled()).WillByDefault(::testing::Return(true)); }
- MOCK_METHOD(bool, enabled, (), (override));
- MOCK_METHOD(android::base::Result<void>, collect, (), (override));
- MOCK_METHOD((const std::unordered_map<uid_t, UidIoUsage>), latestStats, (), (const, override));
- MOCK_METHOD((const std::unordered_map<uid_t, UidIoUsage>), deltaStats, (), (const, override));
- MOCK_METHOD(std::string, filePath, (), (override));
-
- void expectDeltaStats(const std::unordered_map<uid_t, IoUsage>& deltaStats) {
- std::unordered_map<uid_t, UidIoUsage> stats;
- for (const auto& [uid, ios] : deltaStats) {
- stats[uid] = UidIoUsage{.uid = uid, .ios = ios};
- }
- EXPECT_CALL(*this, deltaStats()).WillOnce(::testing::Return(stats));
- }
-};
-
-} // namespace watchdog
-} // namespace automotive
-} // namespace android
-
-#endif // CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATS_H_
diff --git a/cpp/watchdog/server/tests/MockUidIoStatsCollector.h b/cpp/watchdog/server/tests/MockUidIoStatsCollector.h
new file mode 100644
index 0000000..7fe8fc9
--- /dev/null
+++ b/cpp/watchdog/server/tests/MockUidIoStatsCollector.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATSCOLLECTOR_H_
+
+#include "UidIoStatsCollector.h"
+
+#include <android-base/result.h>
+#include <gmock/gmock.h>
+
+#include <string>
+#include <unordered_map>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+class MockUidIoStatsCollector : public UidIoStatsCollectorInterface {
+public:
+ MockUidIoStatsCollector() { ON_CALL(*this, enabled()).WillByDefault(::testing::Return(true)); }
+ MOCK_METHOD(android::base::Result<void>, collect, (), (override));
+ MOCK_METHOD((const std::unordered_map<uid_t, UidIoStats>), latestStats, (), (const, override));
+ MOCK_METHOD((const std::unordered_map<uid_t, UidIoStats>), deltaStats, (), (const, override));
+ MOCK_METHOD(bool, enabled, (), (const, override));
+ MOCK_METHOD(const std::string, filePath, (), (const, override));
+};
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
+
+#endif // CPP_WATCHDOG_SERVER_TESTS_MOCKUIDIOSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/tests/MockUidProcStatsCollector.h b/cpp/watchdog/server/tests/MockUidProcStatsCollector.h
new file mode 100644
index 0000000..a16fa37
--- /dev/null
+++ b/cpp/watchdog/server/tests/MockUidProcStatsCollector.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2020, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_TESTS_MOCKUIDPROCSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_TESTS_MOCKUIDPROCSTATSCOLLECTOR_H_
+
+#include "UidProcStatsCollector.h"
+
+#include <android-base/result.h>
+#include <gmock/gmock.h>
+
+#include <string>
+#include <unordered_map>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+class MockUidProcStatsCollector : public UidProcStatsCollectorInterface {
+public:
+ MockUidProcStatsCollector() {
+ ON_CALL(*this, enabled()).WillByDefault(::testing::Return(true));
+ }
+ MOCK_METHOD(android::base::Result<void>, collect, (), (override));
+ MOCK_METHOD((const std::unordered_map<uid_t, UidProcStats>), latestStats, (),
+ (const, override));
+ MOCK_METHOD((const std::unordered_map<uid_t, UidProcStats>), deltaStats, (), (const, override));
+ MOCK_METHOD(bool, enabled, (), (const, override));
+ MOCK_METHOD(const std::string, dirPath, (), (const, override));
+};
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
+
+#endif // CPP_WATCHDOG_SERVER_TESTS_MOCKUIDPROCSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/tests/MockUidStatsCollector.h b/cpp/watchdog/server/tests/MockUidStatsCollector.h
new file mode 100644
index 0000000..45671c1
--- /dev/null
+++ b/cpp/watchdog/server/tests/MockUidStatsCollector.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_TESTS_MOCKUIDSTATSCOLLECTOR_H_
+#define CPP_WATCHDOG_SERVER_TESTS_MOCKUIDSTATSCOLLECTOR_H_
+
+#include "UidIoStatsCollector.h"
+
+#include <android-base/result.h>
+#include <gmock/gmock.h>
+
+#include <string>
+#include <unordered_map>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+class MockUidStatsCollector : public UidStatsCollectorInterface {
+public:
+ MockUidStatsCollector() { ON_CALL(*this, enabled()).WillByDefault(::testing::Return(true)); }
+ MOCK_METHOD(android::base::Result<void>, collect, (), (override));
+ MOCK_METHOD((const std::vector<UidStats>), latestStats, (), (const, override));
+ MOCK_METHOD((const std::vector<UidStats>), deltaStats, (), (const, override));
+ MOCK_METHOD(bool, enabled, (), (const, override));
+};
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
+
+#endif // CPP_WATCHDOG_SERVER_TESTS_MOCKUIDSTATSCOLLECTOR_H_
diff --git a/cpp/watchdog/server/tests/MockWatchdogPerfService.h b/cpp/watchdog/server/tests/MockWatchdogPerfService.h
index 6f0fb51..114c308 100644
--- a/cpp/watchdog/server/tests/MockWatchdogPerfService.h
+++ b/cpp/watchdog/server/tests/MockWatchdogPerfService.h
@@ -40,8 +40,8 @@
MOCK_METHOD(android::base::Result<void>, onBootFinished, (), (override));
MOCK_METHOD(android::base::Result<void>, onCustomCollection,
(int fd, const Vector<android::String16>& args), (override));
- MOCK_METHOD(android::base::Result<void>, onDump, (int fd), (override));
- MOCK_METHOD(bool, dumpHelpText, (int fd), (override));
+ MOCK_METHOD(android::base::Result<void>, onDump, (int fd), (const, override));
+ MOCK_METHOD(bool, dumpHelpText, (int fd), (const, override));
MOCK_METHOD(void, handleMessage, (const Message&), (override));
};
diff --git a/cpp/watchdog/server/tests/MockWatchdogServiceHelper.h b/cpp/watchdog/server/tests/MockWatchdogServiceHelper.h
index 3d71345..171d48c 100644
--- a/cpp/watchdog/server/tests/MockWatchdogServiceHelper.h
+++ b/cpp/watchdog/server/tests/MockWatchdogServiceHelper.h
@@ -61,6 +61,9 @@
(override));
MOCK_METHOD(android::binder::Status, resetResourceOveruseStats,
(const std::vector<std::string>&), (override));
+ MOCK_METHOD(android::binder::Status, getTodayIoUsageStats,
+ (std::vector<android::automotive::watchdog::internal::UserPackageIoUsageStats>*),
+ (override));
MOCK_METHOD(void, terminate, (), (override));
};
diff --git a/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.cpp b/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.cpp
index 1bf270d..9b67caf 100644
--- a/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.cpp
+++ b/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.cpp
@@ -154,15 +154,15 @@
return threshold;
}
-Matcher<const ResourceOveruseConfiguration> ResourceOveruseConfigurationMatcher(
+Matcher<const ResourceOveruseConfiguration&> ResourceOveruseConfigurationMatcher(
const ResourceOveruseConfiguration& config) {
- std::vector<Matcher<const ResourceSpecificConfiguration>> resourceSpecificConfigMatchers;
+ std::vector<Matcher<const ResourceSpecificConfiguration&>> resourceSpecificConfigMatchers;
for (const auto& resourceSpecificConfig : config.resourceSpecificConfigurations) {
resourceSpecificConfigMatchers.push_back(
IsResourceSpecificConfiguration(resourceSpecificConfig));
}
- std::vector<Matcher<const PackageMetadata>> metadataMatchers;
+ std::vector<Matcher<const PackageMetadata&>> metadataMatchers;
for (const auto& metadata : config.packageMetadata) {
metadataMatchers.push_back(IsPackageMetadata(metadata));
}
diff --git a/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.h b/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.h
index 84c970a..a8991c7 100644
--- a/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.h
+++ b/cpp/watchdog/server/tests/OveruseConfigurationTestUtils.h
@@ -77,7 +77,7 @@
android::automotive::watchdog::internal::IoOveruseAlertThreshold toIoOveruseAlertThreshold(
const int64_t durationInSeconds, const int64_t writtenBytesPerSecond);
-testing::Matcher<const android::automotive::watchdog::internal::ResourceOveruseConfiguration>
+testing::Matcher<const android::automotive::watchdog::internal::ResourceOveruseConfiguration&>
ResourceOveruseConfigurationMatcher(
const android::automotive::watchdog::internal::ResourceOveruseConfiguration& config);
diff --git a/cpp/watchdog/server/tests/PackageInfoResolverTest.cpp b/cpp/watchdog/server/tests/PackageInfoResolverTest.cpp
index f2fe3a6..cf68dda 100644
--- a/cpp/watchdog/server/tests/PackageInfoResolverTest.cpp
+++ b/cpp/watchdog/server/tests/PackageInfoResolverTest.cpp
@@ -16,6 +16,7 @@
#include "MockWatchdogServiceHelper.h"
#include "PackageInfoResolver.h"
+#include "PackageInfoTestUtils.h"
#include <android-base/stringprintf.h>
#include <android/automotive/watchdog/internal/ApplicationCategoryType.h>
@@ -48,20 +49,6 @@
std::unordered_map<std::string,
android::automotive::watchdog::internal::ApplicationCategoryType>;
-PackageInfo constructPackageInfo(const char* packageName, int32_t uid, UidType uidType,
- ComponentType componentType,
- ApplicationCategoryType appCategoryType,
- std::vector<std::string> sharedUidPackages = {}) {
- PackageInfo packageInfo;
- packageInfo.packageIdentifier.name = packageName;
- packageInfo.packageIdentifier.uid = uid;
- packageInfo.uidType = uidType;
- packageInfo.componentType = componentType;
- packageInfo.appCategoryType = appCategoryType;
- packageInfo.sharedUidPackages = sharedUidPackages;
- return packageInfo;
-}
-
std::string toString(const std::unordered_map<uid_t, PackageInfo>& mappings) {
std::string buffer = "{";
for (const auto& [uid, info] : mappings) {
@@ -195,6 +182,8 @@
// system.package.B is native package so this should be ignored.
{"system.package.B", ApplicationCategoryType::MAPS},
{"vendor.package.A", ApplicationCategoryType::MEDIA},
+ {"shared:vendor.package.C", ApplicationCategoryType::MEDIA},
+ {"vendor.package.shared.uid.D", ApplicationCategoryType::MAPS},
};
peer.setPackageConfigurations({"vendor.pkg"}, packagesToAppCategories);
/*
@@ -213,23 +202,38 @@
ApplicationCategoryType::OTHERS)},
{15100,
constructPackageInfo("vendor.package.A", 15100, UidType::APPLICATION,
- ComponentType::VENDOR, ApplicationCategoryType::MEDIA)},
+ ComponentType::VENDOR, ApplicationCategoryType::OTHERS)},
{16700,
constructPackageInfo("vendor.pkg", 16700, UidType::NATIVE, ComponentType::VENDOR,
ApplicationCategoryType::OTHERS)},
+ {18100,
+ constructPackageInfo("shared:vendor.package.C", 18100, UidType::APPLICATION,
+ ComponentType::VENDOR, ApplicationCategoryType::OTHERS)},
+ {19100,
+ constructPackageInfo("shared:vendor.package.D", 19100, UidType::APPLICATION,
+ ComponentType::VENDOR, ApplicationCategoryType::OTHERS,
+ {"vendor.package.shared.uid.D"})},
};
- std::vector<int32_t> expectedUids = {6100, 7700, 15100, 16700};
+ std::vector<int32_t> expectedUids = {6100, 7700, 15100, 16700, 18100, 19100};
std::vector<std::string> expectedPrefixes = {"vendor.pkg"};
std::vector<PackageInfo> injectPackageInfos = {expectedMappings.at(6100),
expectedMappings.at(7700),
expectedMappings.at(15100),
- expectedMappings.at(16700)};
+ expectedMappings.at(16700),
+ expectedMappings.at(18100),
+ expectedMappings.at(19100)};
+
+ expectedMappings.at(15100).appCategoryType = ApplicationCategoryType::MEDIA;
+ expectedMappings.at(18100).appCategoryType = ApplicationCategoryType::MEDIA;
+ expectedMappings.at(19100).appCategoryType = ApplicationCategoryType::MAPS;
+
EXPECT_CALL(*peer.mockWatchdogServiceHelper,
getPackageInfosForUids(expectedUids, expectedPrefixes, _))
.WillOnce(DoAll(SetArgPointee<2>(injectPackageInfos), Return(binder::Status::ok())));
- auto actualMappings = packageInfoResolver->getPackageInfosForUids({6100, 7700, 15100, 16700});
+ auto actualMappings =
+ packageInfoResolver->getPackageInfosForUids({6100, 7700, 15100, 16700, 18100, 19100});
EXPECT_THAT(actualMappings, UnorderedElementsAreArray(expectedMappings))
<< "Expected: " << toString(expectedMappings)
diff --git a/cpp/watchdog/server/tests/PackageInfoTestUtils.cpp b/cpp/watchdog/server/tests/PackageInfoTestUtils.cpp
new file mode 100644
index 0000000..0834b2c
--- /dev/null
+++ b/cpp/watchdog/server/tests/PackageInfoTestUtils.cpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#include "PackageInfoTestUtils.h"
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::automotive::watchdog::internal::ApplicationCategoryType;
+using ::android::automotive::watchdog::internal::ComponentType;
+using ::android::automotive::watchdog::internal::PackageInfo;
+using ::android::automotive::watchdog::internal::UidType;
+
+PackageInfo constructPackageInfo(const char* packageName, int32_t uid, UidType uidType,
+ ComponentType componentType,
+ ApplicationCategoryType appCategoryType,
+ std::vector<std::string> sharedUidPackages) {
+ PackageInfo packageInfo;
+ packageInfo.packageIdentifier.name = packageName;
+ packageInfo.packageIdentifier.uid = uid;
+ packageInfo.uidType = uidType;
+ packageInfo.componentType = componentType;
+ packageInfo.appCategoryType = appCategoryType;
+ packageInfo.sharedUidPackages = sharedUidPackages;
+ return packageInfo;
+}
+
+PackageInfo constructAppPackageInfo(const char* packageName, const ComponentType componentType,
+ const ApplicationCategoryType appCategoryType,
+ const std::vector<std::string>& sharedUidPackages) {
+ return constructPackageInfo(packageName, 0, UidType::APPLICATION, componentType,
+ appCategoryType, sharedUidPackages);
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
diff --git a/cpp/watchdog/server/tests/PackageInfoTestUtils.h b/cpp/watchdog/server/tests/PackageInfoTestUtils.h
new file mode 100644
index 0000000..fc38434
--- /dev/null
+++ b/cpp/watchdog/server/tests/PackageInfoTestUtils.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_TESTS_PACKAGEINFOTESTUTILS_H_
+#define CPP_WATCHDOG_SERVER_TESTS_PACKAGEINFOTESTUTILS_H_
+
+#include <android/automotive/watchdog/internal/ApplicationCategoryType.h>
+#include <android/automotive/watchdog/internal/ComponentType.h>
+#include <android/automotive/watchdog/internal/PackageInfo.h>
+#include <android/automotive/watchdog/internal/UidType.h>
+#include <gmock/gmock.h>
+
+#include <string>
+#include <vector>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+android::automotive::watchdog::internal::PackageInfo constructPackageInfo(
+ const char* packageName, int32_t uid,
+ android::automotive::watchdog::internal::UidType uidType =
+ android::automotive::watchdog::internal::UidType::NATIVE,
+ android::automotive::watchdog::internal::ComponentType componentType =
+ android::automotive::watchdog::internal::ComponentType::UNKNOWN,
+ android::automotive::watchdog::internal::ApplicationCategoryType appCategoryType =
+ android::automotive::watchdog::internal::ApplicationCategoryType::OTHERS,
+ std::vector<std::string> sharedUidPackages = {});
+
+android::automotive::watchdog::internal::PackageInfo constructAppPackageInfo(
+ const char* packageName,
+ const android::automotive::watchdog::internal::ComponentType componentType,
+ const android::automotive::watchdog::internal::ApplicationCategoryType appCategoryType =
+ android::automotive::watchdog::internal::ApplicationCategoryType::OTHERS,
+ const std::vector<std::string>& sharedUidPackages = {});
+
+MATCHER_P(PackageIdentifierEq, expected, "") {
+ const auto& actual = arg;
+ return ::testing::Value(actual.name, ::testing::Eq(expected.name)) &&
+ ::testing::Value(actual.uid, ::testing::Eq(expected.uid));
+}
+
+MATCHER_P(PackageInfoEq, expected, "") {
+ const auto& actual = arg;
+ return ::testing::Value(actual.packageIdentifier,
+ PackageIdentifierEq(expected.packageIdentifier)) &&
+ ::testing::Value(actual.uidType, ::testing::Eq(expected.uidType)) &&
+ ::testing::Value(actual.sharedUidPackages,
+ ::testing::UnorderedElementsAreArray(expected.sharedUidPackages)) &&
+ ::testing::Value(actual.componentType, ::testing::Eq(expected.componentType)) &&
+ ::testing::Value(actual.appCategoryType, ::testing::Eq(expected.appCategoryType));
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
+
+#endif // CPP_WATCHDOG_SERVER_TESTS_PACKAGEINFOTESTUTILS_H_
diff --git a/cpp/watchdog/server/tests/ProcPidDir.cpp b/cpp/watchdog/server/tests/ProcPidDir.cpp
index 134d231..b6662d7 100644
--- a/cpp/watchdog/server/tests/ProcPidDir.cpp
+++ b/cpp/watchdog/server/tests/ProcPidDir.cpp
@@ -16,10 +16,11 @@
#include "ProcPidDir.h"
-#include "ProcPidStat.h"
+#include "UidProcStatsCollector.h"
#include <android-base/file.h>
#include <android-base/result.h>
+
#include <errno.h>
namespace android {
diff --git a/cpp/watchdog/server/tests/ProcPidStatTest.cpp b/cpp/watchdog/server/tests/ProcPidStatTest.cpp
deleted file mode 100644
index 8c3afbc..0000000
--- a/cpp/watchdog/server/tests/ProcPidStatTest.cpp
+++ /dev/null
@@ -1,553 +0,0 @@
-/*
- * Copyright 2020 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.
- */
-
-#include "ProcPidStat.h"
-
-#include "ProcPidDir.h"
-
-#include <android-base/file.h>
-#include <android-base/stringprintf.h>
-#include <gmock/gmock.h>
-#include <inttypes.h>
-
-#include <algorithm>
-#include <string>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-using ::android::automotive::watchdog::testing::populateProcPidDir;
-using ::android::base::StringAppendF;
-using ::android::base::StringPrintf;
-
-namespace {
-
-std::string toString(const PidStat& stat) {
- return StringPrintf("PID: %" PRIu32 ", PPID: %" PRIu32 ", Comm: %s, State: %s, "
- "Major page faults: %" PRIu64 ", Num threads: %" PRIu32
- ", Start time: %" PRIu64,
- stat.pid, stat.ppid, stat.comm.c_str(), stat.state.c_str(),
- stat.majorFaults, stat.numThreads, stat.startTime);
-}
-
-std::string toString(const ProcessStats& stats) {
- std::string buffer;
- StringAppendF(&buffer,
- "Tgid: %" PRIi64 ", UID: %" PRIi64 ", VmPeak: %" PRIu64 ", VmSize: %" PRIu64
- ", VmHWM: %" PRIu64 ", VmRSS: %" PRIu64 ", %s\n",
- stats.tgid, stats.uid, stats.vmPeakKb, stats.vmSizeKb, stats.vmHwmKb,
- stats.vmRssKb, toString(stats.process).c_str());
- StringAppendF(&buffer, "\tThread stats:\n");
- for (const auto& it : stats.threads) {
- StringAppendF(&buffer, "\t\t%s\n", toString(it.second).c_str());
- }
- StringAppendF(&buffer, "\n");
- return buffer;
-}
-
-std::string toString(const std::vector<ProcessStats>& stats) {
- std::string buffer;
- StringAppendF(&buffer, "Number of processes: %d\n", static_cast<int>(stats.size()));
- for (const auto& it : stats) {
- StringAppendF(&buffer, "%s", toString(it).c_str());
- }
- return buffer;
-}
-
-bool isEqual(const PidStat& lhs, const PidStat& rhs) {
- return lhs.pid == rhs.pid && lhs.comm == rhs.comm && lhs.state == rhs.state &&
- lhs.ppid == rhs.ppid && lhs.majorFaults == rhs.majorFaults &&
- lhs.numThreads == rhs.numThreads && lhs.startTime == rhs.startTime;
-}
-
-bool isEqual(std::vector<ProcessStats>* lhs, std::vector<ProcessStats>* rhs) {
- if (lhs->size() != rhs->size()) {
- return false;
- }
- std::sort(lhs->begin(), lhs->end(), [&](const ProcessStats& l, const ProcessStats& r) -> bool {
- return l.process.pid < r.process.pid;
- });
- std::sort(rhs->begin(), rhs->end(), [&](const ProcessStats& l, const ProcessStats& r) -> bool {
- return l.process.pid < r.process.pid;
- });
- return std::equal(lhs->begin(), lhs->end(), rhs->begin(),
- [&](const ProcessStats& l, const ProcessStats& r) -> bool {
- if (l.tgid != r.tgid || l.uid != r.uid || l.vmPeakKb != r.vmPeakKb ||
- l.vmSizeKb != r.vmSizeKb || l.vmHwmKb != r.vmHwmKb ||
- l.vmRssKb != r.vmRssKb || !isEqual(l.process, r.process) ||
- l.threads.size() != r.threads.size()) {
- return false;
- }
- for (const auto& lIt : l.threads) {
- const auto& rIt = r.threads.find(lIt.first);
- if (rIt == r.threads.end()) {
- return false;
- }
- if (!isEqual(lIt.second, rIt->second)) {
- return false;
- }
- }
- return true;
- });
-}
-
-std::string pidStatusStr(pid_t pid, uid_t uid) {
- return StringPrintf("Pid:\t%" PRIu32 "\nTgid:\t%" PRIu32 "\nUid:\t%" PRIu32 "\n", pid, pid,
- uid);
-}
-
-std::string pidStatusStr(pid_t pid, uid_t uid, uint64_t vmPeakKb, uint64_t vmSizeKb,
- uint64_t vmHwmKb, uint64_t vmRssKb) {
- return StringPrintf("%sVmPeak:\t%" PRIu64 "\nVmSize:\t%" PRIu64 "\nVmHWM:\t%" PRIu64
- "\nVmRSS:\t%" PRIu64 "\n",
- pidStatusStr(pid, uid).c_str(), vmPeakKb, vmSizeKb, vmHwmKb, vmRssKb);
-}
-
-} // namespace
-
-TEST(ProcPidStatTest, TestValidStatFiles) {
- std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
- {1, {1, 453}},
- {1000, {1000, 1100}},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 2 0 0\n"},
- {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 1000\n"},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStatus = {
- {1, pidStatusStr(1, 0, 123, 456, 789, 345)},
- {1000, pidStatusStr(1000, 10001234, 234, 567, 890, 123)},
- };
-
- std::unordered_map<pid_t, std::string> perThreadStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 0\n"},
- {453, "453 (init) S 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 0 2 0 275\n"},
- {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 2 0 1000\n"},
- {1100, "1100 (system_server) S 1 0 0 0 0 0 0 0 350 0 0 0 0 0 0 0 2 0 1200\n"},
- };
-
- std::vector<ProcessStats> expected = {
- {.tgid = 1,
- .uid = 0,
- .vmPeakKb = 123,
- .vmSizeKb = 456,
- .vmHwmKb = 789,
- .vmRssKb = 345,
- .process = {1, "init", "S", 0, 220, 2, 0},
- .threads = {{1, {1, "init", "S", 0, 200, 2, 0}},
- {453, {453, "init", "S", 0, 20, 2, 275}}}},
- {.tgid = 1000,
- .uid = 10001234,
- .vmPeakKb = 234,
- .vmSizeKb = 567,
- .vmHwmKb = 890,
- .vmRssKb = 123,
- .process = {1000, "system_server", "R", 1, 600, 2, 1000},
- .threads = {{1000, {1000, "system_server", "R", 1, 250, 2, 1000}},
- {1100, {1100, "system_server", "S", 1, 350, 2, 1200}}}},
- };
-
- TemporaryDir firstSnapshot;
- ASSERT_RESULT_OK(populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat,
- perProcessStatus, perThreadStat));
-
- ProcPidStat procPidStat(firstSnapshot.path);
- ASSERT_TRUE(procPidStat.enabled())
- << "Files under the path `" << firstSnapshot.path << "` are inaccessible";
- ASSERT_RESULT_OK(procPidStat.collect());
-
- auto actual = std::vector<ProcessStats>(procPidStat.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "First snapshot doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
- pidToTids = {
- {1, {1, 453}}, {1000, {1000, 1400}}, // TID 1100 terminated and 1400 instantiated.
- };
-
- perProcessStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 920 0 0 0 0 0 0 0 2 0 0\n"},
- {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 1550 0 0 0 0 0 0 0 2 0 1000\n"},
- };
-
- perThreadStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 0\n"},
- {453, "453 (init) S 0 0 0 0 0 0 0 0 320 0 0 0 0 0 0 0 2 0 275\n"},
- {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 1000\n"},
- // TID 1100 hits +400 major page faults before terminating. This is counted against
- // PID 1000's perProcessStat.
- {1400, "1400 (system_server) S 1 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 8977476\n"},
- };
-
- expected = {
- {.tgid = 1,
- .uid = 0,
- .vmPeakKb = 123,
- .vmSizeKb = 456,
- .vmHwmKb = 789,
- .vmRssKb = 345,
- .process = {1, "init", "S", 0, 700, 2, 0},
- .threads = {{1, {1, "init", "S", 0, 400, 2, 0}},
- {453, {453, "init", "S", 0, 300, 2, 275}}}},
- {.tgid = 1000,
- .uid = 10001234,
- .vmPeakKb = 234,
- .vmSizeKb = 567,
- .vmHwmKb = 890,
- .vmRssKb = 123,
- .process = {1000, "system_server", "R", 1, 950, 2, 1000},
- .threads = {{1000, {1000, "system_server", "R", 1, 350, 2, 1000}},
- {1400, {1400, "system_server", "S", 1, 200, 2, 8977476}}}},
- };
-
- TemporaryDir secondSnapshot;
- ASSERT_RESULT_OK(populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat,
- perProcessStatus, perThreadStat));
-
- procPidStat.mPath = secondSnapshot.path;
- ASSERT_TRUE(procPidStat.enabled())
- << "Files under the path `" << secondSnapshot.path << "` are inaccessible";
- ASSERT_RESULT_OK(procPidStat.collect());
-
- actual = std::vector<ProcessStats>(procPidStat.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "Second snapshot doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
-}
-
-TEST(ProcPidStatTest, TestHandlesProcessTerminationBetweenScanningAndParsing) {
- std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
- {1, {1}},
- {100, {100}}, // Process terminates after scanning PID directory.
- {1000, {1000}}, // Process terminates after reading stat file.
- {2000, {2000}}, // Process terminates after scanning task directory.
- {3000, {3000, 3300}}, // TID 3300 terminates after scanning task directory.
- };
-
- std::unordered_map<pid_t, std::string> perProcessStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 1 0 0\n"},
- // Process 100 terminated.
- {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 1 0 1000\n"},
- {2000, "2000 (logd) R 1 0 0 0 0 0 0 0 1200 0 0 0 0 0 0 0 1 0 4567\n"},
- {3000, "3000 (disk I/O) R 1 0 0 0 0 0 0 0 10300 0 0 0 0 0 0 0 2 0 67890\n"},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStatus = {
- {1, "Pid:\t1\nTgid:\t1\nUid:\t0\t0\t0\t0\n"},
- // Process 1000 terminated.
- {2000, pidStatusStr(2000, 10001234)},
- {3000, pidStatusStr(3000, 10001234)},
- };
-
- std::unordered_map<pid_t, std::string> perThreadStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
- // Process 2000 terminated.
- {3000, "3000 (disk I/O) R 1 0 0 0 0 0 0 0 2400 0 0 0 0 0 0 0 2 0 67890\n"},
- // TID 3300 terminated.
- };
-
- std::vector<ProcessStats> expected = {
- {.tgid = 1,
- .uid = 0,
- .process = {1, "init", "S", 0, 220, 1, 0},
- .threads = {{1, {1, "init", "S", 0, 200, 1, 0}}}},
- {.tgid = -1,
- .uid = -1,
- .process = {1000, "system_server", "R", 1, 600, 1, 1000},
- // Stats common between process and main-thread are copied when
- // main-thread stats are not available.
- .threads = {{1000, {1000, "system_server", "R", 1, 0, 1, 1000}}}},
- {.tgid = 2000,
- .uid = 10001234,
- .process = {2000, "logd", "R", 1, 1200, 1, 4567},
- .threads = {{2000, {2000, "logd", "R", 1, 0, 1, 4567}}}},
- {.tgid = 3000,
- .uid = 10001234,
- .process = {3000, "disk I/O", "R", 1, 10300, 2, 67890},
- .threads = {{3000, {3000, "disk I/O", "R", 1, 2400, 2, 67890}}}},
- };
-
- TemporaryDir procDir;
- ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
- perThreadStat));
-
- ProcPidStat procPidStat(procDir.path);
- ASSERT_TRUE(procPidStat.enabled())
- << "Files under the path `" << procDir.path << "` are inaccessible";
- ASSERT_RESULT_OK(procPidStat.collect());
-
- auto actual = std::vector<ProcessStats>(procPidStat.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "Proc pid contents doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
-}
-
-TEST(ProcPidStatTest, TestHandlesPidTidReuse) {
- std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
- {1, {1, 367, 453, 589}},
- {1000, {1000}},
- {2345, {2345}},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 1200 0 0 0 0 0 0 0 4 0 0\n"},
- {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 1 0 1000\n"},
- {2345, "2345 (logd) R 1 0 0 0 0 0 0 0 54354 0 0 0 0 0 0 0 1 0 456\n"},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStatus = {
- {1, pidStatusStr(1, 0)},
- {1000, pidStatusStr(1000, 10001234)},
- {2345, pidStatusStr(2345, 10001234)},
- };
-
- std::unordered_map<pid_t, std::string> perThreadStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 4 0 0\n"},
- {367, "367 (init) S 0 0 0 0 0 0 0 0 400 0 0 0 0 0 0 0 4 0 100\n"},
- {453, "453 (init) S 0 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 4 0 275\n"},
- {589, "589 (init) S 0 0 0 0 0 0 0 0 500 0 0 0 0 0 0 0 4 0 600\n"},
- {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 1 0 1000\n"},
- {2345, "2345 (logd) R 1 0 0 0 0 0 0 0 54354 0 0 0 0 0 0 0 1 0 456\n"},
- };
-
- std::vector<ProcessStats> expected = {
- {.tgid = 1,
- .uid = 0,
- .process = {1, "init", "S", 0, 1200, 4, 0},
- .threads = {{1, {1, "init", "S", 0, 200, 4, 0}},
- {367, {367, "init", "S", 0, 400, 4, 100}},
- {453, {453, "init", "S", 0, 100, 4, 275}},
- {589, {589, "init", "S", 0, 500, 4, 600}}}},
- {.tgid = 1000,
- .uid = 10001234,
- .process = {1000, "system_server", "R", 1, 250, 1, 1000},
- .threads = {{1000, {1000, "system_server", "R", 1, 250, 1, 1000}}}},
- {.tgid = 2345,
- .uid = 10001234,
- .process = {2345, "logd", "R", 1, 54354, 1, 456},
- .threads = {{2345, {2345, "logd", "R", 1, 54354, 1, 456}}}},
- };
-
- TemporaryDir firstSnapshot;
- ASSERT_RESULT_OK(populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat,
- perProcessStatus, perThreadStat));
-
- ProcPidStat procPidStat(firstSnapshot.path);
- ASSERT_TRUE(procPidStat.enabled())
- << "Files under the path `" << firstSnapshot.path << "` are inaccessible";
- ASSERT_RESULT_OK(procPidStat.collect());
-
- auto actual = std::vector<ProcessStats>(procPidStat.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "First snapshot doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
-
- pidToTids = {
- {1, {1, 589}}, // TID 589 reused by the same process.
- {367, {367, 2000}}, // TID 367 reused as a PID. PID 2000 reused as a TID.
- // PID 1000 reused as a new PID. TID 453 reused by a different PID.
- {1000, {1000, 453}},
- };
-
- perProcessStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 1800 0 0 0 0 0 0 0 2 0 0\n"},
- {367, "367 (system_server) R 1 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 2 0 3450\n"},
- {1000, "1000 (logd) R 1 0 0 0 0 0 0 0 2000 0 0 0 0 0 0 0 2 0 4650\n"},
- };
-
- perProcessStatus = {
- {1, pidStatusStr(1, 0)},
- {367, pidStatusStr(367, 10001234)},
- {1000, pidStatusStr(1000, 10001234)},
- };
-
- perThreadStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 500 0 0 0 0 0 0 0 2 0 0\n"},
- {589, "589 (init) S 0 0 0 0 0 0 0 0 300 0 0 0 0 0 0 0 2 0 2345\n"},
- {367, "367 (system_server) R 1 0 0 0 0 0 0 0 50 0 0 0 0 0 0 0 2 0 3450\n"},
- {2000, "2000 (system_server) R 1 0 0 0 0 0 0 0 50 0 0 0 0 0 0 0 2 0 3670\n"},
- {1000, "1000 (logd) R 1 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 4650\n"},
- {453, "453 (logd) D 1 0 0 0 0 0 0 0 1800 0 0 0 0 0 0 0 2 0 4770\n"},
- };
-
- expected = {
- {.tgid = 1,
- .uid = 0,
- .process = {1, "init", "S", 0, 600, 2, 0},
- .threads = {{1, {1, "init", "S", 0, 300, 2, 0}},
- {589, {589, "init", "S", 0, 300, 2, 2345}}}},
- {.tgid = 367,
- .uid = 10001234,
- .process = {367, "system_server", "R", 1, 100, 2, 3450},
- .threads = {{367, {367, "system_server", "R", 1, 50, 2, 3450}},
- {2000, {2000, "system_server", "R", 1, 50, 2, 3670}}}},
- {.tgid = 1000,
- .uid = 10001234,
- .process = {1000, "logd", "R", 1, 2000, 2, 4650},
- .threads = {{1000, {1000, "logd", "R", 1, 200, 2, 4650}},
- {453, {453, "logd", "D", 1, 1800, 2, 4770}}}},
- };
-
- TemporaryDir secondSnapshot;
- ASSERT_RESULT_OK(populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat,
- perProcessStatus, perThreadStat));
-
- procPidStat.mPath = secondSnapshot.path;
- ASSERT_TRUE(procPidStat.enabled())
- << "Files under the path `" << secondSnapshot.path << "` are inaccessible";
- ASSERT_RESULT_OK(procPidStat.collect());
-
- actual = std::vector<ProcessStats>(procPidStat.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "Second snapshot doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
-}
-
-TEST(ProcPidStatTest, TestErrorOnCorruptedProcessStatFile) {
- std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
- {1, {1}},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStatus = {
- {1, pidStatusStr(1, 0)},
- };
-
- std::unordered_map<pid_t, std::string> perThreadStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
- };
-
- TemporaryDir procDir;
- ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
- perThreadStat));
-
- ProcPidStat procPidStat(procDir.path);
- ASSERT_TRUE(procPidStat.enabled())
- << "Files under the path `" << procDir.path << "` are inaccessible";
- ASSERT_FALSE(procPidStat.collect().ok()) << "No error returned for invalid process stat file";
-}
-
-TEST(ProcPidStatTest, TestErrorOnCorruptedProcessStatusFile) {
- std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
- {1, {1}},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStatus = {
- {1, "Pid:\t1\nTgid:\t1\nCORRUPTED DATA\n"},
- };
-
- std::unordered_map<pid_t, std::string> perThreadStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
- };
-
- TemporaryDir procDir;
- ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
- perThreadStat));
-
- ProcPidStat procPidStat(procDir.path);
- ASSERT_TRUE(procPidStat.enabled())
- << "Files under the path `" << procDir.path << "` are inaccessible";
- ASSERT_FALSE(procPidStat.collect().ok()) << "No error returned for invalid process status file";
-}
-
-TEST(ProcPidStatTest, TestErrorOnCorruptedThreadStatFile) {
- std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
- {1, {1}},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStatus = {
- {1, pidStatusStr(1, 0)},
- };
-
- std::unordered_map<pid_t, std::string> perThreadStat = {
- {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
- };
-
- TemporaryDir procDir;
- ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
- perThreadStat));
-
- ProcPidStat procPidStat(procDir.path);
- ASSERT_TRUE(procPidStat.enabled())
- << "Files under the path `" << procDir.path << "` are inaccessible";
- ASSERT_FALSE(procPidStat.collect().ok()) << "No error returned for invalid thread stat file";
-}
-
-TEST(ProcPidStatTest, TestHandlesSpaceInCommName) {
- std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
- {1, {1}},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStat = {
- {1, "1 (random process name with space) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
- };
-
- std::unordered_map<pid_t, std::string> perProcessStatus = {
- {1, pidStatusStr(1, 0)},
- };
-
- std::unordered_map<pid_t, std::string> perThreadStat = {
- {1, "1 (random process name with space) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
- };
-
- std::vector<ProcessStats> expected = {
- {.tgid = 1,
- .uid = 0,
- .process = {1, "random process name with space", "S", 0, 200, 1, 0},
- .threads = {{1, {1, "random process name with space", "S", 0, 200, 1, 0}}}},
- };
-
- TemporaryDir procDir;
- ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
- perThreadStat));
-
- ProcPidStat procPidStat(procDir.path);
- ASSERT_TRUE(procPidStat.enabled())
- << "Files under the path `" << procDir.path << "` are inaccessible";
- ASSERT_RESULT_OK(procPidStat.collect());
-
- auto actual = std::vector<ProcessStats>(procPidStat.deltaStats());
- EXPECT_TRUE(isEqual(&expected, &actual)) << "Proc pid contents doesn't match.\nExpected:\n"
- << toString(expected) << "\nActual:\n"
- << toString(actual);
-}
-
-TEST(ProcPidStatTest, TestProcPidStatContentsFromDevice) {
- ProcPidStat procPidStat;
- ASSERT_TRUE(procPidStat.enabled()) << "/proc/[pid]/.* files are inaccessible";
- ASSERT_RESULT_OK(procPidStat.collect());
-
- const auto& processStats = procPidStat.deltaStats();
- // The below check should pass because there should be at least one process.
- EXPECT_GT(processStats.size(), 0);
-}
-
-} // namespace watchdog
-} // namespace automotive
-} // namespace android
diff --git a/cpp/watchdog/server/tests/ProcStatTest.cpp b/cpp/watchdog/server/tests/ProcStatTest.cpp
index c886310..ee33162 100644
--- a/cpp/watchdog/server/tests/ProcStatTest.cpp
+++ b/cpp/watchdog/server/tests/ProcStatTest.cpp
@@ -42,7 +42,7 @@
cpuStats.userTime, cpuStats.niceTime, cpuStats.sysTime, cpuStats.idleTime,
cpuStats.ioWaitTime, cpuStats.irqTime, cpuStats.softIrqTime,
cpuStats.stealTime, cpuStats.guestTime, cpuStats.guestNiceTime,
- info.runnableProcessesCnt, info.ioBlockedProcessesCnt);
+ info.runnableProcessCount, info.ioBlockedProcessCount);
}
} // namespace
@@ -56,8 +56,9 @@
"cpu3 1000 20 190 650 109 130 140 0 0 0\n"
"intr 694351583 0 0 0 297062868 0 5922464 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 "
"0 0\n"
- // Skipped most of the intr line as it is not important for testing the ProcStat parsing
- // logic.
+ /* Skipped most of the intr line as it is not important for testing the ProcStat parsing
+ * logic.
+ */
"ctxt 579020168\n"
"btime 1579718450\n"
"processes 113804\n"
@@ -77,8 +78,8 @@
.guestTime = 0,
.guestNiceTime = 0,
};
- expectedFirstDelta.runnableProcessesCnt = 17;
- expectedFirstDelta.ioBlockedProcessesCnt = 5;
+ expectedFirstDelta.runnableProcessCount = 17;
+ expectedFirstDelta.ioBlockedProcessCount = 5;
TemporaryFile tf;
ASSERT_NE(tf.fd, -1);
@@ -120,8 +121,8 @@
.guestTime = 0,
.guestNiceTime = 0,
};
- expectedSecondDelta.runnableProcessesCnt = 10;
- expectedSecondDelta.ioBlockedProcessesCnt = 2;
+ expectedSecondDelta.runnableProcessCount = 10;
+ expectedSecondDelta.ioBlockedProcessCount = 2;
ASSERT_TRUE(WriteStringToFile(secondSnapshot, tf.path));
ASSERT_RESULT_OK(procStat.collect());
@@ -257,10 +258,11 @@
ASSERT_RESULT_OK(procStat.collect());
const auto& info = procStat.deltaStats();
- // The below checks should pass because the /proc/stats file should have the CPU time spent
- // since bootup and there should be at least one running process.
+ /* The below checks should pass because the /proc/stats file should have the CPU time spent
+ * since bootup and there should be at least one running process.
+ */
EXPECT_GT(info.totalCpuTime(), 0);
- EXPECT_GT(info.totalProcessesCnt(), 0);
+ EXPECT_GT(info.totalProcessCount(), 0);
}
} // namespace watchdog
diff --git a/cpp/watchdog/server/tests/UidIoStatsCollectorTest.cpp b/cpp/watchdog/server/tests/UidIoStatsCollectorTest.cpp
new file mode 100644
index 0000000..fe30844
--- /dev/null
+++ b/cpp/watchdog/server/tests/UidIoStatsCollectorTest.cpp
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2020 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.
+ */
+
+#include "UidIoStatsCollector.h"
+
+#include <android-base/file.h>
+#include <android-base/stringprintf.h>
+#include <gmock/gmock.h>
+
+#include <unordered_map>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::base::StringAppendF;
+using ::android::base::WriteStringToFile;
+using ::testing::UnorderedElementsAreArray;
+
+namespace {
+
+std::string toString(std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid) {
+ std::string buffer;
+ for (const auto& [uid, stats] : uidIoStatsByUid) {
+ StringAppendF(&buffer, "{%d: %s}\n", uid, stats.toString().c_str());
+ }
+ return buffer;
+}
+
+} // namespace
+
+TEST(UidIoStatsCollectorTest, TestValidStatFile) {
+ // Format: uid fgRdChar fgWrChar fgRdBytes fgWrBytes bgRdChar bgWrChar bgRdBytes bgWrBytes
+ // fgFsync bgFsync
+ constexpr char firstSnapshot[] = "1001234 5000 1000 3000 500 0 0 0 0 20 0\n"
+ "1005678 500 100 30 50 300 400 100 200 45 60\n"
+ "1009 0 0 0 0 40000 50000 20000 30000 0 300\n"
+ "1001000 4000 3000 2000 1000 400 300 200 100 50 10\n";
+ std::unordered_map<uid_t, UidIoStats> expectedFirstUsage =
+ {{1001234,
+ UidIoStats{/*fgRdBytes=*/3'000, /*bgRdBytes=*/0, /*fgWrBytes=*/500,
+ /*bgWrBytes=*/0, /*fgFsync=*/20, /*bgFsync=*/0}},
+ {1005678,
+ UidIoStats{/*fgRdBytes=*/30, /*bgRdBytes=*/100, /*fgWrBytes=*/50, /*bgWrBytes=*/200,
+ /*fgFsync=*/45, /*bgFsync=*/60}},
+ {1009,
+ UidIoStats{/*fgRdBytes=*/0, /*bgRdBytes=*/20'000, /*fgWrBytes=*/0,
+ /*bgWrBytes=*/30'000,
+ /*fgFsync=*/0, /*bgFsync=*/300}},
+ {1001000,
+ UidIoStats{/*fgRdBytes=*/2'000, /*bgRdBytes=*/200, /*fgWrBytes=*/1'000,
+ /*bgWrBytes=*/100, /*fgFsync=*/50, /*bgFsync=*/10}}};
+ TemporaryFile tf;
+ ASSERT_NE(tf.fd, -1);
+ ASSERT_TRUE(WriteStringToFile(firstSnapshot, tf.path));
+
+ UidIoStatsCollector collector(tf.path);
+ ASSERT_TRUE(collector.enabled()) << "Temporary file is inaccessible";
+ ASSERT_RESULT_OK(collector.collect());
+
+ const auto& actualFirstUsage = collector.deltaStats();
+ EXPECT_THAT(actualFirstUsage, UnorderedElementsAreArray(expectedFirstUsage))
+ << "Expected: " << toString(expectedFirstUsage)
+ << "Actual: " << toString(actualFirstUsage);
+
+ constexpr char secondSnapshot[] = "1001234 10000 2000 7000 950 0 0 0 0 45 0\n"
+ "1005678 600 100 40 50 1000 1000 1000 600 50 70\n"
+ "1003456 300 500 200 300 0 0 0 0 50 0\n"
+ "1001000 400 300 200 100 40 30 20 10 5 1\n";
+ std::unordered_map<uid_t, UidIoStats> expectedSecondUsage =
+ {{1001234,
+ UidIoStats{/*fgRdBytes=*/4'000, /*bgRdBytes=*/0,
+ /*fgWrBytes=*/450, /*bgWrBytes=*/0, /*fgFsync=*/25,
+ /*bgFsync=*/0}},
+ {1005678,
+ UidIoStats{/*fgRdBytes=*/10, /*bgRdBytes=*/900, /*fgWrBytes=*/0, /*bgWrBytes=*/400,
+ /*fgFsync=*/5, /*bgFsync=*/10}},
+ {1003456,
+ UidIoStats{/*fgRdBytes=*/200, /*bgRdBytes=*/0, /*fgWrBytes=*/300, /*bgWrBytes=*/0,
+ /*fgFsync=*/50, /*bgFsync=*/0}}};
+ ASSERT_TRUE(WriteStringToFile(secondSnapshot, tf.path));
+ ASSERT_RESULT_OK(collector.collect());
+
+ const auto& actualSecondUsage = collector.deltaStats();
+ EXPECT_THAT(actualSecondUsage, UnorderedElementsAreArray(expectedSecondUsage))
+ << "Expected: " << toString(expectedSecondUsage)
+ << "Actual: " << toString(actualSecondUsage);
+}
+
+TEST(UidIoStatsCollectorTest, TestErrorOnInvalidStatFile) {
+ // Format: uid fgRdChar fgWrChar fgRdBytes fgWrBytes bgRdChar bgWrChar bgRdBytes bgWrBytes
+ // fgFsync bgFsync
+ constexpr char contents[] = "1001234 5000 1000 3000 500 0 0 0 0 20 0\n"
+ "1005678 500 100 30 50 300 400 100 200 45 60\n"
+ "1009012 0 0 0 0 40000 50000 20000 30000 0 300\n"
+ "1001000 4000 3000 2000 1000 CORRUPTED DATA\n";
+ TemporaryFile tf;
+ ASSERT_NE(tf.fd, -1);
+ ASSERT_TRUE(WriteStringToFile(contents, tf.path));
+
+ UidIoStatsCollector collector(tf.path);
+ ASSERT_TRUE(collector.enabled()) << "Temporary file is inaccessible";
+ EXPECT_FALSE(collector.collect().ok()) << "No error returned for invalid file";
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
diff --git a/cpp/watchdog/server/tests/UidIoStatsTest.cpp b/cpp/watchdog/server/tests/UidIoStatsTest.cpp
deleted file mode 100644
index 6e88ed5..0000000
--- a/cpp/watchdog/server/tests/UidIoStatsTest.cpp
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright 2020 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.
- */
-
-#include "UidIoStats.h"
-
-#include <android-base/file.h>
-#include <android-base/stringprintf.h>
-#include <gmock/gmock.h>
-
-#include <unordered_map>
-
-namespace android {
-namespace automotive {
-namespace watchdog {
-
-using ::android::base::StringAppendF;
-using ::android::base::WriteStringToFile;
-using ::testing::UnorderedElementsAreArray;
-
-namespace {
-
-std::string toString(std::unordered_map<uid_t, UidIoUsage> usages) {
- std::string buffer;
- for (const auto& [uid, usage] : usages) {
- StringAppendF(&buffer, "{%s}\n", usage.toString().c_str());
- }
- return buffer;
-}
-
-} // namespace
-
-TEST(UidIoStatsTest, TestValidStatFile) {
- // Format: uid fgRdChar fgWrChar fgRdBytes fgWrBytes bgRdChar bgWrChar bgRdBytes bgWrBytes
- // fgFsync bgFsync
- constexpr char firstSnapshot[] = "1001234 5000 1000 3000 500 0 0 0 0 20 0\n"
- "1005678 500 100 30 50 300 400 100 200 45 60\n"
- "1009 0 0 0 0 40000 50000 20000 30000 0 300\n"
- "1001000 4000 3000 2000 1000 400 300 200 100 50 10\n";
- std::unordered_map<uid_t, UidIoUsage> expectedFirstUsage =
- {{1001234,
- {.uid = 1001234,
- .ios = {/*fgRdBytes=*/3000, /*bgRdBytes=*/0, /*fgWrBytes=*/500,
- /*bgWrBytes=*/0, /*fgFsync=*/20, /*bgFsync=*/0}}},
- {1005678, {.uid = 1005678, .ios = {30, 100, 50, 200, 45, 60}}},
- {1009, {.uid = 1009, .ios = {0, 20000, 0, 30000, 0, 300}}},
- {1001000, {.uid = 1001000, .ios = {2000, 200, 1000, 100, 50, 10}}}};
- TemporaryFile tf;
- ASSERT_NE(tf.fd, -1);
- ASSERT_TRUE(WriteStringToFile(firstSnapshot, tf.path));
-
- UidIoStats uidIoStats(tf.path);
- ASSERT_TRUE(uidIoStats.enabled()) << "Temporary file is inaccessible";
- ASSERT_RESULT_OK(uidIoStats.collect());
-
- const auto& actualFirstUsage = uidIoStats.deltaStats();
- EXPECT_THAT(actualFirstUsage, UnorderedElementsAreArray(expectedFirstUsage))
- << "Expected: " << toString(expectedFirstUsage)
- << "Actual: " << toString(actualFirstUsage);
-
- constexpr char secondSnapshot[] = "1001234 10000 2000 7000 950 0 0 0 0 45 0\n"
- "1005678 600 100 40 50 1000 1000 1000 600 50 70\n"
- "1003456 300 500 200 300 0 0 0 0 50 0\n"
- "1001000 400 300 200 100 40 30 20 10 5 1\n";
- std::unordered_map<uid_t, UidIoUsage> expectedSecondUsage =
- {{1001234,
- {.uid = 1001234,
- .ios = {/*fgRdBytes=*/4000, /*bgRdBytes=*/0,
- /*fgWrBytes=*/450, /*bgWrBytes=*/0, /*fgFsync=*/25,
- /*bgFsync=*/0}}},
- {1005678, {.uid = 1005678, .ios = {10, 900, 0, 400, 5, 10}}},
- {1003456, {.uid = 1003456, .ios = {200, 0, 300, 0, 50, 0}}}};
- ASSERT_TRUE(WriteStringToFile(secondSnapshot, tf.path));
- ASSERT_RESULT_OK(uidIoStats.collect());
-
- const auto& actualSecondUsage = uidIoStats.deltaStats();
- EXPECT_THAT(actualSecondUsage, UnorderedElementsAreArray(expectedSecondUsage))
- << "Expected: " << toString(expectedSecondUsage)
- << "Actual: " << toString(actualSecondUsage);
-}
-
-TEST(UidIoStatsTest, TestErrorOnInvalidStatFile) {
- // Format: uid fgRdChar fgWrChar fgRdBytes fgWrBytes bgRdChar bgWrChar bgRdBytes bgWrBytes
- // fgFsync bgFsync
- constexpr char contents[] = "1001234 5000 1000 3000 500 0 0 0 0 20 0\n"
- "1005678 500 100 30 50 300 400 100 200 45 60\n"
- "1009012 0 0 0 0 40000 50000 20000 30000 0 300\n"
- "1001000 4000 3000 2000 1000 CORRUPTED DATA\n";
- TemporaryFile tf;
- ASSERT_NE(tf.fd, -1);
- ASSERT_TRUE(WriteStringToFile(contents, tf.path));
-
- UidIoStats uidIoStats(tf.path);
- ASSERT_TRUE(uidIoStats.enabled()) << "Temporary file is inaccessible";
- EXPECT_FALSE(uidIoStats.collect().ok()) << "No error returned for invalid file";
-}
-
-} // namespace watchdog
-} // namespace automotive
-} // namespace android
diff --git a/cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp b/cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp
new file mode 100644
index 0000000..c9eb6d4
--- /dev/null
+++ b/cpp/watchdog/server/tests/UidProcStatsCollectorTest.cpp
@@ -0,0 +1,480 @@
+/*
+ * Copyright 2020 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.
+ */
+
+#include "ProcPidDir.h"
+#include "UidProcStatsCollector.h"
+#include "UidProcStatsCollectorTestUtils.h"
+
+#include <android-base/file.h>
+#include <android-base/stringprintf.h>
+#include <gmock/gmock.h>
+
+#include <inttypes.h>
+
+#include <algorithm>
+#include <string>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::automotive::watchdog::testing::populateProcPidDir;
+using ::android::base::StringAppendF;
+using ::android::base::StringPrintf;
+using ::testing::UnorderedPointwise;
+
+namespace {
+
+MATCHER(UidProcStatsByUidEq, "") {
+ const auto& actual = std::get<0>(arg);
+ const auto& expected = std::get<1>(arg);
+ return actual.first == expected.first &&
+ ExplainMatchResult(UidProcStatsEq(expected.second), actual.second, result_listener);
+}
+
+std::string pidStatusStr(pid_t pid, uid_t uid) {
+ return StringPrintf("Pid:\t%" PRIu32 "\nTgid:\t%" PRIu32 "\nUid:\t%" PRIu32 "\n", pid, pid,
+ uid);
+}
+
+std::string toString(const std::unordered_map<uid_t, UidProcStats>& uidProcStatsByUid) {
+ std::string buffer;
+ StringAppendF(&buffer, "Number of UIDs: %" PRIi32 "\n",
+ static_cast<int>(uidProcStatsByUid.size()));
+ for (const auto& [uid, stats] : uidProcStatsByUid) {
+ StringAppendF(&buffer, "{UID: %d, %s}", uid, stats.toString().c_str());
+ }
+ return buffer;
+}
+
+} // namespace
+
+TEST(UidProcStatsCollectorTest, TestValidStatFiles) {
+ std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+ {1, {1, 453}},
+ {1000, {1000, 1100}},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 2 0 0\n"},
+ {1000, "1000 (system_server) D 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 13400\n"},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStatus = {
+ {1, pidStatusStr(1, 0)},
+ {1000, pidStatusStr(1000, 10001234)},
+ };
+
+ std::unordered_map<pid_t, std::string> perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 0\n"},
+ {453, "453 (init) D 0 0 0 0 0 0 0 0 20 0 0 0 0 0 0 0 2 0 275\n"},
+ {1000, "1000 (system_server) D 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 2 0 13400\n"},
+ {1100, "1100 (system_server) D 1 0 0 0 0 0 0 0 350 0 0 0 0 0 0 0 2 0 13900\n"},
+ };
+
+ std::unordered_map<uid_t, UidProcStats> expected =
+ {{0,
+ UidProcStats{.totalMajorFaults = 220,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 1,
+ .processStatsByPid = {{1, {"init", 0, 220, 2, 1}}}}},
+ {10001234,
+ UidProcStats{.totalMajorFaults = 600,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 2,
+ .processStatsByPid = {{1000, {"system_server", 13'400, 600, 2, 2}}}}}};
+
+ TemporaryDir firstSnapshot;
+ ASSERT_RESULT_OK(populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat,
+ perProcessStatus, perThreadStat));
+
+ UidProcStatsCollector collector(firstSnapshot.path);
+
+ ASSERT_TRUE(collector.enabled())
+ << "Files under the path `" << firstSnapshot.path << "` are inaccessible";
+ ASSERT_RESULT_OK(collector.collect());
+
+ auto actual = collector.deltaStats();
+
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "First snapshot doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
+ pidToTids = {
+ {1, {1, 453}}, {1000, {1000, 1400}}, // TID 1100 terminated and 1400 instantiated.
+ };
+
+ perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 920 0 0 0 0 0 0 0 2 0 0\n"},
+ {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 1550 0 0 0 0 0 0 0 2 0 13400\n"},
+ };
+
+ perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 0\n"},
+ {453, "453 (init) S 0 0 0 0 0 0 0 0 320 0 0 0 0 0 0 0 2 0 275\n"},
+ {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 2 0 13400\n"},
+ // TID 1100 hits +400 major page faults before terminating. This is counted against
+ // PID 1000's perProcessStat.
+ {1400, "1400 (system_server) S 1 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 8977476\n"},
+ };
+
+ expected = {{0,
+ {.totalMajorFaults = 700,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{1, {"init", 0, 700, 2, 0}}}}},
+ {10001234,
+ {.totalMajorFaults = 950,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{1000, {"system_server", 13'400, 950, 2, 0}}}}}};
+
+ TemporaryDir secondSnapshot;
+ ASSERT_RESULT_OK(populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat,
+ perProcessStatus, perThreadStat));
+
+ collector.mPath = secondSnapshot.path;
+
+ ASSERT_TRUE(collector.enabled())
+ << "Files under the path `" << secondSnapshot.path << "` are inaccessible";
+ ASSERT_RESULT_OK(collector.collect());
+
+ actual = collector.deltaStats();
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "Second snapshot doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
+}
+
+TEST(UidProcStatsCollectorTest, TestHandlesProcessTerminationBetweenScanningAndParsing) {
+ std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+ {1, {1}},
+ {100, {100}}, // Process terminates after scanning PID directory.
+ {1000, {1000}}, // Process terminates after reading stat file.
+ {2000, {2000}}, // Process terminates after scanning task directory.
+ {3000, {3000, 3300}}, // TID 3300 terminates after scanning task directory.
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 220 0 0 0 0 0 0 0 1 0 0\n"},
+ // Process 100 terminated.
+ {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 600 0 0 0 0 0 0 0 1 0 1000\n"},
+ {2000, "2000 (logd) R 1 0 0 0 0 0 0 0 1200 0 0 0 0 0 0 0 1 0 4567\n"},
+ {3000, "3000 (disk I/O) R 1 0 0 0 0 0 0 0 10300 0 0 0 0 0 0 0 2 0 67890\n"},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStatus = {
+ {1, pidStatusStr(1, 0)},
+ // Process 1000 terminated.
+ {2000, pidStatusStr(2000, 10001234)},
+ {3000, pidStatusStr(3000, 10001234)},
+ };
+
+ std::unordered_map<pid_t, std::string> perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+ // Process 2000 terminated.
+ {3000, "3000 (disk I/O) R 1 0 0 0 0 0 0 0 2400 0 0 0 0 0 0 0 2 0 67890\n"},
+ // TID 3300 terminated.
+ };
+
+ std::unordered_map<uid_t, UidProcStats> expected =
+ {{0,
+ UidProcStats{.totalMajorFaults = 220,
+ .totalTasksCount = 1,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{1, {"init", 0, 220, 1, 0}}}}},
+ {10001234,
+ UidProcStats{.totalMajorFaults = 11500,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{2000, {"logd", 4567, 1200, 1, 0}},
+ {3000, {"disk I/O", 67890, 10'300, 1, 0}}}}}};
+
+ TemporaryDir procDir;
+ ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat));
+
+ UidProcStatsCollector collector(procDir.path);
+
+ ASSERT_TRUE(collector.enabled())
+ << "Files under the path `" << procDir.path << "` are inaccessible";
+ ASSERT_RESULT_OK(collector.collect());
+
+ auto actual = collector.deltaStats();
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "Proc pid contents doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
+}
+
+TEST(UidProcStatsCollectorTest, TestHandlesPidTidReuse) {
+ std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+ {1, {1, 367, 453, 589}},
+ {1000, {1000}},
+ {2345, {2345}},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 1200 0 0 0 0 0 0 0 4 0 0\n"},
+ {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 1 0 1000\n"},
+ {2345, "2345 (logd) R 1 0 0 0 0 0 0 0 54354 0 0 0 0 0 0 0 1 0 456\n"},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStatus = {
+ {1, pidStatusStr(1, 0)},
+ {1000, pidStatusStr(1000, 10001234)},
+ {2345, pidStatusStr(2345, 10001234)},
+ };
+
+ std::unordered_map<pid_t, std::string> perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 4 0 0\n"},
+ {367, "367 (init) S 0 0 0 0 0 0 0 0 400 0 0 0 0 0 0 0 4 0 100\n"},
+ {453, "453 (init) S 0 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 4 0 275\n"},
+ {589, "589 (init) D 0 0 0 0 0 0 0 0 500 0 0 0 0 0 0 0 4 0 600\n"},
+ {1000, "1000 (system_server) R 1 0 0 0 0 0 0 0 250 0 0 0 0 0 0 0 1 0 1000\n"},
+ {2345, "2345 (logd) R 1 0 0 0 0 0 0 0 54354 0 0 0 0 0 0 0 1 0 456\n"},
+ };
+
+ std::unordered_map<uid_t, UidProcStats> expected =
+ {{0,
+ UidProcStats{.totalMajorFaults = 1200,
+ .totalTasksCount = 4,
+ .ioBlockedTasksCount = 1,
+ .processStatsByPid = {{1, {"init", 0, 1200, 4, 1}}}}},
+ {10001234,
+ UidProcStats{.totalMajorFaults = 54'604,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{1000, {"system_server", 1000, 250, 1, 0}},
+ {2345, {"logd", 456, 54'354, 1, 0}}}}}};
+
+ TemporaryDir firstSnapshot;
+ ASSERT_RESULT_OK(populateProcPidDir(firstSnapshot.path, pidToTids, perProcessStat,
+ perProcessStatus, perThreadStat));
+
+ UidProcStatsCollector collector(firstSnapshot.path);
+
+ ASSERT_TRUE(collector.enabled())
+ << "Files under the path `" << firstSnapshot.path << "` are inaccessible";
+ ASSERT_RESULT_OK(collector.collect());
+
+ auto actual = collector.deltaStats();
+
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "First snapshot doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
+
+ pidToTids = {
+ {1, {1, 589}}, // TID 589 reused by the same process.
+ {367, {367, 2000}}, // TID 367 reused as a PID. PID 2000 reused as a TID.
+ // PID 1000 reused as a new PID. TID 453 reused by a different PID.
+ {1000, {1000, 453}},
+ };
+
+ perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 1800 0 0 0 0 0 0 0 2 0 0\n"},
+ {367, "367 (system_server) R 1 0 0 0 0 0 0 0 100 0 0 0 0 0 0 0 2 0 3450\n"},
+ {1000, "1000 (logd) R 1 0 0 0 0 0 0 0 2000 0 0 0 0 0 0 0 2 0 4650\n"},
+ };
+
+ perProcessStatus = {
+ {1, pidStatusStr(1, 0)},
+ {367, pidStatusStr(367, 10001234)},
+ {1000, pidStatusStr(1000, 10001234)},
+ };
+
+ perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 500 0 0 0 0 0 0 0 2 0 0\n"},
+ {589, "589 (init) S 0 0 0 0 0 0 0 0 300 0 0 0 0 0 0 0 2 0 2345\n"},
+ {367, "367 (system_server) R 1 0 0 0 0 0 0 0 50 0 0 0 0 0 0 0 2 0 3450\n"},
+ {2000, "2000 (system_server) R 1 0 0 0 0 0 0 0 50 0 0 0 0 0 0 0 2 0 3670\n"},
+ {1000, "1000 (logd) R 1 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 4650\n"},
+ {453, "453 (logd) D 1 0 0 0 0 0 0 0 1800 0 0 0 0 0 0 0 2 0 4770\n"},
+ };
+
+ expected = {{0,
+ UidProcStats{.totalMajorFaults = 600,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {{1, {"init", 0, 600, 2, 0}}}}},
+ {10001234,
+ UidProcStats{.totalMajorFaults = 2100,
+ .totalTasksCount = 4,
+ .ioBlockedTasksCount = 1,
+ .processStatsByPid = {{367, {"system_server", 3450, 100, 2, 0}},
+ {1000, {"logd", 4650, 2000, 2, 1}}}}}};
+
+ TemporaryDir secondSnapshot;
+ ASSERT_RESULT_OK(populateProcPidDir(secondSnapshot.path, pidToTids, perProcessStat,
+ perProcessStatus, perThreadStat));
+
+ collector.mPath = secondSnapshot.path;
+
+ ASSERT_TRUE(collector.enabled())
+ << "Files under the path `" << secondSnapshot.path << "` are inaccessible";
+ ASSERT_RESULT_OK(collector.collect());
+
+ actual = collector.deltaStats();
+
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "Second snapshot doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
+}
+
+TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedProcessStatFile) {
+ std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+ {1, {1}},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStatus = {
+ {1, pidStatusStr(1, 0)},
+ };
+
+ std::unordered_map<pid_t, std::string> perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+ };
+
+ TemporaryDir procDir;
+ ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat));
+
+ UidProcStatsCollector collector(procDir.path);
+
+ ASSERT_TRUE(collector.enabled())
+ << "Files under the path `" << procDir.path << "` are inaccessible";
+ ASSERT_FALSE(collector.collect().ok()) << "No error returned for invalid process stat file";
+}
+
+TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedProcessStatusFile) {
+ std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+ {1, {1}},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStatus = {
+ {1, "Pid:\t1\nTgid:\t1\nCORRUPTED DATA\n"},
+ };
+
+ std::unordered_map<pid_t, std::string> perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+ };
+
+ TemporaryDir procDir;
+ ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat));
+
+ UidProcStatsCollector collector(procDir.path);
+
+ ASSERT_TRUE(collector.enabled())
+ << "Files under the path `" << procDir.path << "` are inaccessible";
+ ASSERT_FALSE(collector.collect().ok()) << "No error returned for invalid process status file";
+}
+
+TEST(UidProcStatsCollectorTest, TestErrorOnCorruptedThreadStatFile) {
+ std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+ {1, {1, 234}},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 678\n"},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStatus = {
+ {1, pidStatusStr(1, 0)},
+ };
+
+ std::unordered_map<pid_t, std::string> perThreadStat = {
+ {1, "1 (init) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 2 0 678\n"},
+ {234, "234 (init) D 0 0 0 0 0 0 0 0 200 0 0 0 CORRUPTED DATA\n"},
+ };
+
+ TemporaryDir procDir;
+ ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat));
+
+ UidProcStatsCollector collector(procDir.path);
+
+ ASSERT_TRUE(collector.enabled())
+ << "Files under the path `" << procDir.path << "` are inaccessible";
+ ASSERT_FALSE(collector.collect().ok()) << "No error returned for invalid thread stat file";
+}
+
+TEST(UidProcStatsCollectorTest, TestHandlesSpaceInCommName) {
+ std::unordered_map<pid_t, std::vector<pid_t>> pidToTids = {
+ {1, {1}},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStat = {
+ {1, "1 (random process name with space) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+ };
+
+ std::unordered_map<pid_t, std::string> perProcessStatus = {
+ {1, pidStatusStr(1, 0)},
+ };
+
+ std::unordered_map<pid_t, std::string> perThreadStat = {
+ {1, "1 (random process name with space) S 0 0 0 0 0 0 0 0 200 0 0 0 0 0 0 0 1 0 0\n"},
+ };
+
+ std::unordered_map<uid_t, UidProcStats> expected = {
+ {0,
+ UidProcStats{.totalMajorFaults = 200,
+ .totalTasksCount = 1,
+ .ioBlockedTasksCount = 0,
+ .processStatsByPid = {
+ {1, {"random process name with space", 0, 200, 1, 0}}}}}};
+
+ TemporaryDir procDir;
+ ASSERT_RESULT_OK(populateProcPidDir(procDir.path, pidToTids, perProcessStat, perProcessStatus,
+ perThreadStat));
+
+ UidProcStatsCollector collector(procDir.path);
+
+ ASSERT_TRUE(collector.enabled())
+ << "Files under the path `" << procDir.path << "` are inaccessible";
+ ASSERT_RESULT_OK(collector.collect());
+
+ auto actual = collector.deltaStats();
+
+ EXPECT_THAT(actual, UnorderedPointwise(UidProcStatsByUidEq(), expected))
+ << "Proc pid contents doesn't match.\nExpected:\n"
+ << toString(expected) << "\nActual:\n"
+ << toString(actual);
+}
+
+TEST(UidProcStatsCollectorTest, TestUidProcStatsCollectorContentsFromDevice) {
+ UidProcStatsCollector collector;
+ ASSERT_TRUE(collector.enabled()) << "/proc/[pid]/.* files are inaccessible";
+ ASSERT_RESULT_OK(collector.collect());
+
+ const auto& processStats = collector.deltaStats();
+
+ // The below check should pass because there should be at least one process.
+ EXPECT_GT(processStats.size(), 0);
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
diff --git a/cpp/watchdog/server/tests/UidProcStatsCollectorTestUtils.h b/cpp/watchdog/server/tests/UidProcStatsCollectorTestUtils.h
new file mode 100644
index 0000000..6e5a409
--- /dev/null
+++ b/cpp/watchdog/server/tests/UidProcStatsCollectorTestUtils.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#ifndef CPP_WATCHDOG_SERVER_TESTS_UIDPROCSTATSCOLLECTORTESTUTILS_H_
+#define CPP_WATCHDOG_SERVER_TESTS_UIDPROCSTATSCOLLECTORTESTUTILS_H_
+
+#include "UidProcStatsCollector.h"
+
+#include <gmock/gmock.h>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+MATCHER_P(ProcessStatsEq, expected, "") {
+ const auto& actual = arg;
+ return ::testing::Value(actual.comm, ::testing::Eq(expected.comm)) &&
+ ::testing::Value(actual.startTime, ::testing::Eq(expected.startTime)) &&
+ ::testing::Value(actual.totalMajorFaults, ::testing::Eq(expected.totalMajorFaults)) &&
+ ::testing::Value(actual.totalTasksCount, ::testing::Eq(expected.totalTasksCount)) &&
+ ::testing::Value(actual.ioBlockedTasksCount,
+ ::testing::Eq(expected.ioBlockedTasksCount));
+}
+
+MATCHER(ProcessStatsByPidEq, "") {
+ const auto& actual = std::get<0>(arg);
+ const auto& expected = std::get<1>(arg);
+ return actual.first == expected.first &&
+ ::testing::Value(actual.second, ProcessStatsEq(expected.second));
+}
+
+MATCHER_P(UidProcStatsEq, expected, "") {
+ const auto& actual = arg;
+ return ::testing::Value(actual.totalMajorFaults, ::testing::Eq(expected.totalMajorFaults)) &&
+ ::testing::Value(actual.totalTasksCount, ::testing::Eq(expected.totalTasksCount)) &&
+ ::testing::Value(actual.ioBlockedTasksCount,
+ ::testing::Eq(expected.ioBlockedTasksCount)) &&
+ ::testing::Value(actual.processStatsByPid,
+ ::testing::UnorderedPointwise(ProcessStatsByPidEq(),
+ expected.processStatsByPid));
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
+
+#endif // CPP_WATCHDOG_SERVER_TESTS_UIDPROCSTATSCOLLECTORTESTUTILS_H_
diff --git a/cpp/watchdog/server/tests/UidStatsCollectorTest.cpp b/cpp/watchdog/server/tests/UidStatsCollectorTest.cpp
new file mode 100644
index 0000000..7d3bcb1
--- /dev/null
+++ b/cpp/watchdog/server/tests/UidStatsCollectorTest.cpp
@@ -0,0 +1,440 @@
+/*
+ * Copyright 2021 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.
+ */
+
+#include "MockPackageInfoResolver.h"
+#include "MockUidIoStatsCollector.h"
+#include "MockUidProcStatsCollector.h"
+#include "PackageInfoTestUtils.h"
+#include "UidIoStatsCollector.h"
+#include "UidProcStatsCollector.h"
+#include "UidProcStatsCollectorTestUtils.h"
+#include "UidStatsCollector.h"
+
+#include <android-base/stringprintf.h>
+#include <gmock/gmock.h>
+#include <utils/RefBase.h>
+
+#include <string>
+
+namespace android {
+namespace automotive {
+namespace watchdog {
+
+using ::android::automotive::watchdog::internal::PackageInfo;
+using ::android::automotive::watchdog::internal::UidType;
+using ::android::base::Error;
+using ::android::base::Result;
+using ::android::base::StringAppendF;
+using ::android::base::StringPrintf;
+using ::testing::AllOf;
+using ::testing::Eq;
+using ::testing::ExplainMatchResult;
+using ::testing::Field;
+using ::testing::IsEmpty;
+using ::testing::Matcher;
+using ::testing::Return;
+using ::testing::UnorderedElementsAre;
+using ::testing::UnorderedElementsAreArray;
+
+namespace {
+
+std::string toString(const UidStats& uidStats) {
+ return StringPrintf("UidStats{%s, %s, %s}", uidStats.packageInfo.toString().c_str(),
+ uidStats.ioStats.toString().c_str(), uidStats.procStats.toString().c_str());
+}
+
+std::string toString(const std::vector<UidStats>& uidStats) {
+ std::string buffer;
+ StringAppendF(&buffer, "{");
+ for (const auto& stats : uidStats) {
+ StringAppendF(&buffer, "%s\n", toString(stats).c_str());
+ }
+ StringAppendF(&buffer, "}");
+ return buffer;
+}
+
+MATCHER_P(UidStatsEq, expected, "") {
+ return ExplainMatchResult(AllOf(Field("packageInfo", &UidStats::packageInfo,
+ PackageInfoEq(expected.packageInfo)),
+ Field("ioStats", &UidStats::ioStats, Eq(expected.ioStats)),
+ Field("procStats", &UidStats::procStats,
+ UidProcStatsEq(expected.procStats))),
+ arg, result_listener);
+}
+
+std::vector<Matcher<const UidStats&>> UidStatsMatchers(const std::vector<UidStats>& uidStats) {
+ std::vector<Matcher<const UidStats&>> matchers;
+ for (const auto& stats : uidStats) {
+ matchers.push_back(UidStatsEq(stats));
+ }
+ return matchers;
+}
+
+std::unordered_map<uid_t, PackageInfo> samplePackageInfoByUid() {
+ return {{1001234, constructPackageInfo("system.daemon", 1001234, UidType::NATIVE)},
+ {1005678, constructPackageInfo("kitchensink.app", 1005678, UidType::APPLICATION)}};
+}
+
+std::unordered_map<uid_t, UidIoStats> sampleUidIoStatsByUid() {
+ return {{1001234,
+ UidIoStats{/*fgRdBytes=*/3'000, /*bgRdBytes=*/0,
+ /*fgWrBytes=*/500,
+ /*bgWrBytes=*/0, /*fgFsync=*/20,
+ /*bgFsync=*/0}},
+ {1005678,
+ UidIoStats{/*fgRdBytes=*/30, /*bgRdBytes=*/100,
+ /*fgWrBytes=*/50, /*bgWrBytes=*/200,
+ /*fgFsync=*/45, /*bgFsync=*/60}}};
+}
+
+std::unordered_map<uid_t, UidProcStats> sampleUidProcStatsByUid() {
+ return {{1001234,
+ UidProcStats{.totalMajorFaults = 220,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 1,
+ .processStatsByPid = {{1, {"init", 0, 220, 2, 1}}}}},
+ {1005678,
+ UidProcStats{.totalMajorFaults = 600,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 2,
+ .processStatsByPid = {{1000, {"system_server", 13'400, 600, 2, 2}}}}}};
+}
+
+std::vector<UidStats> sampleUidStats() {
+ return {{.packageInfo = constructPackageInfo("system.daemon", 1001234, UidType::NATIVE),
+ .ioStats = UidIoStats{/*fgRdBytes=*/3'000, /*bgRdBytes=*/0, /*fgWrBytes=*/500,
+ /*bgWrBytes=*/0, /*fgFsync=*/20, /*bgFsync=*/0},
+ .procStats = UidProcStats{.totalMajorFaults = 220,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 1,
+ .processStatsByPid = {{1, {"init", 0, 220, 2, 1}}}}},
+ {.packageInfo = constructPackageInfo("kitchensink.app", 1005678, UidType::APPLICATION),
+ .ioStats = UidIoStats{/*fgRdBytes=*/30, /*bgRdBytes=*/100, /*fgWrBytes=*/50,
+ /*bgWrBytes=*/200,
+ /*fgFsync=*/45, /*bgFsync=*/60},
+ .procStats = UidProcStats{.totalMajorFaults = 600,
+ .totalTasksCount = 2,
+ .ioBlockedTasksCount = 2,
+ .processStatsByPid = {
+ {1000, {"system_server", 13'400, 600, 2, 2}}}}}};
+}
+
+} // namespace
+
+namespace internal {
+
+class UidStatsCollectorPeer : public RefBase {
+public:
+ explicit UidStatsCollectorPeer(sp<UidStatsCollector> collector) : mCollector(collector) {}
+
+ void setPackageInfoResolver(sp<IPackageInfoResolver> packageInfoResolver) {
+ mCollector->mPackageInfoResolver = packageInfoResolver;
+ }
+
+ void setUidIoStatsCollector(sp<UidIoStatsCollectorInterface> uidIoStatsCollector) {
+ mCollector->mUidIoStatsCollector = uidIoStatsCollector;
+ }
+
+ void setUidProcStatsCollector(sp<UidProcStatsCollectorInterface> uidProcStatsCollector) {
+ mCollector->mUidProcStatsCollector = uidProcStatsCollector;
+ }
+
+private:
+ sp<UidStatsCollector> mCollector;
+};
+
+} // namespace internal
+
+class UidStatsCollectorTest : public ::testing::Test {
+protected:
+ virtual void SetUp() {
+ mUidStatsCollector = sp<UidStatsCollector>::make();
+ mUidStatsCollectorPeer = sp<internal::UidStatsCollectorPeer>::make(mUidStatsCollector);
+ mMockPackageInfoResolver = sp<MockPackageInfoResolver>::make();
+ mMockUidIoStatsCollector = sp<MockUidIoStatsCollector>::make();
+ mMockUidProcStatsCollector = sp<MockUidProcStatsCollector>::make();
+ mUidStatsCollectorPeer->setPackageInfoResolver(mMockPackageInfoResolver);
+ mUidStatsCollectorPeer->setUidIoStatsCollector(mMockUidIoStatsCollector);
+ mUidStatsCollectorPeer->setUidProcStatsCollector(mMockUidProcStatsCollector);
+ }
+
+ virtual void TearDown() {
+ mUidStatsCollector.clear();
+ mUidStatsCollectorPeer.clear();
+ mMockPackageInfoResolver.clear();
+ mMockUidIoStatsCollector.clear();
+ mMockUidProcStatsCollector.clear();
+ }
+
+ sp<UidStatsCollector> mUidStatsCollector;
+ sp<internal::UidStatsCollectorPeer> mUidStatsCollectorPeer;
+ sp<MockPackageInfoResolver> mMockPackageInfoResolver;
+ sp<MockUidIoStatsCollector> mMockUidIoStatsCollector;
+ sp<MockUidProcStatsCollector> mMockUidProcStatsCollector;
+};
+
+TEST_F(UidStatsCollectorTest, TestCollect) {
+ EXPECT_CALL(*mMockUidIoStatsCollector, collect()).WillOnce(Return(Result<void>()));
+ EXPECT_CALL(*mMockUidProcStatsCollector, collect()).WillOnce(Return(Result<void>()));
+
+ EXPECT_CALL(*mMockUidIoStatsCollector, latestStats())
+ .WillOnce(Return(std::unordered_map<uid_t, UidIoStats>()));
+ EXPECT_CALL(*mMockUidProcStatsCollector, latestStats())
+ .WillOnce(Return(std::unordered_map<uid_t, UidProcStats>()));
+
+ EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats())
+ .WillOnce(Return(std::unordered_map<uid_t, UidIoStats>()));
+ EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats())
+ .WillOnce(Return(std::unordered_map<uid_t, UidProcStats>()));
+
+ ASSERT_RESULT_OK(mUidStatsCollector->collect());
+}
+
+TEST_F(UidStatsCollectorTest, TestFailsCollectOnUidIoStatsCollectorError) {
+ Result<void> errorResult = Error() << "Failed to collect per-UID I/O stats";
+ EXPECT_CALL(*mMockUidIoStatsCollector, collect()).WillOnce(Return(errorResult));
+
+ ASSERT_FALSE(mUidStatsCollector->collect().ok())
+ << "Must fail to collect when per-UID I/O stats collector fails";
+}
+
+TEST_F(UidStatsCollectorTest, TestFailsCollectOnUidProcStatsCollectorError) {
+ Result<void> errorResult = Error() << "Failed to collect per-UID proc stats";
+ EXPECT_CALL(*mMockUidProcStatsCollector, collect()).WillOnce(Return(errorResult));
+
+ ASSERT_FALSE(mUidStatsCollector->collect().ok())
+ << "Must fail to collect when per-UID proc stats collector fails";
+}
+
+TEST_F(UidStatsCollectorTest, TestCollectLatestStats) {
+ const std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+ const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+ const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+ EXPECT_CALL(*mMockPackageInfoResolver,
+ getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+ .WillOnce(Return(packageInfoByUid));
+ EXPECT_CALL(*mMockUidIoStatsCollector, latestStats()).WillOnce(Return(uidIoStatsByUid));
+ EXPECT_CALL(*mMockUidProcStatsCollector, latestStats()).WillOnce(Return(uidProcStatsByUid));
+
+ ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+ const std::vector<UidStats> expected = sampleUidStats();
+
+ auto actual = mUidStatsCollector->latestStats();
+
+ EXPECT_THAT(actual, UnorderedElementsAreArray(UidStatsMatchers(expected)))
+ << "Latest UID stats doesn't match.\nExpected: " << toString(expected)
+ << "\nActual: " << toString(actual);
+
+ actual = mUidStatsCollector->deltaStats();
+
+ EXPECT_THAT(actual, IsEmpty()) << "Delta UID stats isn't empty.\nActual: " << toString(actual);
+}
+
+TEST_F(UidStatsCollectorTest, TestCollectDeltaStats) {
+ const std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+ const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+ const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+ EXPECT_CALL(*mMockPackageInfoResolver,
+ getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+ .WillOnce(Return(packageInfoByUid));
+ EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+ EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+ ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+ const std::vector<UidStats> expected = sampleUidStats();
+
+ auto actual = mUidStatsCollector->deltaStats();
+
+ EXPECT_THAT(actual, UnorderedElementsAreArray(UidStatsMatchers(expected)))
+ << "Delta UID stats doesn't match.\nExpected: " << toString(expected)
+ << "\nActual: " << toString(actual);
+
+ actual = mUidStatsCollector->latestStats();
+
+ EXPECT_THAT(actual, IsEmpty()) << "Latest UID stats isn't empty.\nActual: " << toString(actual);
+}
+
+TEST_F(UidStatsCollectorTest, TestCollectDeltaStatsWithMissingUidIoStats) {
+ const std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+ std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+ uidIoStatsByUid.erase(1001234);
+ const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+ EXPECT_CALL(*mMockPackageInfoResolver,
+ getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+ .WillOnce(Return(packageInfoByUid));
+ EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+ EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+ ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+ std::vector<UidStats> expected = sampleUidStats();
+ expected[0].ioStats = {};
+
+ auto actual = mUidStatsCollector->deltaStats();
+
+ EXPECT_THAT(actual, UnorderedElementsAreArray(UidStatsMatchers(expected)))
+ << "Delta UID stats doesn't match.\nExpected: " << toString(expected)
+ << "\nActual: " << toString(actual);
+
+ actual = mUidStatsCollector->latestStats();
+
+ EXPECT_THAT(actual, IsEmpty()) << "Latest UID stats isn't empty.\nActual: " << toString(actual);
+}
+
+TEST_F(UidStatsCollectorTest, TestCollectDeltaStatsWithMissingUidProcStats) {
+ const std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+ const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+ std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+ uidProcStatsByUid.erase(1001234);
+
+ EXPECT_CALL(*mMockPackageInfoResolver,
+ getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+ .WillOnce(Return(packageInfoByUid));
+ EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+ EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+ ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+ std::vector<UidStats> expected = sampleUidStats();
+ expected[0].procStats = {};
+
+ auto actual = mUidStatsCollector->deltaStats();
+
+ EXPECT_THAT(actual, UnorderedElementsAreArray(UidStatsMatchers(expected)))
+ << "Delta UID stats doesn't match.\nExpected: " << toString(expected)
+ << "\nActual: " << toString(actual);
+
+ actual = mUidStatsCollector->latestStats();
+
+ EXPECT_THAT(actual, IsEmpty()) << "Latest UID stats isn't empty.\nActual: " << toString(actual);
+}
+
+TEST_F(UidStatsCollectorTest, TestCollectDeltaStatsWithMissingPackageInfo) {
+ std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+ packageInfoByUid.erase(1001234);
+ const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+ const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+ EXPECT_CALL(*mMockPackageInfoResolver,
+ getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+ .WillOnce(Return(packageInfoByUid));
+ EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+ EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+ ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+ std::vector<UidStats> expected = sampleUidStats();
+ expected[0].packageInfo = constructPackageInfo("", 1001234);
+
+ auto actual = mUidStatsCollector->deltaStats();
+
+ EXPECT_THAT(actual, UnorderedElementsAreArray(UidStatsMatchers(expected)))
+ << "Delta UID stats doesn't match.\nExpected: " << toString(expected)
+ << "\nActual: " << toString(actual);
+
+ actual = mUidStatsCollector->latestStats();
+
+ EXPECT_THAT(actual, IsEmpty()) << "Latest UID stats isn't empty.\nActual: " << toString(actual);
+}
+
+TEST_F(UidStatsCollectorTest, TestUidStatsHasPackageInfo) {
+ std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+ packageInfoByUid.erase(1001234);
+ const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+ const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+ EXPECT_CALL(*mMockPackageInfoResolver,
+ getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+ .WillOnce(Return(packageInfoByUid));
+ EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+ EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+ ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+ const auto actual = mUidStatsCollector->deltaStats();
+
+ EXPECT_EQ(actual.size(), 2);
+ for (const auto stats : actual) {
+ if (stats.packageInfo.packageIdentifier.uid == 1001234) {
+ EXPECT_FALSE(stats.hasPackageInfo())
+ << "Stats without package info should return false";
+ } else if (stats.packageInfo.packageIdentifier.uid == 1005678) {
+ EXPECT_TRUE(stats.hasPackageInfo()) << "Stats without package info should return true";
+ } else {
+ FAIL() << "Unexpected uid " << stats.packageInfo.packageIdentifier.uid;
+ }
+ }
+}
+
+TEST_F(UidStatsCollectorTest, TestUidStatsGenericPackageName) {
+ std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+ packageInfoByUid.erase(1001234);
+ const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+ const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+ EXPECT_CALL(*mMockPackageInfoResolver,
+ getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+ .WillOnce(Return(packageInfoByUid));
+ EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+ EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+ ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+ const auto actual = mUidStatsCollector->deltaStats();
+
+ EXPECT_EQ(actual.size(), 2);
+ for (const auto stats : actual) {
+ if (stats.packageInfo.packageIdentifier.uid == 1001234) {
+ EXPECT_EQ(stats.genericPackageName(), "1001234")
+ << "Stats without package info should return UID as package name";
+ } else if (stats.packageInfo.packageIdentifier.uid == 1005678) {
+ EXPECT_EQ(stats.genericPackageName(), "kitchensink.app")
+ << "Stats with package info should return corresponding package name";
+ } else {
+ FAIL() << "Unexpected uid " << stats.packageInfo.packageIdentifier.uid;
+ }
+ }
+}
+
+TEST_F(UidStatsCollectorTest, TestUidStatsUid) {
+ std::unordered_map<uid_t, PackageInfo> packageInfoByUid = samplePackageInfoByUid();
+ packageInfoByUid.erase(1001234);
+ const std::unordered_map<uid_t, UidIoStats> uidIoStatsByUid = sampleUidIoStatsByUid();
+ const std::unordered_map<uid_t, UidProcStats> uidProcStatsByUid = sampleUidProcStatsByUid();
+
+ EXPECT_CALL(*mMockPackageInfoResolver,
+ getPackageInfosForUids(UnorderedElementsAre(1001234, 1005678)))
+ .WillOnce(Return(packageInfoByUid));
+ EXPECT_CALL(*mMockUidIoStatsCollector, deltaStats()).WillOnce(Return(uidIoStatsByUid));
+ EXPECT_CALL(*mMockUidProcStatsCollector, deltaStats()).WillOnce(Return(uidProcStatsByUid));
+
+ ASSERT_RESULT_OK(mUidStatsCollector->collect());
+
+ const auto actual = mUidStatsCollector->deltaStats();
+
+ for (const auto stats : actual) {
+ EXPECT_EQ(stats.uid(), stats.packageInfo.packageIdentifier.uid);
+ }
+}
+
+} // namespace watchdog
+} // namespace automotive
+} // namespace android
diff --git a/cpp/watchdog/server/tests/WatchdogInternalHandlerTest.cpp b/cpp/watchdog/server/tests/WatchdogInternalHandlerTest.cpp
index efa9eef..4646a11 100644
--- a/cpp/watchdog/server/tests/WatchdogInternalHandlerTest.cpp
+++ b/cpp/watchdog/server/tests/WatchdogInternalHandlerTest.cpp
@@ -470,6 +470,19 @@
ASSERT_FALSE(status.isOk()) << status;
}
+TEST_F(WatchdogInternalHandlerTest, TestControlProcessHealthCheck) {
+ setSystemCallingUid();
+ EXPECT_CALL(*mMockWatchdogProcessService, setEnabled(/*isEnabled=*/true)).Times(1);
+ Status status = mWatchdogInternalHandler->controlProcessHealthCheck(false);
+ ASSERT_TRUE(status.isOk()) << status;
+}
+
+TEST_F(WatchdogInternalHandlerTest, TestErrorOnControlProcessHealthCheckWithNonSystemCallingUid) {
+ EXPECT_CALL(*mMockWatchdogProcessService, setEnabled(_)).Times(0);
+ Status status = mWatchdogInternalHandler->controlProcessHealthCheck(false);
+ ASSERT_FALSE(status.isOk()) << status;
+}
+
} // namespace watchdog
} // namespace automotive
} // namespace android
diff --git a/cpp/watchdog/server/tests/WatchdogPerfServiceTest.cpp b/cpp/watchdog/server/tests/WatchdogPerfServiceTest.cpp
index 2bc9803..864d6d1 100644
--- a/cpp/watchdog/server/tests/WatchdogPerfServiceTest.cpp
+++ b/cpp/watchdog/server/tests/WatchdogPerfServiceTest.cpp
@@ -17,13 +17,10 @@
#include "LooperStub.h"
#include "MockDataProcessor.h"
#include "MockProcDiskStats.h"
-#include "MockProcPidStat.h"
#include "MockProcStat.h"
-#include "MockUidIoStats.h"
-#include "ProcPidDir.h"
-#include "ProcPidStat.h"
+#include "MockUidStatsCollector.h"
#include "ProcStat.h"
-#include "UidIoStats.h"
+#include "UidStatsCollector.h"
#include "WatchdogPerfService.h"
#include <WatchdogProperties.sysprop.h>
@@ -42,12 +39,10 @@
using ::android::sp;
using ::android::String16;
using ::android::wp;
-using ::android::automotive::watchdog::internal::PowerCycle;
using ::android::automotive::watchdog::testing::LooperStub;
using ::android::base::Error;
using ::android::base::Result;
using ::testing::_;
-using ::testing::DefaultValue;
using ::testing::InSequence;
using ::testing::Mock;
using ::testing::NiceMock;
@@ -71,19 +66,17 @@
void injectFakes() {
looperStub = sp<LooperStub>::make();
- mockUidIoStats = sp<NiceMock<MockUidIoStats>>::make();
+ mockUidStatsCollector = sp<MockUidStatsCollector>::make();
mockProcDiskStats = sp<NiceMock<MockProcDiskStats>>::make();
mockProcStat = sp<NiceMock<MockProcStat>>::make();
- mockProcPidStat = sp<NiceMock<MockProcPidStat>>::make();
mockDataProcessor = sp<StrictMock<MockDataProcessor>>::make();
{
Mutex::Autolock lock(service->mMutex);
service->mHandlerLooper = looperStub;
- service->mUidIoStats = mockUidIoStats;
+ service->mUidStatsCollector = mockUidStatsCollector;
service->mProcDiskStats = mockProcDiskStats;
service->mProcStat = mockProcStat;
- service->mProcPidStat = mockProcPidStat;
}
EXPECT_CALL(*mockDataProcessor, init()).Times(1);
ASSERT_RESULT_OK(service->registerDataProcessor(mockDataProcessor));
@@ -114,19 +107,17 @@
}
void verifyAndClearExpectations() {
- Mock::VerifyAndClearExpectations(mockUidIoStats.get());
+ Mock::VerifyAndClearExpectations(mockUidStatsCollector.get());
Mock::VerifyAndClearExpectations(mockProcStat.get());
- Mock::VerifyAndClearExpectations(mockProcPidStat.get());
Mock::VerifyAndClearExpectations(mockDataProcessor.get());
}
sp<WatchdogPerfService> service;
// Below fields are populated only on injectFakes.
sp<LooperStub> looperStub;
- sp<MockUidIoStats> mockUidIoStats;
+ sp<MockUidStatsCollector> mockUidStatsCollector;
sp<MockProcDiskStats> mockProcDiskStats;
sp<MockProcStat> mockProcStat;
- sp<MockProcPidStat> mockProcPidStat;
sp<MockDataProcessor> mockDataProcessor;
};
@@ -139,13 +130,13 @@
ASSERT_RESULT_OK(servicePeer->start());
- EXPECT_CALL(*servicePeer->mockUidIoStats, collect()).Times(2);
+ EXPECT_CALL(*servicePeer->mockUidStatsCollector, collect()).Times(2);
EXPECT_CALL(*servicePeer->mockProcStat, collect()).Times(2);
- EXPECT_CALL(*servicePeer->mockProcPidStat, collect()).Times(2);
EXPECT_CALL(*servicePeer->mockDataProcessor,
- onBoottimeCollection(_, wp<UidIoStats>(servicePeer->mockUidIoStats),
- wp<ProcStat>(servicePeer->mockProcStat),
- wp<ProcPidStat>(servicePeer->mockProcPidStat)))
+ onBoottimeCollection(_,
+ wp<UidStatsCollectorInterface>(
+ servicePeer->mockUidStatsCollector),
+ wp<ProcStat>(servicePeer->mockProcStat)))
.Times(2);
// Make sure the collection event changes from EventType::INIT to
@@ -206,17 +197,15 @@
ASSERT_RESULT_OK(servicePeer.start());
- wp<UidIoStats> uidIoStats(servicePeer.mockUidIoStats);
+ wp<UidStatsCollectorInterface> uidStatsCollector(servicePeer.mockUidStatsCollector);
wp<IProcDiskStatsInterface> procDiskStats(servicePeer.mockProcDiskStats);
wp<ProcStat> procStat(servicePeer.mockProcStat);
- wp<ProcPidStat> procPidStat(servicePeer.mockProcPidStat);
// #1 Boot-time collection
- EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+ EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
- EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onBoottimeCollection(_, uidIoStats, procStat, procPidStat))
+ onBoottimeCollection(_, uidStatsCollector, procStat))
.Times(1);
ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -228,11 +217,10 @@
servicePeer.verifyAndClearExpectations();
// #2 Boot-time collection
- EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+ EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
- EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onBoottimeCollection(_, uidIoStats, procStat, procPidStat))
+ onBoottimeCollection(_, uidStatsCollector, procStat))
.Times(1);
ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -245,11 +233,10 @@
servicePeer.verifyAndClearExpectations();
// #3 Last boot-time collection
- EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+ EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
- EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onBoottimeCollection(_, uidIoStats, procStat, procPidStat))
+ onBoottimeCollection(_, uidStatsCollector, procStat))
.Times(1);
ASSERT_RESULT_OK(service->onBootFinished());
@@ -286,12 +273,10 @@
servicePeer.verifyAndClearExpectations();
// #6 Periodic collection
- EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+ EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
- EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onPeriodicCollection(_, SystemState::NORMAL_MODE, uidIoStats, procStat,
- procPidStat))
+ onPeriodicCollection(_, SystemState::NORMAL_MODE, uidStatsCollector, procStat))
.Times(1);
ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -312,12 +297,10 @@
ASSERT_RESULT_OK(service->onCustomCollection(-1, args));
- EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+ EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
- EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onCustomCollection(_, SystemState::NORMAL_MODE, _, uidIoStats, procStat,
- procPidStat))
+ onCustomCollection(_, SystemState::NORMAL_MODE, _, uidStatsCollector, procStat))
.Times(1);
ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -329,12 +312,10 @@
servicePeer.verifyAndClearExpectations();
// #8 Custom collection
- EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+ EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
- EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onCustomCollection(_, SystemState::NORMAL_MODE, _, uidIoStats, procStat,
- procPidStat))
+ onCustomCollection(_, SystemState::NORMAL_MODE, _, uidStatsCollector, procStat))
.Times(1);
ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -362,12 +343,10 @@
<< "Invalid collection event";
// #10 Switch to periodic collection
- EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+ EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
- EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onPeriodicCollection(_, SystemState::NORMAL_MODE, uidIoStats, procStat,
- procPidStat))
+ onPeriodicCollection(_, SystemState::NORMAL_MODE, uidStatsCollector, procStat))
.Times(1);
ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -398,9 +377,8 @@
ASSERT_RESULT_OK(servicePeer.start());
- ON_CALL(*servicePeer.mockUidIoStats, enabled()).WillByDefault(Return(false));
+ ON_CALL(*servicePeer.mockUidStatsCollector, enabled()).WillByDefault(Return(false));
ON_CALL(*servicePeer.mockProcStat, enabled()).WillByDefault(Return(false));
- ON_CALL(*servicePeer.mockProcPidStat, enabled()).WillByDefault(Return(false));
// Collection should terminate and call data processor's terminate method on error.
EXPECT_CALL(*servicePeer.mockDataProcessor, terminate()).Times(1);
@@ -422,7 +400,7 @@
// Inject data collector error.
Result<void> errorRes = Error() << "Failed to collect data";
- EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).WillOnce(Return(errorRes));
+ EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).WillOnce(Return(errorRes));
// Collection should terminate and call data processor's terminate method on error.
EXPECT_CALL(*servicePeer.mockDataProcessor, terminate()).Times(1);
@@ -447,9 +425,10 @@
// Inject data processor error.
Result<void> errorRes = Error() << "Failed to process data";
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onBoottimeCollection(_, wp<UidIoStats>(servicePeer.mockUidIoStats),
- wp<ProcStat>(servicePeer.mockProcStat),
- wp<ProcPidStat>(servicePeer.mockProcPidStat)))
+ onBoottimeCollection(_,
+ wp<UidStatsCollectorInterface>(
+ servicePeer.mockUidStatsCollector),
+ wp<ProcStat>(servicePeer.mockProcStat)))
.WillOnce(Return(errorRes));
// Collection should terminate and call data processor's terminate method on error.
@@ -484,16 +463,15 @@
int maxIterations = static_cast<int>(kTestCustomCollectionDuration.count() /
kTestCustomCollectionInterval.count());
for (int i = 0; i <= maxIterations; ++i) {
- EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+ EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
- EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockDataProcessor,
onCustomCollection(_, SystemState::NORMAL_MODE,
UnorderedElementsAreArray(
{"android.car.cts", "system_server"}),
- wp<UidIoStats>(servicePeer.mockUidIoStats),
- wp<ProcStat>(servicePeer.mockProcStat),
- wp<ProcPidStat>(servicePeer.mockProcPidStat)))
+ wp<UidStatsCollectorInterface>(
+ servicePeer.mockUidStatsCollector),
+ wp<ProcStat>(servicePeer.mockProcStat)))
.Times(1);
ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -529,10 +507,9 @@
ASSERT_NO_FATAL_FAILURE(startPeriodicCollection(&servicePeer));
- wp<UidIoStats> uidIoStats(servicePeer.mockUidIoStats);
+ wp<UidStatsCollectorInterface> uidStatsCollector(servicePeer.mockUidStatsCollector);
wp<IProcDiskStatsInterface> procDiskStats(servicePeer.mockProcDiskStats);
wp<ProcStat> procStat(servicePeer.mockProcStat);
- wp<ProcPidStat> procPidStat(servicePeer.mockProcPidStat);
// Periodic monitor issuing an alert to start new collection.
EXPECT_CALL(*servicePeer.mockProcDiskStats, collect()).Times(1);
@@ -549,12 +526,10 @@
<< " seconds interval";
servicePeer.verifyAndClearExpectations();
- EXPECT_CALL(*servicePeer.mockUidIoStats, collect()).Times(1);
+ EXPECT_CALL(*servicePeer.mockUidStatsCollector, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockProcStat, collect()).Times(1);
- EXPECT_CALL(*servicePeer.mockProcPidStat, collect()).Times(1);
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onPeriodicCollection(_, SystemState::NORMAL_MODE, uidIoStats, procStat,
- procPidStat))
+ onPeriodicCollection(_, SystemState::NORMAL_MODE, uidStatsCollector, procStat))
.Times(1);
ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -575,7 +550,7 @@
ASSERT_NO_FATAL_FAILURE(skipPeriodicMonitorEvents(&servicePeer));
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onPeriodicCollection(_, SystemState::NORMAL_MODE, _, _, _))
+ onPeriodicCollection(_, SystemState::NORMAL_MODE, _, _))
.Times(1);
ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -587,7 +562,7 @@
service->setSystemState(SystemState::GARAGE_MODE);
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onPeriodicCollection(_, SystemState::GARAGE_MODE, _, _, _))
+ onPeriodicCollection(_, SystemState::GARAGE_MODE, _, _))
.Times(1);
ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
@@ -599,7 +574,7 @@
service->setSystemState(SystemState::NORMAL_MODE);
EXPECT_CALL(*servicePeer.mockDataProcessor,
- onPeriodicCollection(_, SystemState::NORMAL_MODE, _, _, _))
+ onPeriodicCollection(_, SystemState::NORMAL_MODE, _, _))
.Times(1);
ASSERT_RESULT_OK(servicePeer.looperStub->pollCache());
diff --git a/cpp/watchdog/server/tests/WatchdogServiceHelperTest.cpp b/cpp/watchdog/server/tests/WatchdogServiceHelperTest.cpp
index 99b65df..1d2f570 100644
--- a/cpp/watchdog/server/tests/WatchdogServiceHelperTest.cpp
+++ b/cpp/watchdog/server/tests/WatchdogServiceHelperTest.cpp
@@ -16,6 +16,7 @@
#include "MockCarWatchdogServiceForSystem.h"
#include "MockWatchdogProcessService.h"
+#include "PackageInfoTestUtils.h"
#include "WatchdogServiceHelper.h"
#include <binder/IBinder.h>
@@ -27,6 +28,8 @@
namespace automotive {
namespace watchdog {
+namespace {
+
namespace aawi = ::android::automotive::watchdog::internal;
using aawi::ApplicationCategoryType;
@@ -35,6 +38,7 @@
using aawi::PackageInfo;
using aawi::PackageIoOveruseStats;
using aawi::UidType;
+using aawi::UserPackageIoUsageStats;
using ::android::IBinder;
using ::android::RefBase;
using ::android::sp;
@@ -48,6 +52,23 @@
using ::testing::SetArgPointee;
using ::testing::UnorderedElementsAreArray;
+UserPackageIoUsageStats sampleUserPackageIoUsageStats(userid_t userId,
+ const std::string& packageName) {
+ UserPackageIoUsageStats stats;
+ stats.userId = userId;
+ stats.packageName = packageName;
+ stats.ioUsageStats.writtenBytes.foregroundBytes = 100;
+ stats.ioUsageStats.writtenBytes.backgroundBytes = 200;
+ stats.ioUsageStats.writtenBytes.garageModeBytes = 300;
+ stats.ioUsageStats.forgivenWriteBytes.foregroundBytes = 1100;
+ stats.ioUsageStats.forgivenWriteBytes.backgroundBytes = 1200;
+ stats.ioUsageStats.forgivenWriteBytes.garageModeBytes = 1300;
+ stats.ioUsageStats.totalOveruses = 10;
+ return stats;
+}
+
+} // namespace
+
namespace internal {
class WatchdogServiceHelperPeer : public RefBase {
@@ -69,22 +90,6 @@
} // namespace internal
-namespace {
-
-PackageInfo constructPackageInfo(const char* packageName, int32_t uid, UidType uidType,
- ComponentType componentType,
- ApplicationCategoryType appCategoryType) {
- PackageInfo packageInfo;
- packageInfo.packageIdentifier.name = packageName;
- packageInfo.packageIdentifier.uid = uid;
- packageInfo.uidType = uidType;
- packageInfo.componentType = componentType;
- packageInfo.appCategoryType = appCategoryType;
- return packageInfo;
-}
-
-} // namespace
-
class WatchdogServiceHelperTest : public ::testing::Test {
protected:
virtual void SetUp() {
@@ -454,6 +459,50 @@
"service API returns error";
}
+TEST_F(WatchdogServiceHelperTest, TestGetTodayIoUsageStats) {
+ std::vector<UserPackageIoUsageStats>
+ expectedStats{sampleUserPackageIoUsageStats(10, "vendor.package"),
+ sampleUserPackageIoUsageStats(11, "third_party.package")};
+
+ registerCarWatchdogService();
+
+ EXPECT_CALL(*mMockCarWatchdogServiceForSystem, getTodayIoUsageStats(_))
+ .WillOnce(DoAll(SetArgPointee<0>(expectedStats), Return(Status::ok())));
+
+ std::vector<UserPackageIoUsageStats> actualStats;
+ Status status = mWatchdogServiceHelper->getTodayIoUsageStats(&actualStats);
+
+ ASSERT_TRUE(status.isOk()) << status;
+ EXPECT_THAT(actualStats, UnorderedElementsAreArray(expectedStats));
+}
+
+TEST_F(WatchdogServiceHelperTest,
+ TestErrorOnGetTodayIoUsageStatsWithNoCarWatchdogServiceRegistered) {
+ EXPECT_CALL(*mMockCarWatchdogServiceForSystem, getTodayIoUsageStats(_)).Times(0);
+
+ std::vector<UserPackageIoUsageStats> actualStats;
+ Status status = mWatchdogServiceHelper->getTodayIoUsageStats(&actualStats);
+
+ ASSERT_FALSE(status.isOk()) << "getTodayIoUsageStats should fail when no "
+ "car watchdog service registered with the helper";
+ EXPECT_THAT(actualStats, IsEmpty());
+}
+
+TEST_F(WatchdogServiceHelperTest,
+ TestErrorOnGetTodayIoUsageStatsWithErrorStatusFromCarWatchdogService) {
+ registerCarWatchdogService();
+
+ EXPECT_CALL(*mMockCarWatchdogServiceForSystem, getTodayIoUsageStats(_))
+ .WillOnce(Return(Status::fromExceptionCode(Status::EX_ILLEGAL_STATE, "Illegal state")));
+
+ std::vector<UserPackageIoUsageStats> actualStats;
+ Status status = mWatchdogServiceHelper->getTodayIoUsageStats(&actualStats);
+
+ ASSERT_FALSE(status.isOk()) << "getTodayIoUsageStats should fail when car watchdog "
+ "service API returns error";
+ ASSERT_TRUE(actualStats.empty());
+}
+
} // namespace watchdog
} // namespace automotive
} // namespace android
diff --git a/packages/CarDeveloperOptions/Android.bp b/packages/CarDeveloperOptions/Android.bp
index e3b559a..4394ed2 100644
--- a/packages/CarDeveloperOptions/Android.bp
+++ b/packages/CarDeveloperOptions/Android.bp
@@ -38,4 +38,8 @@
// TODO(b/176240706): "org.apache.http.legacy" is used by Settings-core,
// get rid of this dependency and remove the "uses_libs" property.
uses_libs: ["org.apache.http.legacy"],
+ optional_uses_libs: [
+ "androidx.window.extensions",
+ "androidx.window.sidecar",
+ ],
}
diff --git a/packages/CarDeveloperOptions/AndroidManifest.xml b/packages/CarDeveloperOptions/AndroidManifest.xml
index cd61380..0b82aea 100644
--- a/packages/CarDeveloperOptions/AndroidManifest.xml
+++ b/packages/CarDeveloperOptions/AndroidManifest.xml
@@ -25,6 +25,9 @@
tools:node="merge"
tools:replace="android:label">
+ <uses-library android:name="androidx.window.extensions" android:required="false"/>
+ <uses-library android:name="androidx.window.sidecar" android:required="false"/>
+
<activity
android:name=".CarDevelopmentSettingsDashboardActivity"
android:enabled="false"
@@ -157,6 +160,15 @@
</activity>
<activity
+ android:name="com.android.settings.Settings$WifiDetailsSettingsActivity"
+ android:enabled="false"
+ android:exported="false"
+ tools:node="merge"
+ tools:replace="android:exported">
+ <intent-filter tools:node="removeAll"/>
+ </activity>
+
+ <activity
android:name="com.android.settings.Settings$ConfigureWifiSettingsActivity"
android:enabled="false"
android:exported="false"
@@ -734,6 +746,15 @@
</activity>
<activity
+ android:name="com.android.settings.Settings$WifiScanningSettingsActivity"
+ android:enabled="false"
+ android:exported="false"
+ tools:node="merge"
+ tools:replace="android:exported">
+ <intent-filter tools:node="removeAll"/>
+ </activity>
+
+ <activity
android:name="com.android.settings.Settings$SecurityDashboardActivity"
android:enabled="false"
android:exported="false"
@@ -2330,6 +2351,15 @@
</activity-alias>
<activity-alias
+ android:name="com.android.settings.DeepLinkHomepageActivity"
+ android:enabled="false"
+ android:exported="false"
+ tools:node="merge"
+ tools:replace="android:exported">
+ <intent-filter tools:node="removeAll"/>
+ </activity-alias>
+
+ <activity-alias
android:name="com.android.settings.Settings$BluetoothSettingsActivity"
android:enabled="false"
android:exported="false"
diff --git a/packages/CarDeveloperOptions/res/values/strings.xml b/packages/CarDeveloperOptions/res/values/strings.xml
new file mode 100644
index 0000000..f92e9a7
--- /dev/null
+++ b/packages/CarDeveloperOptions/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <string name="car_pref_category_title">Car</string>
+ <string name="car_ui_plugin_enabled_pref_title">Enable Car UI library plugin</string>
+ <string name="car_ui_plugin_not_found_text">Car UI plugin not found</string>
+</resources>
\ No newline at end of file
diff --git a/packages/CarDeveloperOptions/res/values/themes.xml b/packages/CarDeveloperOptions/res/values/themes.xml
index 691afd3..73bf8d8 100644
--- a/packages/CarDeveloperOptions/res/values/themes.xml
+++ b/packages/CarDeveloperOptions/res/values/themes.xml
@@ -47,6 +47,16 @@
<style name="Theme.CarDeveloperOptions" parent="@style/Theme.CarUi.WithToolbar">
<item name="alertDialogTheme">@style/Theme.AlertDialog</item>
<item name="switchBarTheme">@style/ThemeOverlay.SwitchBar.Settings</item>
+ <item name="preferenceTheme">@style/CarDeveloperOptionsPreferenceTheme</item>
+ </style>
+
+ <style name="CarDeveloperOptionsPreferenceTheme" parent="@style/CarUiPreferenceTheme">
+ <item name="preferenceFragmentCompatStyle">@style/CarDeveloperOptionsPreferenceFragment</item>
+ <item name="preferenceFragmentStyle">@style/CarDeveloperOptionsPreferenceFragment</item>
+ </style>
+
+ <style name="CarDeveloperOptionsPreferenceFragment" parent="@style/PreferenceFragment.CarUi">
+ <item name="android:layout">@layout/preference_list_fragment</item>
</style>
</resources>
\ No newline at end of file
diff --git a/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentCarUiLibController.java b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentCarUiLibController.java
new file mode 100644
index 0000000..ef2c943
--- /dev/null
+++ b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentCarUiLibController.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2021 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.developeroptions;
+
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static android.content.pm.PackageManager.MATCH_ALL;
+import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+
+import androidx.preference.Preference;
+import androidx.preference.SwitchPreference;
+
+/**
+ * Preference controller for Car UI library plugin enablement.
+ */
+public class CarDevelopmentCarUiLibController extends CarDevelopmentPreferenceController {
+ private static final String CAR_UI_PLUGIN_ENABLED_KEY = "car_ui_plugin_enabled";
+
+ public CarDevelopmentCarUiLibController(Context context) {
+ super(context);
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return CAR_UI_PLUGIN_ENABLED_KEY;
+ }
+
+ @Override
+ public String getPreferenceTitle() {
+ return mContext.getString(R.string.car_ui_plugin_enabled_pref_title);
+ }
+
+ @Override
+ String getPreferenceSummary() {
+ if (getCarUiPluginProviderInfo() == null) {
+ return mContext.getString(R.string.car_ui_plugin_not_found_text);
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ Boolean isPluginEnabled = (Boolean) newValue;
+ ProviderInfo providerInfo = getCarUiPluginProviderInfo();
+ if (providerInfo == null) {
+ throw new IllegalStateException("Car UI plugin not found");
+ }
+
+ ComponentName componentName = new ComponentName(providerInfo.packageName,
+ providerInfo.name);
+ int state = isPluginEnabled ? COMPONENT_ENABLED_STATE_ENABLED
+ : COMPONENT_ENABLED_STATE_DISABLED;
+ mContext.getPackageManager().setComponentEnabledSetting(componentName,
+ state, 0 /* no optional flags */);
+ return true;
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ if (getCarUiPluginProviderInfo() == null) {
+ ((SwitchPreference) mPreference).setChecked(false);
+ mPreference.setEnabled(false);
+ return;
+ }
+
+ ((SwitchPreference) mPreference).setChecked(isCarUiPluginEnabled());
+ }
+
+ private ProviderInfo getCarUiPluginProviderInfo() {
+ String authority = mContext.getString(
+ R.string.car_ui_plugin_package_provider_authority_name);
+ return mContext.getPackageManager().resolveContentProvider(authority,
+ MATCH_ALL | MATCH_DISABLED_COMPONENTS);
+ }
+
+ private boolean isCarUiPluginEnabled() {
+ ProviderInfo providerInfo = getCarUiPluginProviderInfo();
+ if (providerInfo == null) {
+ return false;
+ }
+
+ ComponentName componentName = new ComponentName(providerInfo.packageName,
+ providerInfo.name);
+ int state = mContext.getPackageManager().getComponentEnabledSetting(componentName);
+ if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
+ return providerInfo.enabled;
+ }
+
+ return state == COMPONENT_ENABLED_STATE_ENABLED;
+ }
+}
diff --git a/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentPreferenceController.java b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentPreferenceController.java
new file mode 100644
index 0000000..68f6528
--- /dev/null
+++ b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentPreferenceController.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.developeroptions;
+
+import android.annotation.Nullable;
+import android.content.Context;
+
+import androidx.preference.Preference;
+
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+
+/**
+ * Parent class for Car feature preferences.*
+ */
+public abstract class CarDevelopmentPreferenceController extends
+ DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener,
+ PreferenceControllerMixin {
+
+ abstract String getPreferenceTitle();
+ @Nullable
+ abstract String getPreferenceSummary();
+
+ CarDevelopmentPreferenceController(Context context) {
+ super(context);
+ }
+}
diff --git a/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentSettingsDashboardFragment.java b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentSettingsDashboardFragment.java
index 72e591f..d74b8ea 100644
--- a/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentSettingsDashboardFragment.java
+++ b/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentSettingsDashboardFragment.java
@@ -24,6 +24,11 @@
import android.content.pm.PackageManager;
import android.os.Bundle;
+import androidx.annotation.XmlRes;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.SwitchPreference;
+
import com.android.car.ui.toolbar.MenuItem;
import com.android.car.ui.toolbar.Toolbar;
import com.android.car.ui.toolbar.ToolbarController;
@@ -40,27 +45,68 @@
* {@link PREFERENCES_TO_REMOVE} constant.
*/
public class CarDevelopmentSettingsDashboardFragment extends DevelopmentSettingsDashboardFragment {
+ static final String PREF_KEY_CAR_CATEGORY = "car_development_category";
+ static final String PREF_KEY_DEBUG_MISC_CATEGORY = "debug_misc_category";
- private ToolbarController mToolbar;
+ private final List<CarDevelopmentPreferenceController> mCarFeatureControllers =
+ new ArrayList<>();
@Override
public void onActivityCreated(Bundle icicle) {
super.onActivityCreated(icicle);
- mToolbar = getToolbar();
- if (mToolbar != null) {
+ ToolbarController toolbar = getToolbar();
+ if (toolbar != null) {
List<MenuItem> items = getToolbarMenuItems();
- mToolbar.setTitle(getPreferenceScreen().getTitle());
- mToolbar.setMenuItems(items);
- mToolbar.setNavButtonMode(Toolbar.NavButtonMode.BACK);
- mToolbar.setState(Toolbar.State.SUBPAGE);
+ toolbar.setTitle(getPreferenceScreen().getTitle());
+ toolbar.setMenuItems(items);
+ toolbar.setNavButtonMode(Toolbar.NavButtonMode.BACK);
+ toolbar.setState(Toolbar.State.SUBPAGE);
}
}
@Override
+ public void addPreferencesFromResource(@XmlRes int preferencesResId) {
+ super.addPreferencesFromResource(preferencesResId);
+
+ int miscPrefCategory = getPreferenceScreen().findPreference(
+ PREF_KEY_DEBUG_MISC_CATEGORY).getOrder();
+ PreferenceCategory carCategory = new PreferenceCategory(getContext());
+ carCategory.setOrder(miscPrefCategory + 1);
+ carCategory.setKey(PREF_KEY_CAR_CATEGORY);
+ carCategory.setTitle(getContext().getString(R.string.car_pref_category_title));
+ getPreferenceScreen().addPreference(carCategory);
+
+ for (CarDevelopmentPreferenceController controller : mCarFeatureControllers) {
+ addCarPreference(controller);
+ }
+ }
+
+ void addCarPreference(CarDevelopmentPreferenceController controller) {
+ final Preference dynamicPreference;
+ if (controller instanceof CarDevelopmentCarUiLibController) {
+ dynamicPreference = new SwitchPreference(getContext());
+ } else {
+ throw new UnsupportedOperationException(
+ "Unexpected controller type " + controller.getClass().getSimpleName());
+ }
+
+ dynamicPreference.setKey(controller.getPreferenceKey());
+ dynamicPreference.setTitle(controller.getPreferenceTitle());
+ final String summary = controller.getPreferenceSummary();
+ if (summary != null) {
+ dynamicPreference.setSummary(summary);
+ }
+
+ ((PreferenceCategory) getPreferenceScreen().findPreference(PREF_KEY_CAR_CATEGORY))
+ .addPreference(dynamicPreference);
+ }
+
+ @Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
List<AbstractPreferenceController> controllers = super.createPreferenceControllers(context);
removeControllers(controllers);
addHiddenControllers(context, controllers);
+ addCarControllers(context, controllers);
return controllers;
}
@@ -102,6 +148,12 @@
}
}
+ private void addCarControllers(Context context,
+ List<AbstractPreferenceController> controllers) {
+ mCarFeatureControllers.add(new CarDevelopmentCarUiLibController(context));
+ controllers.addAll(mCarFeatureControllers);
+ }
+
private boolean isDeveloperOptionsModuleEnabled() {
PackageManager pm = getContext().getPackageManager();
ComponentName component = getActivity().getComponentName();
diff --git a/packages/ScriptExecutor/Android.bp b/packages/ScriptExecutor/Android.bp
new file mode 100644
index 0000000..952a9b8
--- /dev/null
+++ b/packages/ScriptExecutor/Android.bp
@@ -0,0 +1,93 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library {
+ name: "libscriptexecutorjni",
+
+ cflags: [
+ "-Wno-unused-parameter",
+ ],
+ header_libs: [
+ "jni_headers",
+ "libnativehelper_header_only",
+ ],
+
+ static_libs: [
+ "liblua",
+ "libbase_ndk",
+ ],
+
+ shared_libs: [
+ "liblog",
+ ],
+
+ srcs: [
+ "src/BundleWrapper.cpp",
+ "src/JniUtils.cpp",
+ "src/LuaEngine.cpp",
+ "src/ScriptExecutorJni.cpp",
+ "src/ScriptExecutorListener.cpp",
+ ],
+
+ sdk_version: "current",
+
+ min_sdk_version: "30",
+
+ stl: "libc++_static",
+
+ // Allow dependents to use the header files.
+ export_include_dirs: ["src"],
+}
+
+android_app {
+ name: "ScriptExecutor",
+
+ srcs: [
+ ":iscriptexecutor_aidl",
+ "src/**/*.java"
+ ],
+
+ resource_dirs: ["res"],
+
+ sdk_version: "module_current",
+
+ min_sdk_version: "30",
+
+ privileged: false,
+
+ // TODO(b/196053524): Enable optimization.
+ optimize: {
+ enabled: false,
+ },
+
+ jni_libs: [
+ "libscriptexecutorjni",
+ ],
+}
+
+java_test_helper_library {
+ name: "scriptexecutor-test-lib",
+
+ srcs: [
+ ":iscriptexecutor_aidl",
+ "src/**/*.java",
+ ],
+
+ sdk_version: "current",
+}
+
diff --git a/packages/ScriptExecutor/AndroidManifest.xml b/packages/ScriptExecutor/AndroidManifest.xml
new file mode 100644
index 0000000..51595fa
--- /dev/null
+++ b/packages/ScriptExecutor/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.scriptexecutor">
+
+ <application android:label="@string/app_title"
+ android:allowBackup="false">
+
+ <service android:name=".ScriptExecutor"
+ android:exported="true"
+ android:isolatedProcess="true"/>
+ </application>
+</manifest>
diff --git a/packages/ScriptExecutor/res/values/strings.xml b/packages/ScriptExecutor/res/values/strings.xml
new file mode 100644
index 0000000..39ca8b2
--- /dev/null
+++ b/packages/ScriptExecutor/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<resources>
+ <string name="app_title" translatable="false">Script executor service</string>
+</resources>
diff --git a/packages/ScriptExecutor/src/BundleWrapper.cpp b/packages/ScriptExecutor/src/BundleWrapper.cpp
new file mode 100644
index 0000000..88403b0
--- /dev/null
+++ b/packages/ScriptExecutor/src/BundleWrapper.cpp
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#include "BundleWrapper.h"
+
+#include "JniUtils.h"
+#include "nativehelper/scoped_local_ref.h"
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+using ::android::base::Error;
+using ::android::base::Result;
+
+BundleWrapper::BundleWrapper(JNIEnv* env) {
+ mJNIEnv = env;
+ ScopedLocalRef<jclass> localBundleClassRef(mJNIEnv,
+ mJNIEnv->FindClass("android/os/PersistableBundle"));
+ mBundleClass = static_cast<jclass>(mJNIEnv->NewGlobalRef(localBundleClassRef.get()));
+
+ jmethodID bundleConstructor = mJNIEnv->GetMethodID(mBundleClass, "<init>", "()V");
+ ScopedLocalRef<jobject> localBundleObjectRef(mJNIEnv,
+ mJNIEnv->NewObject(mBundleClass,
+ bundleConstructor));
+ mBundle = mJNIEnv->NewGlobalRef(localBundleObjectRef.get());
+}
+
+BundleWrapper::~BundleWrapper() {
+ // Delete global JNI references.
+ if (mBundle != NULL) {
+ mJNIEnv->DeleteGlobalRef(mBundle);
+ }
+ if (mBundleClass != NULL) {
+ mJNIEnv->DeleteGlobalRef(mBundleClass);
+ }
+}
+
+Result<void> BundleWrapper::putBoolean(const char* key, bool value) {
+ ScopedLocalRef<jstring> keyStringRef(mJNIEnv, mJNIEnv->NewStringUTF(key));
+ if (keyStringRef == nullptr) {
+ return Error() << "Failed to create a string for a key=" << key << " due to OOM error";
+ }
+
+ // TODO(b/188832769): consider caching the references.
+ jmethodID putBooleanMethod =
+ mJNIEnv->GetMethodID(mBundleClass, "putBoolean", "(Ljava/lang/String;Z)V");
+ mJNIEnv->CallVoidMethod(mBundle, putBooleanMethod, keyStringRef.get(),
+ static_cast<jboolean>(value));
+ return {}; // ok result
+}
+
+Result<void> BundleWrapper::putLong(const char* key, int64_t value) {
+ ScopedLocalRef<jstring> keyStringRef(mJNIEnv, mJNIEnv->NewStringUTF(key));
+ if (keyStringRef == nullptr) {
+ return Error() << "Failed to create a string for a key=" << key << " due to OOM error";
+ }
+
+ jmethodID putLongMethod =
+ mJNIEnv->GetMethodID(mBundleClass, "putLong", "(Ljava/lang/String;J)V");
+ mJNIEnv->CallVoidMethod(mBundle, putLongMethod, keyStringRef.get(), static_cast<jlong>(value));
+ return {}; // ok result
+}
+
+Result<void> BundleWrapper::putDouble(const char* key, double value) {
+ ScopedLocalRef<jstring> keyStringRef(mJNIEnv, mJNIEnv->NewStringUTF(key));
+ if (keyStringRef == nullptr) {
+ return Error() << "Failed to create a string for a key=" << key << " due to OOM error";
+ }
+
+ jmethodID putDoubleMethod =
+ mJNIEnv->GetMethodID(mBundleClass, "putDouble", "(Ljava/lang/String;D)V");
+ mJNIEnv->CallVoidMethod(mBundle, putDoubleMethod, keyStringRef.get(),
+ static_cast<jdouble>(value));
+ return {}; // ok result
+}
+
+Result<void> BundleWrapper::putString(const char* key, const char* value) {
+ jmethodID putStringMethod = mJNIEnv->GetMethodID(mBundleClass, "putString",
+ "(Ljava/lang/String;Ljava/lang/String;)V");
+ ScopedLocalRef<jstring> keyStringRef(mJNIEnv, mJNIEnv->NewStringUTF(key));
+ if (keyStringRef == nullptr) {
+ return Error() << "Failed to create a string for a key=" << key << " due to OOM error";
+ }
+
+ ScopedLocalRef<jstring> valueStringRef(mJNIEnv, mJNIEnv->NewStringUTF(value));
+ if (valueStringRef == nullptr) {
+ return Error() << "Failed to create a string for the provided value due to OOM error";
+ }
+
+ mJNIEnv->CallVoidMethod(mBundle, putStringMethod, keyStringRef.get(), valueStringRef.get());
+ return {}; // ok result
+}
+
+Result<void> BundleWrapper::putLongArray(const char* key, const std::vector<int64_t>& value) {
+ ScopedLocalRef<jstring> keyStringRef(mJNIEnv, mJNIEnv->NewStringUTF(key));
+ if (keyStringRef == nullptr) {
+ return Error() << "Failed to create a string for a key=" << key << " due to OOM error";
+ }
+
+ jmethodID putLongArrayMethod =
+ mJNIEnv->GetMethodID(mBundleClass, "putLongArray", "(Ljava/lang/String;[J)V");
+
+ ScopedLocalRef<jlongArray> arrayRef(mJNIEnv, mJNIEnv->NewLongArray(value.size()));
+ mJNIEnv->SetLongArrayRegion(arrayRef.get(), 0, value.size(), &value[0]);
+ mJNIEnv->CallVoidMethod(mBundle, putLongArrayMethod, keyStringRef.get(), arrayRef.get());
+ return {}; // ok result
+}
+
+Result<void> BundleWrapper::putStringArray(const char* key, const std::vector<std::string>& value) {
+ ScopedLocalRef<jstring> keyStringRef(mJNIEnv, mJNIEnv->NewStringUTF(key));
+ if (keyStringRef == nullptr) {
+ return Error() << "Failed to create a string for a key=" << key << " due to OOM error";
+ }
+
+ jmethodID putStringArrayMethod =
+ mJNIEnv->GetMethodID(mBundleClass, "putStringArray",
+ "(Ljava/lang/String;[Ljava/lang/String;)V");
+
+ ScopedLocalRef<jclass> stringClassRef(mJNIEnv, mJNIEnv->FindClass("java/lang/String"));
+ ScopedLocalRef<jobjectArray> arrayRef(mJNIEnv,
+ mJNIEnv->NewObjectArray(value.size(),
+ mJNIEnv->FindClass(
+ "java/lang/String"),
+ nullptr));
+ for (int i = 0; i < value.size(); i++) {
+ ScopedLocalRef<jstring> valueStringRef(mJNIEnv, mJNIEnv->NewStringUTF(value[i].c_str()));
+ if (valueStringRef == nullptr) {
+ return Error() << "Failed to create a string for provided value due to OOM error";
+ }
+ mJNIEnv->SetObjectArrayElement(arrayRef.get(), i, valueStringRef.get());
+ }
+ mJNIEnv->CallVoidMethod(mBundle, putStringArrayMethod, keyStringRef.get(), arrayRef.get());
+ return {}; // ok result
+}
+
+jobject BundleWrapper::getBundle() {
+ return mBundle;
+}
+
+} // namespace scriptexecutor
+} // namespace car
+} // namespace android
+} // namespace com
diff --git a/packages/ScriptExecutor/src/BundleWrapper.h b/packages/ScriptExecutor/src/BundleWrapper.h
new file mode 100644
index 0000000..a949108
--- /dev/null
+++ b/packages/ScriptExecutor/src/BundleWrapper.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#ifndef PACKAGES_SCRIPTEXECUTOR_SRC_BUNDLEWRAPPER_H_
+#define PACKAGES_SCRIPTEXECUTOR_SRC_BUNDLEWRAPPER_H_
+
+#include "jni.h"
+
+#include <android-base/result.h>
+
+#include <string>
+#include <vector>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+// Used to create a java bundle object and populate its fields one at a time.
+class BundleWrapper {
+public:
+ explicit BundleWrapper(JNIEnv* env);
+ // BundleWrapper is not copyable.
+ BundleWrapper(const BundleWrapper&) = delete;
+ BundleWrapper& operator=(const BundleWrapper&) = delete;
+
+ virtual ~BundleWrapper();
+
+ // Family of methods that puts the provided 'value' into the PersistableBundle
+ // under provided 'key'.
+ ::android::base::Result<void> putBoolean(const char* key, bool value);
+ ::android::base::Result<void> putLong(const char* key, int64_t value);
+ ::android::base::Result<void> putDouble(const char* key, double value);
+ ::android::base::Result<void> putString(const char* key, const char* value);
+ ::android::base::Result<void> putLongArray(const char* key, const std::vector<int64_t>& value);
+ ::android::base::Result<void> putStringArray(const char* key,
+ const std::vector<std::string>& value);
+
+ jobject getBundle();
+
+private:
+ // The class asks Java to create PersistableBundle object and stores the reference.
+ // When the instance of this class is destroyed the actual Java PersistableBundle object behind
+ // this reference stays on and is managed by Java.
+ jobject mBundle;
+
+ // Reference to java PersistableBundle class cached for performance reasons.
+ jclass mBundleClass;
+
+ // Stores a JNIEnv* pointer.
+ JNIEnv* mJNIEnv; // not owned
+};
+
+} // namespace scriptexecutor
+} // namespace car
+} // namespace android
+} // namespace com
+
+#endif // PACKAGES_SCRIPTEXECUTOR_SRC_BUNDLEWRAPPER_H_
diff --git a/packages/ScriptExecutor/src/JniUtils.cpp b/packages/ScriptExecutor/src/JniUtils.cpp
new file mode 100644
index 0000000..0d3f472
--- /dev/null
+++ b/packages/ScriptExecutor/src/JniUtils.cpp
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#include "JniUtils.h"
+
+#include "nativehelper/scoped_local_ref.h"
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+void pushBundleToLuaTable(JNIEnv* env, lua_State* lua, jobject bundle) {
+ lua_newtable(lua);
+ // null bundle object is allowed. We will treat it as an empty table.
+ if (bundle == nullptr) {
+ return;
+ }
+
+ // TODO(b/188832769): Consider caching some of these JNI references for
+ // performance reasons.
+ ScopedLocalRef<jclass> persistableBundleClass(env,
+ env->FindClass("android/os/PersistableBundle"));
+ jmethodID getKeySetMethod =
+ env->GetMethodID(persistableBundleClass.get(), "keySet", "()Ljava/util/Set;");
+ ScopedLocalRef<jobject> keys(env, env->CallObjectMethod(bundle, getKeySetMethod));
+ ScopedLocalRef<jclass> setClass(env, env->FindClass("java/util/Set"));
+ jmethodID iteratorMethod =
+ env->GetMethodID(setClass.get(), "iterator", "()Ljava/util/Iterator;");
+ ScopedLocalRef<jobject> keySetIteratorObject(env,
+ env->CallObjectMethod(keys.get(), iteratorMethod));
+
+ ScopedLocalRef<jclass> iteratorClass(env, env->FindClass("java/util/Iterator"));
+ jmethodID hasNextMethod = env->GetMethodID(iteratorClass.get(), "hasNext", "()Z");
+ jmethodID nextMethod = env->GetMethodID(iteratorClass.get(), "next", "()Ljava/lang/Object;");
+
+ ScopedLocalRef<jclass> booleanClass(env, env->FindClass("java/lang/Boolean"));
+ ScopedLocalRef<jclass> integerClass(env, env->FindClass("java/lang/Integer"));
+ ScopedLocalRef<jclass> longClass(env, env->FindClass("java/lang/Long"));
+ ScopedLocalRef<jclass> numberClass(env, env->FindClass("java/lang/Number"));
+ ScopedLocalRef<jclass> stringClass(env, env->FindClass("java/lang/String"));
+ ScopedLocalRef<jclass> intArrayClass(env, env->FindClass("[I"));
+ ScopedLocalRef<jclass> longArrayClass(env, env->FindClass("[J"));
+ ScopedLocalRef<jclass> stringArrayClass(env, env->FindClass("[Ljava/lang/String;"));
+ // TODO(b/188816922): Handle more types such as float and integer arrays,
+ // and perhaps nested Bundles.
+
+ jmethodID getMethod = env->GetMethodID(persistableBundleClass.get(), "get",
+ "(Ljava/lang/String;)Ljava/lang/Object;");
+
+ // Iterate over key set of the bundle one key at a time.
+ while (env->CallBooleanMethod(keySetIteratorObject.get(), hasNextMethod)) {
+ // Read the value object that corresponds to this key.
+ ScopedLocalRef<jstring> key(env,
+ (jstring)env->CallObjectMethod(keySetIteratorObject.get(),
+ nextMethod));
+ ScopedLocalRef<jobject> value(env, env->CallObjectMethod(bundle, getMethod, key.get()));
+
+ // Get the value of the type, extract it accordingly from the bundle and
+ // push the extracted value and the key to the Lua table.
+ if (env->IsInstanceOf(value.get(), booleanClass.get())) {
+ jmethodID boolMethod = env->GetMethodID(booleanClass.get(), "booleanValue", "()Z");
+ bool boolValue = static_cast<bool>(env->CallBooleanMethod(value.get(), boolMethod));
+ lua_pushboolean(lua, boolValue);
+ } else if (env->IsInstanceOf(value.get(), integerClass.get())) {
+ jmethodID intMethod = env->GetMethodID(integerClass.get(), "intValue", "()I");
+ lua_pushinteger(lua, env->CallIntMethod(value.get(), intMethod));
+ } else if (env->IsInstanceOf(value.get(), longClass.get())) {
+ jmethodID longMethod = env->GetMethodID(longClass.get(), "longValue", "()J");
+ lua_pushinteger(lua, env->CallLongMethod(value.get(), longMethod));
+ } else if (env->IsInstanceOf(value.get(), numberClass.get())) {
+ // Condense other numeric types using one class. Because lua supports only
+ // integer or double, and we handled integer in previous if clause.
+ jmethodID numberMethod = env->GetMethodID(numberClass.get(), "doubleValue", "()D");
+ /* Pushes a double onto the stack */
+ lua_pushnumber(lua, env->CallDoubleMethod(value.get(), numberMethod));
+ } else if (env->IsInstanceOf(value.get(), stringClass.get())) {
+ // Produces a string in Modified UTF-8 encoding. Any null character
+ // inside the original string is converted into two-byte encoding.
+ // This way we can directly use the output of GetStringUTFChars in C API that
+ // expects a null-terminated string.
+ const char* rawStringValue =
+ env->GetStringUTFChars(static_cast<jstring>(value.get()), nullptr);
+ lua_pushstring(lua, rawStringValue);
+ env->ReleaseStringUTFChars(static_cast<jstring>(value.get()), rawStringValue);
+ } else if (env->IsInstanceOf(value.get(), intArrayClass.get())) {
+ jintArray intArray = static_cast<jintArray>(value.get());
+ const auto kLength = env->GetArrayLength(intArray);
+ // Arrays are represented as a table of sequential elements in Lua.
+ // We are creating a nested table to represent this array. We specify number of elements
+ // in the Java array to preallocate memory accordingly.
+ lua_createtable(lua, kLength, 0);
+ jint* rawIntArray = env->GetIntArrayElements(intArray, nullptr);
+ // Fills in the table at stack idx -2 with key value pairs, where key is a
+ // Lua index and value is an integer from the byte array at that index
+ for (int i = 0; i < kLength; i++) {
+ // Stack at index -1 is rawIntArray[i] after this push.
+ lua_pushinteger(lua, rawIntArray[i]);
+ lua_rawseti(lua, /* idx= */ -2,
+ i + 1); // lua index starts from 1
+ }
+ // JNI_ABORT is used because we do not need to copy back elements.
+ env->ReleaseIntArrayElements(intArray, rawIntArray, JNI_ABORT);
+ } else if (env->IsInstanceOf(value.get(), longArrayClass.get())) {
+ jlongArray longArray = static_cast<jlongArray>(value.get());
+ const auto kLength = env->GetArrayLength(longArray);
+ // Arrays are represented as a table of sequential elements in Lua.
+ // We are creating a nested table to represent this array. We specify number of elements
+ // in the Java array to preallocate memory accordingly.
+ lua_createtable(lua, kLength, 0);
+ jlong* rawLongArray = env->GetLongArrayElements(longArray, nullptr);
+ // Fills in the table at stack idx -2 with key value pairs, where key is a
+ // Lua index and value is an integer from the byte array at that index
+ for (int i = 0; i < kLength; i++) {
+ lua_pushinteger(lua, rawLongArray[i]);
+ lua_rawseti(lua, /* idx= */ -2,
+ i + 1); // lua index starts from 1
+ }
+ // JNI_ABORT is used because we do not need to copy back elements.
+ env->ReleaseLongArrayElements(longArray, rawLongArray, JNI_ABORT);
+ } else if (env->IsInstanceOf(value.get(), stringArrayClass.get())) {
+ jobjectArray stringArray = static_cast<jobjectArray>(value.get());
+ const auto kLength = env->GetArrayLength(stringArray);
+ // Arrays are represented as a table of sequential elements in Lua.
+ // We are creating a nested table to represent this array. We specify number of elements
+ // in the Java array to preallocate memory accordingly.
+ lua_createtable(lua, kLength, 0);
+ // Fills in the table at stack idx -2 with key value pairs, where key is a Lua index and
+ // value is an string value extracted from the object array at that index
+ for (int i = 0; i < kLength; i++) {
+ ScopedLocalRef<jobject> localStringRef(env,
+ env->GetObjectArrayElement(stringArray, i));
+ jstring element = static_cast<jstring>(localStringRef.get());
+ const char* rawStringValue = env->GetStringUTFChars(element, nullptr);
+ lua_pushstring(lua, rawStringValue);
+ env->ReleaseStringUTFChars(element, rawStringValue);
+ // lua index starts from 1
+ lua_rawseti(lua, /* idx= */ -2, i + 1);
+ }
+ } else {
+ // Other types are not implemented yet, skipping.
+ continue;
+ }
+
+ const char* rawKey = env->GetStringUTFChars(key.get(), nullptr);
+ // table[rawKey] = value, where value is on top of the stack,
+ // and the table is the next element in the stack.
+ lua_setfield(lua, /* idx= */ -2, rawKey);
+ env->ReleaseStringUTFChars(key.get(), rawKey);
+ }
+}
+
+} // namespace scriptexecutor
+} // namespace car
+} // namespace android
+} // namespace com
diff --git a/packages/ScriptExecutor/src/JniUtils.h b/packages/ScriptExecutor/src/JniUtils.h
new file mode 100644
index 0000000..542a91a
--- /dev/null
+++ b/packages/ScriptExecutor/src/JniUtils.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+#ifndef PACKAGES_SCRIPTEXECUTOR_SRC_JNIUTILS_H_
+#define PACKAGES_SCRIPTEXECUTOR_SRC_JNIUTILS_H_
+
+#include "jni.h"
+
+extern "C" {
+#include "lua.h"
+}
+
+#include <android-base/result.h>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+// Helper function which takes android.os.Bundle object in "bundle" argument
+// and converts it to Lua table on top of Lua stack. All key-value pairs are
+// converted to the corresponding key-value pairs of the Lua table as long as
+// the Bundle value types are supported. At this point, we support boolean,
+// integer, double and String types in Java.
+void pushBundleToLuaTable(JNIEnv* env, lua_State* lua, jobject bundle);
+
+} // namespace scriptexecutor
+} // namespace car
+} // namespace android
+} // namespace com
+
+#endif // PACKAGES_SCRIPTEXECUTOR_SRC_JNIUTILS_H_
diff --git a/packages/ScriptExecutor/src/LuaEngine.cpp b/packages/ScriptExecutor/src/LuaEngine.cpp
new file mode 100644
index 0000000..c26198f
--- /dev/null
+++ b/packages/ScriptExecutor/src/LuaEngine.cpp
@@ -0,0 +1,350 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#include "LuaEngine.h"
+
+#include "BundleWrapper.h"
+
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+extern "C" {
+#include "lauxlib.h"
+#include "lua.h"
+#include "lualib.h"
+}
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+using ::android::base::Error;
+using ::android::base::Result;
+
+namespace {
+
+enum LuaNumReturnedResults {
+ ZERO_RETURNED_RESULTS = 0,
+};
+
+// TODO(b/199415783): Revisit the topic of limits to potentially move it to standalone file.
+constexpr int MAX_ARRAY_SIZE = 1000;
+
+// Helper method that goes over Lua table fields one by one and populates PersistableBundle
+// object wrapped in BundleWrapper.
+// It is assumed that Lua table is located on top of the Lua stack.
+//
+// Returns false if the conversion encountered unrecoverable error.
+// Otherwise, returns true for success.
+// In case of an error, there is no need to pop elements or clean the stack. When Lua calls C,
+// the stack used to pass data between Lua and C is private for each call. According to
+// https://www.lua.org/pil/26.1.html, after C function returns back to Lua, Lua
+// removes everything that is in the stack below the returned results.
+// TODO(b/200849134): Refactor this function.
+Result<void> convertLuaTableToBundle(lua_State* lua, BundleWrapper* bundleWrapper) {
+ // Iterate over Lua table which is expected to be at the top of Lua stack.
+ // lua_next call pops the key from the top of the stack and finds the next
+ // key-value pair. It returns 0 if the next pair was not found.
+ // More on lua_next in: https://www.lua.org/manual/5.3/manual.html#lua_next
+ lua_pushnil(lua); // First key is a null value.
+ while (lua_next(lua, /* index = */ -2) != 0) {
+ // 'key' is at index -2 and 'value' is at index -1
+ // -1 index is the top of the stack.
+ // remove 'value' and keep 'key' for next iteration
+ // Process each key-value depending on a type and push it to Java PersistableBundle.
+ // TODO(b/199531928): Consider putting limits on key sizes as well.
+ const char* key = lua_tostring(lua, /* index = */ -2);
+ Result<void> bundleInsertionResult;
+ if (lua_isboolean(lua, /* index = */ -1)) {
+ bundleInsertionResult =
+ bundleWrapper->putBoolean(key,
+ static_cast<bool>(
+ lua_toboolean(lua, /* index = */ -1)));
+ } else if (lua_isinteger(lua, /* index = */ -1)) {
+ bundleInsertionResult =
+ bundleWrapper->putLong(key,
+ static_cast<int64_t>(
+ lua_tointeger(lua, /* index = */ -1)));
+ } else if (lua_isnumber(lua, /* index = */ -1)) {
+ bundleInsertionResult =
+ bundleWrapper->putDouble(key,
+ static_cast<double>(
+ lua_tonumber(lua, /* index = */ -1)));
+ } else if (lua_isstring(lua, /* index = */ -1)) {
+ // TODO(b/199415783): We need to have a limit on how long these strings could be.
+ bundleInsertionResult =
+ bundleWrapper->putString(key, lua_tostring(lua, /* index = */ -1));
+ } else if (lua_istable(lua, /* index =*/-1)) {
+ // Lua uses tables to represent an array.
+
+ // TODO(b/199438375): Document to users that we expect tables to be either only indexed
+ // or keyed but not both. If the table contains consecutively indexed values starting
+ // from 1, we will treat it as an array. lua_rawlen call returns the size of the indexed
+ // part. We copy this part into an array, but any keyed values in this table are
+ // ignored. There is a test that documents this current behavior. If a user wants a
+ // nested table to be represented by a PersistableBundle object, they must make sure
+ // that the nested table does not contain indexed data, including no key=1.
+ const auto kTableLength = lua_rawlen(lua, -1);
+ if (kTableLength > MAX_ARRAY_SIZE) {
+ return Error()
+ << "Returned table " << key << " exceeds maximum allowed size of "
+ << MAX_ARRAY_SIZE
+ << " elements. This key-value cannot be unpacked successfully. This error "
+ "is unrecoverable.";
+ }
+ if (kTableLength <= 0) {
+ return Error() << "A value with key=" << key
+ << " appears to be a nested table that does not represent an array "
+ "of data. "
+ "Such nested tables are not supported yet. This script error is "
+ "unrecoverable.";
+ }
+
+ std::vector<int64_t> longArray;
+ std::vector<std::string> stringArray;
+ int originalLuaType = LUA_TNIL;
+ for (int i = 0; i < kTableLength; i++) {
+ lua_rawgeti(lua, -1, i + 1);
+ // Lua allows arrays to have values of varying type. We need to force all Lua
+ // arrays to stick to single type within the same array. We use the first value
+ // in the array to determine the type of all values in the array that follow
+ // after. If the second, third, etc element of the array does not match the type
+ // of the first element we stop the extraction and return an error via a
+ // callback.
+ if (i == 0) {
+ originalLuaType = lua_type(lua, /* index = */ -1);
+ }
+ int currentType = lua_type(lua, /* index= */ -1);
+ if (currentType != originalLuaType) {
+ return Error()
+ << "Returned Lua arrays must have elements of the same type. Returned "
+ "table with key="
+ << key << " has the first element of type=" << originalLuaType
+ << ", but the element at index=" << i + 1 << " has type=" << currentType
+ << ". Integer type codes are defined in lua.h file. This error is "
+ "unrecoverable.";
+ }
+ switch (currentType) {
+ case LUA_TNUMBER:
+ if (!lua_isinteger(lua, /* index = */ -1)) {
+ return Error() << "Returned value for key=" << key
+ << " contains a floating number array, which is not "
+ "supported yet.";
+ } else {
+ longArray.push_back(lua_tointeger(lua, /* index = */ -1));
+ }
+ break;
+ case LUA_TSTRING:
+ // TODO(b/200833728): Investigate optimizations to minimize string
+ // copying. For example, populate JNI object array one element at a
+ // time, as we go.
+ stringArray.push_back(lua_tostring(lua, /* index = */ -1));
+ break;
+ default:
+ return Error() << "Returned value for key=" << key
+ << " is an array with values of type="
+ << lua_typename(lua, lua_type(lua, /* index = */ -1))
+ << ", which is not supported yet.";
+ }
+ lua_pop(lua, 1);
+ }
+ switch (originalLuaType) {
+ case LUA_TNUMBER:
+ bundleInsertionResult = bundleWrapper->putLongArray(key, longArray);
+ break;
+ case LUA_TSTRING:
+ bundleInsertionResult = bundleWrapper->putStringArray(key, stringArray);
+ break;
+ }
+ } else {
+ return Error() << "key=" << key << " has a Lua type="
+ << lua_typename(lua, lua_type(lua, /* index = */ -1))
+ << ", which is not supported yet.";
+ }
+ // Pop value from the stack, keep the key for the next iteration.
+ lua_pop(lua, 1);
+ // The key is at index -1, the table is at index -2 now.
+
+ // Check if insertion of the current key-value into the bundle was successful. If not,
+ // fail-fast out of this extraction routine.
+ if (!bundleInsertionResult.ok()) {
+ return bundleInsertionResult;
+ }
+ }
+ return {}; // ok result
+}
+
+} // namespace
+
+ScriptExecutorListener* LuaEngine::sListener = nullptr;
+
+LuaEngine::LuaEngine() {
+ // Instantiate Lua environment
+ mLuaState = luaL_newstate();
+ luaL_openlibs(mLuaState);
+}
+
+LuaEngine::~LuaEngine() {
+ lua_close(mLuaState);
+}
+
+lua_State* LuaEngine::getLuaState() {
+ return mLuaState;
+}
+
+void LuaEngine::resetListener(ScriptExecutorListener* listener) {
+ if (sListener != nullptr) {
+ delete sListener;
+ }
+ sListener = listener;
+}
+
+int LuaEngine::loadScript(const char* scriptBody) {
+ // As the first step in Lua script execution we want to load
+ // the body of the script into Lua stack and have it processed by Lua
+ // to catch any errors.
+ // More on luaL_dostring: https://www.lua.org/manual/5.3/manual.html#lual_dostring
+ // If error, pushes the error object into the stack.
+ const auto status = luaL_dostring(mLuaState, scriptBody);
+ if (status) {
+ // Removes error object from the stack.
+ // Lua stack must be properly maintained due to its limited size,
+ // ~20 elements and its critical function because all interaction with
+ // Lua happens via the stack.
+ // Starting read about Lua stack: https://www.lua.org/pil/24.2.html
+ lua_pop(mLuaState, 1);
+ std::ostringstream out;
+ out << "Error encountered while loading the script. A possible cause could be syntax "
+ "errors in the script.";
+ sListener->onError(ERROR_TYPE_LUA_RUNTIME_ERROR, out.str().c_str(), "");
+ return status;
+ }
+
+ // Register limited set of reserved methods for Lua to call native side.
+ lua_register(mLuaState, "on_success", LuaEngine::onSuccess);
+ lua_register(mLuaState, "on_script_finished", LuaEngine::onScriptFinished);
+ lua_register(mLuaState, "on_error", LuaEngine::onError);
+ return status;
+}
+
+int LuaEngine::pushFunction(const char* functionName) {
+ // Interaction between native code and Lua happens via Lua stack.
+ // In such model, a caller first pushes the name of the function
+ // that needs to be called, followed by the function's input
+ // arguments, one input value pushed at a time.
+ // More info: https://www.lua.org/pil/24.2.html
+ lua_getglobal(mLuaState, functionName);
+ const auto status = lua_isfunction(mLuaState, /*idx= */ -1);
+ if (status == 0) {
+ lua_pop(mLuaState, 1);
+ std::ostringstream out;
+ out << "Wrong function name. Provided functionName=" << functionName
+ << " does not correspond to any function in the provided script";
+ sListener->onError(ERROR_TYPE_LUA_RUNTIME_ERROR, out.str().c_str(), "");
+ }
+ return status;
+}
+
+int LuaEngine::run() {
+ // Performs blocking call of the provided Lua function. Assumes all
+ // input arguments are in the Lua stack as well in proper order.
+ // On how to call Lua functions: https://www.lua.org/pil/25.2.html
+ // Doc on lua_pcall: https://www.lua.org/manual/5.3/manual.html#lua_pcall
+ int status = lua_pcall(mLuaState, /* nargs= */ 2, /* nresults= */ 0, /*errfunc= */ 0);
+ if (status) {
+ lua_pop(mLuaState, 1); // pop the error object from the stack.
+ std::ostringstream out;
+ out << "Error encountered while running the script. The returned error code=" << status
+ << ". Refer to lua.h file of Lua C API library for error code definitions.";
+ sListener->onError(ERROR_TYPE_LUA_RUNTIME_ERROR, out.str().c_str(), "");
+ }
+ return status;
+}
+
+int LuaEngine::onSuccess(lua_State* lua) {
+ // Any script we run can call on_success only with a single argument of Lua table type.
+ if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
+ sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
+ "on_success can push only a single parameter from Lua - a Lua table",
+ "");
+ return ZERO_RETURNED_RESULTS;
+ }
+
+ // Helper object to create and populate Java PersistableBundle object.
+ BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
+ const auto status = convertLuaTableToBundle(lua, &bundleWrapper);
+ if (!status.ok()) {
+ sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, status.error().message().c_str(), "");
+ // We explicitly must tell Lua how many results we return, which is 0 in this case.
+ // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
+ return ZERO_RETURNED_RESULTS;
+ }
+
+ // Forward the populated Bundle object to Java callback.
+ sListener->onSuccess(bundleWrapper.getBundle());
+
+ // We explicitly must tell Lua how many results we return, which is 0 in this case.
+ // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
+ return ZERO_RETURNED_RESULTS;
+}
+
+int LuaEngine::onScriptFinished(lua_State* lua) {
+ // Any script we run can call on_success only with a single argument of Lua table type.
+ if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
+ sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
+ "on_script_finished can push only a single parameter from Lua - a Lua "
+ "table",
+ "");
+ return ZERO_RETURNED_RESULTS;
+ }
+
+ // Helper object to create and populate Java PersistableBundle object.
+ BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
+ const auto status = convertLuaTableToBundle(lua, &bundleWrapper);
+ if (!status.ok()) {
+ sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, status.error().message().c_str(), "");
+ // We explicitly must tell Lua how many results we return, which is 0 in this case.
+ // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
+ return ZERO_RETURNED_RESULTS;
+ }
+
+ // Forward the populated Bundle object to Java callback.
+ sListener->onScriptFinished(bundleWrapper.getBundle());
+
+ // We explicitly must tell Lua how many results we return, which is 0 in this case.
+ // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
+ return ZERO_RETURNED_RESULTS;
+}
+
+int LuaEngine::onError(lua_State* lua) {
+ // Any script we run can call on_error only with a single argument of Lua string type.
+ if (lua_gettop(lua) != 1 || !lua_isstring(lua, /* index = */ -1)) {
+ sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
+ "on_error can push only a single string parameter from Lua", "");
+ return ZERO_RETURNED_RESULTS;
+ }
+ sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, lua_tostring(lua, /* index = */ -1),
+ /* stackTrace =*/"");
+ return ZERO_RETURNED_RESULTS;
+}
+
+} // namespace scriptexecutor
+} // namespace car
+} // namespace android
+} // namespace com
diff --git a/packages/ScriptExecutor/src/LuaEngine.h b/packages/ScriptExecutor/src/LuaEngine.h
new file mode 100644
index 0000000..e5ffd0d
--- /dev/null
+++ b/packages/ScriptExecutor/src/LuaEngine.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#ifndef PACKAGES_SCRIPTEXECUTOR_SRC_LUAENGINE_H_
+#define PACKAGES_SCRIPTEXECUTOR_SRC_LUAENGINE_H_
+
+#include "ScriptExecutorListener.h"
+
+#include <memory>
+
+extern "C" {
+#include "lua.h"
+}
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+// Encapsulates Lua script execution environment.
+class LuaEngine {
+public:
+ LuaEngine();
+
+ virtual ~LuaEngine();
+
+ // Returns pointer to Lua state object.
+ lua_State* getLuaState();
+
+ // Loads Lua script provided as scriptBody string.
+ // Returns 0 if successful. Otherwise returns non-zero Lua error code.
+ int loadScript(const char* scriptBody);
+
+ // Pushes a Lua function under provided name into the stack.
+ // Returns 1 if successful. Otherwise, an error is sent back to the client via the callback
+ // and 0 is returned.
+ int pushFunction(const char* functionName);
+
+ // Invokes function with the inputs provided in the stack.
+ // Assumes that the script body has been already loaded and successfully
+ // compiled and run, and all input arguments, and the function have been
+ // pushed to the stack.
+ // Returns 0 if successful. Otherwise returns non-zero Lua error code
+ // and sends the error via a callback back to the client.
+ int run();
+
+ // Updates stored listener and destroys the previous one.
+ static void resetListener(ScriptExecutorListener* listener);
+
+private:
+ // Invoked by a running Lua script to store intermediate results.
+ // The script will provide the results as a Lua table.
+ // We currently support only non-nested fields in the table and the fields can be the following
+ // Lua types: boolean, number, integer, and string.
+ // The result pushed by Lua is converted to PersistableBundle and forwarded to
+ // ScriptExecutor service via callback interface.
+ // This method returns 0 to indicate that no results were pushed to Lua stack according
+ // to Lua C function calling convention.
+ // More info: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
+ static int onSuccess(lua_State* lua);
+
+ // Invoked by a running Lua script to effectively mark the completion of the script's lifecycle,
+ // and send the final results to CarTelemetryService and then to the user.
+ // The script will provide the final results as a Lua table.
+ // We currently support only non-nested fields in the table and the fields can be the following
+ // Lua types: boolean, number, integer, and string.
+ // The result pushed by Lua is converted to Android PersistableBundle and forwarded to
+ // ScriptExecutor service via callback interface.
+ // This method returns 0 to indicate that no results were pushed to Lua stack according
+ // to Lua C function calling convention.
+ // More info: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
+ static int onScriptFinished(lua_State* lua);
+
+ // Invoked by a running Lua script to indicate than an error occurred. This is the mechanism to
+ // for a script author to receive error logs. The caller script encapsulates all the information
+ // about the error that the author wants to provide in a single string parameter.
+ // This method returns 0 to indicate that no results were pushed to Lua stack according
+ // to Lua C function calling convention.
+ // More info: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
+ static int onError(lua_State* lua);
+
+ // Points to the current listener object.
+ // Lua cannot call non-static class methods. We need to access listener object instance in
+ // Lua callbacks. Therefore, callbacks callable by Lua are static class methods and the pointer
+ // to a listener object needs to be static, since static methods cannot access non-static
+ // members.
+ // Only one listener is supported at any given time.
+ // Since listeners are heap-allocated, the destructor does not need to run at shutdown
+ // of the service because the memory allocated to the current listener object will be
+ // reclaimed by the OS.
+ static ScriptExecutorListener* sListener;
+
+ lua_State* mLuaState; // owned
+};
+
+} // namespace scriptexecutor
+} // namespace car
+} // namespace android
+} // namespace com
+
+#endif // PACKAGES_SCRIPTEXECUTOR_SRC_LUAENGINE_H_
diff --git a/packages/ScriptExecutor/src/ScriptExecutorJni.cpp b/packages/ScriptExecutor/src/ScriptExecutorJni.cpp
new file mode 100644
index 0000000..d8ddd55
--- /dev/null
+++ b/packages/ScriptExecutor/src/ScriptExecutorJni.cpp
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#include "JniUtils.h"
+#include "LuaEngine.h"
+#include "ScriptExecutorListener.h"
+#include "jni.h"
+
+#include <cstdint>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+extern "C" {
+
+JNIEXPORT jlong JNICALL Java_com_android_car_scriptexecutor_ScriptExecutor_nativeInitLuaEngine(
+ JNIEnv* env, jobject object) {
+ // Cast first to intptr_t to ensure int can hold the pointer without loss.
+ return static_cast<jlong>(reinterpret_cast<intptr_t>(new LuaEngine()));
+}
+
+JNIEXPORT void JNICALL Java_com_android_car_scriptexecutor_ScriptExecutor_nativeDestroyLuaEngine(
+ JNIEnv* env, jobject object, jlong luaEnginePtr) {
+ delete reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+}
+
+// Parses the inputs and loads them to Lua one at a time.
+// Loading of data into Lua also triggers checks on Lua side to verify the
+// inputs are valid. For example, pushing "functionName" into Lua stack verifies
+// that the function name actually exists in the previously loaded body of the
+// script.
+//
+// The steps are:
+// Step 1: Parse the inputs for obvious programming errors.
+// Step 2: Parse and load the body of the script.
+// Step 3: Parse and push function name we want to execute in the provided
+// script body to Lua stack. If the function name doesn't exist, we exit.
+// Step 4: Parse publishedData, convert it into Lua table and push it to the
+// stack.
+// Step 5: Parse savedState Bundle object, convert it into Lua table and push it
+// to the stack.
+// Any errors that occur at the stage above result in quick exit or crash.
+//
+// All interaction with Lua happens via Lua stack. Therefore, order of how the
+// inputs are parsed and processed is critical because Lua API methods such as
+// lua_pcall assume specific order between function name and the input arguments
+// on the stack.
+// More information about how to work with Lua stack: https://www.lua.org/pil/24.2.html
+// and how Lua functions are called via Lua API: https://www.lua.org/pil/25.2.html
+//
+// Finally, once parsing and pushing to Lua stack is complete, we go on to the final step,
+// Step 6: Attempt to run the provided function.
+JNIEXPORT void JNICALL Java_com_android_car_scriptexecutor_ScriptExecutor_nativeInvokeScript(
+ JNIEnv* env, jobject object, jlong luaEnginePtr, jstring scriptBody, jstring functionName,
+ jobject publishedData, jobject savedState, jobject listener) {
+ if (!luaEnginePtr) {
+ env->FatalError("luaEnginePtr parameter cannot be nil");
+ }
+ if (scriptBody == nullptr) {
+ env->FatalError("scriptBody parameter cannot be null");
+ }
+ if (functionName == nullptr) {
+ env->FatalError("functionName parameter cannot be null");
+ }
+ if (listener == nullptr) {
+ env->FatalError("listener parameter cannot be null");
+ }
+
+ LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+ LuaEngine::resetListener(new ScriptExecutorListener(env, listener));
+ // Objects passed to native methods are local JNI references.
+ // ScriptExecutorListener constructor above converts local "listener" reference to global
+ // reference. Delete local "listener" reference here because it is no longer needed.
+ env->DeleteLocalRef(listener);
+
+ // Load and parse the script
+ const char* scriptStr = env->GetStringUTFChars(scriptBody, nullptr);
+ auto status = engine->loadScript(scriptStr);
+ env->ReleaseStringUTFChars(scriptBody, scriptStr);
+ // status == 0 if the script loads successfully.
+ if (status != 0) {
+ return;
+ }
+
+ // Push the function name we want to invoke to Lua stack
+ const char* functionNameStr = env->GetStringUTFChars(functionName, nullptr);
+ status = engine->pushFunction(functionNameStr);
+ env->ReleaseStringUTFChars(functionName, functionNameStr);
+ // status == 1 if the name is a valid function.
+ if (status == 0) {
+ return;
+ }
+
+ // Unpack bundle in publishedData, convert to Lua table and push it to Lua stack.
+ pushBundleToLuaTable(env, engine->getLuaState(), publishedData);
+
+ // Unpack bundle in savedState, convert to Lua table and push it to Lua
+ // stack.
+ pushBundleToLuaTable(env, engine->getLuaState(), savedState);
+
+ // Execute the function. This will block until complete or error.
+ engine->run();
+}
+
+} // extern "C"
+
+} // namespace scriptexecutor
+} // namespace car
+} // namespace android
+} // namespace com
diff --git a/packages/ScriptExecutor/src/ScriptExecutorListener.cpp b/packages/ScriptExecutor/src/ScriptExecutorListener.cpp
new file mode 100644
index 0000000..b9cd7c8
--- /dev/null
+++ b/packages/ScriptExecutor/src/ScriptExecutorListener.cpp
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#include "ScriptExecutorListener.h"
+
+#include "JniUtils.h"
+#include "nativehelper/scoped_local_ref.h"
+
+#include <android-base/logging.h>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+ScriptExecutorListener::~ScriptExecutorListener() {
+ JNIEnv* env = getCurrentJNIEnv();
+ if (mScriptExecutorListener != nullptr) {
+ env->DeleteGlobalRef(mScriptExecutorListener);
+ }
+}
+
+ScriptExecutorListener::ScriptExecutorListener(JNIEnv* env, jobject scriptExecutorListener) {
+ mScriptExecutorListener = env->NewGlobalRef(scriptExecutorListener);
+ env->GetJavaVM(&mJavaVM);
+}
+
+void ScriptExecutorListener::onSuccess(jobject bundle) {
+ JNIEnv* env = getCurrentJNIEnv();
+ ScopedLocalRef<jclass> listenerClassRef(env, env->GetObjectClass(mScriptExecutorListener));
+ jmethodID onSuccessMethod = env->GetMethodID(listenerClassRef.get(), "onSuccess",
+ "(Landroid/os/PersistableBundle;)V");
+ env->CallVoidMethod(mScriptExecutorListener, onSuccessMethod, bundle);
+}
+
+void ScriptExecutorListener::onScriptFinished(jobject bundle) {
+ JNIEnv* env = getCurrentJNIEnv();
+ ScopedLocalRef<jclass> listenerClassRef(env, env->GetObjectClass(mScriptExecutorListener));
+ jmethodID onScriptFinished = env->GetMethodID(listenerClassRef.get(), "onScriptFinished",
+ "(Landroid/os/PersistableBundle;)V");
+ env->CallVoidMethod(mScriptExecutorListener, onScriptFinished, bundle);
+}
+
+void ScriptExecutorListener::onError(const ErrorType errorType, const char* message,
+ const char* stackTrace) {
+ JNIEnv* env = getCurrentJNIEnv();
+ ScopedLocalRef<jclass> listenerClassRef(env, env->GetObjectClass(mScriptExecutorListener));
+ jmethodID onErrorMethod = env->GetMethodID(listenerClassRef.get(), "onError",
+ "(ILjava/lang/String;Ljava/lang/String;)V");
+
+ ScopedLocalRef<jstring> messageStringRef(env, env->NewStringUTF(message));
+ if (messageStringRef == nullptr) {
+ LOG(ERROR) << "Failed to create a Java string for provided error message due to OOM error";
+ return;
+ }
+
+ ScopedLocalRef<jstring> stackTraceRef(env, env->NewStringUTF(stackTrace));
+ if (stackTraceRef == nullptr) {
+ LOG(ERROR) << "Failed to create a Java string for stack trace due to OOM error";
+ return;
+ }
+
+ env->CallVoidMethod(mScriptExecutorListener, onErrorMethod, static_cast<int>(errorType),
+ messageStringRef.get(), stackTraceRef.get());
+}
+
+JNIEnv* ScriptExecutorListener::getCurrentJNIEnv() {
+ JNIEnv* env;
+ if (mJavaVM->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
+ LOG(FATAL) << "Unable to return JNIEnv from JavaVM";
+ }
+ return env;
+}
+
+} // namespace scriptexecutor
+} // namespace car
+} // namespace android
+} // namespace com
diff --git a/packages/ScriptExecutor/src/ScriptExecutorListener.h b/packages/ScriptExecutor/src/ScriptExecutorListener.h
new file mode 100644
index 0000000..44e0e2b
--- /dev/null
+++ b/packages/ScriptExecutor/src/ScriptExecutorListener.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#ifndef PACKAGES_SCRIPTEXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
+#define PACKAGES_SCRIPTEXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
+
+#include "jni.h"
+
+#include <string>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+
+// Changes in this enum must also be reflected in:
+// p/s/C/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl
+// p/s/C/service/src/com/android/car/telemetry/proto/telemetry.proto
+enum ErrorType {
+ /**
+ * Default error type.
+ */
+ ERROR_TYPE_UNSPECIFIED = 0,
+
+ /**
+ * Used when an error occurs in the ScriptExecutor code.
+ */
+ ERROR_TYPE_SCRIPT_EXECUTOR_ERROR = 1,
+
+ /**
+ * Used when an error occurs while executing the Lua script (such as
+ * errors returned by lua_pcall)
+ */
+ ERROR_TYPE_LUA_RUNTIME_ERROR = 2,
+
+ /**
+ * Used to log errors by a script itself, for instance, when a script received
+ * inputs outside of expected range.
+ */
+ ERROR_TYPE_LUA_SCRIPT_ERROR = 3,
+};
+
+// Wrapper class for IScriptExecutorListener.aidl.
+class ScriptExecutorListener {
+public:
+ ScriptExecutorListener(JNIEnv* jni, jobject scriptExecutorListener);
+
+ virtual ~ScriptExecutorListener();
+
+ void onScriptFinished(jobject bundle);
+
+ void onSuccess(jobject bundle);
+
+ void onError(const ErrorType errorType, const char* message, const char* stackTrace);
+
+ JNIEnv* getCurrentJNIEnv();
+
+private:
+ // Stores a jni global reference to Java Script Executor listener object.
+ jobject mScriptExecutorListener;
+
+ // Stores JavaVM pointer in order to be able to get JNIEnv pointer.
+ // This is done because JNIEnv cannot be shared between threads.
+ // https://developer.android.com/training/articles/perf-jni.html#javavm-and-jnienv
+ JavaVM* mJavaVM;
+};
+
+} // namespace scriptexecutor
+} // namespace car
+} // namespace android
+} // namespace com
+
+#endif // PACKAGES_SCRIPTEXECUTOR_SRC_SCRIPTEXECUTORLISTENER_H_
diff --git a/packages/ScriptExecutor/src/com/android/car/scriptexecutor/ScriptExecutor.java b/packages/ScriptExecutor/src/com/android/car/scriptexecutor/ScriptExecutor.java
new file mode 100644
index 0000000..abd4145
--- /dev/null
+++ b/packages/ScriptExecutor/src/com/android/car/scriptexecutor/ScriptExecutor.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2021 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.scriptexecutor;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutor;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Executes Lua code in an isolated process with provided source code
+ * and input arguments.
+ */
+public final class ScriptExecutor extends Service {
+
+ static {
+ System.loadLibrary("scriptexecutorjni");
+ }
+
+ private static final String TAG = ScriptExecutor.class.getSimpleName();
+
+ // Dedicated "worker" thread to handle all calls related to native code.
+ private HandlerThread mNativeHandlerThread;
+ // Handler associated with the native worker thread.
+ private Handler mNativeHandler;
+
+ private final class IScriptExecutorImpl extends IScriptExecutor.Stub {
+ @Override
+ public void invokeScript(String scriptBody, String functionName,
+ PersistableBundle publishedData, PersistableBundle savedState,
+ IScriptExecutorListener listener) {
+ mNativeHandler.post(() ->
+ nativeInvokeScript(mLuaEnginePtr, scriptBody, functionName, publishedData,
+ savedState, listener));
+ }
+
+ @Override
+ public void invokeScriptForLargeInput(String scriptBody, String functionName,
+ ParcelFileDescriptor publishedDataFileDescriptor, PersistableBundle savedState,
+ IScriptExecutorListener listener) {
+ mNativeHandler.post(() -> {
+ PersistableBundle publishedData;
+ try (InputStream input = new ParcelFileDescriptor.AutoCloseInputStream(
+ publishedDataFileDescriptor)) {
+ publishedData = PersistableBundle.readFromStream(input);
+ } catch (IOException e) {
+ try {
+ listener.onError(IScriptExecutorListener.ERROR_TYPE_SCRIPT_EXECUTOR_ERROR,
+ e.getMessage(), "");
+ } catch (RemoteException remoteException) {
+ if (Log.isLoggable(TAG, Log.ERROR)) {
+ // At least log "message" here, in case it was never sent back via
+ // the callback.
+ Log.e(TAG, "failed while calling listener with exception ", e);
+ }
+ }
+ return;
+ }
+
+ nativeInvokeScript(mLuaEnginePtr, scriptBody, functionName, publishedData,
+ savedState, listener);
+ });
+ }
+ }
+
+ private IScriptExecutorImpl mScriptExecutorBinder;
+
+ // Memory location of Lua Engine object which is allocated in native code.
+ private long mLuaEnginePtr;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mNativeHandlerThread = new HandlerThread(ScriptExecutor.class.getSimpleName());
+ mNativeHandlerThread.start();
+ mNativeHandler = new Handler(mNativeHandlerThread.getLooper());
+
+ mLuaEnginePtr = nativeInitLuaEngine();
+ mScriptExecutorBinder = new IScriptExecutorImpl();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ nativeDestroyLuaEngine(mLuaEnginePtr);
+ mNativeHandlerThread.quit();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mScriptExecutorBinder;
+ }
+
+ /**
+ * Initializes Lua Engine.
+ *
+ * <p>Returns memory location of Lua Engine.
+ */
+ private native long nativeInitLuaEngine();
+
+ /**
+ * Destroys LuaEngine at the provided memory address.
+ */
+ private native void nativeDestroyLuaEngine(long luaEnginePtr);
+
+ /**
+ * Calls provided Lua function.
+ *
+ * @param luaEnginePtr memory address of the stored LuaEngine instance.
+ * @param scriptBody complete body of Lua script that also contains the function to be
+ * invoked.
+ * @param functionName the name of the function to execute.
+ * @param publishedData input data provided by the source which the function handles.
+ * @param savedState key-value pairs preserved from the previous invocation of the function.
+ * @param listener callback for the sandboxed environent to report back script execution
+ * results
+ * and errors.
+ */
+ private native void nativeInvokeScript(long luaEnginePtr, String scriptBody,
+ String functionName, PersistableBundle publishedData, PersistableBundle savedState,
+ IScriptExecutorListener listener);
+}
diff --git a/packages/ScriptExecutor/tests/unit/Android.bp b/packages/ScriptExecutor/tests/unit/Android.bp
new file mode 100644
index 0000000..80ba55f
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/Android.bp
@@ -0,0 +1,70 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "ScriptExecutorUnitTest",
+
+ srcs: ["src/**/*.java"],
+
+ platform_apis: true,
+
+ certificate: "platform",
+
+ instrumentation_for: "ScriptExecutor",
+
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.runner",
+ "junit",
+ "scriptexecutor-test-lib",
+ "truth-prebuilt",
+ ],
+
+ test_suites: ["general-tests"],
+
+ jni_libs: [
+ "libscriptexecutorjni",
+ "libscriptexecutorjniutils-test",
+ ],
+}
+
+cc_library {
+ name: "libscriptexecutorjniutils-test",
+
+ cflags: [
+ "-Wno-unused-parameter",
+ ],
+
+ srcs: [
+ "src/com/android/car/scriptexecutor/JniUtilsTestHelper.cpp",
+ ],
+
+ stl: "libc++_static",
+
+ shared_libs: [
+ "libbase",
+ "libnativehelper",
+ ],
+
+ static_libs: [
+ "libscriptexecutorjni",
+ "liblua",
+ ],
+}
diff --git a/packages/ScriptExecutor/tests/unit/AndroidManifest.xml b/packages/ScriptExecutor/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..f393408
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.car.scriptexecutor_test">
+
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+
+ <queries>
+ <package android:name="com.android.car.scriptexecutor" />
+ </queries>
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.car.scriptexecutor_test"
+ android:label="Tests for ScriptExecutor"/>
+</manifest>
diff --git a/packages/ScriptExecutor/tests/unit/README.md b/packages/ScriptExecutor/tests/unit/README.md
new file mode 100644
index 0000000..8c5a67a
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/README.md
@@ -0,0 +1,52 @@
+<!--
+ Copyright (C) 2021 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
+ -->
+
+# How to run unit tests for ScriptExecutor
+
+**1. Navigate to the root of the repo and do full build:**
+
+`m -j`
+
+**2. Flash the device with this build:**
+
+`aae flash`
+
+**3. Run the tests. For example**
+
+`atest ScriptExecutorUnitTest:ScriptExecutorTest`
+
+
+## How to rerun the tests after changes
+Sometimes a test needs to be modified. These are the steps to do incremental update instead of full
+device flash.
+
+**1. Navigate to ScriptExecutor unit test location and build its targets:**
+`cd packages/services/Car/packages/ScriptExecutor/tests/unit`
+
+`mm -j`
+
+**2. Sync the device with all the files that need to be updated:**
+
+`adb root`
+
+`adb remount`
+
+`adb sync && adb shell stop && adb shell start`
+
+**3. At this point we are ready to run the tests again. For example:**
+
+`atest ScriptExecutorUnitTest:ScriptExecutorTest`
+
diff --git a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTest.java b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTest.java
new file mode 100644
index 0000000..3292009
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2021 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.scriptexecutor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.PersistableBundle;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class JniUtilsTest {
+
+ private static final String TAG = JniUtilsTest.class.getSimpleName();
+
+ private static final String BOOLEAN_KEY = "boolean_key";
+ private static final String INT_KEY = "int_key";
+ private static final String STRING_KEY = "string_key";
+ private static final String NUMBER_KEY = "number_key";
+ private static final String INT_ARRAY_KEY = "int_array_key";
+ private static final String LONG_ARRAY_KEY = "long_array_key";
+
+ private static final boolean BOOLEAN_VALUE = true;
+ private static final double NUMBER_VALUE = 0.1;
+ private static final int INT_VALUE = 10;
+ private static final String STRING_VALUE = "test";
+ private static final int[] INT_ARRAY_VALUE = new int[]{1, 2, 3};
+ private static final long[] LONG_ARRAY_VALUE = new long[]{1, 2, 3, 4};
+
+ // Pointer to Lua Engine instantiated in native space.
+ private long mLuaEnginePtr = 0;
+
+ static {
+ System.loadLibrary("scriptexecutorjniutils-test");
+ }
+
+ @Before
+ public void setUp() {
+ mLuaEnginePtr = nativeCreateLuaEngine();
+ }
+
+ @After
+ public void tearDown() {
+ nativeDestroyLuaEngine(mLuaEnginePtr);
+ }
+
+ // Simply invokes PushBundleToLuaTable native method under test.
+ private native void nativePushBundleToLuaTableCaller(
+ long luaEnginePtr, PersistableBundle bundle);
+
+ // Creates an instance of LuaEngine on the heap and returns the pointer.
+ private native long nativeCreateLuaEngine();
+
+ // Destroys instance of LuaEngine on the native side at provided memory address.
+ private native void nativeDestroyLuaEngine(long luaEnginePtr);
+
+ // Returns size of a Lua object located at the specified position on the stack.
+ private native int nativeGetObjectSize(long luaEnginePtr, int index);
+
+ /*
+ * Family of methods to check if the table on top of the stack has
+ * the given value under provided key.
+ */
+ private native boolean nativeHasBooleanValue(long luaEnginePtr, String key, boolean value);
+
+ private native boolean nativeHasStringValue(long luaEnginePtr, String key, String value);
+
+ private native boolean nativeHasIntValue(long luaEnginePtr, String key, int value);
+
+ private native boolean nativeHasDoubleValue(long luaEnginePtr, String key, double value);
+
+ private native boolean nativeHasIntArrayValue(long luaEnginePtr, String key, int[] value);
+
+ private native boolean nativeHasLongArrayValue(long luaEnginePtr, String key, long[] value);
+
+ @Test
+ public void pushBundleToLuaTable_nullBundleMakesEmptyLuaTable() {
+ nativePushBundleToLuaTableCaller(mLuaEnginePtr, null);
+ // Get the size of the object on top of the stack,
+ // which is where our table is supposed to be.
+ assertThat(nativeGetObjectSize(mLuaEnginePtr, 1)).isEqualTo(0);
+ }
+
+ @Test
+ public void pushBundleToLuaTable_valuesOfDifferentTypes() {
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
+ bundle.putInt(INT_KEY, INT_VALUE);
+ bundle.putDouble(NUMBER_KEY, NUMBER_VALUE);
+ bundle.putString(STRING_KEY, STRING_VALUE);
+
+ // Invokes the corresponding helper method to convert the bundle
+ // to Lua table on Lua stack.
+ nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
+
+ // Check contents of Lua table.
+ assertThat(nativeHasBooleanValue(mLuaEnginePtr, BOOLEAN_KEY, BOOLEAN_VALUE)).isTrue();
+ assertThat(nativeHasIntValue(mLuaEnginePtr, INT_KEY, INT_VALUE)).isTrue();
+ assertThat(nativeHasDoubleValue(mLuaEnginePtr, NUMBER_KEY, NUMBER_VALUE)).isTrue();
+ assertThat(nativeHasStringValue(mLuaEnginePtr, STRING_KEY, STRING_VALUE)).isTrue();
+ }
+
+ @Test
+ public void pushBundleToLuaTable_wrongKey() {
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
+
+ // Invokes the corresponding helper method to convert the bundle
+ // to Lua table on Lua stack.
+ nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
+
+ // Check contents of Lua table.
+ assertThat(nativeHasBooleanValue(mLuaEnginePtr, "wrong key", BOOLEAN_VALUE)).isFalse();
+ }
+
+ @Test
+ public void pushBundleToLuaTable_arrays() {
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putIntArray(INT_ARRAY_KEY, INT_ARRAY_VALUE);
+ bundle.putLongArray(LONG_ARRAY_KEY, LONG_ARRAY_VALUE);
+
+ // Invokes the corresponding helper method to convert the bundle
+ // to Lua table on Lua stack.
+ nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
+
+ // Check contents of Lua table.
+ // Java int and long arrays both end up being arrays of Lua's Integer type,
+ // which is interpreted as a 8-byte int type.
+ assertThat(nativeHasIntArrayValue(mLuaEnginePtr, INT_ARRAY_KEY, INT_ARRAY_VALUE)).isTrue();
+ assertThat(
+ nativeHasLongArrayValue(mLuaEnginePtr, LONG_ARRAY_KEY, LONG_ARRAY_VALUE)).isTrue();
+ }
+}
diff --git a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTestHelper.cpp b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTestHelper.cpp
new file mode 100644
index 0000000..a205b32
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/JniUtilsTestHelper.cpp
@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2021, 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.
+ */
+
+#include "JniUtils.h"
+#include "LuaEngine.h"
+#include "jni.h"
+
+#include <cstdint>
+#include <cstring>
+
+namespace com {
+namespace android {
+namespace car {
+namespace scriptexecutor {
+namespace {
+
+template <typename T>
+bool hasIntegerArray(JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, T rawInputArray,
+ const int arrayLength) {
+ const char* rawKey = env->GetStringUTFChars(key, nullptr);
+ LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+ // Assumes the table is on top of the stack.
+ auto* luaState = engine->getLuaState();
+ lua_pushstring(luaState, rawKey);
+ env->ReleaseStringUTFChars(key, rawKey);
+ lua_gettable(luaState, -2);
+ bool result = false;
+ if (!lua_istable(luaState, -1)) {
+ result = false;
+ } else {
+ // First, compare the input and Lua array sizes.
+ const auto kActualLength = lua_rawlen(luaState, -1);
+ if (arrayLength != kActualLength) {
+ // No need to compare further if number of elements in the two arrays are not equal.
+ result = false;
+ } else {
+ // Do element by element comparison.
+ bool is_equal = true;
+ for (int i = 0; i < arrayLength; ++i) {
+ lua_rawgeti(luaState, -1, i + 1);
+ if (!lua_isinteger(luaState, /* index = */ -1) ||
+ (lua_tointeger(luaState, /* index = */ -1) != rawInputArray[i])) {
+ is_equal = false;
+ }
+ lua_pop(luaState, 1);
+ if (!is_equal) break;
+ }
+ result = is_equal;
+ }
+ }
+ lua_pop(luaState, 1);
+ return result;
+}
+
+extern "C" {
+
+#include "lua.h"
+
+JNIEXPORT jlong JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeCreateLuaEngine(
+ JNIEnv* env, jobject object) {
+ // Cast first to intptr_t to ensure int can hold the pointer without loss.
+ return static_cast<jlong>(reinterpret_cast<intptr_t>(new LuaEngine()));
+}
+
+JNIEXPORT void JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeDestroyLuaEngine(
+ JNIEnv* env, jobject object, jlong luaEnginePtr) {
+ delete reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+}
+
+JNIEXPORT void JNICALL
+Java_com_android_car_scriptexecutor_JniUtilsTest_nativePushBundleToLuaTableCaller(
+ JNIEnv* env, jobject object, jlong luaEnginePtr, jobject bundle) {
+ LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+ pushBundleToLuaTable(env, engine->getLuaState(), bundle);
+}
+
+JNIEXPORT jint JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeGetObjectSize(
+ JNIEnv* env, jobject object, jlong luaEnginePtr, jint index) {
+ LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+ return lua_rawlen(engine->getLuaState(), static_cast<int>(index));
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasBooleanValue(
+ JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jboolean value) {
+ const char* rawKey = env->GetStringUTFChars(key, nullptr);
+ LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+ auto* luaState = engine->getLuaState();
+ lua_pushstring(luaState, rawKey);
+ env->ReleaseStringUTFChars(key, rawKey);
+ lua_gettable(luaState, -2);
+ bool result = false;
+ if (!lua_isboolean(luaState, -1))
+ result = false;
+ else
+ result = static_cast<bool>(lua_toboolean(luaState, -1)) == static_cast<bool>(value);
+ lua_pop(luaState, 1);
+ return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasIntValue(
+ JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jint value) {
+ const char* rawKey = env->GetStringUTFChars(key, nullptr);
+ LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+ // Assumes the table is on top of the stack.
+ auto* luaState = engine->getLuaState();
+ lua_pushstring(luaState, rawKey);
+ env->ReleaseStringUTFChars(key, rawKey);
+ lua_gettable(luaState, -2);
+ bool result = false;
+ if (!lua_isinteger(luaState, -1))
+ result = false;
+ else
+ result = lua_tointeger(luaState, -1) == static_cast<int>(value);
+ lua_pop(luaState, 1);
+ return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasDoubleValue(
+ JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jdouble value) {
+ const char* rawKey = env->GetStringUTFChars(key, nullptr);
+ LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+ // Assumes the table is on top of the stack.
+ auto* luaState = engine->getLuaState();
+ lua_pushstring(luaState, rawKey);
+ env->ReleaseStringUTFChars(key, rawKey);
+ lua_gettable(luaState, -2);
+ bool result = false;
+ if (!lua_isnumber(luaState, -1))
+ result = false;
+ else
+ result = static_cast<double>(lua_tonumber(luaState, -1)) == static_cast<double>(value);
+ lua_pop(luaState, 1);
+ return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasStringValue(
+ JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jstring value) {
+ const char* rawKey = env->GetStringUTFChars(key, nullptr);
+ LuaEngine* engine = reinterpret_cast<LuaEngine*>(static_cast<intptr_t>(luaEnginePtr));
+ // Assumes the table is on top of the stack.
+ auto* luaState = engine->getLuaState();
+ lua_pushstring(luaState, rawKey);
+ env->ReleaseStringUTFChars(key, rawKey);
+ lua_gettable(luaState, -2);
+ bool result = false;
+ if (!lua_isstring(luaState, -1)) {
+ result = false;
+ } else {
+ std::string s = lua_tostring(luaState, -1);
+ const char* rawValue = env->GetStringUTFChars(value, nullptr);
+ result = strcmp(lua_tostring(luaState, -1), rawValue) == 0;
+ env->ReleaseStringUTFChars(value, rawValue);
+ }
+ lua_pop(luaState, 1);
+ return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasIntArrayValue(
+ JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jintArray value) {
+ jint* rawInputArray = env->GetIntArrayElements(value, nullptr);
+ const auto kInputLength = env->GetArrayLength(value);
+ bool result = hasIntegerArray(env, object, luaEnginePtr, key, rawInputArray, kInputLength);
+ env->ReleaseIntArrayElements(value, rawInputArray, JNI_ABORT);
+ return result;
+}
+
+JNIEXPORT bool JNICALL Java_com_android_car_scriptexecutor_JniUtilsTest_nativeHasLongArrayValue(
+ JNIEnv* env, jobject object, jlong luaEnginePtr, jstring key, jlongArray value) {
+ jlong* rawInputArray = env->GetLongArrayElements(value, nullptr);
+ const auto kInputLength = env->GetArrayLength(value);
+ bool result = hasIntegerArray(env, object, luaEnginePtr, key, rawInputArray, kInputLength);
+ env->ReleaseLongArrayElements(value, rawInputArray, JNI_ABORT);
+ return result;
+}
+
+} // extern "C"
+
+} // namespace
+} // namespace scriptexecutor
+} // namespace car
+} // namespace android
+} // namespace com
diff --git a/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/ScriptExecutorTest.java b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/ScriptExecutorTest.java
new file mode 100644
index 0000000..1a4028a
--- /dev/null
+++ b/packages/ScriptExecutor/tests/unit/src/com/android/car/scriptexecutor/ScriptExecutorTest.java
@@ -0,0 +1,907 @@
+/*
+ * Copyright (C) 2021 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.scriptexecutor;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutor;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.OutputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public final class ScriptExecutorTest {
+
+ private IScriptExecutor mScriptExecutor;
+ private ScriptExecutor mInstance;
+ private Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+
+ private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
+ public PersistableBundle mSavedBundle;
+ public PersistableBundle mFinalResult;
+ public int mErrorType;
+ public String mMessage;
+ public String mStackTrace;
+ public final CountDownLatch mResponseLatch = new CountDownLatch(1);
+
+ @Override
+ public void onScriptFinished(PersistableBundle result) {
+ mFinalResult = result;
+ mResponseLatch.countDown();
+ }
+
+ @Override
+ public void onSuccess(PersistableBundle stateToPersist) {
+ mSavedBundle = stateToPersist;
+ mResponseLatch.countDown();
+ }
+
+ @Override
+ public void onError(int errorType, String message, String stackTrace) {
+ mErrorType = errorType;
+ mMessage = message;
+ mStackTrace = stackTrace;
+ mResponseLatch.countDown();
+ }
+ }
+
+ private final ScriptExecutorListener mFakeScriptExecutorListener =
+ new ScriptExecutorListener();
+
+ private final PersistableBundle mPublishedData = new PersistableBundle();
+ private final PersistableBundle mSavedState = new PersistableBundle();
+
+ private final CountDownLatch mBindLatch = new CountDownLatch(1);
+
+ private static final int BIND_SERVICE_TIMEOUT_SEC = 5;
+ private static final int SCRIPT_PROCESSING_TIMEOUT_SEC = 10;
+
+
+ private final ServiceConnection mScriptExecutorConnection =
+ new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mScriptExecutor = IScriptExecutor.Stub.asInterface(service);
+ mBindLatch.countDown();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ fail("Service unexpectedly disconnected");
+ }
+ };
+
+ // Helper method to invoke the script and wait for it to complete and return a response.
+ private void runScriptAndWaitForResponse(String script, String function,
+ PersistableBundle publishedData, PersistableBundle previousState)
+ throws RemoteException {
+ mScriptExecutor.invokeScript(script, function, publishedData, previousState,
+ mFakeScriptExecutorListener);
+ try {
+ if (!mFakeScriptExecutorListener.mResponseLatch.await(SCRIPT_PROCESSING_TIMEOUT_SEC,
+ TimeUnit.SECONDS)) {
+ fail("Failed to get the callback method called by the script on time");
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ fail(e.getMessage());
+ }
+ }
+
+ private void runScriptAndWaitForError(String script, String function) throws RemoteException {
+ runScriptAndWaitForResponse(script, function, new PersistableBundle(),
+ new PersistableBundle());
+ }
+
+ @Before
+ public void setUp() throws InterruptedException {
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName("com.android.car.scriptexecutor",
+ "com.android.car.scriptexecutor.ScriptExecutor"));
+ mContext.bindServiceAsUser(intent, mScriptExecutorConnection, Context.BIND_AUTO_CREATE,
+ UserHandle.SYSTEM);
+ if (!mBindLatch.await(BIND_SERVICE_TIMEOUT_SEC, TimeUnit.SECONDS)) {
+ fail("Failed to bind to ScriptExecutor service");
+ }
+ }
+
+ @Test
+ public void invokeScript_returnsResult() throws RemoteException {
+ String returnResultScript =
+ "function hello(data, state)\n"
+ + " result = {hello=\"world\"}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(returnResultScript, "hello", mPublishedData, mSavedState);
+
+ // Expect to get back a bundle with a single string key: string value pair:
+ // {"hello": "world"}
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("hello")).isEqualTo(
+ "world");
+ }
+
+ @Test
+ public void invokeScript_allSupportedPrimitiveTypes() throws RemoteException {
+ String script =
+ "function knows(data, state)\n"
+ + " result = {string=\"hello\", boolean=true, integer=1, number=1.1}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "knows", mPublishedData, mSavedState);
+
+ // Expect to get back a bundle with 4 keys, each corresponding to a distinct supported type.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("string")).isEqualTo(
+ "hello");
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getBoolean("boolean")).isEqualTo(true);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getLong("integer")).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getDouble("number")).isEqualTo(1.1);
+ }
+
+ @Test
+ public void invokeScript_skipsUnsupportedNestedTables() throws RemoteException {
+ String script =
+ "function nested(data, state)\n"
+ + " result = {string=\"hello\", boolean=true, integer=1, number=1.1}\n"
+ + " result.nested_table = {x=0, y=0}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "nested", mPublishedData, mSavedState);
+
+ // Verify that expected error is received.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).contains(
+ "nested tables are not supported");
+ }
+
+ @Test
+ public void invokeScript_emptyBundle() throws RemoteException {
+ String script =
+ "function empty(data, state)\n"
+ + " result = {}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "empty", mPublishedData, mSavedState);
+
+ // If a script returns empty table as the result, we get an empty bundle.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle).isNotNull();
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void invokeScript_processPreviousStateAndReturnResult() throws RemoteException {
+ // Here we verify that the script actually processes provided state from a previous run
+ // and makes calculation based on that and returns the result.
+ String script =
+ "function update(data, state)\n"
+ + " result = {y = state.x+1}\n"
+ + " on_success(result)\n"
+ + "end\n";
+ PersistableBundle previousState = new PersistableBundle();
+ previousState.putInt("x", 1);
+
+ runScriptAndWaitForResponse(script, "update", mPublishedData, previousState);
+
+ // Verify that y = 2, because y = x + 1 and x = 1.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getLong("y")).isEqualTo(2);
+ }
+
+ @Test
+ public void invokeScript_allSupportedPrimitiveTypesWorkRoundTripWithKeyNamesPreserved()
+ throws RemoteException {
+ // Here we verify that all supported primitive types in supplied previous state Bundle
+ // are interpreted by the script as expected.
+ String script =
+ "function update_all(data, state)\n"
+ + " result = {}\n"
+ + " result.integer = state.integer + 1\n"
+ + " result.number = state.number + 0.1\n"
+ + " result.boolean = not state.boolean\n"
+ + " result.string = state.string .. \"CADABRA\"\n"
+ + " on_success(result)\n"
+ + "end\n";
+ PersistableBundle previousState = new PersistableBundle();
+ previousState.putInt("integer", 1);
+ previousState.putDouble("number", 0.1);
+ previousState.putBoolean("boolean", false);
+ previousState.putString("string", "ABRA");
+
+ runScriptAndWaitForResponse(script, "update_all", mPublishedData, previousState);
+
+ // Verify that keys are preserved but the values are modified as expected.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(4);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getLong("integer")).isEqualTo(2);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getDouble("number")).isEqualTo(0.2);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getBoolean("boolean")).isEqualTo(true);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("string")).isEqualTo(
+ "ABRACADABRA");
+ }
+
+ @Test
+ public void invokeScript_allSupportedArrayTypesWorkRoundTripWithKeyNamesPreserved()
+ throws RemoteException {
+ // Here we verify that all supported array types in supplied previous state Bundle are
+ // interpreted by the script as expected.
+ String script =
+ "function arrays(data, state)\n"
+ + " result = {}\n"
+ + " result.int_array = state.int_array\n"
+ + " result.long_array = state.long_array\n"
+ + " result.string_array = state.string_array\n"
+ + " on_success(result)\n"
+ + "end\n";
+ PersistableBundle previousState = new PersistableBundle();
+ int[] int_array = new int[]{1, 2};
+ long[] int_array_in_long = new long[]{1, 2};
+ long[] long_array = new long[]{1, 2, 3};
+ String[] string_array = new String[]{"one", "two", "three"};
+ previousState.putIntArray("int_array", int_array);
+ previousState.putLongArray("long_array", long_array);
+ previousState.putStringArray("string_array", string_array);
+
+ runScriptAndWaitForResponse(script, "arrays", mPublishedData, previousState);
+
+ // Verify that keys are preserved but the values are modified as expected.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(3);
+ // Lua has only one lua_Integer. Here Java long is used to represent it when data is
+ // transferred from Lua to CarTelemetryService.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("int_array")).isEqualTo(
+ int_array_in_long);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("long_array")).isEqualTo(
+ long_array);
+ assertThat(
+ mFakeScriptExecutorListener.mSavedBundle.getStringArray("string_array")).isEqualTo(
+ string_array);
+ }
+
+ @Test
+ public void invokeScript_modifiesArray()
+ throws RemoteException {
+ // Verify that an array modified by a script is properly sent back by the callback.
+ String script =
+ "function modify_array(data, state)\n"
+ + " result = {}\n"
+ + " result.long_array = state.long_array\n"
+ + " result.long_array[2] = 100\n"
+ + " on_success(result)\n"
+ + "end\n";
+ PersistableBundle previousState = new PersistableBundle();
+ long[] long_array = new long[]{1, 2, 3};
+ previousState.putLongArray("long_array", long_array);
+ long[] expected_array = new long[]{1, 100, 3};
+
+ runScriptAndWaitForResponse(script, "modify_array", mPublishedData, previousState);
+
+ // Verify that keys are preserved but the values are modified as expected.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("long_array")).isEqualTo(
+ expected_array);
+ }
+
+ @Test
+ public void invokeScript_processesStringArray()
+ throws RemoteException {
+ // Verify that an array modified by a script is properly sent back by the callback.
+ String script =
+ "function process_string_array(data, state)\n"
+ + " result = {}\n"
+ + " result.answer = state.string_array[1] .. state.string_array[2]\n"
+ + " on_success(result)\n"
+ + "end\n";
+ PersistableBundle previousState = new PersistableBundle();
+ String[] string_array = new String[]{"Hello ", "world!"};
+ previousState.putStringArray("string_array", string_array);
+
+ runScriptAndWaitForResponse(script, "process_string_array", mPublishedData, previousState);
+
+ // Verify that keys are preserved but the values are modified as expected.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getString("answer")).isEqualTo(
+ "Hello world!");
+ }
+
+ @Test
+ public void invokeScript_arraysWithLengthAboveLimitCauseError()
+ throws RemoteException {
+ // Verifies that arrays pushed by Lua that have their size over the limit cause error.
+ String script =
+ "function size_limit(data, state)\n"
+ + " result = {}\n"
+ + " result.huge_array = {}\n"
+ + " for i=1, 10000 do\n"
+ + " result.huge_array[i]=i\n"
+ + " end\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "size_limit", mPublishedData, mSavedState);
+
+ // Verify that expected error is received.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+ "Returned table huge_array exceeds maximum allowed size of 1000 "
+ + "elements. This key-value cannot be unpacked successfully. This error "
+ + "is unrecoverable.");
+ }
+
+ @Test
+ public void invokeScript_arrayContainingVaryingTypesCausesError()
+ throws RemoteException {
+ // Verifies that values in returned array must be the same integer type.
+ // For example string values in a Lua array are not allowed.
+ String script =
+ "function table_with_numbers_and_strings(data, state)\n"
+ + " result = {}\n"
+ + " result.mixed_array = state.long_array\n"
+ + " result.mixed_array[2] = 'a'\n"
+ + " on_success(result)\n"
+ + "end\n";
+ PersistableBundle previousState = new PersistableBundle();
+ long[] long_array = new long[]{1, 2, 3};
+ previousState.putLongArray("long_array", long_array);
+
+ runScriptAndWaitForResponse(script, "table_with_numbers_and_strings", mPublishedData,
+ previousState);
+
+ // Verify that expected error is received.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).contains(
+ "Returned Lua arrays must have elements of the same type.");
+ }
+
+ @Test
+ public void invokeScript_InTablesWithBothKeysAndIndicesCopiesOnlyIndexedData()
+ throws RemoteException {
+ // Documents the current behavior that copies only indexed values in a Lua table that
+ // contains both keyed and indexed data.
+ String script =
+ "function keys_and_indices(data, state)\n"
+ + " result = {}\n"
+ + " result.mixed_array = state.long_array\n"
+ + " result.mixed_array['a'] = 130\n"
+ + " on_success(result)\n"
+ + "end\n";
+ PersistableBundle previousState = new PersistableBundle();
+ long[] long_array = new long[]{1, 2, 3};
+ previousState.putLongArray("long_array", long_array);
+
+ runScriptAndWaitForResponse(script, "keys_and_indices", mPublishedData, previousState);
+
+ // Verify that keys are preserved but the values are modified as expected.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getLongArray("mixed_array")).isEqualTo(
+ long_array);
+ }
+
+ @Test
+ public void invokeScript_noLuaBufferOverflowForLargeInputArrays() throws RemoteException {
+ // Tests that arrays with length that exceed internal Lua buffer size of 20 elements
+ // do not cause buffer overflow and are handled properly.
+ String script =
+ "function large_input_array(data, state)\n"
+ + " sum = 0\n"
+ + " for _, val in ipairs(state.long_array) do\n"
+ + " sum = sum + val\n"
+ + " end\n"
+ + " result = {total = sum}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+ PersistableBundle previousState = new PersistableBundle();
+ int n = 10000;
+ long[] longArray = new long[n];
+ for (int i = 0; i < n; i++) {
+ longArray[i] = i;
+ }
+ previousState.putLongArray("long_array", longArray);
+ long expected_sum =
+ (longArray[0] + longArray[n - 1]) * n / 2; // sum of an arithmetic sequence.
+
+ runScriptAndWaitForResponse(script, "large_input_array", mPublishedData, previousState);
+
+ // Verify that keys are preserved but the values are modified as expected.
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mSavedBundle.getLong("total")).isEqualTo(
+ expected_sum);
+ }
+
+ @Test
+ public void invokeScript_scriptCallsOnError() throws RemoteException {
+ String script =
+ "function calls_on_error()\n"
+ + " if 1 ~= 2 then\n"
+ + " on_error(\"one is not equal to two\")\n"
+ + " return\n"
+ + " end\n"
+ + "end\n";
+
+ runScriptAndWaitForError(script, "calls_on_error");
+
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo("one is not equal to two");
+ }
+
+ @Test
+ public void invokeScript_tooManyParametersInOnError() throws RemoteException {
+ String script =
+ "function too_many_params_in_on_error()\n"
+ + " if 1 ~= 2 then\n"
+ + " on_error(\"param1\", \"param2\")\n"
+ + " return\n"
+ + " end\n"
+ + "end\n";
+
+ runScriptAndWaitForError(script, "too_many_params_in_on_error");
+
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+ "on_error can push only a single string parameter from Lua");
+ }
+
+ @Test
+ public void invokeScript_onErrorOnlyAcceptsString() throws RemoteException {
+ String script =
+ "function only_string()\n"
+ + " if 1 ~= 2 then\n"
+ + " on_error(false)\n"
+ + " return\n"
+ + " end\n"
+ + "end\n";
+
+ runScriptAndWaitForError(script, "only_string");
+
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+ "on_error can push only a single string parameter from Lua");
+ }
+
+ @Test
+ public void invokeScript_returnsFinalResult() throws RemoteException {
+ String returnFinalResultScript =
+ "function script_finishes(data, state)\n"
+ + " result = {data = state.input + 1}\n"
+ + " on_script_finished(result)\n"
+ + "end\n";
+ PersistableBundle previousState = new PersistableBundle();
+ previousState.putInt("input", 1);
+
+ runScriptAndWaitForResponse(returnFinalResultScript, "script_finishes", mPublishedData,
+ previousState);
+
+ // Expect to get back a bundle with a single key-value pair {"data": 2}
+ // because data = state.input + 1 as in the script body above.
+ assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getLong("data")).isEqualTo(2);
+ }
+
+ @Test
+ public void invokeScript_emptyStringValueIsValidValue() throws RemoteException {
+ // Verify that an empty string value is a valid value to be returned from a script.
+ String returnFinalResultScript =
+ "function empty_string(data, state)\n"
+ + " result = {data = \"\"}\n"
+ + " on_script_finished(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(returnFinalResultScript, "empty_string", mPublishedData,
+ new PersistableBundle());
+
+ // Expect to get back a bundle with a single key-value pair {"data": ""}
+ assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getString("data")).isEqualTo("");
+ }
+
+ @Test
+ public void invokeScript_allPrimitiveSupportedTypesForReturningFinalResult()
+ throws RemoteException {
+ // Here we verify that all supported primitive types are present in the returned final
+ // result bundle are present.
+ String script =
+ "function finalize_all(data, state)\n"
+ + " result = {}\n"
+ + " result.integer = state.integer + 1\n"
+ + " result.number = state.number + 0.1\n"
+ + " result.boolean = not state.boolean\n"
+ + " result.string = state.string .. \"CADABRA\"\n"
+ + " on_script_finished(result)\n"
+ + "end\n";
+ PersistableBundle previousState = new PersistableBundle();
+ previousState.putInt("integer", 1);
+ previousState.putDouble("number", 0.1);
+ previousState.putBoolean("boolean", false);
+ previousState.putString("string", "ABRA");
+
+ runScriptAndWaitForResponse(script, "finalize_all", mPublishedData, previousState);
+
+ // Verify that keys are preserved but the values are modified as expected.
+ assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(4);
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getLong("integer")).isEqualTo(2);
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getDouble("number")).isEqualTo(0.2);
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getBoolean("boolean")).isEqualTo(true);
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getString("string")).isEqualTo(
+ "ABRACADABRA");
+ }
+
+ @Test
+ public void invokeScript_emptyFinalResultBundle() throws RemoteException {
+ String script =
+ "function empty_final_result(data, state)\n"
+ + " result = {}\n"
+ + " on_script_finished(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "empty_final_result", mPublishedData, mSavedState);
+
+ // If a script returns empty table as the final result, we get an empty bundle.
+ assertThat(mFakeScriptExecutorListener.mFinalResult).isNotNull();
+ assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void invokeScript_wrongNumberOfCallbackInputsInOnScriptFinished()
+ throws RemoteException {
+ String script =
+ "function wrong_number_of_outputs_in_on_script_finished(data, state)\n"
+ + " result = {}\n"
+ + " extra = 1\n"
+ + " on_script_finished(result, extra)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "wrong_number_of_outputs_in_on_script_finished",
+ mPublishedData, mSavedState);
+
+ // We expect to get an error here because we expect only 1 input parameter in
+ // on_script_finished.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+ "on_script_finished can push only a single parameter from Lua - a Lua table");
+ }
+
+ @Test
+ public void invokeScript_wrongNumberOfCallbackInputsInOnSuccess() throws RemoteException {
+ String script =
+ "function wrong_number_of_outputs_in_on_success(data, state)\n"
+ + " result = {}\n"
+ + " extra = 1\n"
+ + " on_success(result, extra)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "wrong_number_of_outputs_in_on_success",
+ mPublishedData, mSavedState);
+
+ // We expect to get an error here because we expect only 1 input parameter in on_success.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+ "on_success can push only a single parameter from Lua - a Lua table");
+ }
+
+ @Test
+ public void invokeScript_wrongTypeInOnSuccess() throws RemoteException {
+ String script =
+ "function wrong_type_in_on_success(data, state)\n"
+ + " result = 1\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "wrong_type_in_on_success",
+ mPublishedData, mSavedState);
+
+ // We expect to get an error here because the type of the input parameter for on_success
+ // must be a Lua table.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+ "on_success can push only a single parameter from Lua - a Lua table");
+ }
+
+ @Test
+ public void invokeScript_wrongTypeInOnScriptFinished() throws RemoteException {
+ String script =
+ "function wrong_type_in_on_script_finished(data, state)\n"
+ + " result = 1\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "wrong_type_in_on_script_finished",
+ mPublishedData, mSavedState);
+
+ // We expect to get an error here because the type of the input parameter for
+ // on_script_finished must be a Lua table.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).isEqualTo(
+ "on_success can push only a single parameter from Lua - a Lua table");
+ }
+
+ @Test
+ public void invokeScriptLargeInput_largePublishedData() throws Exception {
+ // Verifies that large input does not overwhelm Binder's buffer because pipes are used
+ // instead.
+ String script =
+ "function large_published_data(data, state)\n"
+ + " sum = 0\n"
+ + " for _, val in ipairs(data.array) do\n"
+ + " sum = sum + val\n"
+ + " end\n"
+ + " result = {total = sum}\n"
+ + " on_script_finished(result)\n"
+ + "end\n";
+
+ ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
+ ParcelFileDescriptor writeFd = fds[1];
+ ParcelFileDescriptor readFd = fds[0];
+
+ PersistableBundle bundle = new PersistableBundle();
+ int n = 1 << 20; // 1024 * 1024 values, roughly 1 Million.
+ long[] array8Mb = new long[n];
+ for (int i = 0; i < n; i++) {
+ array8Mb[i] = i;
+ }
+ bundle.putLongArray("array", array8Mb);
+ long expectedSum =
+ (array8Mb[0] + array8Mb[n - 1]) * n / 2; // sum of an arithmetic sequence.
+
+ mScriptExecutor.invokeScriptForLargeInput(script, "large_published_data", readFd,
+ mSavedState,
+ mFakeScriptExecutorListener);
+
+ readFd.close();
+ try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeFd)) {
+ bundle.writeToStream(outputStream);
+ }
+
+ boolean gotResponse = mFakeScriptExecutorListener.mResponseLatch.await(
+ SCRIPT_PROCESSING_TIMEOUT_SEC,
+ TimeUnit.SECONDS);
+
+ assertWithMessage("Failed to get the callback method called by the script on time")
+ .that(gotResponse).isTrue();
+ assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getLong("total"))
+ .isEqualTo(expectedSum);
+ }
+
+ @Test
+ public void invokeScript_bothPublishedDataAndPreviousStateAreProvided() throws RemoteException {
+ // Verifies that both published data and previous state PersistableBundles
+ // are piped into script.
+ String script =
+ "function data_and_state(data, state)\n"
+ + " result = {answer = data.a .. data.b .. state.c .. state.d}\n"
+ + " on_script_finished(result)\n"
+ + "end\n";
+
+ PersistableBundle publishedData = new PersistableBundle();
+ publishedData.putString("a", "A");
+ publishedData.putString("b", "B");
+
+ PersistableBundle previousState = new PersistableBundle();
+ previousState.putString("c", "C");
+ previousState.putString("d", "D");
+
+ runScriptAndWaitForResponse(script, "data_and_state", publishedData, previousState);
+
+ // Lua script combines both input published data and previous state into a single result.
+ assertThat(mFakeScriptExecutorListener.mFinalResult).isNotNull();
+ assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getString("answer")).isEqualTo(
+ "ABCD");
+ }
+
+ @Test
+ public void invokeScript_outputIntAndLongAreTreatedAsLong() throws RemoteException {
+ // Verifies that we treat output both integer and long as long integer type although we
+ // distinguish between int and long in the script input.
+ String script =
+ "function int_and_long_are_output_long(data, state)\n"
+ + " result = {int = data.int, long = state.long}\n"
+ + " on_script_finished(result)\n"
+ + "end\n";
+
+ PersistableBundle publishedData = new PersistableBundle();
+ publishedData.putInt("int", 100);
+
+ PersistableBundle previousState = new PersistableBundle();
+ previousState.putLong("long", 200);
+
+ runScriptAndWaitForResponse(script, "int_and_long_are_output_long",
+ publishedData, previousState);
+
+ // If a script returns empty table as the final result, we get an empty bundle.
+ assertThat(mFakeScriptExecutorListener.mFinalResult).isNotNull();
+ assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(2);
+ // getInt should always return "empty" value (zero) because all integer types are treated
+ // as Java long.
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getInt("int")).isEqualTo(0);
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getInt("long")).isEqualTo(0);
+ // Instead all expected integer values are successfully retrieved using getLong method
+ // from the output bundle.
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getLong("int")).isEqualTo(100);
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getLong("long")).isEqualTo(200);
+ }
+
+ @Test
+ public void invokeScript_nonUTFCharactersDoNotCauseErrors() throws RemoteException {
+ // Tries to create an output string value that does not conform to Modified UTF-8.
+ // JNI gracefully handles it by parsing on the string as is.
+ String script =
+ "function non_utf_key_string(data, state)\n"
+ + " result = {answer = \"i\0np\200\200ut\"}\n"
+ + " on_script_finished(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "non_utf_key_string", new PersistableBundle(),
+ new PersistableBundle());
+
+ // The output will still have all characters, including those that do not conform to
+ // Modified UTF-8.
+ assertThat(mFakeScriptExecutorListener.mFinalResult).isNotNull();
+ assertThat(mFakeScriptExecutorListener.mFinalResult.size()).isEqualTo(1);
+ assertThat(mFakeScriptExecutorListener.mFinalResult.getString("answer")).isEqualTo(
+ "i\0np\200\200ut");
+ }
+
+ @Test
+ public void invokeScript_wrongFunctionNameProvided() throws RemoteException {
+ // Verifies that not specifying function name correctly is handled through error callback.
+ String script =
+ "function correct_function(data, state)\n"
+ + "end\n";
+
+ runScriptAndWaitForError(script, "wrong_function");
+
+ // Verify that the expected error is received.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_RUNTIME_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).contains(
+ "Wrong function name");
+ }
+
+ @Test
+ public void invokeScript_runtimeErrorDueToSyntax() throws RemoteException {
+ // Verifies that syntax errors during script loading are handled gracefully.
+ String script =
+ "function wrong_syntax(data, state)\n"
+ + " x == 1\n"
+ + "end\n";
+
+ runScriptAndWaitForError(script, "wrong_syntax");
+
+ // Verify that the expected error is received.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_RUNTIME_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).contains(
+ "Error encountered while loading the script");
+ }
+
+ @Test
+ public void invokeScript_runtimeErrorDueToUndefinedMethod() throws RemoteException {
+ // Verifies that runtime errors encountered during Lua script execution trigger an error
+ // returned via a callback.
+ String script =
+ "function runtime_error(data, state)\n"
+ + " on_problem(data, state)\n"
+ + "end\n";
+
+ runScriptAndWaitForError(script, "runtime_error");
+
+ // Verify that the expected error is received.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_RUNTIME_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).contains(
+ "Error encountered while running the script");
+ }
+
+ @Test
+ public void invokeScript_returnedValuesOfUnsupportedTypesReturnError() throws RemoteException {
+ // Verifies that if we try to return a value of unsupported type, we get an error instead.
+ // In this case, the unsupported type is LUA_TFUNCTION type.
+ String script =
+ "function function_type(data, state)\n"
+ + " result = {fn = function_type}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "function_type", mPublishedData, mSavedState);
+
+ // Verify that the expected error is received.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).contains(
+ "has a Lua type=function, which is not supported yet");
+ }
+
+ @Test
+ public void invokeScript_returnedFloatingArraysNotSupported() throws RemoteException {
+ // Verifies that we do not support return values that contain floating number arrays.
+ String script =
+ "function floating_point_arrays(data, state)\n"
+ + " array = {}\n"
+ + " array[0] = 1.1\n"
+ + " array[1] = 1.2\n"
+ + " result = {data = array}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "floating_point_arrays", mPublishedData, mSavedState);
+
+ // Verify that the expected error is received.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).contains(
+ "a floating number array, which is not supported yet");
+ }
+
+ @Test
+ public void invokeScript_returnedBooleanArraysNotSupported() throws RemoteException {
+ // Verifies that we do not yet support return values that contain boolean arrays.
+ String script =
+ "function array_of_booleans(data, state)\n"
+ + " array = {}\n"
+ + " array[0] = false\n"
+ + " array[1] = true\n"
+ + " result = {data = array}\n"
+ + " on_success(result)\n"
+ + "end\n";
+
+ runScriptAndWaitForResponse(script, "array_of_booleans", mPublishedData, mSavedState);
+
+ // Verify that the expected error is received.
+ assertThat(mFakeScriptExecutorListener.mErrorType).isEqualTo(
+ IScriptExecutorListener.ERROR_TYPE_LUA_SCRIPT_ERROR);
+ assertThat(mFakeScriptExecutorListener.mMessage).contains(
+ "is an array with values of type=boolean, which is not supported yet");
+ }
+}
+
diff --git a/service/Android.bp b/service/Android.bp
index 709c928..58a8e7c 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -29,11 +29,13 @@
}
car_service_sources = [
+ ":iscriptexecutor_aidl",
"src/**/*.java",
":statslog-Car-java-gen",
]
common_lib_deps = [
+ "android.automotive.telemetry.internal-java", // ICarTelemetryInternal
"android.car.cluster.navigation",
"android.car.userlib",
"android.car.watchdoglib",
@@ -57,6 +59,7 @@
"car-admin-ui-lib",
"Slogf",
"cartelemetry-protos",
+ "carwatchdog-protos",
]
android_app {
@@ -85,9 +88,14 @@
jni_libs: [
"libcarservicejni",
- "libscriptexecutorjni",
],
+ aidl: {
+ include_dirs: [
+ "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+ ],
+ },
+
required: ["allowed_privapp_com.android.car"],
// Disable build in PDK, missing aidl import breaks build
@@ -138,6 +146,12 @@
static_libs: common_lib_deps,
+ aidl: {
+ include_dirs: [
+ "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+ ],
+ },
+
product_variables: {
pdk: {
enabled: false,
@@ -162,9 +176,24 @@
"car-frameworks-service",
],
+ aidl: {
+ include_dirs: [
+ "frameworks/native/aidl/binder", // For PersistableBundle.aidl
+ ],
+ },
+
product_variables: {
pdk: {
enabled: false,
},
},
}
+
+filegroup {
+ name: "iscriptexecutor_aidl",
+ srcs: [
+ "src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutor.aidl",
+ "src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl",
+ ],
+ path: "src",
+}
diff --git a/service/AndroidManifest.xml b/service/AndroidManifest.xml
index cd2e3bf..51fe718 100644
--- a/service/AndroidManifest.xml
+++ b/service/AndroidManifest.xml
@@ -465,7 +465,7 @@
android:label="@string/car_permission_car_display_in_cluster"
android:description="@string/car_permission_desc_car_display_in_cluster"/>
- <!-- Allows an application to lunch applications in the instrument cluster.
+ <!-- Allows an application to launch applications in the instrument cluster.
<p>Protection level: signature|privileged
-->
<permission android:name="android.car.permission.CAR_INSTRUMENT_CLUSTER_CONTROL"
@@ -473,6 +473,14 @@
android:label="@string/car_permission_car_cluster_control"
android:description="@string/car_permission_desc_car_cluster_control"/>
+ <!-- Allows an application to listen for navigation state changes in instrument cluster.
+ <p>Protection level: signature|privileged
+ -->
+ <permission android:name="android.car.permission.CAR_MONITOR_CLUSTER_NAVIGATION_STATE"
+ android:protectionLevel="signature|privileged"
+ android:label="@string/car_permission_car_cluster_control"
+ android:description="@string/car_permission_desc_car_cluster_control"/>
+
<!-- Allows an application to communicate with a device in AOAP mode.
<p>Protection level: signature|privileged
-->
@@ -892,6 +900,14 @@
android:label="@string/car_permission_label_template_renderer"
android:description="@string/car_permission_desc_template_renderer"/>
+ <!-- Allows an application to control launching applications in Car.
+ <p>Protection level: signature|privileged
+ -->
+ <permission android:name="android.car.permission.CONTROL_CAR_APP_LAUNCH"
+ android:description="@string/car_permission_desc_control_car_app_launch"
+ android:label="@string/car_permission_label_control_car_app_launch"
+ android:protectionLevel="signature|privileged" />
+
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.DEVICE_POWER"/>
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
@@ -935,9 +951,18 @@
</service>
<service android:name=".PerUserCarService"
android:exported="false"/>
- <service android:name=".telemetry.ScriptExecutor"
- android:exported="false"
- android:isolatedProcess="true"/>
+ <service
+ android:name="com.android.car.pm.CarSafetyAccessibilityService"
+ android:singleUser="true"
+ android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.accessibilityservice.AccessibilityService" />
+ </intent-filter>
+ <meta-data
+ android:name="android.accessibilityservice"
+ android:resource="@xml/car_safety_accessibility_service_config" />
+ </service>
<activity android:name="com.android.car.pm.ActivityBlockingActivity"
android:documentLaunchMode="always"
diff --git a/service/jni/evs/StreamHandler.cpp b/service/jni/evs/StreamHandler.cpp
index 6f64160..826ff02 100644
--- a/service/jni/evs/StreamHandler.cpp
+++ b/service/jni/evs/StreamHandler.cpp
@@ -161,9 +161,17 @@
auto it = mReceivedBuffers.begin();
while (it != mReceivedBuffers.end()) {
if (it->bufferId == buffer.bufferId) {
+ // We intentionally do not update the iterator to detect a
+ // request to return an unknown buffer.
mReceivedBuffers.erase(it);
break;
}
+ ++it;
+ }
+
+ if (it == mReceivedBuffers.end()) {
+ LOG(DEBUG) << "Ignores a request to return unknown buffer";
+ return;
}
}
diff --git a/service/res/values-af/strings.xml b/service/res/values-af/strings.xml
index c301554..22c404c 100644
--- a/service/res/values-af/strings.xml
+++ b/service/res/values-af/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Beheer motorkragbeleid."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"lewer template"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Lewer template."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"beheer die oopmaak van programme"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Beheer die oopmaak van programme."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"My Toestel"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gas"</string>
<string name="importance_default" msgid="8587741629268312938">"Verstekbelang"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Stel later terug"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Inligtingvermaakstelsel stel terug as kar begin."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Die motor moet geparkeer wees om terug te stel."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> beïnvloed tans jou stelselprestasie"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Deaktiveer program om stelselprestasie te verbeter. Jy kan die program in Instellings weer aktiveer."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioritiseer program om aan te hou om program te gebruik."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Deïnstalleer program om stelselprestasie te verbeter."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Deaktiveer program"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioritiseer program"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Deïnstalleer program"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> is gedeaktiveer. Jy kan dit in instellings weer aktiveer."</string>
</resources>
diff --git a/service/res/values-am/strings.xml b/service/res/values-am/strings.xml
index 75710a5..65b84a5 100644
--- a/service/res/values-am/strings.xml
+++ b/service/res/values-am/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"የመኪና ኃይል መመሪያን ተቆጣጠር።"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"የቅንብር ደንቦችን ምስል ሥራ።"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"የቅንብር ደንቦችን ምስል ሥራ።"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"መተግበሪያዎችን ማስጀመር መቆጣጠር"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"መተግበሪያዎችን ማስጀመር መቆጣጠር።"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"የእኔ መሣሪያ"</string>
<string name="default_guest_name" msgid="2912812799433131476">"እንግዳ"</string>
<string name="importance_default" msgid="8587741629268312938">"ነባሪ አስፈላጊነት"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"በኋላ ዳግም አስጀምር"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"መኪናው በሚቀጥለው ጊዜ ሲጀምር የኢንፎቴይንመንት ሥርዓቱ ዳግም ይጀምራል።"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"ዳግም ማስጀመርን ለመጀመር መኪናው መቆም አለበት።"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> በስርዓትዎ አፈጻጸም ላይ ተጽዕኖ እያሳረፈ ነው"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"የስርዓት አፈጻጸምን ለማሻሻል መተግበሪያን ያሰናክሉ። በቅንብሮች ውስጥ መተግበሪያውን እንደገና ማንቃት ይችላሉ።"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"መተግበሪያን መጠቀሙን ለመቀጠል ለመተግበሪያ ቅድሚያ ይስጡ።"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"የስርዓት አፈጻጸምን ለማሻሻል መተግበሪያን ያራግፉ።"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"መተግበሪያን አሰናክል"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ለመተግበሪያ ቅድሚያ ስጥ"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"መተግበሪያን አራግፍ"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> ተሰናክሏል። በቅንብሮች ውስጥ እንደገና ሊያነቁት ይችላሉ።"</string>
</resources>
diff --git a/service/res/values-ar/strings.xml b/service/res/values-ar/strings.xml
index f1a6290..0b086d4 100644
--- a/service/res/values-ar/strings.xml
+++ b/service/res/values-ar/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"التحكُّم في سياسة تشغيل ميزات السيارة"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"عرض النماذج"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"عرض النماذج"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"التحكم في تشغيل التطبيقات"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"التحكم في تشغيل التطبيقات"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"جهازي"</string>
<string name="default_guest_name" msgid="2912812799433131476">"ضيف"</string>
<string name="importance_default" msgid="8587741629268312938">"مدى الأهمية التلقائي"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"إعادة الضبط لاحقًا"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"سيُعاد ضبط \"نظام الترفيه والمعلومات\" عند تشغيل السيارة لاحقًا."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"يجب أن تكون السيارة متوقفة لبدء إعادة الضبط."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> يؤثر في أداء نظامك"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"يجب إيقاف التطبيق لتحسين أداء النظام. يمكنك تفعيل التطبيق مرة أخرى في الإعدادات."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"يجب منح أولوية للتطبيق لمواصلة استخدامه."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"يجب إلغاء تثبيت التطبيق لتحسين أداء النظام."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"إيقاف التطبيق"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"منح أولوية للتطبيق"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"إلغاء تثبيت التطبيق"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"تم إيقاف <xliff:g id="ID_1">^1</xliff:g>. يمكنك تفعيله مجددًا في الإعدادات."</string>
</resources>
diff --git a/service/res/values-as/strings.xml b/service/res/values-as/strings.xml
index 3374926..c484cd4 100644
--- a/service/res/values-as/strings.xml
+++ b/service/res/values-as/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"গাড়ীৰ পাৱাৰ পলিচী নিয়ন্ত্ৰণ কৰে।"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"টেমপ্লে’ট প্ৰদান কৰক"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"টেমপ্লে’ট প্ৰদান কৰক।"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"এপ্লিকেশ্বন লঞ্চ হোৱাটো নিয়ন্ত্ৰণ কৰে"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"এপ্লিকেশ্বন লঞ্চ হোৱাটো নিয়ন্ত্ৰণ কৰে।"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"মোৰ ডিভাইচ"</string>
<string name="default_guest_name" msgid="2912812799433131476">"অতিথি"</string>
<string name="importance_default" msgid="8587741629268312938">"ডিফ’ল্ট গুৰুত্ব"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"পাছত ৰিছেট কৰক"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"ইনফ’টেইনমেণ্ট ছিষ্টেমটোৱে পৰৱৰ্তী সময়ত গাড়ীখন ষ্টাৰ্ট হ\'লে ৰিছেট কৰিব।"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"ৰিছেট কৰাটো আৰম্ভ কৰিবলৈ গাড়ীখন ৰখাই থ\'বই লাগিব।"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g>এ আপোনাৰ ছিষ্টেমৰ কাৰ্যদক্ষতাত প্ৰভাৱ পেলাইছে"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"ছিষ্টেমৰ কাৰ্যদক্ষতা উন্নত কৰিবলৈ এপ্ অক্ষম কৰক। আপুনি ছেটিঙত পুনৰ এপ্টো সক্ষম কৰিব পাৰে।"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"এপ্টো ব্যৱহাৰ কৰি থাকিবলৈ এপক অগ্ৰাধিকাৰ দিয়ক।"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"ছিষ্টেমৰ কাৰ্যদক্ষতা উন্নত কৰিবলৈ এপ্ আনইনষ্টল কৰক।"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"এপ্ অক্ষম কৰক"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"এপক অগ্ৰাধিকাৰ দিয়ক"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"এপ্ আনইনষ্টল কৰক"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g>ক অক্ষম কৰা হৈছে। আপুনি এইটো ছেটিঙত পুনৰ সক্ষম কৰিব পাৰে।"</string>
</resources>
diff --git a/service/res/values-az/strings.xml b/service/res/values-az/strings.xml
index 462f07b..1ce1069 100644
--- a/service/res/values-az/strings.xml
+++ b/service/res/values-az/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Avtomobilin enerji siyasətini idarə edə bilir."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"şablonları vizualizasiya edin"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Şablonları vizualizasiya edin."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"tətbiqlərin başladılmasına nəzarət"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Tətbiqlərin başladılmasına nəzarət."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Cihazım"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Qonaq"</string>
<string name="importance_default" msgid="8587741629268312938">"Defolt önəm"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Sonra sıfırlayın"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Məlumat-əyləncə sistemi avtomobil növbəti dəfə işə düşəndə sıfırlanacaq."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Sıfırlamanı başlatmaq üçün avtomobil park edilməlidir."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> sisteminizin performansına təsir edir"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Sistemin performansını yaxşılaşdırmaq üçün tətbiqi deaktiv edin. Tətbiqi yenidən Ayarlarda aktiv edə bilərsiniz."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Tətbiqdən istifadə etməyə davam etmək üçün tətbiqi prioritetləşdirin."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Sistemin performansını yaxşılaşdırmaq üçün tətbiqi sistemdən silin."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Tətbiqi deaktiv edin"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Tətbiqi prioritetləşdirin"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Tətbiqi sistemdən silin"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> deaktiv edilib. Yenidən Ayarlarda aktiv edə bilərsiniz."</string>
</resources>
diff --git a/service/res/values-b+sr+Latn/strings.xml b/service/res/values-b+sr+Latn/strings.xml
index d6879f4..143525b 100644
--- a/service/res/values-b+sr+Latn/strings.xml
+++ b/service/res/values-b+sr+Latn/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Kontrola smernica za napajanje automobila."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"prikazivanje šablona"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Prikazivanje šablona."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"kontrola pokretanja aplikacija"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Kontroliše pokretanje aplikacija."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Moj uređaj"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gost"</string>
<string name="importance_default" msgid="8587741629268312938">"Podrazumevana važnost"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Resetuj kasnije"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Sis. za info-zab. se reset. kada opet upal. kola."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Morate da parkirate kola za početak resetovanja."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> utiče na performanse sistema"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Onemogućite aplikaciju da biste poboljšali performanse sistema. Aplikaciju možete ponovo da omogućite u Podešavanjima."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Dajte prioritet aplikaciji da biste nastavili da je koristite."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Deinstalirajte aplikaciju da biste poboljšali performanse sistema."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Onemogući aplikaciju"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Dajte prioritet aplikaciji"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Deinstalirajte aplikaciju"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Aplikacija <xliff:g id="ID_1">^1</xliff:g> je onemogućena. Možete ponovo da je omogućite u Podešavanjima."</string>
</resources>
diff --git a/service/res/values-be/strings.xml b/service/res/values-be/strings.xml
index 3782c86..fd2a287 100644
--- a/service/res/values-be/strings.xml
+++ b/service/res/values-be/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Кіраваць палітыкай сілкавання аўтамабіля."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"візуалізацыя шаблонаў"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Візуалізацыя шаблонаў."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"кіраванне запускам праграм"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Кіраванне запускам праграм."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Мая прылада"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Госць"</string>
<string name="importance_default" msgid="8587741629268312938">"Стандартная важнасць"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Скінуць пазней"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Скід адбудзецца пры наступным запуску аўтамабіля."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Каб пачаць скід налад, прыпаркуйце аўтамабіль."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"Праграма \"<xliff:g id="ID_1">^1</xliff:g>\" аказвае ўздзеянне на прадукцыйнасць сістэмы"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Адключыце праграму, каб павысіць прадукцыйнасць сістэмы. Праграму можна будзе ўключыць зноў у Наладах."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Зрабіце праграму прыярытэтнай, каб працягваць ёй карыстацца."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Выдаліце праграму, каб павысіць прадукцыйнасць сістэмы."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Адключыць праграму"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Зрабіць праграму прыярытэтнай"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Выдаліць праграму"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Праграма \"<xliff:g id="ID_1">^1</xliff:g>\" адключана. Зноў уключыць яе можна ў Наладах."</string>
</resources>
diff --git a/service/res/values-bg/strings.xml b/service/res/values-bg/strings.xml
index 5242ae5..185e9a7 100644
--- a/service/res/values-bg/strings.xml
+++ b/service/res/values-bg/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Управление на правилата за захранването на автомобила."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"рендериране на шаблони"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Рендериране на шаблони."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"управление на приложенията, които се стартират"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Управление на приложенията, които се стартират."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Моето устройство"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Гост"</string>
<string name="importance_default" msgid="8587741629268312938">"Важност по подразбиране"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Нулиране по-късно"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Основното у-во ще се нулира при следващото стартиране на колата."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Колата трябва да е паркирана, за да старт. нулирането."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> засяга ефективността на системата ви."</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Деактивирайте приложението с цел подобряване на ефективността на системата. Можете да го активирате отново от настройките."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Приоритизирайте приложението, за да продължите да го използвате."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Деинсталирайте приложение с цел подобряване на ефективността на системата."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Деактивиране на приложението"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Приоритизиране на приложението"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Деинсталиране на приложението"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Приложението <xliff:g id="ID_1">^1</xliff:g> е деактивирано. Можете да го активирате отново от настройките."</string>
</resources>
diff --git a/service/res/values-bn/strings.xml b/service/res/values-bn/strings.xml
index 3e30d75..deb0dbc 100644
--- a/service/res/values-bn/strings.xml
+++ b/service/res/values-bn/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"গাড়ির পাওয়ার নীতি নিয়ন্ত্রণ করুন।"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"টেম্পলেট রেন্ডার করুন"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"টেম্পলেট রেন্ডার করুন।"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"অ্যাপ্লিকেশন চালু করা প্রক্রিয়া নিয়ন্ত্রণ করুন"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"অ্যাপ্লিকেশন চালু করা প্রক্রিয়া নিয়ন্ত্রণ করুন।"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"আমার ডিভাইস"</string>
<string name="default_guest_name" msgid="2912812799433131476">"অতিথি"</string>
<string name="importance_default" msgid="8587741629268312938">"ডিফল্ট গুরুত্ব"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"পরে রিসেট করুন"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"ইনফোটেইনমেন্ট সিস্টেম পরের বার গাড়ি চালু হলে রিসেট হবে।"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"রিসেট শুরু করতে গাড়িকে পার্কিং অবস্থায় থাকতে হবে।"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> আপনার সিস্টেমের পারফর্ম্যান্সে প্রভাব ফেলছে"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"সিস্টেমের পারফর্ম্যান্স উন্নত করতে অ্যাপ বন্ধ করুন। সেটিংস থেকে আপনি আবার অ্যাপ চালু করতে পারবেন।"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ব্যবহার করা চালিয়ে যেতে অ্যাপকে অগ্রাধিকার দিন।"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"সিস্টেমের পারফর্ম্যান্স উন্নত করতে অ্যাপটি আনইনস্টল করুন।"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"অ্যাপ বন্ধ করুন"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"অ্যাপকে অগ্রাধিকার দিন"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"অ্যাপ আনইনস্টল করুন"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> বন্ধ করা হয়েছে। সেটিংস থেকে আপনি আবার এটি চালু করতে পারবেন।"</string>
</resources>
diff --git a/service/res/values-bs/strings.xml b/service/res/values-bs/strings.xml
index a4f2241..c5a7c4b 100644
--- a/service/res/values-bs/strings.xml
+++ b/service/res/values-bs/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Upravljajte pravilima uključivanja/isključivanja komponenti automobila."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"iscrtavanje šablona"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Iscrtavanje šablona."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"upravljanje pokretanjem aplikacija"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Upravljanje pokretanjem aplikacija."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Moj uređaj"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gost"</string>
<string name="importance_default" msgid="8587741629268312938">"Zadana važnost"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Vrati na zad. kasnije"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Info-zab. sist. će se vratiti na zadano kad opet pokrenete auto."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Da počnete vraćanje na zadano, parkirajte automobil."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> utiče na performanse sistema"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Onemogućite aplikaciju da poboljšate performanse sistema. Ponovo možete omogućiti aplikaciju u Postavkama."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Dodijelite prioritet aplikaciji da je nastavite koristiti."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Deinstalirajte aplikaciju da poboljšate performanse sistema."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Onemogući aplikaciju"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Dodijeli prioritet aplikaciji"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Deinstaliraj aplikaciju"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Aplikacija <xliff:g id="ID_1">^1</xliff:g> je onemogućena. Ponovo je možete omogućiti u Postavkama."</string>
</resources>
diff --git a/service/res/values-ca/strings.xml b/service/res/values-ca/strings.xml
index b0c6377..333279c 100644
--- a/service/res/values-ca/strings.xml
+++ b/service/res/values-ca/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Controla la política d\'energia del cotxe."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"renderitzar plantilles"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Renderitzar plantilles."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"controla l\'inici de les aplicacions"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Controla l\'inici de les aplicacions."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"El meu dispositiu"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Convidat"</string>
<string name="importance_default" msgid="8587741629268312938">"Importància predeterminada"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Restableix més tard"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"El sist. d\'infoentreteniment es restablirà en engegar el cotxe."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"El cotxe ha d\'estar aparcat per al restabliment."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> està afectant el rendiment del sistema"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Desactiva l\'aplicació per millorar el rendiment del sistema. Pots tornar-la a activar a Configuració."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioritza l\'aplicació per continuar utilitzant-la."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Desinstal·la l\'aplicació per millorar el rendiment del sistema."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Desactiva l\'aplicació"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioritza l\'aplicació"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Desinstal·la l\'aplicació"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> s\'ha desactivat. Pots tornar a activar l\'aplicació a Configuració."</string>
</resources>
diff --git a/service/res/values-cs/strings.xml b/service/res/values-cs/strings.xml
index b0a4aad..c2d1537 100644
--- a/service/res/values-cs/strings.xml
+++ b/service/res/values-cs/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Ovládání zásad pro napájení komponentů auta."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"vykreslování šablon"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Vykreslování šablon."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"ovládání spouštěných aplikací"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Ovládání spouštěných aplikací."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Moje zařízení"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Host"</string>
<string name="importance_default" msgid="8587741629268312938">"Výchozí důležitost"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Resetovat později"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Systém se resetuje při příštím nastartování auta."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Před zahájením resetu musí být auto zaparkované."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"Aplikace <xliff:g id="ID_1">^1</xliff:g> ovlivňuje výkon systému."</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Deaktivováním aplikace vylepšete výkon systému. Aplikaci můžete znovu aktivovat v Nastavení."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Upřednostněte aplikaci a nadále ji používejte."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Odinstalací aplikace vylepšete výkon systému."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Deaktivovat aplikaci"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Upřednostnit aplikaci"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Odinstalovat aplikaci"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Aplikace <xliff:g id="ID_1">^1</xliff:g> byla deaktivována. Znovu aktivovat ji můžete v Nastavení."</string>
</resources>
diff --git a/service/res/values-da/strings.xml b/service/res/values-da/strings.xml
index cac56bd..1196664 100644
--- a/service/res/values-da/strings.xml
+++ b/service/res/values-da/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Styr bilens politik for aktivering af komponenter."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"gengive skabeloner"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Gengive skabeloner."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"styre startapps"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Styre startapps."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Min enhed"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gæst"</string>
<string name="importance_default" msgid="8587741629268312938">"Standardvigtighed"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Nulstil senere"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Infotainmentsystemet nulstilles, næste gang bilen starter."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Nulstillingen kan kun startes, når bilen er parkeret."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> påvirker systemets ydeevne."</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Deaktiver appen for at forbedre systemets ydeevne. Du kan aktivere appen igen under Indstillinger."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioriter appen for at fortsætte med at bruge den."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Afinstaller appen for at forbedre systemets ydeevne."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Deaktiver app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioriter app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Afinstaller app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> er blevet deaktiveret. Du kan aktivere den igen under Indstillinger."</string>
</resources>
diff --git a/service/res/values-de/strings.xml b/service/res/values-de/strings.xml
index 156265d..3c76d9e 100644
--- a/service/res/values-de/strings.xml
+++ b/service/res/values-de/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Richtlinie zur Stromversorgung von Komponenten im Auto steuern."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"Vorlagen werden gerendert"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Vorlagen werden gerendert."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"Der Start von Anwendungen wird gesteuert"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Der Start von Anwendungen wird gesteuert."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Mein Gerät"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gast"</string>
<string name="importance_default" msgid="8587741629268312938">"Wichtigkeit: Standard"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Später zurücksetzen"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Das Infotainmentsystem wird beim nächsten Start zurückgesetzt."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Zum Zurücksetzen muss das Auto stehen."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> beeinträchtigt die Systemleistung"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Wenn du die Systemleistung verbessern möchtest, deaktiviere die App. Du kannst sie in den Einstellungen später wieder aktivieren."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Wenn du die App weiterhin nutzen möchtest, priorisiere sie."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Wenn du die Systemleistung verbessern möchtest, deinstalliere die App."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"App deaktivieren"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"App priorisieren"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"App deinstallieren"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> wurde deaktiviert. Du kannst die App in den Einstellungen wieder aktivieren."</string>
</resources>
diff --git a/service/res/values-el/strings.xml b/service/res/values-el/strings.xml
index 5023f46..bf3be05 100644
--- a/service/res/values-el/strings.xml
+++ b/service/res/values-el/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Έλεγχος πολιτικής ισχύος αυτοκινήτου."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"απόδοση προτύπων"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Να αποδίδει πρότυπα."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"έλεγχος εφαρμογών εκκίνησης"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Έλεγχος εφαρμογών εκκίνησης."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Η συσκευή μου"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Επισκέπτης"</string>
<string name="importance_default" msgid="8587741629268312938">"Προεπιλεγμένη βαρύτητα"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Επαναφορά αργότερα"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Επαναφ. συστήμ. ενημέρ. και ψυχαγ. στην επόμενη εκκίνηση αυτοκ."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Σταθμεύστε το αυτοκίνητο για έναρξη επαναφοράς."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"Η εφαρμογή <xliff:g id="ID_1">^1</xliff:g> επηρεάζει την απόδοση του συστήματος"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Απενεργοποιήστε την εφαρμογή για βελτίωση της απόδοσης του συστήματος. Μπορείτε να ενεργοποιήσετε ξανά την εφαρμογή στις Ρυθμίσεις."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Δώστε προτεραιότητα στην εφαρμογή για να συνεχίσετε τη χρήση της."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Απεγκαταστήστε την εφαρμογή για βελτίωση της απόδοσης του συστήματος."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Απενεργοποίηση εφαρμογής"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Προτεραιότητα εφαρμογής"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Απεγκατάσταση εφαρμογής"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Η εφαρμογή <xliff:g id="ID_1">^1</xliff:g> έχει απενεργοποιηθεί. Μπορείτε να την ενεργοποιήσετε ξανά στις Ρυθμίσεις."</string>
</resources>
diff --git a/service/res/values-en-rAU/strings.xml b/service/res/values-en-rAU/strings.xml
index 8542b74..79fee75 100644
--- a/service/res/values-en-rAU/strings.xml
+++ b/service/res/values-en-rAU/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Control car power policy."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"render templates"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Render templates."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"control launching applications"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Control launching applications."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"My Device"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Guest"</string>
<string name="importance_default" msgid="8587741629268312938">"Default importance"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Reset later"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"The infotainment system will reset the next time the car starts."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"The car must be parked to start the reset."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> is affecting your system performance"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Disable app to improve system performance. You can enable the app once again in Settings."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioritise app to keep using app."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Uninstall app to improve system performance."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Disable app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioritise app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Uninstall app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> has been disabled. You can enable it again in Settings."</string>
</resources>
diff --git a/service/res/values-en-rCA/strings.xml b/service/res/values-en-rCA/strings.xml
index 8542b74..79fee75 100644
--- a/service/res/values-en-rCA/strings.xml
+++ b/service/res/values-en-rCA/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Control car power policy."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"render templates"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Render templates."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"control launching applications"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Control launching applications."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"My Device"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Guest"</string>
<string name="importance_default" msgid="8587741629268312938">"Default importance"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Reset later"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"The infotainment system will reset the next time the car starts."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"The car must be parked to start the reset."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> is affecting your system performance"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Disable app to improve system performance. You can enable the app once again in Settings."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioritise app to keep using app."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Uninstall app to improve system performance."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Disable app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioritise app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Uninstall app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> has been disabled. You can enable it again in Settings."</string>
</resources>
diff --git a/service/res/values-en-rGB/strings.xml b/service/res/values-en-rGB/strings.xml
index 8542b74..79fee75 100644
--- a/service/res/values-en-rGB/strings.xml
+++ b/service/res/values-en-rGB/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Control car power policy."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"render templates"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Render templates."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"control launching applications"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Control launching applications."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"My Device"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Guest"</string>
<string name="importance_default" msgid="8587741629268312938">"Default importance"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Reset later"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"The infotainment system will reset the next time the car starts."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"The car must be parked to start the reset."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> is affecting your system performance"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Disable app to improve system performance. You can enable the app once again in Settings."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioritise app to keep using app."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Uninstall app to improve system performance."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Disable app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioritise app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Uninstall app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> has been disabled. You can enable it again in Settings."</string>
</resources>
diff --git a/service/res/values-en-rIN/strings.xml b/service/res/values-en-rIN/strings.xml
index 8542b74..79fee75 100644
--- a/service/res/values-en-rIN/strings.xml
+++ b/service/res/values-en-rIN/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Control car power policy."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"render templates"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Render templates."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"control launching applications"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Control launching applications."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"My Device"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Guest"</string>
<string name="importance_default" msgid="8587741629268312938">"Default importance"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Reset later"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"The infotainment system will reset the next time the car starts."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"The car must be parked to start the reset."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> is affecting your system performance"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Disable app to improve system performance. You can enable the app once again in Settings."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioritise app to keep using app."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Uninstall app to improve system performance."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Disable app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioritise app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Uninstall app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> has been disabled. You can enable it again in Settings."</string>
</resources>
diff --git a/service/res/values-en-rXC/strings.xml b/service/res/values-en-rXC/strings.xml
index b84bee0..3d8443e 100644
--- a/service/res/values-en-rXC/strings.xml
+++ b/service/res/values-en-rXC/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Control car power policy."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"render templates"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Render templates."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"control launching applications"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Control launching applications."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"My Device"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Guest"</string>
<string name="importance_default" msgid="8587741629268312938">"Default importance"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Reset Later"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"The infotainment system will reset the next time the car starts."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"The car must be parked to start the reset."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> is affecting your system performance"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Disable app to improve system performance. You can enable the app once again in Settings."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioritize app to keep using app."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Uninstall app to improve system performance."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Disable app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioritize app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Uninstall app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> has been disabled. You can enable it again in Settings."</string>
</resources>
diff --git a/service/res/values-es-rUS/strings.xml b/service/res/values-es-rUS/strings.xml
index 61192df..75a3f27 100644
--- a/service/res/values-es-rUS/strings.xml
+++ b/service/res/values-es-rUS/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Controla la política de activación de componentes del vehículo."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"renderizar plantillas"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Renderizar plantillas"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"controla el inicio de las aplicaciones"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Controla el inicio de las aplicaciones."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Mi dispositivo"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Invitado"</string>
<string name="importance_default" msgid="8587741629268312938">"Importancia de config. predeterminada"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Restablecer luego"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Se restablecerá el sistema cuando arranques el auto."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Estaciona el vehículo para el restablecimiento."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> está afectando el rendimiento del sistema"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Inhabilita la app para mejorar el rendimiento del sistema. Puedes volver a habilitarla en Configuración."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioriza la app para seguir usándola."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Desinstala la app para mejorar el rendimiento del sistema."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Inhabilitar app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Priorizar app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Desinstalar app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Se inhabilitó <xliff:g id="ID_1">^1</xliff:g>. Puedes volver a habilitarla en Configuración."</string>
</resources>
diff --git a/service/res/values-es/strings.xml b/service/res/values-es/strings.xml
index 12ee40f..43db6e3 100644
--- a/service/res/values-es/strings.xml
+++ b/service/res/values-es/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Controlar política de energía del coche."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"renderizar plantillas"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Renderizar plantillas."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"controla las aplicaciones abiertas"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Controla las aplicaciones abiertas."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Mi dispositivo"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Invitado"</string>
<string name="importance_default" msgid="8587741629268312938">"Importancia predeterminada"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Restablecer más tarde"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"El sistema se restablecerá cuando arranques el coche."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"El coche debe estar aparcado para restablecerlo."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> está afectando al rendimiento de tu sistema"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Inhabilita la aplicación para mejorar el rendimiento del sistema. Puedes habilitarla de nuevo en Ajustes."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioriza la aplicación para seguir usándola."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Desinstala la aplicación para mejorar el rendimiento del sistema."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Inhabilitar aplicación"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Priorizar aplicación"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Desinstalar aplicación"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> se ha inhabilitado. Puedes habilitarla de nuevo en Ajustes."</string>
</resources>
diff --git a/service/res/values-et/strings.xml b/service/res/values-et/strings.xml
index 328f8ad..d8b4461 100644
--- a/service/res/values-et/strings.xml
+++ b/service/res/values-et/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Auto toitereeglite haldamine."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"mallide renderdamine"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Mallide renderdamine."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"rakenduste käivitamise juhtimine"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Rakenduste käivitamise juhtimine."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Minu seade"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Külaline"</string>
<string name="importance_default" msgid="8587741629268312938">"Vaiketähtsus"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Lähtesta hiljem"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Teabe ja meelelahutuse süsteem lähtestatakse järgmisel korral, kui auto käivitatakse."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Lähtestamise alustamiseks peab auto olema pargitud."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> mõjutab teie süsteemi toimivust."</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Keelake rakendus, et täiustada süsteemi toimivust. Seadetes saate rakenduse uuesti lubada."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioriseerige rakendus, et selle kasutamist jätkata."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Desinstallige rakendus, et süsteemi toimivust parandada."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Keela rakendus"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioriseeri rakendus"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Desinstalli rakendus"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> on keelatud. Seadetes saate selle uuesti lubada."</string>
</resources>
diff --git a/service/res/values-eu/strings.xml b/service/res/values-eu/strings.xml
index 8a7737d..59319f6 100644
--- a/service/res/values-eu/strings.xml
+++ b/service/res/values-eu/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Kontrolatu autoaren osagaiak aktibatzeko gidalerroak."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"errendatu txantiloiak"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Errendatu txantiloiak."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"kontrolatu abiarazteko aplikazioak"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Kontrolatu abiarazteko aplikazioak."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Nire gailua"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gonbidatua"</string>
<string name="importance_default" msgid="8587741629268312938">"Garrantzi lehenetsia"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Berrezarri geroago"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Informazio- eta aisia-sistema autoa abiarazten den hurrengo aldian berrezarriko da."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Autoak aparkatuta egon behar du berrezartzea hasteko."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"Sistemaren errendimendua oztopatzen ari da <xliff:g id="ID_1">^1</xliff:g>"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Desgaitu aplikazioa sistemaren errendimendua hobetzeko. Berriro gaitu nahi izanez gero, joan ezarpenetara."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Eman lehentasuna aplikazioari hura erabiltzen jarraitzeko"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Desinstalatu aplikazioa sistemaren errendimendua hobetzeko."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Desgaitu aplikazioa"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Eman lehentasuna aplikazioari"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Desinstalatu aplikazioa"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> desgaitu egin da. Hura berriro gaitzeko, joan ezarpenetara."</string>
</resources>
diff --git a/service/res/values-fa/strings.xml b/service/res/values-fa/strings.xml
index b457177..7554bf8 100644
--- a/service/res/values-fa/strings.xml
+++ b/service/res/values-fa/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"کنترل خطمشی روشن/خاموش شدن مؤلفههای خودرو."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"پرداز زدن الگوها"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"پرداز زدن الگوها."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"کنترل راهاندازی برنامهها"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"کنترل راهاندازی برنامهها."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"دستگاه من"</string>
<string name="default_guest_name" msgid="2912812799433131476">"مهمان"</string>
<string name="importance_default" msgid="8587741629268312938">"اهمیت پیشفرض"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"بعداً بازنشانی شود"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"دفعه بعدی که خودرو راهاندازی شد، سیستم اطلاعات-سرگرمی بازنشانی میشود."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"برای شروع بازنشانی، خودرو باید پارک شده باشد."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> عملکرد سیستم را تحتتأثیر قرار داده است"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"برای بهبود عملکرد سیستم، برنامه را غیرفعال کنید. در «تنظیمات» میتوانید برنامه را دوباره فعال کنید."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"برای ادامه استفاده از برنامه، برنامه را اولویتبندی کنید."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"برای بهبود عملکرد سیستم، برنامه را حذف نصب کنید."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"غیرفعال کردن برنامه"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"الویتبندی برنامه"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"حذف نصب برنامه"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> غیرفعال شد. در «تنظیمات» میتوانید آن را دوباره فعال کنید."</string>
</resources>
diff --git a/service/res/values-fi/strings.xml b/service/res/values-fi/strings.xml
index 48cb46a..2721fd5 100644
--- a/service/res/values-fi/strings.xml
+++ b/service/res/values-fi/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Ohjaa auton virtakäytäntöä"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"renderöidä malleja"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Renderöi mallit."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"hallita sovellusten käynnistymistä"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"hallita sovellusten käynnistymistä."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Oma laite"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Vieras"</string>
<string name="importance_default" msgid="8587741629268312938">"Oletustärkeys"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Palauta myöhemmin"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Järjestelmä palautetaan, kun auto taas käynnistyy."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Auto on pysäköitävä palautusta varten."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> vaikuttaa järjestelmän toimintaan"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Poista sovellus käytöstä parantaaksesi järjestelmän toimintaa. Voit ottaa sovelluksen uudelleen käyttöön asetuksista."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Priorisoi sovellusta, jos haluat jatkaa sen käyttöä."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Poista sovellus parantaaksesi järjestelmän toimintaa."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Poista sovellus käytöstä"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Priorisoi sovellusta"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Poista sovellus"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> on poistettu käytöstä. Voit ottaa sen uudelleen käyttöön asetuksista."</string>
</resources>
diff --git a/service/res/values-fr-rCA/strings.xml b/service/res/values-fr-rCA/strings.xml
index 41cc1ba..a7f7ef2 100644
--- a/service/res/values-fr-rCA/strings.xml
+++ b/service/res/values-fr-rCA/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Politique sur la gestion de l\'alimentation dans la voiture"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"render effectuer un rendu des modèles"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Effectuer un rendu des modèles."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"contrôler le lancement d\'applications"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Contrôler le lancement d\'applications."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Mon appareil"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Invité"</string>
<string name="importance_default" msgid="8587741629268312938">"Importance par défaut"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Réinit. plus tard"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Le système d\'infodivertissement se réinitialisera au prochain démarrage du véhicule."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Garez-vous pour lancer la réinitialisation."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> perturbe les performances de votre système"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Désactivez l\'application pour améliorer les performances du système. Vous pouvez réactiver l\'application dans les paramètres."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Priorisez l\'application pour continuer à l\'utiliser."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Désinstallez l\'application pour améliorer les performances du système."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Désactiver l\'application"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioriser l\'application"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Désinstaller l\'application"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> a été désactivé. Vous pouvez le réactiver dans les paramètres."</string>
</resources>
diff --git a/service/res/values-fr/strings.xml b/service/res/values-fr/strings.xml
index 2e876bf..db33d50 100644
--- a/service/res/values-fr/strings.xml
+++ b/service/res/values-fr/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Contrôlez la règle d\'activation pour la voiture."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"afficher les modèles"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Afficher les modèles."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"contrôler le lancement d\'applications."</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Contrôler le lancement d\'applications."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Mon appareil"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Invité"</string>
<string name="importance_default" msgid="8587741629268312938">"Importance par défaut"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Réinitialiser plus tard"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Le système d\'infoloisirs se réinitialisera au prochain démarrage de la voiture."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Garez-vous pour lancer la réinitialisation."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> affecte les performances du système"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Désactivez l\'appli pour améliorer les performances du système. Vous pourrez la réactiver dans les paramètres."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Donnez priorité à l\'appli pour continuer à l\'utiliser."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Désinstallez l\'appli pour améliorer les performances du système."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Désactiver l\'appli"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Donner priorité à l\'appli"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Désinstaller l\'appli"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> a été désactivée. Vous pouvez la réactiver dans les paramètres."</string>
</resources>
diff --git a/service/res/values-gl/strings.xml b/service/res/values-gl/strings.xml
index fec88b2..89fdb60 100644
--- a/service/res/values-gl/strings.xml
+++ b/service/res/values-gl/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Controlar a política de enerxía do coche."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"xerar modelos"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Xerar modelos."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"controlar o inicio de aplicacións"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Controlar o inicio de aplicacións."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Dispositivo"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Convidado"</string>
<string name="importance_default" msgid="8587741629268312938">"Importancia predeterminada"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Restablecer despois"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"O sistema restablecerase cando se volva arrincar o coche."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"O coche debe estar aparcado para restablecelo."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> está prexudicando o rendemento do sistema"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Desactiva a aplicación para mellorar o rendemento do sistema. Podes activala de novo en Configuración."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioriza a aplicación para seguir usándoa."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Desinstala a aplicación para mellorar o rendemento do sistema."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Desactivar aplicación"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Priorizar aplicación"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Desinstalar aplicación"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Desactivouse a aplicación <xliff:g id="ID_1">^1</xliff:g>. Podes activala de novo en Configuración."</string>
</resources>
diff --git a/service/res/values-gu/strings.xml b/service/res/values-gu/strings.xml
index 8146cc2..1542a42 100644
--- a/service/res/values-gu/strings.xml
+++ b/service/res/values-gu/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"કારની સુવિધાઓ ચાલુ/બંધ રહેવા વિશેની નીતિને નિયંત્રિત કરો."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"નમૂના જનરેટ કરો"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"નમૂના જનરેટ કરો."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"લૉન્ચિંગ ઍપ્લિકેશનો નિયંત્રિત કરો"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"લૉન્ચિંગ ઍપ્લિકેશનો નિયંત્રિત કરો."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"મારું ડિવાઇસ"</string>
<string name="default_guest_name" msgid="2912812799433131476">"અતિથિ"</string>
<string name="importance_default" msgid="8587741629268312938">"ડિફૉલ્ટ મહત્ત્વ"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"પછીથી રીસેટ કરો"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"આગલી વખતે જ્યારે કારને ચાલુ કરવામાં આવે ત્યારે ઇન્ફોટેનમેન્ટ સિસ્ટમ રીસેટ થશે."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"રીસેટ શરૂ કરવા માટે કાર પાર્ક કરેલી હોવી જરૂરી છે."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> તમારી સિસ્ટમની કાર્યક્ષમતાને અસર કરી રહી છે"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"સિસ્ટમની કાર્યક્ષમતા બહેતર બનાવવા માટે, ઍપ બંધ કરો. તમે સેટિંગમાંથી ઍપ ફરીથી ચાલુ કરી શકશો."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ઍપનો ઉપયોગ કરવાનું ચાલુ રાખવા માટે, ઍપને પ્રાધાન્યતા આપો."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"સિસ્ટમની કાર્યક્ષમતા બહેતર બનાવવા માટે, ઍપ અનઇન્સ્ટૉલ કરો."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"ઍપ બંધ કરો"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ઍપને પ્રાધાન્યતા આપો"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"ઍપ અનઇન્સ્ટૉલ કરો"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> બંધ કરવામાં આવી છે. તમે સેટિંગમાંથી તેને ફરીથી ચાલુ કરી શકો છો."</string>
</resources>
diff --git a/service/res/values-hi/strings.xml b/service/res/values-hi/strings.xml
index fe135a0..28d6dcd 100644
--- a/service/res/values-hi/strings.xml
+++ b/service/res/values-hi/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"कार पावर नीति कंट्रोल करें."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"टेंप्लेट बनाएं"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"टेंप्लेट बनाएं."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"लॉन्च किए जाने वाले ऐप्लिकेशन कंट्रोल करता है"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"लॉन्च किए जाने वाले ऐप्लिकेशन कंट्रोल करता है."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"मेरा डिवाइस"</string>
<string name="default_guest_name" msgid="2912812799433131476">"मेहमान"</string>
<string name="importance_default" msgid="8587741629268312938">"डिफ़ॉल्ट अहमियत"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"बाद में रीसेट करें"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"अगली बार कार के स्टार्ट होने पर, सूचना और मनोरंजन की सुविधा देने वाला डिवाइस रीसेट होगा."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"रीसेट शुरू करने के लिए ज़रूरी है कि कार पार्क हो."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g>, आपके सिस्टम की परफ़ॉर्मेंस पर असर डालता है"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"सिस्टम की परफ़ॉर्मेंस को बेहतर बनाने के लिए, ऐप्लिकेशन बंद करें. आप सेटिंग में जाकर, ऐप्लिकेशन को दोबारा चालू कर सकते हैं."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ऐप्लिकेशन का इस्तेमाल करते रहने के लिए, उसे प्राथमिकता दें."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"सिस्टम की परफ़ॉर्मेंस को बेहतर बनाने के लिए, ऐप्लिकेशन को अनइंस्टॉल करें."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"ऐप्लिकेशन बंद करें"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ऐप्लिकेशन को प्राथमिकता दें"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"ऐप्लिकेशन अनइंस्टॉल करें"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> को बंद कर दिया गया है. आप सेटिंग में जाकर, इसे दोबारा चालू कर सकते हैं."</string>
</resources>
diff --git a/service/res/values-hr/strings.xml b/service/res/values-hr/strings.xml
index b2d20a9..a640db0 100644
--- a/service/res/values-hr/strings.xml
+++ b/service/res/values-hr/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Upravljanje pravilom napajanja automobila."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"generirati predloške"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Generirati predloške."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"upravljajte pokretanjem aplikacija"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Upravljajte pokretanjem aplikacija."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Moj uređaj"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gost"</string>
<string name="importance_default" msgid="8587741629268312938">"Zadana važnost"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Resetiraj kasnije"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Sustav za informiranje i zabavu resetirat će se kad pokrenete auto."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Da biste resetirali, automobil mora biti parkiran."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"Aplikacija <xliff:g id="ID_1">^1</xliff:g> utječe na izvedbu sustava"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Onemogućite aplikaciju radi poboljšanja izvedbe sustava. Aplikaciju možete ponovo omogućiti u postavkama."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Da biste nastavili upotrebljavati aplikaciju, postavite je kao prioritet."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Deinstalirajte aplikaciju radi poboljšanja izvedbe sustava."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Onemogući aplikaciju"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioritet: aplikacija"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Deinstaliraj aplikaciju"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Aplikacija <xliff:g id="ID_1">^1</xliff:g> je onemogućena. Možete je ponovo omogućiti u postavkama."</string>
</resources>
diff --git a/service/res/values-hu/strings.xml b/service/res/values-hu/strings.xml
index e64c727..8f06ed1 100644
--- a/service/res/values-hu/strings.xml
+++ b/service/res/values-hu/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Vezérelheti az autó energiaellátási házirendjét."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"sablonok renderelése"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Sablonok renderelése."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"alkalmazásindítás vezérlése"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Alkalmazásindítás vezérlése."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Saját eszköz"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Vendég"</string>
<string name="importance_default" msgid="8587741629268312938">"Alapértelmezett fontosságú"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Visszaállítás később"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"A rendszer a következő indításkor áll vissza."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"A visszaállításhoz le kell parkolni az autót."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"A(z) <xliff:g id="ID_1">^1</xliff:g> befolyásolja a rendszer teljesítményét"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Az alkalmazás letiltásával javíthatja a rendszer teljesítményét. Az alkalmazást a Beállítások között engedélyezheti újra."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Adjon prioritást az alkalmazásnak, hogy továbbra is használhassa."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Az alkalmazás eltávolításával javíthatja a rendszer teljesítményét."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Alkalmazás letiltása"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Alkalmazás rangsorolása"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Alkalmazás eltávolítása"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"A(z) <xliff:g id="ID_1">^1</xliff:g> le lett tiltva. A Beállítások között ismét bekapcsolhatja."</string>
</resources>
diff --git a/service/res/values-hy/strings.xml b/service/res/values-hy/strings.xml
index fdacf52..cf0c83f 100644
--- a/service/res/values-hy/strings.xml
+++ b/service/res/values-hy/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Կառավարել ավտոմեքենայի հզորության քաղաքականությունը։"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"արտապատկերել ձևանմուշները"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Ձևանմուշների արտապատկերում։"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"վերահսկել հավելվածների գործարկումը"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Վերահսկել հավելվածների գործարկումը"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Իմ սարքը"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Հյուր"</string>
<string name="importance_default" msgid="8587741629268312938">"Կարևորություն (կանխադրված)"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Վերակայել ավելի ուշ"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Համակարգը կվերակայվի, երբ մեքենան հաջորդ անգամ միացնեք։"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Վերակայումը սկսելու համար մեքենան կայանեք։"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"«<xliff:g id="ID_1">^1</xliff:g>» հավելվածն ազդում է ձեր համակարգի աշխատանքի արդյունավետության վրա"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Անջատեք հավելվածը՝ համակարգի աշխատանքի արդյունավետությունը բարելավելու համար։ Հավելվածը նորից կարող եք միացնել Կարգավորումներում։"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Առաջնահերթ դարձրեք հավելվածը, որպեսզի կարողանաք օգտագործել այն։"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Ապատեղադրեք հավելվածը՝ համակարգի աշխատանքի արդյունավետությունը բարելավելու համար։"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Անջատել հավելվածը"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Առաջնահերթ դարձնել հավելվածը"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Ապատեղադրել հավելվածը"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"«<xliff:g id="ID_1">^1</xliff:g>» հավելվածն անջատվել է։ Այն նորից կարող եք միացնել Կարգավորումներում։"</string>
</resources>
diff --git a/service/res/values-in/strings.xml b/service/res/values-in/strings.xml
index befdb9f..90679b6 100644
--- a/service/res/values-in/strings.xml
+++ b/service/res/values-in/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Kontrol kebijakan daya mobil."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"merender template"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Merender template."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"mengontrol aplikasi yang diluncurkan"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Kontrol aplikasi yang diluncurkan."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Perangkat Saya"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Tamu"</string>
<string name="importance_default" msgid="8587741629268312938">"Tingkat kepentingan: default"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Reset Nanti"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Sistem infotainmen akan direset nanti."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Mobil harus diparkir untuk memulai reset."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> memengaruhi performa sistem Anda"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Nonaktifkan aplikasi untuk meningkatkan performa sistem. Anda dapat mengaktifkan aplikasi tersebut sekali lagi di Setelan."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioritaskan aplikasi untuk terus menggunakannya."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Uninstal aplikasi untuk meningkatkan performa sistem."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Nonaktifkan aplikasi"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioritaskan aplikasi"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Uninstal aplikasi"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> telah dinonaktifkan. Anda dapat mengaktifkannya lagi di Setelan."</string>
</resources>
diff --git a/service/res/values-is/strings.xml b/service/res/values-is/strings.xml
index 6c6cb91..aa3a48c 100644
--- a/service/res/values-is/strings.xml
+++ b/service/res/values-is/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Stjórna ræsistýringum bíls."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"teikna sniðmát"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Teikna sniðmát."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"stjórna ræsingu forrita"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Stjórna ræsingu forrita."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Tækið mitt"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gestur"</string>
<string name="importance_default" msgid="8587741629268312938">"Sjálfgefið mikilvægi"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Endurstilla síðar"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Upplýsinga- og afþreyingarkerfið endurstillist næst þegar bíllinn er ræstur."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Bíllinn verður að vera kyrr til að endurstilla."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> hefur áhrif á afköst kerfisins"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Slökktu á forriti til að bæta afköst kerfisins. Þú getur kveikt aftur á forritinu í stillingunum."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Forgangsraðaðu forriti til að nota forrit áfram."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Fjarlægðu forrit til að bæta afköst kerfisins."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Slökkva á forriti"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Forgangsraða forriti"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Fjarlægja forrit"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Slökkt var á <xliff:g id="ID_1">^1</xliff:g>. Þú getur kveikt á því aftur í stillingunum."</string>
</resources>
diff --git a/service/res/values-it/strings.xml b/service/res/values-it/strings.xml
index 566a98a..765fcfe 100644
--- a/service/res/values-it/strings.xml
+++ b/service/res/values-it/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Controlla i criteri di alimentazione dell\'auto."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"Visualizzazione di modelli"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Consente di visualizzare i modelli."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"controllo avvio delle applicazioni"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Controllo avvio delle applicazioni."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Mio dispositivo"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Ospite"</string>
<string name="importance_default" msgid="8587741629268312938">"Importanza predefinita"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Ripristina più tardi"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Il sistema di infotainment verrà ripristinato alla successiva accensione del motore."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Parcheggia per avviare il ripristino."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> influenza le prestazioni del tuo sistema operativo"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Disattiva l\'app per migliorare le prestazioni del sistema. Puoi riattivarla in Impostazioni."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Assegna la priorità all\'app per continuare a utilizzarla."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Disinstalla l\'app per migliorare le prestazioni del sistema."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Disattiva app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Assegna priorità all\'app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Disinstalla app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"L\'app <xliff:g id="ID_1">^1</xliff:g> è stata disattivata. Puoi riattivarla in Impostazioni."</string>
</resources>
diff --git a/service/res/values-iw/strings.xml b/service/res/values-iw/strings.xml
index c591716..47b51e3 100644
--- a/service/res/values-iw/strings.xml
+++ b/service/res/values-iw/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"שליטה במדיניות הזנת המתח של הרכב."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"עיבוד תבניות"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"עיבוד תבניות."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"שליטה בהפעלת אפליקציות"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"יש שליטה בהפעלת אפליקציות."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"המכשיר שלי"</string>
<string name="default_guest_name" msgid="2912812799433131476">"אורח"</string>
<string name="importance_default" msgid="8587741629268312938">"חשיבות ברירת מחדל"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"לאיפוס מאוחר יותר"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"מערכת המידע והבידור תתאפס בהתנעה הבאה של המכונית."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"אפשר להתחיל באיפוס רק כשהמכונית חונה."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"האפליקציה <xliff:g id="ID_1">^1</xliff:g> משפיעה על ביצועי המערכת"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"כדי לשפר את ביצועי המערכת, כדאי להשבית את האפליקציה. יש לך אפשרות להפעיל שוב את האפליקציה ב\'הגדרות\'."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"כדי להמשיך להשתמש באפליקציה, צריך לקבוע לה עדיפות גבוהה."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"כדי לשפר את ביצועי המערכת, צריך להסיר את האפליקציה."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"השבתת האפליקציה"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"קביעת עדיפות לאפליקציה"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"הסרת האפליקציה"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> הושבתה. אפשר להפעיל אותה שוב ב\'הגדרות\'."</string>
</resources>
diff --git a/service/res/values-ja/strings.xml b/service/res/values-ja/strings.xml
index 1e8d7df..63a86b7 100644
--- a/service/res/values-ja/strings.xml
+++ b/service/res/values-ja/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"車の電源ポリシーの制御。"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"テンプレートの表示"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"テンプレートの表示。"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"起動アプリの管理"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"起動アプリの管理。"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"自分のデバイス"</string>
<string name="default_guest_name" msgid="2912812799433131476">"ゲスト"</string>
<string name="importance_default" msgid="8587741629268312938">"重要度: デフォルト"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"後でリセット"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"インフォテインメント システムは、車の次回始動時にリセットされます。"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"リセットを開始するには、車を駐車してください。"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> がシステムのパフォーマンスに影響しています"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"システムのパフォーマンスを改善するには、アプリを無効にしてください。[設定] でアプリを有効に戻すことができます。"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"アプリを引き続き使用するには、優先アプリとして指定してください。"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"システムのパフォーマンスを改善するには、アプリをアンインストールしてください。"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"アプリを無効にする"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"優先アプリとして指定する"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"アプリをアンインストールする"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> を無効にしました。[設定] で有効に戻すことができます。"</string>
</resources>
diff --git a/service/res/values-ka/strings.xml b/service/res/values-ka/strings.xml
index 38f615d..e1b0832 100644
--- a/service/res/values-ka/strings.xml
+++ b/service/res/values-ka/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"აკონტროლებს მანქანაში კომპონენტების ჩართვის წესებს."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"შაბლონების ჩვენება"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"შაბლონების ჩვენება."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"აპლიკაციების გაშვების კონტროლი"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"აპლიკაციების გაშვების კონტროლი."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"ჩემი მოწყობილობა"</string>
<string name="default_guest_name" msgid="2912812799433131476">"სტუმარი"</string>
<string name="importance_default" msgid="8587741629268312938">"ნაგულისხმევი მნიშვნელოვნება"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"მოგვიან. გადაყენება"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"გართობის/საინფორმაციო სისტემა გადაყენდება მანქანის შემდეგ დაქოქვაზე."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"გადაყენებისთვის მანქანა პარკირებული უნდა იყოს."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> აფერხებს თქვენი სისტემის ეფექტურობას"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"სისტემის ეფექტურობის გასაუმჯობესებლად გათიშეთ აპი. აპის ხელახლა ჩართვა პარამეტრებიდან შეგიძლიათ."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"აპის გამოყენების გასაგრძელებლად საჭიროა მისი პრიორიტეტიზაცია."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"სისტემის ეფექტურობის გასაუმჯობესებლად საჭიროა აპის დეინსტალაცია."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"აპის გათიშვა"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"აპის პრიორიტეტიზაცია"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"აპის დეინსტალაცია"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> გაითიშა. შეგიძლიათ, ხელახლა ჩართოთ პარამეტრებში."</string>
</resources>
diff --git a/service/res/values-kk/strings.xml b/service/res/values-kk/strings.xml
index b7c0075..b80ca10 100644
--- a/service/res/values-kk/strings.xml
+++ b/service/res/values-kk/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Көлік қуаты туралы саясатты басқару."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"рендеринг үлгілері"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Рендеринг үлгілері"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"қолданбалардың іске қосылуын басқару"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Қолданбалардың іске қосылуын басқара аласыз."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Құрылғым"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Қонақ"</string>
<string name="importance_default" msgid="8587741629268312938">"Әдепкі маңыздылық"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Кейінірек қайтару"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Ақпараттық ойын-сауық жүйесі келесіде көлік оталған кезде бастапқы күйге қайтарылады."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Бастапқы күйге қайтару үшін көлікті тұраққа қою керек."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> қолданбасы жүйе өнімділігіне әсер етуде"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Жүйе өнімділігін жақсарту үшін қолданбаны өшіріңіз. Оны параметрлерден қайта қосуыңызға болады."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Қолданбаны әрі қарай пайдалану үшін оған басымдық беріңіз."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Жүйе өнімділігін жақсарту үшін қолданбаны жойыңыз."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Қолданбаны өшіру"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Қолданбаға басымдық беру"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Қолданбаны жою"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> қолданбасы өшірілді. Оны параметрлерден қайта қосуыңызға болады."</string>
</resources>
diff --git a/service/res/values-km/strings.xml b/service/res/values-km/strings.xml
index 17187af..fef239b 100644
--- a/service/res/values-km/strings.xml
+++ b/service/res/values-km/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"គ្រប់គ្រងគោលការណ៍ថាមពលរថយន្ត។"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"បំប្លែងទម្រង់គំរូ"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"បំប្លែងទម្រង់គំរូ។"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"គ្រប់គ្រងការចាប់ផ្ដើមកម្មវិធី"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"គ្រប់គ្រងការចាប់ផ្ដើមកម្មវិធី។"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"ឧបករណ៍របស់ខ្ញុំ"</string>
<string name="default_guest_name" msgid="2912812799433131476">"ភ្ញៀវ"</string>
<string name="importance_default" msgid="8587741629268312938">"កម្រិតសំខាន់តាមលំនាំដើម"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"ចាប់ផ្ដើមឡើងវិញពេលក្រោយ"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"ប្រព័ន្ធព័ត៌មាន និងកម្សាន្តនឹងកំណត់ឡើងវិញ នៅពេលរថយន្តចាប់ផ្ដើមលើកក្រោយ។"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"ត្រូវតែចតរថយន្ត ដើម្បីចាប់ផ្ដើមការកំណត់ឡើងវិញ។"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> កំពុងធ្វើឱ្យប៉ះពាល់ដល់ប្រតិបត្តិការប្រព័ន្ធរបស់អ្នក"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"បិទកម្មវិធី ដើម្បីកែលម្អប្រតិបត្តិការប្រព័ន្ធ។ អ្នកអាចបើកកម្មវិធីម្ដងទៀតនៅក្នុងការកំណត់។"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"កំណត់អាទិភាពកម្មវិធី ដើម្បីបន្តប្រើកម្មវិធី។"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"លុបកម្មវិធី ដើម្បីកែលម្អប្រតិបត្តិការប្រព័ន្ធ។"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"បិទកម្មវិធី"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"កំណត់អាទិភាពកម្មវិធី"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"លុបកម្មវិធី"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> ត្រូវបានបិទ។ អ្នកអាចបើកវាម្ដងទៀតនៅក្នុងការកំណត់។"</string>
</resources>
diff --git a/service/res/values-kn/strings.xml b/service/res/values-kn/strings.xml
index 2feb952..7f854b4 100644
--- a/service/res/values-kn/strings.xml
+++ b/service/res/values-kn/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"ಕಾರಿನ ಪವರ್ ನೀತಿಯನ್ನು ನಿಯಂತ್ರಿಸಿ."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"ಟೆಂಪ್ಲೇಟ್ಗಳನ್ನು ರೆಂಡರ್ ಮಾಡಿ"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"ಟೆಂಪ್ಲೇಟ್ಗಳನ್ನು ರೆಂಡರ್ ಮಾಡಿ."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"ಪ್ರಾರಂಭಿಸುವ ಅಪ್ಲಿಕೇಶನ್ಗಳನ್ನು ನಿಯಂತ್ರಿಸಿ"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"ಪ್ರಾರಂಭಿಸುವ ಅಪ್ಲಿಕೇಶನ್ಗಳನ್ನು ನಿಯಂತ್ರಿಸಿ."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"ನನ್ನ ಸಾಧನ"</string>
<string name="default_guest_name" msgid="2912812799433131476">"ಅತಿಥಿ"</string>
<string name="importance_default" msgid="8587741629268312938">"ಮಹತ್ವವಾದುದು: ಡೀಫಾಲ್ಟ್"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"ನಂತರ ರೀಸೆಟ್ ಮಾಡಿ"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"ಮುಂದಿನ ಬಾರಿ ಕಾರ್ ಆರಂಭವಾದಾಗ ಇನ್ಫೋಟೈನ್ಮೆಂಟ್ ಸಿಸ್ಟಂ ಅನ್ನು ರೀಸೆಟ್ ಮಾಡಲಾಗುತ್ತದೆ."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"ರೀಸೆಟ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಲು ಕಾರ್ ಅನ್ನು ನಿಲ್ಲಿಸಿರಬೇಕು."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> ನಿಮ್ಮ ಸಿಸ್ಟಂ ಕಾರ್ಯಕ್ಷಮತೆಯ ಮೇಲೆ ಪರಿಣಾಮ ಬೀರುತ್ತಿದೆ"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"ಸಿಸ್ಟಂ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಲು ಆ್ಯಪ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ. ಸೆಟ್ಟಿಂಗ್ಗಳಲ್ಲಿ ನೀವು ಆ್ಯಪ್ ಅನ್ನು ಪುನಃ ಸಕ್ರಿಯಗೊಳಿಸಬಹುದು."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ಆ್ಯಪ್ ಬಳಸುವುದನ್ನು ಮುಂದುವರಿಸಲು ಆ್ಯಪ್ ಅನ್ನು ಆದ್ಯತೆಗೊಳಿಸಿ."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"ಸಿಸ್ಟಂ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಲು ಆ್ಯಪ್ ಅನ್ನು ಅನ್ಇನ್ಸ್ಟಾಲ್ ಮಾಡಿ."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"ಆ್ಯಪ್ ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಿ"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ಆ್ಯಪ್ ಅನ್ನು ಆದ್ಯತೆಗೊಳಿಸಿ"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"ಆ್ಯಪ್ ಅನ್ಇನ್ಸ್ಟಾಲ್ ಮಾಡಿ"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ. ಸೆಟ್ಟಿಂಗ್ಗಳಲ್ಲಿ ನೀವು ಇದನ್ನು ಪುನಃ ಸಕ್ರಿಯಗೊಳಿಸಬಹುದು."</string>
</resources>
diff --git a/service/res/values-ko/strings.xml b/service/res/values-ko/strings.xml
index 2abaa2c..f0c546e 100644
--- a/service/res/values-ko/strings.xml
+++ b/service/res/values-ko/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"차량 전원 정책을 관리합니다."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"템플릿 렌더링"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"템플릿 렌더링"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"애플리케이션 실행을 제어합니다."</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"애플리케이션 실행을 제어합니다."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"내 기기"</string>
<string name="default_guest_name" msgid="2912812799433131476">"게스트"</string>
<string name="importance_default" msgid="8587741629268312938">"중요도가 기본인 알림 채널"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"나중에 초기화"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"다음에 차 시동이 걸리면 인포테인먼트 시스템이 초기화됩니다."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"초기화하려면 차량이 주차된 상태여야 합니다."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> 앱이 시스템 성능에 영향을 미침"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"앱을 사용 중지하여 시스템 성능을 개선하세요. 설정에서 앱을 다시 한번 사용 설정할 수 있습니다."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"앱을 계속 사용하려면 앱을 우선순위로 지정하세요."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"앱을 제거하여 시스템 성능을 개선하세요."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"앱 사용 중지"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"앱 우선순위 지정"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"앱 제거"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> 앱이 사용 중지되었습니다. 설정에서 다시 사용 설정할 수 있습니다."</string>
</resources>
diff --git a/service/res/values-ky/strings.xml b/service/res/values-ky/strings.xml
index 04afe22..eb35ce1 100644
--- a/service/res/values-ky/strings.xml
+++ b/service/res/values-ky/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Унаанын компоненттерин өчүрүү/күйгүзүү саясатын башкаруу."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"үлгүлөрдү түзүү"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Үлгүлөрдү түзүү."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"иштетилген колдонмолорду көзөмөлдөө"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Иштетилген колдонмолорду көзөмөлдөңүз."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Түзмөгүм"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Конок"</string>
<string name="importance_default" msgid="8587741629268312938">"Демейки маанилүүлүк"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Кийинчерээк баштапкы абалга келтирүү"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Инфозоок тутуму унаа кайра от алганда баштапкы абалга келтирилет."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Баштапкы абалга келтирүү үчүн унааны токтотуңуз."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> тутумдун ишине кедергисин тийгизип жатат"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Тутумдун ишин жакшыртуу үчүн колдонмону өчүрүп коюңуз. Аны кайра жөндөөлөрдөн иштетип койсоңуз болот."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Колдонмону пайдалана берүү үчүн ага артыкчылык бериңиз."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Тутумдун ишин жакшыртуу үчүн колдонмону чыгарып салыңыз."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Колдонмону өчүрүп коюу"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Колдонмого артыкчылык берүү"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Колдонмону чыгарып салуу"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> өчүрүлдү. Аны кайра жөндөөлөрдөн иштетип койсоңуз болот."</string>
</resources>
diff --git a/service/res/values-lo/strings.xml b/service/res/values-lo/strings.xml
index 1b60468..5bdf57c 100644
--- a/service/res/values-lo/strings.xml
+++ b/service/res/values-lo/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"ຄວບຄຸມນະໂຍບາຍພະລັງງານສຳລັບລົດຍົນ."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"ສະແດງແມ່ແບບ"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"ສະແດງແມ່ແບບ."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"ຄວບຄຸມການເປີດໃຊ້ແອັບພລິເຄຊັນ"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"ຄວບຄຸມການເປີດໃຊ້ແອັບພລິເຄຊັນ."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"ອຸປະກອນຂອງຂ້ອຍ"</string>
<string name="default_guest_name" msgid="2912812799433131476">"ແຂກ"</string>
<string name="importance_default" msgid="8587741629268312938">"ຄວາມສຳຄັນເລີ່ມຕົ້ນ"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"ຣີເຊັດໃນພາຍຫຼັງ"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"ລະບົບສາລະບັນເທີງຈະຣີເຊັດໃນຄັ້ງຕໍ່ໄປທີ່ລົດຕິດຈັກ."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"ຕ້ອງຈອດລົດເພື່ອເລີ່ມຕົ້ນການຣີເຊັດ."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> ກຳລັງກະທົບກັບປະສິດທິພາບລະບົບຂອງທ່ານ"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"ປິດການນຳໃຊ້ແອັບເພື່ອປັບປຸງປະສິດທິພາບລະບົບ. ທ່ານສາມາດເປີດການນຳໃຊ້ແອັບອີກຄັ້ງໄດ້ໃນການຕັ້ງຄ່າ."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ຈັດລຳດັບຄວາມສຳຄັນແອັບເພື່ອສືບຕໍ່ນຳໃຊ້ແອັບ."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"ຖອນການຕິດຕັ້ງແອັບເພື່ອປັບປຸງປະສິດທິພາບລະບົບ."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"ປິດການນຳໃຊ້ແອັບ"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ຈັດລຳດັບຄວາມສຳຄັນແອັບ"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"ຖອນການຕິດຕັ້ງແອັບ"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> ໄດ້ຖືກປິດການນຳໃຊ້ແລ້ວ. ທ່ານສາມາດເປີດການນຳໃຊ້ໃໝ່ໄດ້ໃນການຕັ້ງຄ່າ."</string>
</resources>
diff --git a/service/res/values-lt/strings.xml b/service/res/values-lt/strings.xml
index 51ed42c..3e83e46 100644
--- a/service/res/values-lt/strings.xml
+++ b/service/res/values-lt/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Valdyti automobilio komponentų suaktyvinimo politiką."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"Pateikti šablonus"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Pateikti šablonus."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"Valdyti paleidžiamas programas"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Valdyti paleidžiamas programas."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Mano įrenginys"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Svečias"</string>
<string name="importance_default" msgid="8587741629268312938">"Numatytoji svarba"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Vėliau nustatyti iš naujo"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Informacinė pramoginė sistema iš naujo bus nustatyta kitą kartą užvedus automobilį."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Norint nustatyti iš naujo, automobilis turi stovėti."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"Programa „<xliff:g id="ID_1">^1</xliff:g>“ paveikia sistemos našumą"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Išjunkite programą, kad pagerintumėte sistemos našumą. Ją galite vėl įgalinti skiltyje „Nustatymai“."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Suteikite prioritetą programai, kad ji būtų toliau naudojama."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Pašalinkite programą, kad pagerintumėte sistemos našumą."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Išjungti programą"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Suteikti prioritetą programai"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Pašalinti programą"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Programa „<xliff:g id="ID_1">^1</xliff:g>“ buvo išjungta. Ją galite vėl įgalinti skiltyje „Nustatymai“."</string>
</resources>
diff --git a/service/res/values-lv/strings.xml b/service/res/values-lv/strings.xml
index d1990e2..ae8e385 100644
--- a/service/res/values-lv/strings.xml
+++ b/service/res/values-lv/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Kontrolēt automašīnas komponentu aktivizācijas politiku."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"atveidot veidnes"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Atveidot veidnes."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"lietojumprogrammu palaišanas kontrole"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Lietojumprogrammu palaišanas kontrole."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Mana ierīce"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Viesis"</string>
<string name="importance_default" msgid="8587741629268312938">"Noklusējuma svarīgums"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Atiestatīt vēlāk"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Informatīvi izklaidējošā sistēma tiks atiestatīta, nākamreiz palaižot automašīnu."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Automašīnai jābūt apturētai, lai sāktu atiestatīšanu."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"Lietotne <xliff:g id="ID_1">^1</xliff:g> ietekmē jūsu sistēmas veiktspēju"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Atspējojiet lietotni, lai uzlabotu sistēmas veiktspēju. Iestatījumos varat atkal iespējot lietotni."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Piešķiriet lietotnei prioritāti, lai turpinātu izmantot lietotni."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Atinstalējiet lietotni, lai uzlabotu sistēmas veiktspēju."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Atspējot lietotni"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Piešķirt lietotnei prioritāti"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Atinstalēt lietotni"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Lietotne <xliff:g id="ID_1">^1</xliff:g> ir atspējota. Iestatījumos varat to atkal iespējot."</string>
</resources>
diff --git a/service/res/values-mk/strings.xml b/service/res/values-mk/strings.xml
index 8c33039..347b068 100644
--- a/service/res/values-mk/strings.xml
+++ b/service/res/values-mk/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Контролирање на правилото за напојување на автомобилот."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"прикажување шаблони"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Прикажување шаблони."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"да го контролира стартувањето апликации"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"да го контролира стартувањето апликации"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Мојот уред"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Гостин"</string>
<string name="importance_default" msgid="8587741629268312938">"Стандардна важност"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Ресетирај подоцна"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Системот ќе се ресетира при следното вклучување."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Автомобилот мора да биде паркиран за ресетирањето."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> има влијание врз изведбата на вашиот систем."</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Оневозможете ја апликацијата за да се подобри изведбата на системот. Може да ја овозможите апликацијата уште еднаш во „Поставки“."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Дајте предност на апликацијата за да продолжите со користење."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Деинсталирајте ја апликацијата за да се подобри изведбата на системот."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Оневозможете ја апликацијата"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Дајте предност на апликацијата"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Деинсталирајте ја апликацијата"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> е оневозможена. Може да ја овозможите повторно во „Поставки“."</string>
</resources>
diff --git a/service/res/values-ml/strings.xml b/service/res/values-ml/strings.xml
index 3b44cc7..dc8dbe6 100644
--- a/service/res/values-ml/strings.xml
+++ b/service/res/values-ml/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"കാറിന്റെ പവർ പോളിസി നിയന്ത്രിക്കുക."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"ടെംപ്ലേറ്റുകൾ റെൻഡർ ചെയ്യുക"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"ടെംപ്ലേറ്റുകൾ റെൻഡർ ചെയ്യുക."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"ലോഞ്ച് ചെയ്യുന്ന ആപ്പുകൾ നിയന്ത്രിക്കാം"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"ലോഞ്ച് ചെയ്യുന്ന ആപ്പുകൾ നിയന്ത്രിക്കാം."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"എന്റെ ഉപകരണം"</string>
<string name="default_guest_name" msgid="2912812799433131476">"അതിഥി"</string>
<string name="importance_default" msgid="8587741629268312938">"ഡിഫോൾട്ട് പ്രാധാന്യം"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"പിന്നീട് റീസെറ്റ് ചെയ്യൂ"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"അടുത്ത തവണ കാർ സ്റ്റാർട്ട് ചെയ്യുമ്പോൾ ഈ ഇൻഫോറ്റേയിൻമെന്റ് സിസ്റ്റം റീസെറ്റ് ചെയ്യും."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"റീസെറ്റ് ആരംഭിക്കാൻ കാർ പാർക്ക് ചെയ്തിരിക്കണം."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"നിങ്ങളുടെ സിസ്റ്റത്തിന്റെ പ്രകടനത്തെ <xliff:g id="ID_1">^1</xliff:g> ബാധിക്കുന്നു"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"സിസ്റ്റത്തിന്റെ പ്രകടനം മെച്ചപ്പെടുത്താൻ ആപ്പ് പ്രവർത്തനരഹിതമാക്കുക. ക്രമീകരണത്തിൽ ആപ്പ് നിങ്ങൾക്ക് വീണ്ടും പ്രവർത്തനക്ഷമമാക്കാം."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ആപ്പ് ഉപയോഗിക്കുന്നത് തുടരാൻ അതിന് മുൻഗണന നൽകുക."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"സിസ്റ്റത്തിന്റെ പ്രകടനം മെച്ചപ്പെടുത്താൻ ആപ്പ് അണ്ഇൻസ്റ്റാള് ചെയ്യുക."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"ആപ്പ് പ്രവർത്തനരഹിതമാക്കുക"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ആപ്പിന് മുൻഗണന നൽകുക"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"ആപ്പ് അൺഇൻസ്റ്റാൾ ചെയ്യുക"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> പ്രവർത്തനരഹിതമാക്കി. ക്രമീകരണത്തിൽ ഇത് നിങ്ങൾക്ക് വീണ്ടും പ്രവർത്തനക്ഷമമാക്കാം."</string>
</resources>
diff --git a/service/res/values-mn/strings.xml b/service/res/values-mn/strings.xml
index 9463cf8..f2c8754 100644
--- a/service/res/values-mn/strings.xml
+++ b/service/res/values-mn/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Машины асаах/унтраах бодлогыг хянана."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"загварыг буулгах"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Загварыг буулгана."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"аппликэйшн эхлүүлэхийг хянаарай"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Аппликэйшн эхлүүлэхийг хянаарай."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Миний төхөөрөмж"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Зочин"</string>
<string name="importance_default" msgid="8587741629268312938">"Өгөгдмөл ач холбогдол"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Дараа шинэчлэх"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Машин дараа асахад инфотэйнмент системийг шинэчилнэ"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Шинэчилж эхлэхэд машиныг зогсоосон байх ёстой."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> нь таны системийн гүйцэтгэлд нөлөөлж байна"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Системийн гүйцэтгэлийг сайжруулахын тулд аппыг идэвхгүй болгоно уу. Та аппыг Тохиргоо хэсэгт дахин идэвхжүүлэх боломжтой."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Аппыг үргэлжлүүлэн ашиглахын тулд аппыг эрэмбэлнэ үү."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Системийн гүйцэтгэлийг сайжруулахын тулд аппыг устгана уу."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Aппыг идэвхгүй болгох"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Аппыг эрэмбэлэх"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Аппыг устгах"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g>-г идэвхгүй болгосон. Та үүнийг Тохиргоо хэсэгт дахин идэвхжүүлэх боломжтой."</string>
</resources>
diff --git a/service/res/values-mr/strings.xml b/service/res/values-mr/strings.xml
index 7a189d5..0706224 100644
--- a/service/res/values-mr/strings.xml
+++ b/service/res/values-mr/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"कार पॉवर धोरण नियंत्रित करा."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"टेम्पलेट रेंडर करा"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"टेम्पलेट रेंडर करा."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"लाँच होत असलेली अॅप्लिकेशन नियंत्रित करा"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"लाँच होत असलेली अॅप्लिकेशन नियंत्रित करा."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"माझे डिव्हाइस"</string>
<string name="default_guest_name" msgid="2912812799433131476">"अतिथी"</string>
<string name="importance_default" msgid="8587741629268312938">"डीफॉल्ट महत्त्वाची"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"नंतर रीसेट करा"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"पुढील वेळी कार सुरू झाल्यावर इंफोटेनमेंट सिस्टम रीसेट होईल."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"रीसेट सुरू करण्यासाठी कार पार्क केलेली असावी."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"तुमच्या सिस्टीमच्या परफॉर्मन्सवर <xliff:g id="ID_1">^1</xliff:g> मुळे परिणाम होत आहे"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"सिस्टीमच्या परफॉर्मन्समध्ये सुधारणा करण्यासाठी ॲप बंद करा. तुम्ही सेटिंग्ज मध्ये ॲप पुन्हा एकदा सुरू करू शकता."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ॲप वापरणे सुरू ठेवण्यासाठी ॲपला प्राधान्य द्या."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"सिस्टीमच्या परफॉर्मन्समध्ये सुधारणा करण्यासाठी ॲप अनइंस्टॉल करा."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"अॅप बंद करा"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ॲपला प्राधान्य द्या"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"अॅप अनइंस्टॉल करा"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> बंद केले आहे. तुम्ही सेटिंग्ज मध्ये ते पुन्हा सुरू करू शकता."</string>
</resources>
diff --git a/service/res/values-ms/strings.xml b/service/res/values-ms/strings.xml
index 805a1c4..54bc619 100644
--- a/service/res/values-ms/strings.xml
+++ b/service/res/values-ms/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Kawal dasar kuasa kereta."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"paparkan templat"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Paparkan templat."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"mengawal pelancaran aplikasi"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Mengawal pelancaran aplikasi."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Peranti Saya"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Tetamu"</string>
<string name="importance_default" msgid="8587741629268312938">"Kepentingan lalai"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Tetapkan Semula Kemudian"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Sistem maklumat hibur akan ditetapkan semula selepas kereta dihidupkan nanti."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Kereta mesti diparkir untuk memulakan penetapan semula."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> sedang menjejaskan prestasi sistem anda"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Lumpuhkan apl untuk meningkatkan prestasi sistem. Anda boleh mendayakan apl semula dalam Tetapan."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Utamakan apl untuk terus menggunakan apl."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Nyahpasang apl untuk meningkatkan prestasi sistem."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Lumpuhkan apl"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Utamakan apl"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Nyahpasang apl"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> telah dilumpuhkan. Anda boleh mendayakan apl ini semula dalam Tetapan."</string>
</resources>
diff --git a/service/res/values-my/strings.xml b/service/res/values-my/strings.xml
index db73b9d..5c06c6e 100644
--- a/service/res/values-my/strings.xml
+++ b/service/res/values-my/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"ကား၏စွမ်းအင်အသုံးပြုမှု မူဝါဒကို ထိန်းချုပ်မည်။"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"နမူနာပုံစံများ ပုံဖော်ရန်"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"နမူနာပုံစံများ ပုံဖော်မည်။"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"ဖွင့်ထားသည့် အပလီကေးရှင်းများကို ထိန်းချုပ်ခြင်း"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"ဖွင့်ထားသည့် အပလီကေးရှင်းများကို ထိန်းချုပ်ခြင်း။"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"ကျွန်ုပ်၏စက်"</string>
<string name="default_guest_name" msgid="2912812799433131476">"ဧည့်သည်"</string>
<string name="importance_default" msgid="8587741629268312938">"ပုံမှန် အရေးပါမှု"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"နောက်မှ ပြင်ဆင်သတ်မှတ်ရန်"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"နောက်တစ်ကြိမ် ကားစက်နှိုးသောအခါ သတင်းနှင့်ဖျော်ဖြေရေး စနစ်ကို ပြင်ဆင်သတ်မှတ်ပါမည်။"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"ပြင်ဆင်သတ်မှတ်မှု စတင်ရန် ကားကို ရပ်နားထားရမည်။"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> သည် သင့်စနစ်၏ စွမ်းဆောင်ရည်ကို ထိခိုက်စေသည်"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"စနစ်စွမ်းဆောင်ရည် ပိုကောင်းမွန်စေရန် အက်ပ်ကိုပိတ်ပါ။ ဆက်တင်များတွင် အက်ပ်ကို ထပ်ဖွင့်နိုင်သည်။"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"အက်ပ်ဆက်သုံးရန် ၎င်းကို ဦးစားပေးပါ။"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"စနစ်စွမ်းဆောင်ရည် ပိုကောင်းမွန်စေရန် အက်ပ်ကိုဖယ်ရှားပါ။"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"အက်ပ်ကို ပိတ်ရန်"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"အက်ပ်ကို ဦးစားပေးရန်"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"အက်ပ်ကို ဖယ်ရှားရန်"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> ကို ပိတ်လိုက်ပြီ။ ဆက်တင်များတွင် ၎င်းကို ထပ်ဖွင့်နိုင်သည်။"</string>
</resources>
diff --git a/service/res/values-nb/strings.xml b/service/res/values-nb/strings.xml
index 86fe916..9147649 100644
--- a/service/res/values-nb/strings.xml
+++ b/service/res/values-nb/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Kontrollér bilens regler for av/på."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"gjengi maler"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Gjengi maler."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"kontrollér åpning av apper"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Kontrollér åpning av apper."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Enheten min"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gjest"</string>
<string name="importance_default" msgid="8587741629268312938">"Standard viktighet"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Tilbakestill senere"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Infotainment-systemet tilbakestilles neste gang bilen starter."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Bilen må være parkert før tilbakestillingen kan starte."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> påvirker systemytelsen"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Deaktiver appen for å forbedre systemytelsen. Du kan aktivere appen igjen i innstillingene."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioriter appen for å fortsette å bruke den."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Avinstaller appen for å forbedre systemytelsen."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Deaktiver appen"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioriter appen"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Avinstaller appen"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> er deaktivert. Du kan aktivere den igjen i innstillingene."</string>
</resources>
diff --git a/service/res/values-ne/strings.xml b/service/res/values-ne/strings.xml
index 7697da8..e91b23d 100644
--- a/service/res/values-ne/strings.xml
+++ b/service/res/values-ne/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"कारको पावरसम्बन्धी नीति नियन्त्रण गर्नुहोस्।"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"टेम्प्लेटहरू रेन्डर गर्ने"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"टेम्प्लेटहरू रेन्डर गर्ने।"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"एपहरू लन्च गर्ने कुरा नियन्त्रण गर्नुहोस्"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"एपहरू लन्च गर्ने कुरा नियन्त्रण गर्नुहोस्।"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"मेरो डिभाइस"</string>
<string name="default_guest_name" msgid="2912812799433131476">"अतिथि"</string>
<string name="importance_default" msgid="8587741629268312938">"डिफल्ट महत्त्व"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"पछि रिसेट गर्नुहोस्"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"अर्को पटक कार स्टार्ट हुँदा इन्फोटेनमेन्ट प्रणाली रिसेट हुने छ।"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"रिसेट गर्न कार पार्क गरिएको अवस्थामा हुनु पर्छ।"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> ले तपाईंको सिस्टमको पर्फर्मेन्समा असर गरिरहेको छ"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"सिस्टमको पर्फर्मेन्स सुधार गर्न एप अफ गर्नुहोस्। तपाईं सेटिङमा गई एप फेरि अन गर्न सक्नुहुन्छ।"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"एप प्रयोग गरिरहन एपलाई प्राथमिकता दिनुहोस्।"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"सिस्टमको पर्फर्मेन्स सुधार गर्न एप अनइन्स्टल गर्नुहोस्।"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"एप अफ गर्नुहोस्"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"एपलाई प्राथमिकता दिनुहोस्"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"एप अनइन्स्टल गर्नुहोस्"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> अफ गरिएको छ। तपाईं सेटिङमा गई यो एप फेरि अन गर्न सक्नुहुन्छ।"</string>
</resources>
diff --git a/service/res/values-nl/strings.xml b/service/res/values-nl/strings.xml
index 7cff70e..c3ebee5 100644
--- a/service/res/values-nl/strings.xml
+++ b/service/res/values-nl/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"In-/uitschakelbeleid voor auto beheren."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"templates renderen"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Templates renderen."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"starten van apps beheren"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Starten van apps beheren."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Mijn apparaat"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gast"</string>
<string name="importance_default" msgid="8587741629268312938">"Standaardbelang"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Later resetten"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Het infotainmentsysteem wordt gereset als de auto weer wordt gestart."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Parkeer de auto om de reset te starten."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> is van invloed op je systeemprestaties"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Zet de app uit om de systeemprestaties te verbeteren. Je kunt de app weer aanzetten via Instellingen."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioriteer de app als je deze wilt blijven gebruiken."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Verwijder de app om de systeemprestaties te verbeteren."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"App uitzetten"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"App prioriteren"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"App verwijderen"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> is uitgezet. Je kunt de app weer aanzetten via Instellingen."</string>
</resources>
diff --git a/service/res/values-or/strings.xml b/service/res/values-or/strings.xml
index 0aad28e..c85f4b2 100644
--- a/service/res/values-or/strings.xml
+++ b/service/res/values-or/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"କାର୍ ପାୱାର୍ ନୀତି ନିୟନ୍ତ୍ରଣ କରନ୍ତୁ।"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"ଟେମ୍ପଲେଟ୍ ରେଣ୍ଡର୍ କରନ୍ତୁ"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"ଟେମ୍ପଲେଟ୍ ରେଣ୍ଡର୍ କରନ୍ତୁ।"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"ଆପ୍ଲିକେସନଗୁଡ଼ିକର ଲଞ୍ଚିଂକୁ ନିୟନ୍ତ୍ରଣ କରନ୍ତୁ"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"ଆପ୍ଲିକେସନଗୁଡ଼ିକର ଲଞ୍ଚିଂକୁ ନିୟନ୍ତ୍ରଣ କରନ୍ତୁ।"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"ମୋ ଡିଭାଇସ୍"</string>
<string name="default_guest_name" msgid="2912812799433131476">"ଅତିଥି"</string>
<string name="importance_default" msgid="8587741629268312938">"ଡିଫଲ୍ଟ ଗୁରୁତ୍ୱ"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"ପରେ ରିସେଟ୍ କରନ୍ତୁ"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"କାର୍ ଷ୍ଟାର୍ଟ ହେଲେ ଇନଫୋଟେନମେଣ୍ଟ ସିଷ୍ଟମ ରିସେଟ୍ ହେବ।"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"ରିସେଟ୍ ଆରମ୍ଭ କରିବାକୁ କାର୍ ପାର୍କ କରାଯିବା ଆବଶ୍ୟକ।"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> ଆପଣଙ୍କ ସିଷ୍ଟମର ପରଫରମାନ୍ସକୁ ପ୍ରଭାବିତ କରୁଛି"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"ସିଷ୍ଟମର ପରଫରମାନ୍ସକୁ ଉନ୍ନତ କରିବା ପାଇଁ ଆପକୁ ଅକ୍ଷମ କରନ୍ତୁ। ଆପଣ ସେଟିଂସରେ ପୁଣି ଥରେ ଆପକୁ ସକ୍ଷମ କରିପାରିବେ।"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ଆପ ବ୍ୟବହାର କରିବା ଜାରି ରଖିବା ପାଇଁ ଆପକୁ ପ୍ରାଥମିକତା ଦିଅନ୍ତୁ।"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"ସିଷ୍ଟମର ପରଫରମାନ୍ସକୁ ଉନ୍ନତ କରିବା ପାଇଁ ଆପକୁ ଅନଇନଷ୍ଟଲ କରନ୍ତୁ।"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"ଆପକୁ ଅକ୍ଷମ କରନ୍ତୁ"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ଆପକୁ ପ୍ରାଥମିକତା ଦିଅନ୍ତୁ"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"ଆପକୁ ଅନଇନଷ୍ଟଲ କରନ୍ତୁ"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g>କୁ ଅକ୍ଷମ କରିଦିଆଯାଇଛି। ଆପଣ ସେଟିଂସରେ ପୁଣି ଏହାକୁ ସକ୍ଷମ କରିପାରିବେ।"</string>
</resources>
diff --git a/service/res/values-pa/strings.xml b/service/res/values-pa/strings.xml
index 42a8a86..8882ccb 100644
--- a/service/res/values-pa/strings.xml
+++ b/service/res/values-pa/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"ਕਾਰ ਪਾਵਰ ਨੀਤੀ ਨੂੰ ਕੰਟਰੋਲ ਕਰੋ।"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"ਟੈਮਪਲੇਟਾਂ ਨੂੰ ਰੈਂਡਰ ਕਰੋ"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"ਟੈਮਪਲੇਟਾਂ ਨੂੰ ਰੈਂਡਰ ਕਰੋ।"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"ਲਾਂਚ ਕੀਤੀਆਂ ਜਾਣ ਵਾਲੀਆਂ ਐਪਲੀਕੇਸ਼ਨਾਂ ਨੂੰ ਕੰਟਰੋਲ ਕਰੋ"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"ਲਾਂਚ ਕੀਤੀਆਂ ਜਾਣ ਵਾਲੀਆਂ ਐਪਲੀਕੇਸ਼ਨਾਂ ਨੂੰ ਕੰਟਰੋਲ ਕਰੋ।"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"ਮੇਰਾ ਡੀਵਾਈਸ"</string>
<string name="default_guest_name" msgid="2912812799433131476">"ਮਹਿਮਾਨ"</string>
<string name="importance_default" msgid="8587741629268312938">"ਪੂਰਵ-ਨਿਰਧਾਰਿਤ ਮਹੱਤਤਾ"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"ਬਾਅਦ ਵਿੱਚ ਰੀਸੈੱਟ ਕਰੋ"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"ਅਗਲੀ ਵਾਰ ਕਾਰ ਦੇ ਸ਼ੁਰੂ ਹੋਣ \'ਤੇ ਵਾਹਨ ਆਡੀਓ ਸਿਸਟਮ ਰੀਸੈੱਟ ਹੋਵੇਗਾ।"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"ਰੀਸੈੱਟ ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਕਾਰ ਪਾਰਕ ਕੀਤੀ ਜਾਣੀ ਲਾਜ਼ਮੀ ਹੈ।"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> ਐਪ ਤੁਹਾਡੇ ਸਿਸਟਮ ਦੀ ਕਾਰਗੁਜ਼ਾਰੀ \'ਤੇ ਪ੍ਰਭਾਵ ਪਾ ਰਹੀ ਹੈ"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"ਸਿਸਟਮ ਦੀ ਕਾਰਗੁਜ਼ਾਰੀ ਨੂੰ ਬਿਹਤਰ ਬਣਾਉਣ ਲਈ ਐਪ ਨੂੰ ਬੰਦ ਕਰੋ। ਤੁਸੀਂ ਐਪ ਨੂੰ ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਦੁਬਾਰਾ ਚਾਲੂ ਕਰ ਸਕਦੇ ਹੋ।"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ਐਪ ਨੂੰ ਵਰਤਦੇ ਰਹਿਣ ਲਈ ਐਪ ਨੂੰ ਤਰਜੀਹ ਦਿਓ।"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"ਸਿਸਟਮ ਦੀ ਕਾਰਗੁਜ਼ਾਰੀ ਨੂੰ ਬਿਹਤਰ ਬਣਾਉਣ ਲਈ ਐਪ ਨੂੰ ਅਣਸਥਾਪਤ ਕਰੋ।"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"ਐਪ ਨੂੰ ਬੰਦ ਕਰੋ"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ਐਪ ਨੂੰ ਤਰਜੀਹ ਦਿਓ"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"ਐਪ ਅਣਸਥਾਪਤ ਕਰੋ"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> ਐਪ ਨੂੰ ਬੰਦ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ। ਤੁਸੀਂ ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਜਾ ਕੇ ਇਸਨੂੰ ਦੁਬਾਰਾ ਚਾਲੂ ਕਰ ਸਕਦੇ ਹੋ।"</string>
</resources>
diff --git a/service/res/values-pl/strings.xml b/service/res/values-pl/strings.xml
index 41af14a..c5b7b48 100644
--- a/service/res/values-pl/strings.xml
+++ b/service/res/values-pl/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Sterowanie zasadami zasilania samochodu."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"renderowanie szablonów"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Renderowanie szablonów."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"kontrola nad uruchamianiem aplikacji"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Kontrola nad uruchamianiem aplikacji."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Moje urządzenie"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gość"</string>
<string name="importance_default" msgid="8587741629268312938">"Domyślny stopień ważności"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Zresetuj później"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"System zresetuje się, gdy samochód się uruchomi."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Aby rozpocząć resetowanie, zaparkuj samochód."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> wpływa na wydajność systemu"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Wyłącz aplikację, aby zwiększyć wydajność systemu. Możesz ją ponownie włączyć w Ustawieniach."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Nadaj priorytet aplikacji, aby z niej nadal korzystać."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Odinstaluj aplikację, aby zwiększyć wydajność systemu."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Wyłącz aplikację"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Nadaj priorytet aplikacji"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Odinstaluj aplikację"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Aplikacja <xliff:g id="ID_1">^1</xliff:g> została wyłączona. Możesz ją ponownie włączyć w Ustawieniach."</string>
</resources>
diff --git a/service/res/values-pt-rPT/strings.xml b/service/res/values-pt-rPT/strings.xml
index a2714a9..4fcbafd 100644
--- a/service/res/values-pt-rPT/strings.xml
+++ b/service/res/values-pt-rPT/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Controlar a política de energia do automóvel."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"renderizar modelos"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Renderizar modelos."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"controlar o início de aplicações"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Controlar o início de aplicações."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Dispositivo"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Convidado"</string>
<string name="importance_default" msgid="8587741629268312938">"Importância predefinida"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Repor mais tarde"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"O sist. infoentretenim. é reposto quando ligar o carro de novo."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"O carro deve estar estacionado para a reposição."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"A app <xliff:g id="ID_1">^1</xliff:g> está a afetar o desempenho do sistema."</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Desative a app para melhorar o desempenho do sistema. Pode ativar a app novamente nas Definições."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Dê prioridade à app para continuar a utilizá-la."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Desinstale a app para melhorar o desempenho do sistema."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Desativar app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Dar prioridade à app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Desinstalar app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"A app <xliff:g id="ID_1">^1</xliff:g> foi desativada. Pode ativá-la novamente nas Definições."</string>
</resources>
diff --git a/service/res/values-pt/strings.xml b/service/res/values-pt/strings.xml
index 5d86714..15a9614 100644
--- a/service/res/values-pt/strings.xml
+++ b/service/res/values-pt/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Controle a política de energia do carro."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"renderizar modelos"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Renderizar modelos."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"controla a inicialização de aplicativos"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Controla a inicialização de aplicativos."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Meu dispositivo"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Visitante"</string>
<string name="importance_default" msgid="8587741629268312938">"Importância padrão"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Redefinir mais tarde"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"O sistema de infoentretenimento se redefinirá ao ligar o carro."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"É preciso estacionar para iniciar a redefinição."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"O app <xliff:g id="ID_1">^1</xliff:g> está afetando o desempenho do sistema"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Desative o app para melhorar o desempenho do sistema. Você pode ativá-lo novamente em \"Configurações\"."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Se quiser continuar usando o app, priorize-o."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Desinstale o app para melhorar o desempenho do sistema."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Desativar app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Priorizar app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Desinstalar app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"O app <xliff:g id="ID_1">^1</xliff:g> foi desativado. Você pode ativá-lo novamente em \"Configurações\"."</string>
</resources>
diff --git a/service/res/values-ro/strings.xml b/service/res/values-ro/strings.xml
index 4be284a..3cc5c30 100644
--- a/service/res/values-ro/strings.xml
+++ b/service/res/values-ro/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Controlați politica pentru încărcarea mașinii."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"redați șabloane"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Redați șabloane."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"să controleze aplicațiile lansate"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Controlează aplicațiile lansate."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Dispozitivul meu"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Invitat"</string>
<string name="importance_default" msgid="8587741629268312938">"Importanță prestabilită"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Resetați mai târziu"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Sistemul de infotainment se va reseta când pornește mașina."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Mașina trebuie să fie parcată pentru a reseta."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> afectează performanța sistemului dvs."</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Dezactivați aplicația pentru a îmbunătăți performanța sistemului. Puteți activa din nou aplicația din Setări."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioritizați aplicația pentru a o folosi în continuare."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Dezinstalați aplicația pentru a îmbunătăți performanța sistemului."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Dezactivați aplicația"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioritizați aplicația"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Dezinstalați aplicația"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> a fost dezactivată. O puteți activa din nou din Setări."</string>
</resources>
diff --git a/service/res/values-ru/strings.xml b/service/res/values-ru/strings.xml
index 45fd8ee..db6d212 100644
--- a/service/res/values-ru/strings.xml
+++ b/service/res/values-ru/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Управление правилом о питании автомобиля."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"Обработка шаблонов"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Обработка шаблонов."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"управление запуском приложений"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Управление запуском приложений."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Мое устройство"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Гость"</string>
<string name="importance_default" msgid="8587741629268312938">"Важность по умолчанию"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Сбросить позже"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Сброс произойдет при следующем запуске автомобиля."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Чтобы сбросить настройки, припаркуйте автомобиль."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"Приложение \"<xliff:g id="ID_1">^1</xliff:g>\" снижает производительность системы"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Чтобы повысить производительность системы, отключите приложение. Включить его снова можно в настройках."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Чтобы и дальше использовать приложение, приоритизируйте его."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Чтобы повысить производительность системы, удалите приложение."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Отключить приложение"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Приоритизировать приложение"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Удалить приложение"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Приложение \"<xliff:g id="ID_1">^1</xliff:g>\" отключено. Вы можете снова включить его в настройках."</string>
</resources>
diff --git a/service/res/values-si/strings.xml b/service/res/values-si/strings.xml
index 2c4c87f..ba49280 100644
--- a/service/res/values-si/strings.xml
+++ b/service/res/values-si/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"මෝටර් රථයේ බල ප්රතිපත්තිය පාලනය කරන්න."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"අච්චු විදහන්න"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"අච්චු විදහන්න."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"යෙදුම් දියත් කිරීම කළමනාකරණය කරන්න"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"යෙදුම් දියත් කිරීම කළමනාකරණය කරන්න."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"මගේ උපාංගය"</string>
<string name="default_guest_name" msgid="2912812799433131476">"අමුත්තා"</string>
<string name="importance_default" msgid="8587741629268312938">"පෙරනිමි වැදගත්කම"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"පසුව යළි සකසන්න"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"ඊළඟට මෝටර් රථය අරඹන විට තොරතුරු විනෝදාස්වාද පද්ධතිය යළි සැකසේ."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"යළි සැකසීම ඇරඹීමට මෝටර් රථය නවතා තැබිය යුතුය."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"ඔබගේ පද්ධති කාර්යසාධනයට <xliff:g id="ID_1">^1</xliff:g> බලපාමින් තිබේ"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"පද්ධති කාර්යසාධනය වැඩි දියුණු කිරීමට යෙදුම අබල කරන්න. ඔබට සැකසීම් තුළ යෙදුම නැවත වරක් සබල කළ හැකිය."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"යෙදුම දිගටම භාවිත කිරීමට යෙදුම ප්රමුඛ කරන්න."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"පද්ධති කාර්යසාධනය වැඩි දියුණු කිරීමට යෙදුම අස්ථාපනය කරන්න."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"යෙදුම අබල කරන්න"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"යෙදුම ප්රමුඛ කරන්න"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"යෙදුම අස්ථාපනය කරන්න"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> අබල කර ඇත. ඔබට සැකසීම් තුළ එය නැවත සබල කළ හැකිය."</string>
</resources>
diff --git a/service/res/values-sk/strings.xml b/service/res/values-sk/strings.xml
index 4483020..c87b3e2 100644
--- a/service/res/values-sk/strings.xml
+++ b/service/res/values-sk/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Ovládanie pravidiel pre napájanie komponentov auta."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"vykresľovanie šablón"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Vykresľovanie šablón."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"ovládanie spúšťaných aplikácií"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Ovládanie spúšťaných aplikácií."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Moje zariadenie"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Hosť"</string>
<string name="importance_default" msgid="8587741629268312938">"Predvolená dôležitosť"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Resetovať neskôr"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Palubný systém sa resetuje pri naštartovaní auta."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Ak chcete resetovať, auto musí byť zaparkované."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> ovplyvňuje výkon systému"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Ak chcete zlepšiť výkon systému, deaktivujte aplikáciu. Môžete ju znova aktivovať v Nastaveniach."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Ak chcete túto aplikáciu naďalej používať, uprednostnite ju."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Ak chcete zlepšiť výkon systému, odinštalujte aplikáciu."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Deaktivovať aplikáciu"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Uprednostniť aplikáciu"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Odinštalovať aplikáciu"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Aplikácia <xliff:g id="ID_1">^1</xliff:g> bola deaktivovaná. Môžete ju znova aktivovať v Nastaveniach."</string>
</resources>
diff --git a/service/res/values-sl/strings.xml b/service/res/values-sl/strings.xml
index 476cb85..a734675 100644
--- a/service/res/values-sl/strings.xml
+++ b/service/res/values-sl/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Nadziranje pravilnika o delovanju komponent v avtomobilu."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"upodabljanje predlog"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Upodabljanje predlog."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"nadziranje zaganjanja aplikacij"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Nadziranje zaganjanja aplikacij"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Moja naprava"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gost"</string>
<string name="importance_default" msgid="8587741629268312938">"Privzeta pomembnost"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Ponastavi pozneje"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Informativno-razvedrilni sistem se bo ponastavil ob naslednjem zagonu avta."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Za začetek ponastavitve mora biti avto parkiran."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"Aplikacija <xliff:g id="ID_1">^1</xliff:g> vpliva na delovanje sistema"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Onemogočite aplikacijo zaradi izboljšanja delovanja sistema. Aplikacijo lahko znova omogočite v nastavitvah."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Če želite še naprej uporabljati aplikacijo, ji dajte prednost."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Odmestite aplikacijo zaradi izboljšanja delovanja sistema."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Onemogočanje aplikacije"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Dajanje prednosti aplikaciji"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Odmestitev aplikacije"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Aplikacija <xliff:g id="ID_1">^1</xliff:g> je onemogočena. Znova jo lahko omogočite v nastavitvah."</string>
</resources>
diff --git a/service/res/values-sq/strings.xml b/service/res/values-sq/strings.xml
index 01bc06e..4f5a17e 100644
--- a/service/res/values-sq/strings.xml
+++ b/service/res/values-sq/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Kontrollo politikën e makinës për aktivizimin."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"të paraqesë shabllonet"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Të paraqesë shabllonet."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"kontrollo hapjen e aplikacioneve"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Kontrollo hapjen e aplikacioneve."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Pajisja ime"</string>
<string name="default_guest_name" msgid="2912812799433131476">"I ftuar"</string>
<string name="importance_default" msgid="8587741629268312938">"Rëndësia e parazgjedhur"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Rivendose më vonë"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Sistemi info-argëtues do të rivendoset herën tjetër që do të ndizet makina."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Makina duhet të jetë e parkuar për të nisur rivendosjen."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> po ndikon në performancën e sistemit"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Çaktivizo aplikacionin për të përmirësuar performancën e sistemit. Mund ta aktivizosh aplikacionin sërish te \"Cilësimet\"."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Jepi përparësi aplikacionit për të vazhduar ta përdorësh."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Çinstalo aplikacionin për të përmirësuar performancën e sistemit."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Çaktivizo aplikacionin"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Jepi përparësi aplikacionit"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Çinstalo aplikacionin"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> është çaktivizuar. Mund ta aktivizosh përsëri te \"Cilësimet\"."</string>
</resources>
diff --git a/service/res/values-sr/strings.xml b/service/res/values-sr/strings.xml
index a50b736..d8b88f8 100644
--- a/service/res/values-sr/strings.xml
+++ b/service/res/values-sr/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Контрола смерница за напајање аутомобила."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"приказивање шаблона"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Приказивање шаблона."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"контрола покретања апликација"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Контролише покретање апликација."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Мој уређај"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Гост"</string>
<string name="importance_default" msgid="8587741629268312938">"Подразумевана важност"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Ресетуј касније"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Сис. за инфо-заб. се ресет. када опет упал. кола."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Морате да паркирате кола за почетак ресетовања."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> утиче на перформансе система"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Онемогућите апликацију да бисте побољшали перформансе система. Апликацију можете поново да омогућите у Подешавањима."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Дајте приоритет апликацији да бисте наставили да је користите."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Деинсталирајте апликацију да бисте побољшали перформансе система."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Онемогући апликацију"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Дајте приоритет апликацији"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Деинсталирајте апликацију"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Апликација <xliff:g id="ID_1">^1</xliff:g> је онемогућена. Можете поново да је омогућите у Подешавањима."</string>
</resources>
diff --git a/service/res/values-sv/strings.xml b/service/res/values-sv/strings.xml
index d33c17f..a485cd7 100644
--- a/service/res/values-sv/strings.xml
+++ b/service/res/values-sv/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Styr principer för bilens laddning."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"rendera mallar"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Rendera mallar."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"styr startande appar"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Styr startande appar."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Min enhet"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Gäst"</string>
<string name="importance_default" msgid="8587741629268312938">"Standardviktighet"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Återställ senare"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Infotainmentsystemet återställs när bilen startas."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Bilen måste vara parkerad när du återställer."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> påverkar systemprestandan."</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Inaktivera appen för att förbättra systemprestandan. Du kan aktivera appen på nytt i inställningarna."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Prioritera appen för att fortsätta använda den."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Avinstallera appen för att förbättra systemprestandan."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Inaktivera app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Prioritera app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Avinstallera app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> har inaktiverats. Du kan aktivera den på nytt i inställningarna."</string>
</resources>
diff --git a/service/res/values-sw/strings.xml b/service/res/values-sw/strings.xml
index f577aaf..43a1353 100644
--- a/service/res/values-sw/strings.xml
+++ b/service/res/values-sw/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Dhibiti sera ya nishati ya gari."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"kutekeleza violezo"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Kutekeleza violezo."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"kudhibiti programu za kufungua"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Dhibiti programu za kufungua."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Kifaa Changu"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Mgeni"</string>
<string name="importance_default" msgid="8587741629268312938">"Umuhimu wa kiwango cha chaguomsingi"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Weka Upya Baadaye"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Mfumo wa habari na burudani utawekwa upya gari litakapowashwa."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Ni lazima uegeshe gari ili uanze kuweka upya."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> inaathiri utendaji wa mfumo wako"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Zima programu ili uboreshe utendaji wa mfumo. Unaweza kuwasha programu tena kwenye Mipangilio."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Ipe programu kipaumbele ili uendelee kuitumia."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Ondoa programu ili uboreshe utendaji wa mfumo."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Zima programu"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Ipe programu kipaumbele"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Ondoa programu"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> imezimwa. Unaweza kuiwasha tena katika Mipangilio."</string>
</resources>
diff --git a/service/res/values-ta/strings.xml b/service/res/values-ta/strings.xml
index c0c5981..4cb6282 100644
--- a/service/res/values-ta/strings.xml
+++ b/service/res/values-ta/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"காரின் பவர் கொள்கையைக் கட்டுப்படுத்தும்."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"டெம்ப்ளேட்டுகளைக் காட்டுதல்"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"டெம்ப்ளேட்டுகளைக் காட்டுதல்."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"தொடங்கும் ஆப்ஸைக் கட்டுப்படுத்தலாம்"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"தொடங்கும் ஆப்ஸைக் கட்டுப்படுத்தலாம்."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"எனது சாதனம்"</string>
<string name="default_guest_name" msgid="2912812799433131476">"விருந்தினர்"</string>
<string name="importance_default" msgid="8587741629268312938">"இயல்புநிலை முக்கியத்துவம்"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"பிறகு மீட்டமை"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"அடுத்தமுறை காரை இயக்கத் தொடங்கும்போது இன்ஃபோடெயின்மென்ட் சிஸ்டம் மீட்டமைக்கப்படும்."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"மீட்டமைத்தலைத் தொடங்க காரை நிறுத்த வேண்டும்."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"உங்கள் சிஸ்டத்தின் செயல்திறனை <xliff:g id="ID_1">^1</xliff:g> பாதிக்கிறது"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"சிஸ்டத்தின் செயல்திறனை மேம்படுத்த ஆப்ஸை முடக்கவும். அமைப்புகளுக்குச் சென்று மீண்டும் ஆப்ஸை இயக்கலாம்."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ஆப்ஸைத் தொடர்ந்து பயன்படுத்த அதை முன்னுரிமைப்படுத்தவும்."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"சிஸ்டத்தின் செயல்திறனை மேம்படுத்த ஆப்ஸை நிறுவல் நீக்கவும்."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"ஆப்ஸை முடக்கு"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ஆப்ஸை முன்னுரிமைப்படுத்துங்கள்"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"ஆப்ஸை நிறுவல் நீக்குங்கள்"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> முடக்கப்பட்டது. அமைப்புகளுக்குச் சென்று மீண்டும் அதை இயக்கலாம்."</string>
</resources>
diff --git a/service/res/values-te/strings.xml b/service/res/values-te/strings.xml
index 9993cd8..c3b3c42 100644
--- a/service/res/values-te/strings.xml
+++ b/service/res/values-te/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"కారు పవర్ పాలసీని కంట్రోల్ చేయండి."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"టెంప్లేట్లను రెండర్ చేస్తుంది"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"టెంప్లేట్లను రెండర్ చేస్తుంది."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"లాంచ్ చేయబడుతున్న యాప్లను కంట్రోల్ చేయండి"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"లాంచ్ చేయబడుతున్న యాప్లను కంట్రోల్ చేయండి."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"నా పరికరం"</string>
<string name="default_guest_name" msgid="2912812799433131476">"అతిథి"</string>
<string name="importance_default" msgid="8587741629268312938">"ప్రాముఖ్యత ఆటోమేటిక్ సెట్టింగ్గా ఉన్నది"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"తర్వాత రీసెట్ చేయి"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"ఇన్ఫోటైన్మెంట్ సిస్టం కారు స్టార్టయ్యాక రీసెట్ అవుతుంది."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"రీసెట్ ప్రారంభించడానికి మీ కారును పార్క్ చేయండి."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g>, మీ సిస్టమ్ పనితీరును ప్రభావితం చేస్తోంది"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"సిస్టమ్ పనితీరును మెరుగుపరచడానికి యాప్ను డిజేబుల్ చేయండి. మీరు సెట్టింగ్లలో యాప్ను మరోసారి ఎనేబుల్ చేయవచ్చు."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"యాప్ను ఉపయోగించడాన్ని కొనసాగించడానికి యాప్నకు ప్రాధాన్యత ఇవ్వండి."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"సిస్టమ్ పనితీరును మెరుగుపరచడానికి యాప్ను అన్ఇన్స్టాల్ చేయండి."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"యాప్ను డిజేబుల్ చేయండి"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"యాప్నకు ప్రాధాన్యత ఇవ్వండి"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"యాప్ను అన్ఇన్స్టాల్ చేయండి"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> డిజేబుల్ చేయబడింది. మీరు సెట్టింగ్లలో దీన్ని మళ్లీ ఎనేబుల్ చేయవచ్చు."</string>
</resources>
diff --git a/service/res/values-th/strings.xml b/service/res/values-th/strings.xml
index 3b2ebe5..90d0629 100644
--- a/service/res/values-th/strings.xml
+++ b/service/res/values-th/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"ควบคุมนโยบายทางไฟฟ้าสำหรับรถยนต์"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"แสดงผลเทมเพลต"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"แสดงผลเทมเพลต"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"ควบคุมการเปิดใช้งานแอปพลิเคชัน"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"ควบคุมการเปิดใช้งานแอปพลิเคชัน"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"อุปกรณ์ของฉัน"</string>
<string name="default_guest_name" msgid="2912812799433131476">"ผู้มาเยือน"</string>
<string name="importance_default" msgid="8587741629268312938">"ความสำคัญเริ่มต้น"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"รีเซ็ตภายหลัง"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"ระบบสาระบันเทิงจะรีเซ็ตเมื่อสตาร์ทรถในครั้งถัดไป"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"ต้องจอดรถเพื่อเริ่มการรีเซ็ต"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> กำลังส่งผลต่อประสิทธิภาพการทำงานของระบบ"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"ปิดใช้แอปเพื่อเพิ่มประสิทธิภาพระบบ คุณเปิดใช้แอปได้อีกครั้งในการตั้งค่า"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ตั้งเป็นแอปสำคัญเพื่อใช้แอปต่อไป"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"ถอนการติดตั้งแอปเพื่อเพิ่มประสิทธิภาพระบบ"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"ปิดใช้แอป"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ตั้งเป็นแอปสำคัญ"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"ถอนการติดตั้งแอป"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"ปิดใช้ <xliff:g id="ID_1">^1</xliff:g> แล้ว คุณจะเปิดใช้ได้อีกครั้งในการตั้งค่า"</string>
</resources>
diff --git a/service/res/values-tl/strings.xml b/service/res/values-tl/strings.xml
index 2862c72..dcb6b85 100644
--- a/service/res/values-tl/strings.xml
+++ b/service/res/values-tl/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Kontrolin ang patakaran sa power ng kotse."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"mag-render nga template"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Mag-render ng mga template."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"kontrolin ang paglunsad ng mga application"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Kontrolin ang paglunsad ng mga application."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Aking Device"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Bisita"</string>
<string name="importance_default" msgid="8587741629268312938">"Default na halaga"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"I-reset Mamaya"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Magre-reset ang infotainment system sa susunod na mag-start ang sasakyan."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Dapat nakaparada ang sasakyan para makapag-reset."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"Nakakaapekto ang <xliff:g id="ID_1">^1</xliff:g> sa performance ng iyong system"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"I-disable ang app para pahusayin ang performance ng system. Puwede mo ulit i-enable ang app sa Mga Setting."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Gawing priyoridad ang app para patuloy na magamit ang app."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"I-uninstall ang app para pahusayin ang performance ng system."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"I-disable ang app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Gawing priyoridad ang app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"I-uninstall ang app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Na-disable ang <xliff:g id="ID_1">^1</xliff:g>. Puwede mo itong i-enable ulit sa Mga Setting."</string>
</resources>
diff --git a/service/res/values-tr/strings.xml b/service/res/values-tr/strings.xml
index 0a15817..d21eccb 100644
--- a/service/res/values-tr/strings.xml
+++ b/service/res/values-tr/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Araba gücü politikasını kontrol edin."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"şablonları oluşturma"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Şablonları oluşturma."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"başlatılan uygulamaları kontrol etme"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Başlatılan uygulamaları kontrol etme."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Cihazım"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Misafir"</string>
<string name="importance_default" msgid="8587741629268312938">"Varsayılan önem"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Daha Sonra Sıfırla"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Bilgi-eğlence sistemi, araç tekrar çalıştırıldığında sıfırlanacaktır."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Sıfırlamayı başlatmak için araç park edilmelidir."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g>, sistem performansınızı etkiliyor"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Sistem performansını iyileştirmek için uygulamayı devre dışı bırakın. Uygulamayı Ayarlar\'dan tekrar etkinleştirebilirsiniz."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Kullanmaya devam etmek için uygulamaya öncelik verin."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Sistem performansını iyileştirmek için uygulamayı kaldırın."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Uygulamayı devre dışı bırak"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Uygulamaya öncelik ver"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Uygulamanın yüklemesini kaldır"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> devre dışı bırakıldı. Ayarlar\'dan tekrar etkinleştirebilirsiniz."</string>
</resources>
diff --git a/service/res/values-uk/strings.xml b/service/res/values-uk/strings.xml
index 6f3cf6f..bd19473 100644
--- a/service/res/values-uk/strings.xml
+++ b/service/res/values-uk/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Керувати правилами щодо ввімкнення компонентів автомобіля."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"відображати шаблони"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Відображати шаблони."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"керувати додатками, що запускаються"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Керувати додатками, що запускаються."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Мій пристрій"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Гість"</string>
<string name="importance_default" msgid="8587741629268312938">"Пріоритет за умовчанням"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Скинути пізніше"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Систему буде скинуто, коли ви наступного разу заведете авто."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Налаштування не можна скинути, доки ви за кермом."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"Додаток <xliff:g id="ID_1">^1</xliff:g> впливає на продуктивність системи"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Вимкніть додаток, щоб покращити продуктивність системи. Ви зможете знову ввімкнути його в налаштуваннях."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Пріоритезуйте додаток, щоб продовжувати користуватися ним."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Видаліть додаток, щоб покращити продуктивність системи."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Вимкнути додаток"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Пріоритезувати додаток"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Видалити додаток"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Додаток <xliff:g id="ID_1">^1</xliff:g> вимкнено. Ви можете знову ввімкнути його в налаштуваннях."</string>
</resources>
diff --git a/service/res/values-ur/strings.xml b/service/res/values-ur/strings.xml
index c29597b..dce0c35 100644
--- a/service/res/values-ur/strings.xml
+++ b/service/res/values-ur/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"کار کے پاور کی پالیسی کو کنٹرول کریں۔"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"تمثیلات تیار کریں"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"تمثیلات تیار کریں۔"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"شروع ہونے والی ایپلیکیشنز کو کنٹرول کریں"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"شروع ہونے والی ایپلیکیشنز کو کنٹرول کریں۔"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"میرا آلہ"</string>
<string name="default_guest_name" msgid="2912812799433131476">"مہمان"</string>
<string name="importance_default" msgid="8587741629268312938">"ڈیفالٹ اہمیت"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"بعد میں ری سیٹ کریں"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"اگلی بار کار کے شروع ہونے پر معلوماتی انٹرٹینمنٹ سسٹم ری سیٹ ہو جائے گا۔"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"ری سیٹ شروع کرنے کیلئے کار کا پارک ہونا ضروری ہے۔"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> سے آپ کے سسٹم کی کارکردگی متاثر ہو رہی ہے"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"سسٹم کی کارکردگی کو بہتر بنانے کے لیے ایپ کو غیر فعال کریں۔ آپ ترتیبات میں پھر سے ایپ کو فعال کر سکتے ہیں۔"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"ایپ کا استعمال جاری رکھنے کے لیے ایپ کو ترجیح دیں۔"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"سسٹم کی کارکردگی کو بہتر بنانے کے لیے ایپ کو اَن انسٹال کریں۔"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"ایپ کو غیر فعال کریں"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"ایپ کی ترتیب کو ترجیح دیں"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"ایپ کو اَن انسٹال کریں"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> کو غیر فعال کر دیا گیا ہے۔ آپ ترتیبات میں اسے دوبارہ فعال کر سکتے ہیں۔"</string>
</resources>
diff --git a/service/res/values-uz/strings.xml b/service/res/values-uz/strings.xml
index c4a0941..3cb2dde 100644
--- a/service/res/values-uz/strings.xml
+++ b/service/res/values-uz/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Avtomobilning yoqish qoidalarini boshqarish."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"andozalarni renderlash"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Andozalarni renderlash."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"ilovani ishga tushirishni boshqarish"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Ilovani ishga tushirishni boshqarish."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Qurilmam"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Mehmon"</string>
<string name="importance_default" msgid="8587741629268312938">"Birlamchi muhimlik"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Keyinroq bajarish"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Axborot-hordiq tizimi avtomobil yonganda tiklanadi"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Asliga qaytarish uchun avtomobilni toʻxtating."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> ilovasi tizim unumdorligiga taʼsir qilmoqda."</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Tizim unumdorligini oshirish uchun ilovani faolsizlantiring. Ilovani Sozlamalar oqali qayta yoqishingiz mumkin."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Foydalanishda davom etish uchun ilovaga ustunlik bering."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Tizim unumdorligini yaxshilash uchun ilovani oʻchirib tashlang."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Ilovani faolsizlantirish"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Ilovaga ustunlik berish"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Ilovani oʻchirib tashlash"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"<xliff:g id="ID_1">^1</xliff:g> faolsizlantirildi. Uni sozlamalar orqali qayta yoqishingiz mumkin."</string>
</resources>
diff --git a/service/res/values-vi/strings.xml b/service/res/values-vi/strings.xml
index 8ace1ce..74f6090 100644
--- a/service/res/values-vi/strings.xml
+++ b/service/res/values-vi/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Kiểm soát nguyên tắc sử dụng điện của ô tô."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"mẫu kết xuất hình ảnh"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Mẫu kết xuất hình ảnh."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"kiểm soát việc mở ứng dụng"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Kiểm soát việc mở ứng dụng."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Thiết bị của tôi"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Khách"</string>
<string name="importance_default" msgid="8587741629268312938">"Tầm quan trọng mặc định"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Đặt lại sau"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Hệ thống thông tin giải trí sẽ được đặt lại vào lần khởi động xe tiếp theo."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Bạn phải đỗ xe để bắt đầu quá trình đặt lại."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> đang ảnh hưởng đến hiệu suất của hệ thống."</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Vô hiệu hóa ứng dụng để cải thiện hiệu suất của hệ thống. Bạn có thể bật lại ứng dụng đó trong phần Cài đặt."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Ưu tiên ứng dụng để tiếp tục dùng ứng dụng."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Hủy cài đặt ứng dụng để cải thiện hiệu suất của hệ thống."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Vô hiệu hóa ứng dụng"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Ưu tiên ứng dụng"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Gỡ cài đặt ứng dụng"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"Đã vô hiệu hóa <xliff:g id="ID_1">^1</xliff:g>. Bạn có thể bật lại ứng dụng đó trong phần Cài đặt."</string>
</resources>
diff --git a/service/res/values-zh-rCN/strings.xml b/service/res/values-zh-rCN/strings.xml
index 0497b25..236297e 100644
--- a/service/res/values-zh-rCN/strings.xml
+++ b/service/res/values-zh-rCN/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"控制汽车电源政策。"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"渲染模板"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"渲染模板。"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"控制应用启动"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"控制应用启动。"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"我的设备"</string>
<string name="default_guest_name" msgid="2912812799433131476">"访客"</string>
<string name="importance_default" msgid="8587741629268312938">"重要性:默认"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"以后再重置"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"信息娱乐系统将在下次汽车发动时重置。"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"必须停车才能启动重置流程。"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"“<xliff:g id="ID_1">^1</xliff:g>”给系统性能造成了影响"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"停用应用可提高系统性能。您可以在“设置”中重新启用应用。"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"优先运行应用可继续使用应用。"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"卸载应用可提高系统性能。"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"停用应用"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"优先运行应用"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"卸载应用"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"已停用“<xliff:g id="ID_1">^1</xliff:g>”。您可以在“设置”中重新启用该应用。"</string>
</resources>
diff --git a/service/res/values-zh-rHK/strings.xml b/service/res/values-zh-rHK/strings.xml
index d54e578..6445c69 100644
--- a/service/res/values-zh-rHK/strings.xml
+++ b/service/res/values-zh-rHK/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"控制汽車能源政策。"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"輸出範本"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"輸出範本。"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"控制啟動應用程式"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"控制啟動應用程式。"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"我的裝置"</string>
<string name="default_guest_name" msgid="2912812799433131476">"訪客"</string>
<string name="importance_default" msgid="8587741629268312938">"預設重要性"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"稍後重設"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"資訊娛樂系統將在汽車下次啟動時重設。"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"必須泊車才能開始重設。"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"<xliff:g id="ID_1">^1</xliff:g> 正在影響系統效能"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"停用應用程式即可改善系統效能。您可以在「設定」中再次啟用應用程式。"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"優先處理應用程式即可繼續使用應用程式。"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"解除安裝應用程式即可改善系統效能。"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"停用應用程式"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"優先處理應用程式"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"解除安裝應用程式"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"「<xliff:g id="ID_1">^1</xliff:g>」已停用。您可以在「設定」中再次啟用。"</string>
</resources>
diff --git a/service/res/values-zh-rTW/strings.xml b/service/res/values-zh-rTW/strings.xml
index b305673..8af0c44 100644
--- a/service/res/values-zh-rTW/strings.xml
+++ b/service/res/values-zh-rTW/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"控制車輛電源政策。"</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"算繪範本"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"算繪範本。"</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"控制要啟動的應用程式"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"控制要啟動的應用程式。"</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"我的裝置"</string>
<string name="default_guest_name" msgid="2912812799433131476">"訪客"</string>
<string name="importance_default" msgid="8587741629268312938">"預設重要性"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"稍後重設"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"資訊娛樂系統將在下次車輛發動時重設。"</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"必須停車才能啟動重設程序。"</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"「<xliff:g id="ID_1">^1</xliff:g>」目前已對系統效能造成影響"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"停用應用程式可改善系統效能。你可以前往「設定」再次啟用應用程式。"</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"如要繼續使用應用程式,請將應用程式設為優先。"</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"解除安裝應用程式可改善系統效能。"</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"停用應用程式"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"將應用程式設為優先"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"解除安裝應用程式"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"「<xliff:g id="ID_1">^1</xliff:g>」已停用。你可以前往「設定」重新啟用應用程式。"</string>
</resources>
diff --git a/service/res/values-zu/strings.xml b/service/res/values-zu/strings.xml
index bf5e9d46..2e9feea 100644
--- a/service/res/values-zu/strings.xml
+++ b/service/res/values-zu/strings.xml
@@ -169,6 +169,8 @@
<string name="car_permission_desc_control_car_power_policy" msgid="8565782440893507028">"Lawula inqubomgomo yamandla emoto."</string>
<string name="car_permission_label_template_renderer" msgid="3464887382919754850">"nikezela izifanekiso"</string>
<string name="car_permission_desc_template_renderer" msgid="6047233999260920122">"Nikezela izifanekiso."</string>
+ <string name="car_permission_label_control_car_app_launch" msgid="214632389637409226">"lawula ama-application okuqalisa"</string>
+ <string name="car_permission_desc_control_car_app_launch" msgid="4245527461733374198">"Lawula ama-application okuqalisa."</string>
<string name="trust_device_default_name" msgid="4213625926070261253">"Idivayisi yami"</string>
<string name="default_guest_name" msgid="2912812799433131476">"Isihambeli"</string>
<string name="importance_default" msgid="8587741629268312938">"Ukubaluleka okuzenzakalelayo"</string>
@@ -182,4 +184,12 @@
<string name="factory_reset_later_button" msgid="2401829720674483843">"Setha Kabusha Kamuva"</string>
<string name="factory_reset_later_text" msgid="5896142140528784784">"Isistimu ye-infotainment izosethwa kabusha ngesikhathi esizayo lapho imoto iqala."</string>
<string name="factory_reset_driving_text" msgid="6702298505761254553">"Imoto kufanele ipakwe ukuze uqale ukusetha kabusha."</string>
+ <string name="resource_overuse_notification_title" msgid="3385149030747234969">"I-<xliff:g id="ID_1">^1</xliff:g> ithinta ukusebenza kwesistimu yakho"</string>
+ <string name="resource_overuse_notification_text_disable_app" msgid="4538000369374274293">"Khubaza i-app ukuze uthuthukise ukusebenza kwesistimu. Ungaphinda unike i-app amandla futhi Kumasethingi."</string>
+ <string name="resource_overuse_notification_text_prioritize_app" msgid="4782324719261106243">"Yenza kubaluleke i-app ukuze uqhubeke usebenzisa i-app."</string>
+ <string name="resource_overuse_notification_text_uninstall_app" msgid="531108846448668467">"Khipha i-app ukuze uthuthukise ukusebenza kwesistimu."</string>
+ <string name="resource_overuse_notification_button_disable_app" msgid="5511548570206345274">"Khubaza i-app"</string>
+ <string name="resource_overuse_notification_button_prioritize_app" msgid="5327141954014335559">"Yenza kubaluleke i-app"</string>
+ <string name="resource_overuse_notification_button_uninstall_app" msgid="7327141273608850448">"Khipha i-app"</string>
+ <string name="resource_overuse_toast_disable_app_now" msgid="3182983639177825069">"I-<xliff:g id="ID_1">^1</xliff:g> ikhutshaziwe. Ungayinika amandla futhi Kumasethingi."</string>
</resources>
diff --git a/service/res/values/config.xml b/service/res/values/config.xml
index f0c611c..ae487da 100644
--- a/service/res/values/config.xml
+++ b/service/res/values/config.xml
@@ -413,4 +413,14 @@
<!-- A configuration flag to adjust Wifi for suspend. -->
<bool name="config_wifiAdjustmentForSuspend">false</bool>
+ <!-- A configuration flag to prevent the templated apps from showing dialogs. This is done in
+ the view of driver safety as templated apps can potentially show a dialog with custom UI
+ which can be a distraction hazard for the driver. -->
+ <bool name="config_preventTemplatedAppsFromShowingDialog">true</bool>
+
+ <!-- The class name of the templated activities. This is used to detect currently running
+ templated activity.-->
+ <string name="config_template_activity_class_name">
+ androidx.car.app.activity.CarAppActivity
+ </string>
</resources>
diff --git a/service/res/values/strings.xml b/service/res/values/strings.xml
index 0709784..70a6ecf 100644
--- a/service/res/values/strings.xml
+++ b/service/res/values/strings.xml
@@ -548,6 +548,11 @@
<!-- Permission text: app can render templates provided by another app [CHAR LIMIT=NONE] -->
<string name="car_permission_desc_template_renderer">Render templates.</string>
+ <!-- Permission text: app can control launching applications in Car [CHAR LIMIT=NONE] -->
+ <string name="car_permission_label_control_car_app_launch">control launching applications</string>
+ <!-- Permission text: app can control launching applications in Car [CHAR LIMIT=NONE] -->
+ <string name="car_permission_desc_control_car_app_launch">Control launching applications.</string>
+
<!-- The default name of device enrolled as trust device [CHAR LIMIT=NONE] -->
<string name="trust_device_default_name">My Device</string>
@@ -584,4 +589,20 @@
<string name="new_user_managed_device_acceptance" translatable="false">Accept and continue</string>
<string name="new_user_managed_notification_title" translatable="false">Managed device</string>
+ <!-- Title of notification shown when an app overuses system resources [CHAR LIMIT=100] -->
+ <string name="resource_overuse_notification_title"><xliff:g name="app_name" example="Maps">^1</xliff:g> is affecting your system performance</string>
+ <!-- Message of notification shown when an app overuses system resources [CHAR LIMIT=NONE] -->
+ <string name="resource_overuse_notification_text_disable_app">Disable app to improve system performance. You can enable the app once again in Settings.</string>
+ <!-- Message of notification shown when an app overuses system resources [CHAR LIMIT=NONE] -->
+ <string name="resource_overuse_notification_text_prioritize_app">Prioritize app to keep using app.</string>
+ <!-- Message of notification shown when an app overuses system resources [CHAR LIMIT=NONE] -->
+ <string name="resource_overuse_notification_text_uninstall_app">Uninstall app to improve system performance.</string>
+ <!-- Label for button that will disable the app now [CHAR LIMIT=30] -->
+ <string name="resource_overuse_notification_button_disable_app">Disable app</string>
+ <!-- Label for button that will redirect user to prioritize app setting [CHAR LIMIT=30] -->
+ <string name="resource_overuse_notification_button_prioritize_app">Prioritize app</string>
+ <!-- Label for button that will redirect user to uninstall app setting [CHAR LIMIT=30] -->
+ <string name="resource_overuse_notification_button_uninstall_app">Uninstall app</string>
+ <!-- Text of the toast shown when the app is disabled [CHAR_LIMIT=100]-->
+ <string name="resource_overuse_toast_disable_app_now"><xliff:g name="app_name" example="Maps">^1</xliff:g> has been disabled. You can enable it again in Settings.</string>
</resources>
diff --git a/service/res/xml/car_safety_accessibility_service_config.xml b/service/res/xml/car_safety_accessibility_service_config.xml
new file mode 100644
index 0000000..9029ec2
--- /dev/null
+++ b/service/res/xml/car_safety_accessibility_service_config.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accessibilityEventTypes="typeWindowStateChanged"
+ android:accessibilityFlags="flagDefault"
+ android:accessibilityFeedbackType="feedbackAllMask"/>
\ No newline at end of file
diff --git a/service/src/com/android/car/AppFocusService.java b/service/src/com/android/car/AppFocusService.java
index 477529d..6b1c7b4 100644
--- a/service/src/com/android/car/AppFocusService.java
+++ b/service/src/com/android/car/AppFocusService.java
@@ -15,11 +15,14 @@
*/
package com.android.car;
+import android.car.Car;
import android.car.CarAppFocusManager;
import android.car.IAppFocus;
import android.car.IAppFocusListener;
import android.car.IAppFocusOwnershipCallback;
import android.content.Context;
+import android.content.PermissionChecker;
+import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Handler;
import android.os.HandlerThread;
@@ -36,6 +39,7 @@
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -77,9 +81,11 @@
getClass().getSimpleName());
private final DispatchHandler mDispatchHandler = new DispatchHandler(mHandlerThread.getLooper(),
this);
+ private final Context mContext;
public AppFocusService(Context context,
SystemActivityMonitoringService systemActivityMonitoringService) {
+ mContext = context;
mSystemActivityMonitoringService = systemActivityMonitoringService;
mAllChangeClients = new ClientHolder(mAllBinderEventHandler);
mAllOwnershipClients = new OwnershipClientHolder(this);
@@ -121,6 +127,22 @@
}
@Override
+ public List<String> getAppTypeOwner(@CarAppFocusManager.AppFocusType int appType) {
+ OwnershipClientInfo owner;
+ synchronized (mLock) {
+ owner = mFocusOwners.get(appType);
+ }
+ if (owner == null) {
+ return null;
+ }
+ String[] packageNames = mContext.getPackageManager().getPackagesForUid(owner.getUid());
+ if (packageNames == null) {
+ return null;
+ }
+ return Arrays.asList(packageNames);
+ }
+
+ @Override
public boolean isOwningFocus(IAppFocusOwnershipCallback callback, int appType) {
OwnershipClientInfo info;
synchronized (mLock) {
@@ -146,10 +168,10 @@
if (!alreadyOwnedAppTypes.contains(appType)) {
OwnershipClientInfo ownerInfo = mFocusOwners.get(appType);
if (ownerInfo != null && ownerInfo != info) {
- if (mSystemActivityMonitoringService.isInForeground(
- ownerInfo.getPid(), ownerInfo.getUid())
- && !mSystemActivityMonitoringService.isInForeground(
- info.getPid(), info.getUid())) {
+ // Allow receiving focus if the requester has a foreground activity OR if the
+ // requester is privileged service.
+ if (isInForeground(ownerInfo) && !isInForeground(info)
+ && !hasPrivilegedPermission()) {
Slog.w(CarLog.TAG_APP_FOCUS, "Focus request failed for non-foreground app("
+ "pid=" + info.getPid() + ", uid=" + info.getUid() + ")."
+ "Foreground app (pid=" + ownerInfo.getPid() + ", uid="
@@ -190,6 +212,15 @@
return CarAppFocusManager.APP_FOCUS_REQUEST_SUCCEEDED;
}
+ private boolean isInForeground(OwnershipClientInfo info) {
+ return mSystemActivityMonitoringService.isInForeground(info.getPid(), info.getUid());
+ }
+
+ private boolean hasPrivilegedPermission() {
+ return mContext.checkCallingOrSelfPermission(Car.PERMISSION_CAR_DISPLAY_IN_CLUSTER)
+ == PermissionChecker.PERMISSION_GRANTED;
+ }
+
@Override
public void abandonAppFocus(IAppFocusOwnershipCallback callback, int appType) {
synchronized (mLock) {
diff --git a/service/src/com/android/car/CarFeatureController.java b/service/src/com/android/car/CarFeatureController.java
index f5df9e4..528353d 100644
--- a/service/src/com/android/car/CarFeatureController.java
+++ b/service/src/com/android/car/CarFeatureController.java
@@ -63,6 +63,7 @@
Car.APP_FOCUS_SERVICE,
Car.AUDIO_SERVICE,
Car.BLUETOOTH_SERVICE,
+ Car.CAR_ACTIVITY_SERVICE,
Car.CAR_BUGREPORT_SERVICE,
Car.CAR_DEVICE_POLICY_SERVICE,
Car.CAR_DRIVING_STATE_SERVICE,
diff --git a/service/src/com/android/car/CarInputService.java b/service/src/com/android/car/CarInputService.java
index 5870b32..f63a6e0 100644
--- a/service/src/com/android/car/CarInputService.java
+++ b/service/src/com/android/car/CarInputService.java
@@ -60,6 +60,7 @@
import com.android.car.hal.InputHalService;
import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.internal.common.UserHelperLite;
+import com.android.car.pm.CarSafetyAccessibilityService;
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -69,6 +70,7 @@
import com.android.server.utils.Slogf;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
@@ -81,7 +83,8 @@
*/
public class CarInputService extends ICarInput.Stub
implements CarServiceBase, InputHalService.InputListener {
-
+ public static final String ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ":";
+ private static final int MAX_RETRIES_FOR_ENABLING_ACCESSIBILITY_SERVICES = 5;
private static final String TAG = CarLog.TAG_INPUT;
/** An interface to receive {@link KeyEvent}s as they occur. */
@@ -232,7 +235,7 @@
private final CarUserManager.UserLifecycleListener mUserLifecycleListener = event -> {
Slogf.d(TAG, "CarInputService.onEvent(%s)", event);
if (CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING == event.getEventType()) {
- updateRotaryServiceSettings(event.getUserId());
+ updateCarAccessibilityServicesSettings(event.getUserId());
}
};
@@ -327,9 +330,7 @@
mBluetoothProfileServiceListener, BluetoothProfile.HEADSET_CLIENT);
});
}
- if (!TextUtils.isEmpty(mRotaryServiceComponentName)) {
- mUserService.addUserLifecycleListener(mUserLifecycleListener);
- }
+ mUserService.addUserLifecycleListener(mUserLifecycleListener);
}
@Override
@@ -344,9 +345,7 @@
mBluetoothHeadsetClient = null;
}
}
- if (!TextUtils.isEmpty(mRotaryServiceComponentName)) {
- mUserService.removeUserLifecycleListener(mUserLifecycleListener);
- }
+ mUserService.removeUserLifecycleListener(mUserLifecycleListener);
}
@Override
@@ -450,6 +449,12 @@
InputDevice.SOURCE_CLASS_BUTTON);
}
+ /**
+ * Requests capturing of input event for the specified display for all requested input types.
+ *
+ * Currently this method requires {@code android.car.permission.CAR_MONITOR_INPUT} or
+ * {@code android.permission.MONITOR_INPUT} permissions (any of them will be acceptable).
+ */
@Override
public int requestInputEventCapture(ICarInputCallback callback,
@DisplayTypeEnum int targetDisplayType,
@@ -458,6 +463,13 @@
requestFlags);
}
+ /**
+ * Overloads #requestInputEventCapture(int, int[], int, CarInputCaptureCallback) by providing
+ * a {@link java.util.concurrent.Executor} to be used when invoking the callback argument.
+ *
+ * Currently this method requires {@code android.car.permission.CAR_MONITOR_INPUT} or
+ * {@code android.permission.MONITOR_INPUT} permissions (any of them will be acceptable).
+ */
@Override
public void releaseInputEventCapture(ICarInputCallback callback,
@DisplayTypeEnum int targetDisplayType) {
@@ -691,6 +703,26 @@
return true;
}
+ private List<String> getAccessibilityServicesToBeEnabled() {
+ String carSafetyAccessibilityServiceComponentName = mContext.getPackageName()
+ + "/"
+ + CarSafetyAccessibilityService.class.getName();
+ ArrayList<String> accessibilityServicesToBeEnabled = new ArrayList<>();
+ accessibilityServicesToBeEnabled.add(carSafetyAccessibilityServiceComponentName);
+ if (!TextUtils.isEmpty(mRotaryServiceComponentName)) {
+ accessibilityServicesToBeEnabled.add(mRotaryServiceComponentName);
+ }
+ return accessibilityServicesToBeEnabled;
+ }
+
+ private static List<String> createServiceListFromSettingsString(
+ String accessibilityServicesString) {
+ return TextUtils.isEmpty(accessibilityServicesString)
+ ? new ArrayList<>()
+ : Arrays.asList(accessibilityServicesString.split(
+ ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR));
+ }
+
@Override
@ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
public void dump(IndentingPrintWriter writer) {
@@ -701,15 +733,44 @@
mCaptureController.dump(writer);
}
- private void updateRotaryServiceSettings(@UserIdInt int userId) {
+ private void updateCarAccessibilityServicesSettings(@UserIdInt int userId) {
if (UserHelperLite.isHeadlessSystemUser(userId)) {
return;
}
+ List<String> accessibilityServicesToBeEnabled = getAccessibilityServicesToBeEnabled();
ContentResolver contentResolver = mContext.getContentResolver();
- Settings.Secure.putStringForUser(contentResolver,
- Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
- mRotaryServiceComponentName,
- userId);
+ List<String> alreadyEnabledServices = createServiceListFromSettingsString(
+ Settings.Secure.getStringForUser(contentResolver,
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ userId));
+
+ int retry = 0;
+ while (!alreadyEnabledServices.containsAll(accessibilityServicesToBeEnabled)
+ && retry <= MAX_RETRIES_FOR_ENABLING_ACCESSIBILITY_SERVICES) {
+ ArrayList<String> enabledServicesList = new ArrayList<>(alreadyEnabledServices);
+ int numAccessibilityServicesToBeEnabled = accessibilityServicesToBeEnabled.size();
+ for (int i = 0; i < numAccessibilityServicesToBeEnabled; i++) {
+ String serviceToBeEnabled = accessibilityServicesToBeEnabled.get(i);
+ if (!enabledServicesList.contains(serviceToBeEnabled)) {
+ enabledServicesList.add(serviceToBeEnabled);
+ }
+ }
+ Settings.Secure.putStringForUser(contentResolver,
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ String.join(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR, enabledServicesList),
+ userId);
+ // Read again to account for any race condition with other parts of the code that might
+ // be enabling other accessibility services.
+ alreadyEnabledServices = createServiceListFromSettingsString(
+ Settings.Secure.getStringForUser(contentResolver,
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ userId));
+ retry++;
+ }
+ if (!alreadyEnabledServices.containsAll(accessibilityServicesToBeEnabled)) {
+ Slogf.e(TAG, "Failed to enable accessibility services");
+ }
+
Settings.Secure.putStringForUser(contentResolver,
Settings.Secure.ACCESSIBILITY_ENABLED,
"1",
diff --git a/service/src/com/android/car/CarLog.java b/service/src/com/android/car/CarLog.java
index 4585eda..3cec3bb 100644
--- a/service/src/com/android/car/CarLog.java
+++ b/service/src/com/android/car/CarLog.java
@@ -43,6 +43,7 @@
public static final String TAG_SERVICE = "CAR.SERVICE";
public static final String TAG_STORAGE = "CAR.STORAGE";
public static final String TAG_TELEMETRY = "CAR.TELEMETRY";
+ public static final String TAG_TIME = "CAR.TIME";
public static final String TAG_WATCHDOG = "CAR.WATCHDOG";
/**
diff --git a/service/src/com/android/car/CarMediaService.java b/service/src/com/android/car/CarMediaService.java
index 8cee561..1f6375e 100644
--- a/service/src/com/android/car/CarMediaService.java
+++ b/service/src/com/android/car/CarMediaService.java
@@ -21,6 +21,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
+import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.car.Car;
import android.car.hardware.power.CarPowerPolicy;
@@ -60,6 +61,7 @@
import android.os.UserManager;
import android.service.media.MediaBrowserService;
import android.text.TextUtils;
+import android.util.DebugUtils;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
@@ -68,6 +70,7 @@
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -76,6 +79,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.stream.Collectors;
/**
@@ -87,7 +91,9 @@
* it were being browsed only. However, that source is still considered the active source, and
* should be the source displayed in any Media related UIs (Media Center, home screen, etc).
*/
-public class CarMediaService extends ICarMedia.Stub implements CarServiceBase {
+public final class CarMediaService extends ICarMedia.Stub implements CarServiceBase {
+
+ private static final boolean DEBUG = false;
private static final String SOURCE_KEY = "media_source_component";
private static final String SOURCE_KEY_SEPARATOR = "_";
@@ -96,6 +102,7 @@
private static final String COMPONENT_NAME_SEPARATOR = ",";
private static final String MEDIA_CONNECTION_ACTION = "com.android.car.media.MEDIA_CONNECTION";
private static final String EXTRA_AUTOPLAY = "com.android.car.media.autoplay";
+ private static final String LAST_UPDATE_KEY = "last_update";
private static final int MEDIA_SOURCE_MODES = 2;
@@ -126,6 +133,8 @@
private boolean mWasPreviouslyDisabledByPowerPolicy;
@GuardedBy("mLock")
private boolean mWasPlayingBeforeDisabled;
+
+ // NOTE: must use getSharedPrefsForWriting() to write to it
private SharedPreferences mSharedPrefs;
private SessionChangedListener mSessionsListener;
private int mPlayOnMediaSourceChangedConfig;
@@ -151,6 +160,7 @@
private ComponentName[] mRemovedMediaSourceComponents = new ComponentName[MEDIA_SOURCE_MODES];
private final IntentFilter mPackageUpdateFilter;
+ @GuardedBy("mLock")
private boolean mIsPackageUpdateReceiverRegistered;
/**
@@ -230,7 +240,7 @@
maybeInitUser(event.getUserId());
break;
case CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED:
- onUserUnlock(event.getUserId());
+ onUserUnlocked(event.getUserId());
break;
}
};
@@ -309,8 +319,9 @@
@Override
// This method is called from ICarImpl after CarMediaService is created.
public void init() {
- int currentUser = ActivityManager.getCurrentUser();
- maybeInitUser(currentUser);
+ int currentUserId = ActivityManager.getCurrentUser();
+ Slog.d(CarLog.TAG_MEDIA, "init(): currentUser=" + currentUserId);
+ maybeInitUser(currentUserId);
setPowerPolicyListener();
}
@@ -325,21 +336,16 @@
}
}
- private void initUser(int userId) {
- // SharedPreferences are shared among different users thus only need initialized once. And
- // they should be initialized after user 0 is unlocked because SharedPreferences in
- // credential encrypted storage are not available until after user 0 is unlocked.
- // initUser() is called when the current foreground user is unlocked, and by that time user
- // 0 has been unlocked already, so initializing SharedPreferences in initUser() is fine.
- synchronized (mLock) {
- if (mSharedPrefs == null) {
- mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE);
- }
+ private void initUser(@UserIdInt int userId) {
+ Slog.d(CarLog.TAG_MEDIA, "initUser(): userId=" + userId + ", mSharedPrefs=" + mSharedPrefs);
+ UserHandle currentUser = new UserHandle(userId);
+ maybeInitSharedPrefs(userId);
+
+ synchronized (mLock) {
if (mIsPackageUpdateReceiverRegistered) {
mContext.unregisterReceiver(mPackageUpdateReceiver);
}
- UserHandle currentUser = new UserHandle(userId);
mContext.registerReceiverAsUser(mPackageUpdateReceiver, currentUser,
mPackageUpdateFilter, null, null);
mIsPackageUpdateReceiverRegistered = true;
@@ -358,6 +364,30 @@
}
}
+ private void maybeInitSharedPrefs(@UserIdInt int userId) {
+ // SharedPreferences are shared among different users thus only need initialized once. And
+ // they should be initialized after user 0 is unlocked because SharedPreferences in
+ // credential encrypted storage are not available until after user 0 is unlocked.
+ // initUser() is called when the current foreground user is unlocked, and by that time user
+ // 0 has been unlocked already, so initializing SharedPreferences in initUser() is fine.
+ if (mSharedPrefs != null) {
+ Slog.i(CarLog.TAG_MEDIA, "Shared preferences already set (on directory "
+ + mContext.getDataDir() + ") when initializing user " + userId);
+ return;
+ }
+ Slog.i(CarLog.TAG_MEDIA, "Getting shared preferences when initializing user "
+ + userId);
+ mSharedPrefs = mContext.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE);
+
+ // Try to access the properties to make sure they were properly open
+ if (DEBUG) {
+ Slogf.i(CarLog.TAG_MEDIA, "Number of prefs: %d", mSharedPrefs.getAll().size());
+
+ } else if (Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG)) {
+ Slogf.d(CarLog.TAG_MEDIA, "Number of prefs: %d", mSharedPrefs.getAll().size());
+ }
+ }
+
/**
* Starts a service on the current user that binds to the media browser of the current media
* source. We start a new service because this one runs on user 0, and MediaBrowser doesn't
@@ -373,20 +403,19 @@
}
private boolean sharedPrefsInitialized() {
- if (mSharedPrefs == null) {
- // It shouldn't reach this but let's be cautious.
- Slog.e(CarLog.TAG_MEDIA, "SharedPreferences are not initialized!");
- String className = getClass().getName();
- for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
- // Let's print the useful logs only.
- String log = ste.toString();
- if (log.contains(className)) {
- Slog.e(CarLog.TAG_MEDIA, log);
- }
+ if (mSharedPrefs != null) return true;
+
+ // It shouldn't reach this but let's be cautious.
+ Slog.e(CarLog.TAG_MEDIA, "SharedPreferences are not initialized!");
+ String className = getClass().getName();
+ for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
+ // Let's print the useful logs only.
+ String log = ste.toString();
+ if (log.contains(className)) {
+ Slog.e(CarLog.TAG_MEDIA, log);
}
- return false;
}
- return true;
+ return false;
}
private boolean isCurrentUserEphemeral() {
@@ -414,43 +443,85 @@
@Override
public void dump(IndentingPrintWriter writer) {
+ writer.println("*CarMediaService*");
+ writer.increaseIndent();
+
+ writer.printf("Pending init: %b\n", mPendingInit);
+ boolean hasSharedPrefs;
synchronized (mLock) {
- writer.println("*CarMediaService*");
- writer.increaseIndent();
- writer.printf("Current playback media component: %s\n",
- mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK] == null ? "-"
- : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_PLAYBACK].flattenToString());
- writer.printf("Current browse media component: %s\n",
- mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE] == null ? "-"
- : mPrimaryMediaComponents[MEDIA_SOURCE_MODE_BROWSE].flattenToString());
+ hasSharedPrefs = mSharedPrefs != null;
+ dumpCurrentMediaComponent(writer, "playback", MEDIA_SOURCE_MODE_PLAYBACK);
+ dumpCurrentMediaComponent(writer, "browse", MEDIA_SOURCE_MODE_BROWSE);
if (mActiveUserMediaController != null) {
writer.printf("Current media controller: %s\n",
mActiveUserMediaController.getPackageName());
writer.printf("Current browse service extra: %s\n",
getClassName(mActiveUserMediaController));
+ } else {
+ writer.println("no active user media controller");
}
- writer.printf("Number of active media sessions: %s\n", mMediaSessionManager
- .getActiveSessionsForUser(null,
- new UserHandle(ActivityManager.getCurrentUser())).size());
+ int userId = ActivityManager.getCurrentUser();
+ writer.printf("Number of active media sessions (for current user %d): %d\n", userId,
+ mMediaSessionManager.getActiveSessionsForUser(/* notificationListener= */ null,
+ new UserHandle(userId)).size());
- writer.println("Playback media source history:");
- writer.increaseIndent();
- for (ComponentName name : getLastMediaSources(MEDIA_SOURCE_MODE_PLAYBACK)) {
- writer.println(name.flattenToString());
- }
- writer.decreaseIndent();
- writer.println("Browse media source history:");
- writer.increaseIndent();
- for (ComponentName name : getLastMediaSources(MEDIA_SOURCE_MODE_BROWSE)) {
- writer.println(name.flattenToString());
- }
- writer.decreaseIndent();
writer.printf("Disabled by power policy: %s\n", mIsDisabledByPowerPolicy);
if (mIsDisabledByPowerPolicy) {
writer.printf("Before being disabled by power policy, audio was %s\n",
mWasPlayingBeforeDisabled ? "active" : "inactive");
}
}
+
+ if (hasSharedPrefs) {
+ dumpLastMediaSources(writer, "Playback", MEDIA_SOURCE_MODE_PLAYBACK);
+ dumpLastMediaSources(writer, "Browse", MEDIA_SOURCE_MODE_BROWSE);
+ dumpSharedPrefs(writer);
+ } else {
+ writer.println("No shared preferences");
+ }
+
+ writer.decreaseIndent();
+ }
+
+ private void dumpCurrentMediaComponent(IndentingPrintWriter writer, String name,
+ @CarMediaManager.MediaSourceMode int mode) {
+ ComponentName componentName = mPrimaryMediaComponents[mode];
+ writer.printf("Current %s media component: %s\n", name, componentName == null
+ ? "-"
+ : componentName.flattenToString());
+ }
+
+ private void dumpLastMediaSources(IndentingPrintWriter writer, String name,
+ @CarMediaManager.MediaSourceMode int mode) {
+ writer.printf("%s media source history:\n", name);
+ writer.increaseIndent();
+ List<ComponentName> lastMediaSources = getLastMediaSources(mode);
+ for (int i = 0; i < lastMediaSources.size(); i++) {
+ ComponentName componentName = lastMediaSources.get(i);
+ if (componentName == null) {
+ Slogf.e(CarLog.TAG_MEDIA, "dump(): empty last media source of %s at index %d: %s",
+ mediaModeToString(mode), i, lastMediaSources);
+ continue;
+ }
+ writer.println(componentName.flattenToString());
+ }
+ writer.decreaseIndent();
+ }
+
+ private void dumpSharedPrefs(IndentingPrintWriter writer) {
+ Map<String, ?> allPrefs = mSharedPrefs.getAll();
+ writer.printf("%d shared preferences (saved on directory %s)",
+ allPrefs.size(), mContext.getDataDir());
+ if (!Log.isLoggable(CarLog.TAG_MEDIA, Log.VERBOSE) || allPrefs.isEmpty()) {
+ writer.println();
+ return;
+ }
+ writer.println(':');
+ writer.increaseIndent();
+ for (Entry<String, ?> pref : allPrefs.entrySet()) {
+ writer.printf("%s = %s\n", pref.getKey(), pref.getValue());
+ }
+ writer.decreaseIndent();
}
/**
@@ -531,10 +602,12 @@
}
// TODO(b/153115826): this method was used to be called from the ICar binder thread, but it's
- // now called by UserCarService. Currently UserCarServie is calling every listener in one
+ // now called by UserCarService. Currently UserCarService is calling every listener in one
// non-main thread, but it's not clear how the final behavior will be. So, for now it's ok
// to post it to mMainHandler, but once b/145689885 is fixed, we might not need it.
- private void onUserUnlock(int userId) {
+ private void onUserUnlocked(@UserIdInt int userId) {
+ Slog.d(CarLog.TAG_MEDIA, "onUserUnlocked(): userId=" + userId
+ + ", mPendingInit=" + mPendingInit);
mMainHandler.post(() -> {
// No need to handle system user, non current foreground user.
if (userId == UserHandle.USER_SYSTEM
@@ -904,13 +977,28 @@
String componentName = component.flattenToString();
String key = getMediaSourceKey(mode);
String serialized = mSharedPrefs.getString(key, null);
+ String modeName = null;
+ boolean debug = DEBUG || Log.isLoggable(CarLog.TAG_MEDIA, Log.DEBUG);
+ if (debug) {
+ modeName = mediaModeToString(mode);
+ }
+
if (serialized == null) {
- mSharedPrefs.edit().putString(key, componentName).apply();
+ if (debug) {
+ Slogf.d(CarLog.TAG_MEDIA, "saveLastMediaSource(%s, %s): no value for key %s",
+ componentName, modeName, key);
+ }
+ getSharedPrefsForWriting().putString(key, componentName).apply();
} else {
Deque<String> componentNames = new ArrayDeque<>(getComponentNameList(serialized));
componentNames.remove(componentName);
componentNames.addFirst(componentName);
- mSharedPrefs.edit().putString(key, serializeComponentNameList(componentNames)).apply();
+ String newSerialized = serializeComponentNameList(componentNames);
+ if (debug) {
+ Slogf.d(CarLog.TAG_MEDIA, "saveLastMediaSource(%s, %s): updating %s from %s to %s",
+ componentName, modeName, key, serialized, newSerialized);
+ }
+ getSharedPrefsForWriting().putString(key, newSerialized).apply();
}
}
@@ -960,7 +1048,8 @@
mCurrentPlaybackState = state;
}
String key = getPlaybackStateKey();
- mSharedPrefs.edit().putInt(key, state).apply();
+ Slogf.d(CarLog.TAG_MEDIA, "savePlaybackState(): %s = %d)", key, state);
+ getSharedPrefsForWriting().putInt(key, state).apply();
}
/**
@@ -1031,6 +1120,15 @@
}
}
+ /**
+ * Gets the editor used to update shared preferences.
+ */
+ private SharedPreferences.Editor getSharedPrefsForWriting() {
+ long now = System.currentTimeMillis();
+ Slogf.i(CarLog.TAG_MEDIA, "Updating %s to %d", LAST_UPDATE_KEY, now);
+ return mSharedPrefs.edit().putLong(LAST_UPDATE_KEY, now);
+ }
+
@NonNull
private static String getClassName(@NonNull MediaController controller) {
Bundle sessionExtras = controller.getExtras();
@@ -1039,4 +1137,8 @@
Car.CAR_EXTRA_BROWSE_SERVICE_FOR_SESSION);
return value != null ? value : "";
}
+
+ private static String mediaModeToString(@CarMediaManager.MediaSourceMode int mode) {
+ return DebugUtils.constantToString(CarMediaManager.class, "MEDIA_SOURCE_", mode);
+ }
}
diff --git a/service/src/com/android/car/CarServiceUtils.java b/service/src/com/android/car/CarServiceUtils.java
index 2dddeeb..c4f77cf 100644
--- a/service/src/com/android/car/CarServiceUtils.java
+++ b/service/src/com/android/car/CarServiceUtils.java
@@ -154,8 +154,17 @@
}
public static float[] toFloatArray(List<Float> list) {
- final int size = list.size();
- final float[] array = new float[size];
+ int size = list.size();
+ float[] array = new float[size];
+ for (int i = 0; i < size; ++i) {
+ array[i] = list.get(i);
+ }
+ return array;
+ }
+
+ public static long[] toLongArray(List<Long> list) {
+ int size = list.size();
+ long[] array = new long[size];
for (int i = 0; i < size; ++i) {
array[i] = list.get(i);
}
@@ -163,8 +172,8 @@
}
public static int[] toIntArray(List<Integer> list) {
- final int size = list.size();
- final int[] array = new int[size];
+ int size = list.size();
+ int[] array = new int[size];
for (int i = 0; i < size; ++i) {
array[i] = list.get(i);
}
@@ -172,8 +181,8 @@
}
public static byte[] toByteArray(List<Byte> list) {
- final int size = list.size();
- final byte[] array = new byte[size];
+ int size = list.size();
+ byte[] array = new byte[size];
for (int i = 0; i < size; ++i) {
array[i] = list.get(i);
}
diff --git a/service/src/com/android/car/CarShellCommand.java b/service/src/com/android/car/CarShellCommand.java
index 083eeb8..19d8a1a 100644
--- a/service/src/com/android/car/CarShellCommand.java
+++ b/service/src/com/android/car/CarShellCommand.java
@@ -18,6 +18,7 @@
import static android.car.Car.PERMISSION_CAR_CONTROL_AUDIO_VOLUME;
import static android.car.Car.PERMISSION_CAR_POWER;
import static android.car.Car.PERMISSION_CONTROL_CAR_WATCHDOG_CONFIG;
+import static android.car.Car.PERMISSION_USE_CAR_WATCHDOG;
import static android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue.ASSOCIATE_CURRENT_USER;
import static android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue.DISASSOCIATE_ALL_USERS;
import static android.hardware.automotive.vehicle.V2_0.UserIdentificationAssociationSetValue.DISASSOCIATE_CURRENT_USER;
@@ -172,6 +173,10 @@
private static final String COMMAND_APPLY_POWER_POLICY = "apply-power-policy";
private static final String COMMAND_DEFINE_POWER_POLICY_GROUP = "define-power-policy-group";
private static final String COMMAND_SET_POWER_POLICY_GROUP = "set-power-policy-group";
+ private static final String COMMAND_APPLY_CTS_VERIFIER_POWER_OFF_POLICY =
+ "apply-cts-verifier-power-off-policy";
+ private static final String COMMAND_APPLY_CTS_VERIFIER_POWER_ON_POLICY =
+ "apply-cts-verifier-power-on-policy";
private static final String COMMAND_POWER_OFF = "power-off";
private static final String POWER_OFF_SKIP_GARAGEMODE = "--skip-garagemode";
private static final String POWER_OFF_SHUTDOWN = "--shutdown";
@@ -194,10 +199,14 @@
private static final String COMMAND_SET_REARVIEW_CAMERA_ID = "set-rearview-camera-id";
private static final String COMMAND_GET_REARVIEW_CAMERA_ID = "get-rearview-camera-id";
+ private static final String COMMAND_WATCHDOG_CONTROL_PACKAGE_KILLABLE_STATE =
+ "watchdog-control-package-killable-state";
private static final String COMMAND_WATCHDOG_IO_SET_3P_FOREGROUND_BYTES =
"watchdog-io-set-3p-foreground-bytes";
private static final String COMMAND_WATCHDOG_IO_GET_3P_FOREGROUND_BYTES =
"watchdog-io-get-3p-foreground-bytes";
+ private static final String COMMAND_WATCHDOG_CONTROL_PROCESS_HEALTH_CHECK =
+ "watchdog-control-health-check";
private static final String COMMAND_DRIVING_SAFETY_SET_REGION =
"set-drivingsafety-region";
@@ -253,6 +262,10 @@
android.Manifest.permission.DEVICE_POWER);
USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_SET_POWER_POLICY_GROUP,
android.Manifest.permission.DEVICE_POWER);
+ USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_APPLY_CTS_VERIFIER_POWER_OFF_POLICY,
+ android.Manifest.permission.DEVICE_POWER);
+ USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_APPLY_CTS_VERIFIER_POWER_ON_POLICY,
+ android.Manifest.permission.DEVICE_POWER);
USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_SILENT_MODE,
PERMISSION_CAR_POWER);
USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_GET_INITIAL_USER,
@@ -269,10 +282,14 @@
android.Manifest.permission.INJECT_EVENTS);
USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_INJECT_ROTARY,
android.Manifest.permission.INJECT_EVENTS);
+ USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_WATCHDOG_CONTROL_PACKAGE_KILLABLE_STATE,
+ PERMISSION_CONTROL_CAR_WATCHDOG_CONFIG);
USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_WATCHDOG_IO_SET_3P_FOREGROUND_BYTES,
PERMISSION_CONTROL_CAR_WATCHDOG_CONFIG);
USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_WATCHDOG_IO_GET_3P_FOREGROUND_BYTES,
PERMISSION_CONTROL_CAR_WATCHDOG_CONFIG);
+ USER_BUILD_COMMAND_TO_PERMISSION_MAP.put(COMMAND_WATCHDOG_CONTROL_PROCESS_HEALTH_CHECK,
+ PERMISSION_USE_CAR_WATCHDOG);
}
private static final String PARAM_DAY_MODE = "day";
@@ -598,6 +615,14 @@
pw.println("\t Sets power policy group which is defined in /vendor/etc/power_policy.xml ");
pw.printf("\t or by %s command\n", COMMAND_DEFINE_POWER_POLICY_GROUP);
+ pw.printf("\t%s\n", COMMAND_APPLY_CTS_VERIFIER_POWER_OFF_POLICY);
+ pw.println("\t Define and apply the cts_verifier_off power policy with "
+ + "--disable WIFI,LOCATION,BLUETOOTH");
+
+ pw.printf("\t%s\n", COMMAND_APPLY_CTS_VERIFIER_POWER_ON_POLICY);
+ pw.println("\t Define and apply the cts_verifier_on power policy with "
+ + "--enable WIFI,LOCATION,BLUETOOTH");
+
pw.printf("\t%s [%s] [%s]\n", COMMAND_POWER_OFF, POWER_OFF_SKIP_GARAGEMODE,
POWER_OFF_SHUTDOWN);
pw.println("\t Powers off the car.");
@@ -611,12 +636,19 @@
pw.println("\t Gets the name of the camera device CarEvsService is using for " +
"the rearview.");
+ pw.printf("\t%s true|false <PACKAGE_NAME>\n",
+ COMMAND_WATCHDOG_CONTROL_PACKAGE_KILLABLE_STATE);
+ pw.println("\t Marks PACKAGE_NAME as killable or not killable on resource overuse ");
+
pw.printf("\t%s <FOREGROUND_MODE_BYTES>\n", COMMAND_WATCHDOG_IO_SET_3P_FOREGROUND_BYTES);
pw.println("\t Sets third-party apps foreground I/O overuse threshold");
pw.printf("\t%s\n", COMMAND_WATCHDOG_IO_GET_3P_FOREGROUND_BYTES);
pw.println("\t Gets third-party apps foreground I/O overuse threshold");
+ pw.printf("\t%s enable|disable\n", COMMAND_WATCHDOG_CONTROL_PROCESS_HEALTH_CHECK);
+ pw.println("\t Enables/disables car watchdog process health check.");
+
pw.printf("\t%s [REGION_STRING]", COMMAND_DRIVING_SAFETY_SET_REGION);
pw.println("\t Set driving safety region.");
pw.println("\t Skipping REGION_STRING leads into resetting to all regions");
@@ -933,6 +965,10 @@
return definePowerPolicyGroup(args, writer);
case COMMAND_SET_POWER_POLICY_GROUP:
return setPowerPolicyGroup(args, writer);
+ case COMMAND_APPLY_CTS_VERIFIER_POWER_OFF_POLICY:
+ return applyCtsVerifierPowerOffPolicy(args, writer);
+ case COMMAND_APPLY_CTS_VERIFIER_POWER_ON_POLICY:
+ return applyCtsVerifierPowerOnPolicy(args, writer);
case COMMAND_POWER_OFF:
powerOff(args, writer);
break;
@@ -942,12 +978,18 @@
case COMMAND_GET_REARVIEW_CAMERA_ID:
getRearviewCameraId(writer);
break;
+ case COMMAND_WATCHDOG_CONTROL_PACKAGE_KILLABLE_STATE:
+ controlWatchdogPackageKillableState(args, writer);
+ break;
case COMMAND_WATCHDOG_IO_SET_3P_FOREGROUND_BYTES:
setWatchdogIoThirdPartyForegroundBytes(args, writer);
break;
case COMMAND_WATCHDOG_IO_GET_3P_FOREGROUND_BYTES:
getWatchdogIoThirdPartyForegroundBytes(writer);
break;
+ case COMMAND_WATCHDOG_CONTROL_PROCESS_HEALTH_CHECK:
+ controlWatchdogProcessHealthCheck(args, writer);
+ break;
case COMMAND_DRIVING_SAFETY_SET_REGION:
setDrivingSafetyRegion(args, writer);
break;
@@ -1994,6 +2036,29 @@
return RESULT_ERROR;
}
+ private int applyCtsVerifierPowerPolicy(String policyId, String ops, String cmdName,
+ IndentingPrintWriter writer) {
+ String[] defArgs = {"define-power-policy", policyId, ops, "WIFI,BLUETOOTH,LOCATION"};
+ mCarPowerManagementService.definePowerPolicyFromCommand(defArgs, writer);
+
+ String[] appArgs = {"apply-power-policy", policyId};
+ boolean result = mCarPowerManagementService.applyPowerPolicyFromCommand(appArgs, writer);
+ if (result) return RESULT_OK;
+
+ writer.printf("\nUsage: cmd car_service %s\n", cmdName);
+ return RESULT_ERROR;
+ }
+
+ private int applyCtsVerifierPowerOffPolicy(String[] unusedArgs, IndentingPrintWriter writer) {
+ return applyCtsVerifierPowerPolicy("cts_verifier_off", "--disable",
+ COMMAND_APPLY_CTS_VERIFIER_POWER_OFF_POLICY, writer);
+ }
+
+ private int applyCtsVerifierPowerOnPolicy(String[] unusedArgs, IndentingPrintWriter writer) {
+ return applyCtsVerifierPowerPolicy("cts_verifier_on", "--enable",
+ COMMAND_APPLY_CTS_VERIFIER_POWER_ON_POLICY, writer);
+ }
+
private void powerOff(String[] args, IndentingPrintWriter writer) {
int index = 1;
boolean skipGarageMode = false;
@@ -2127,6 +2192,23 @@
mCarEvsService.getRearviewCameraIdFromCommand());
}
+ private void controlWatchdogPackageKillableState(String[] args, IndentingPrintWriter writer) {
+ if (args.length != 3) {
+ showInvalidArguments(writer);
+ return;
+ }
+ if (!args[1].equals("true") && !args[1].equals("false")) {
+ writer.println("Failed to parse killable state argument. "
+ + "Valid arguments: killable | not-killable");
+ return;
+ }
+ int currentUserId = ActivityManager.getCurrentUser();
+ mCarWatchdogService.setKillablePackageAsUser(
+ args[2], UserHandle.of(currentUserId), args[1].equals("true"));
+ writer.printf("Set package killable state as '%s' for user '%d' and package '%s'\n",
+ args[1].equals("true") ? "killable" : "not killable", currentUserId, args[2]);
+ }
+
// Set third-party foreground I/O threshold for car watchdog
private void setWatchdogIoThirdPartyForegroundBytes(String[] args,
IndentingPrintWriter writer) {
@@ -2221,6 +2303,19 @@
.setIoOveruseConfiguration(configuration.getIoOveruseConfiguration());
}
+ private void controlWatchdogProcessHealthCheck(String[] args, IndentingPrintWriter writer) {
+ if (args.length != 2) {
+ showInvalidArguments(writer);
+ return;
+ }
+ if (!args[1].equals("enable") && !args[1].equals("disable")) {
+ writer.println("Failed to parse argument. Valid arguments: enable | disable");
+ return;
+ }
+ mCarWatchdogService.controlProcessHealthCheck(args[1].equals("disable"));
+ writer.printf("Watchdog health checking is now %sd \n", args[1]);
+ }
+
// Check if the given property is global
private static boolean isPropertyAreaTypeGlobal(@Nullable String property) {
if (property == null) {
diff --git a/service/src/com/android/car/FastPairGattServer.java b/service/src/com/android/car/FastPairGattServer.java
index 6cbaae0..b5b9881 100644
--- a/service/src/com/android/car/FastPairGattServer.java
+++ b/service/src/com/android/car/FastPairGattServer.java
@@ -86,6 +86,7 @@
private static final boolean DBG = FastPairUtils.DBG;
private static final int MAX_KEY_COUNT = 10;
private static final int KEY_LIFESPAN = 10_000;
+ private static final int INVALID = -1;
private final boolean mAutomaticPasskeyConfirmation;
private final byte[] mModelId;
@@ -95,7 +96,7 @@
private ArrayList<AccountKey> mKeys = new ArrayList<>();
private BluetoothGattServer mBluetoothGattServer;
private BluetoothManager mBluetoothManager;
- private int mPairingPasskey = -1;
+ private int mPairingPasskey = INVALID;
private int mFailureCount = 0;
private int mSuccessCount = 0;
private BluetoothGattService mFastPairService = new BluetoothGattService(
@@ -149,7 +150,9 @@
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
super.onConnectionStateChange(device, status, newState);
- Log.d(TAG, "onConnectionStateChange " + newState + "Device: " + device.toString());
+ if (DBG) {
+ Log.d(TAG, "onConnectionStateChange " + newState + "Device: " + device.toString());
+ }
if (newState == 0) {
mPairingPasskey = -1;
mSharedSecretKey = null;
@@ -219,8 +222,6 @@
.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset,
mEncryptedResponse);
processPairingKey(value);
- mBluetoothGattServer
- .notifyCharacteristicChanged(device, mPasskeyCharacteristic, false);
} else {
Log.w(TAG, "onWriteOther" + characteristic.getUuid());
@@ -249,12 +250,13 @@
Log.d(TAG, intent.getAction());
}
if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction())) {
- if (DBG) {
- Log.d(TAG, "PairingCode " + intent
- .getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, -1));
- }
mRemotePairingDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
- mPairingPasskey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, -1);
+ mPairingPasskey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, INVALID);
+ if (DBG) {
+ Log.d(TAG, "DeviceAddress: " + mRemotePairingDevice
+ + " PairingCode: " + mPairingPasskey);
+ }
+ sendPairingResponse(mPairingPasskey);
}
}
};
@@ -309,6 +311,9 @@
} catch (Exception e) {
Log.e(TAG, e.toString());
}
+ if (DBG) {
+ Log.w(TAG, "Encryption Failed, clear key");
+ }
mHandler.removeCallbacks(mClearSharedSecretKey);
mSharedSecretKey = null;
return null;
@@ -439,7 +444,7 @@
byte[] encryptedRequest = Arrays.copyOfRange(pairingRequest, 0, 16);
if (DBG) {
- Log.d(TAG, "Checking " + possibleKeys.size() + "Keys");
+ Log.d(TAG, "Checking " + possibleKeys.size() + " Keys");
}
// check all the keys for a valid pairing request
for (SecretKeySpec key : possibleKeys) {
@@ -486,7 +491,7 @@
.getRemoteDevice(remoteAddressBytes);
if (DBG) {
Log.d(TAG, "Local RPA = " + mLocalRpaDevice);
- Log.d(TAG, "Decrypted, LocalMacAddress" + localAddress + "remoteAddress"
+ Log.d(TAG, "Decrypted, LocalMacAddress: " + localAddress + " remoteAddress: "
+ reportedDevice.toString());
}
// Test that the received device address matches this devices address
@@ -494,7 +499,6 @@
if (DBG) {
Log.d(TAG, "SecretKey Validated");
}
-
// encrypt and respond to the seeker with the local public address
byte[] rawResponse = new byte[16];
new Random().nextBytes(rawResponse);
@@ -536,16 +540,23 @@
}
mRemotePairingDevice.setPairingConfirmation(true);
}
- } else {
+ } else if (mPairingPasskey != INVALID) {
Log.w(TAG, "Passkeys don't match, rejecting");
mRemotePairingDevice.setPairingConfirmation(false);
}
+ return true;
+ }
+ void sendPairingResponse(int passkey) {
+ if (!isConnected()) return;
+ if (DBG) {
+ Log.d(TAG, "sendPairingResponse + " + passkey);
+ }
// Send an encrypted response to the seeker with the Bluetooth passkey as required
byte[] decryptedResponse = new byte[16];
new Random().nextBytes(decryptedResponse);
ByteBuffer pairingPasskeyBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(
- mPairingPasskey);
+ passkey);
decryptedResponse[0] = 0x3;
decryptedResponse[1] = pairingPasskeyBytes.get(1);
decryptedResponse[2] = pairingPasskeyBytes.get(2);
@@ -553,11 +564,11 @@
mEncryptedResponse = encrypt(decryptedResponse);
if (mEncryptedResponse == null) {
- return false;
+ return;
}
mPasskeyCharacteristic.setValue(mEncryptedResponse);
- return true;
-
+ mBluetoothGattServer
+ .notifyCharacteristicChanged(mRemoteGattDevice, mPasskeyCharacteristic, false);
}
/**
diff --git a/service/src/com/android/car/FastPairProvider.java b/service/src/com/android/car/FastPairProvider.java
index dc63058..90529d4 100644
--- a/service/src/com/android/car/FastPairProvider.java
+++ b/service/src/com/android/car/FastPairProvider.java
@@ -18,6 +18,7 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -48,6 +49,7 @@
private final Context mContext;
private boolean mStarted;
private int mScanMode;
+ private BluetoothAdapter mBluetoothAdapter;
private FastPairAdvertiser mFastPairModelAdvertiser;
private FastPairAdvertiser mFastPairAccountAdvertiser;
private FastPairGattServer mFastPairGattServer;
@@ -93,7 +95,11 @@
Slog.d(TAG, "NewScanMode = " + mScanMode);
}
if (mScanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
- advertiseModelId();
+ if (mBluetoothAdapter.isDiscovering()) {
+ advertiseModelId();
+ } else {
+ stopAdvertising();
+ }
} else if (mScanMode == BluetoothAdapter.SCAN_MODE_CONNECTABLE
&& mFastPairGattServer != null
&& !mFastPairGattServer.isConnected()) {
@@ -130,6 +136,7 @@
mModelId = res.getInteger(R.integer.fastPairModelId);
mAntiSpoofKey = res.getString(R.string.fastPairAntiSpoofKey);
mAutomaticAcceptance = res.getBoolean(R.bool.fastPairAutomaticAcceptance);
+ mBluetoothAdapter = mContext.getSystemService(BluetoothManager.class).getAdapter();
}
/**
@@ -161,6 +168,20 @@
}
}
+ void stopAdvertising() {
+ mFastPairAdvertiserHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mFastPairAccountAdvertiser != null) {
+ mFastPairAccountAdvertiser.stopAdvertising();
+ }
+ if (mFastPairModelAdvertiser != null) {
+ mFastPairModelAdvertiser.stopAdvertising();
+ }
+ }
+ });
+ }
+
void advertiseModelId() {
mFastPairAdvertiserHandler.post(new Runnable() {
@Override
@@ -195,7 +216,6 @@
mFastPairAccountAdvertiser.advertiseAccountKeys();
}
});
-
}
void startGatt() {
diff --git a/service/src/com/android/car/ICarImpl.java b/service/src/com/android/car/ICarImpl.java
index df600de..4b7c75d 100644
--- a/service/src/com/android/car/ICarImpl.java
+++ b/service/src/com/android/car/ICarImpl.java
@@ -54,6 +54,7 @@
import com.android.car.admin.CarDevicePolicyService;
import com.android.car.admin.FactoryResetActivity;
+import com.android.car.am.CarActivityService;
import com.android.car.am.FixedActivityService;
import com.android.car.audio.CarAudioService;
import com.android.car.cluster.ClusterHomeService;
@@ -136,6 +137,7 @@
private final ClusterHomeService mClusterHomeService;
private final CarEvsService mCarEvsService;
private final CarTelemetryService mCarTelemetryService;
+ private final CarActivityService mCarActivityService;
private final CarServiceBase[] mAllServices;
@@ -354,10 +356,12 @@
}
if (mFeatureController.isFeatureEnabled(Car.CAR_TELEMETRY_SERVICE)) {
- mCarTelemetryService = new CarTelemetryService(serviceContext);
+ mCarTelemetryService = new CarTelemetryService(serviceContext, mCarPropertyService);
} else {
mCarTelemetryService = null;
}
+ mCarActivityService = constructWithTrace(t, CarActivityService.class,
+ () -> new CarActivityService(serviceContext));
// Be careful with order. Service depending on other service should be inited later.
List<CarServiceBase> allServices = new ArrayList<>();
@@ -394,6 +398,7 @@
addServiceIfNonNull(allServices, mClusterHomeService);
addServiceIfNonNull(allServices, mCarEvsService);
addServiceIfNonNull(allServices, mCarTelemetryService);
+ allServices.add(mCarActivityService);
// Always put mCarExperimentalFeatureServiceController in last.
addServiceIfNonNull(allServices, mCarExperimentalFeatureServiceController);
@@ -463,6 +468,7 @@
mSystemInterface.setCarServiceHelper(carServiceHelper);
mCarOccupantZoneService.setCarServiceHelper(carServiceHelper);
mCarUserService.setCarServiceHelper(carServiceHelper);
+ mCarActivityService.setICarServiceHelper(carServiceHelper);
bundle = new Bundle();
bundle.putBinder(ICAR_SYSTEM_SERVER_CLIENT, mICarSystemServerClientImpl.asBinder());
@@ -631,6 +637,8 @@
return mCarEvsService;
case Car.CAR_TELEMETRY_SERVICE:
return mCarTelemetryService;
+ case Car.CAR_ACTIVITY_SERVICE:
+ return mCarActivityService;
default:
IBinder service = null;
if (mCarExperimentalFeatureServiceController != null) {
diff --git a/service/src/com/android/car/InputCaptureClientController.java b/service/src/com/android/car/InputCaptureClientController.java
index 7719b24..5f9ebfa 100644
--- a/service/src/com/android/car/InputCaptureClientController.java
+++ b/service/src/com/android/car/InputCaptureClientController.java
@@ -246,7 +246,8 @@
public int requestInputEventCapture(ICarInputCallback callback,
@DisplayTypeEnum int targetDisplayType,
int[] inputTypes, int requestFlags) {
- ICarImpl.assertPermission(mContext, Car.PERMISSION_CAR_MONITOR_INPUT);
+ ICarImpl.assertAnyPermission(mContext, Car.PERMISSION_CAR_MONITOR_INPUT,
+ android.Manifest.permission.MONITOR_INPUT);
Preconditions.checkArgument(SUPPORTED_DISPLAY_TYPES.contains(targetDisplayType),
"Display not supported yet:" + targetDisplayType);
diff --git a/service/src/com/android/car/am/CarActivityService.java b/service/src/com/android/car/am/CarActivityService.java
new file mode 100644
index 0000000..61d309e
--- /dev/null
+++ b/service/src/com/android/car/am/CarActivityService.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2021 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.am;
+
+import android.app.ActivityManager;
+import android.car.Car;
+import android.car.app.CarActivityManager;
+import android.car.app.ICarActivityService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.IndentingPrintWriter;
+
+import com.android.car.CarServiceBase;
+import com.android.car.internal.ICarServiceHelper;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Service responsible for Activities in Car.
+ */
+public final class CarActivityService extends ICarActivityService.Stub
+ implements CarServiceBase {
+
+ private final Context mContext;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ ICarServiceHelper mICarServiceHelper;
+
+ public CarActivityService(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void init() {}
+
+ @Override
+ public void release() {}
+
+ /**
+ * Sets {@code ICarServiceHelper}.
+ */
+ public void setICarServiceHelper(ICarServiceHelper helper) {
+ synchronized (mLock) {
+ mICarServiceHelper = helper;
+ }
+ }
+
+ @Override
+ public int setPersistentActivity(ComponentName activity, int displayId, int featureId) throws
+ RemoteException {
+ if (PackageManager.PERMISSION_GRANTED != mContext.checkCallingOrSelfPermission(
+ Car.PERMISSION_CONTROL_CAR_APP_LAUNCH)) {
+ throw new SecurityException("Requires " + Car.PERMISSION_CONTROL_CAR_APP_LAUNCH);
+ }
+ int caller = getCaller();
+ if (caller != UserHandle.USER_SYSTEM && caller != ActivityManager.getCurrentUser()) {
+ return CarActivityManager.RESULT_INVALID_USER;
+ }
+
+ ICarServiceHelper helper;
+ synchronized (mLock) {
+ helper = mICarServiceHelper;
+ }
+ if (helper == null) {
+ throw new IllegalStateException("ICarServiceHelper isn't connected yet");
+ }
+ return helper.setPersistentActivity(activity, displayId, featureId);
+ }
+
+ @VisibleForTesting
+ int getCaller() { // Non static for mocking.
+ return UserHandle.getUserId(Binder.getCallingUid());
+ }
+
+ @Override
+ public void dump(IndentingPrintWriter writer) {}
+}
diff --git a/service/src/com/android/car/am/FixedActivityService.java b/service/src/com/android/car/am/FixedActivityService.java
index bbc76ff..c969fc1 100644
--- a/service/src/com/android/car/am/FixedActivityService.java
+++ b/service/src/com/android/car/am/FixedActivityService.java
@@ -61,6 +61,7 @@
import com.android.car.R;
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.util.List;
@@ -417,12 +418,20 @@
return false;
}
for (int i = mRunningActivities.size() - 1; i >= 0; i--) {
+ int displayIdForActivity = mRunningActivities.keyAt(i);
+ Display display = mDm.getDisplay(displayIdForActivity);
+ if (display == null) {
+ Slog.e(TAG_AM, "Stop fixed activity for non-available display"
+ + displayIdForActivity);
+ mRunningActivities.removeAt(i);
+ continue;
+ }
+
RunningActivityInfo activityInfo = mRunningActivities.valueAt(i);
activityInfo.isVisible = false;
if (isUserAllowedToLaunchActivity(activityInfo.userId)) {
continue;
}
- final int displayIdForActivity = mRunningActivities.keyAt(i);
if (activityInfo.taskId != INVALID_TASK_ID) {
Slog.i(TAG_AM, "Finishing fixed activity on user switching:"
+ activityInfo);
@@ -432,12 +441,6 @@
Slog.e(TAG_AM, "remote exception from AM", e);
}
CarServiceUtils.runOnMain(() -> {
- Display display = mDm.getDisplay(displayIdForActivity);
- if (display == null) {
- Slog.e(TAG_AM, "Display not available, cannot launnch window:"
- + displayIdForActivity);
- return;
- }
Presentation p = new Presentation(mContext, display,
android.R.style.Theme_Black_NoTitleBar_Fullscreen,
// TYPE_PRESENTATION can't be used in the internal display.
@@ -546,7 +549,8 @@
}
}
- private void launchIfNecessary() {
+ @VisibleForTesting
+ void launchIfNecessary() {
launchIfNecessary(Display.INVALID_DISPLAY);
}
@@ -609,6 +613,13 @@
return true;
}
+ @VisibleForTesting
+ RunningActivityInfo getRunningFixedActivity(int displayId) {
+ synchronized (mLock) {
+ return mRunningActivities.get(displayId);
+ }
+ }
+
/**
* Checks {@link InstrumentClusterRenderingService#startFixedActivityModeForDisplayAndUser(
* Intent, ActivityOptions, int)}
diff --git a/service/src/com/android/car/audio/CarAudioPolicyVolumeCallback.java b/service/src/com/android/car/audio/CarAudioPolicyVolumeCallback.java
index 6fccbf9..14b1a6f 100644
--- a/service/src/com/android/car/audio/CarAudioPolicyVolumeCallback.java
+++ b/service/src/com/android/car/audio/CarAudioPolicyVolumeCallback.java
@@ -31,6 +31,8 @@
import com.android.car.audio.CarAudioContext.AudioContext;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
+
import java.util.Objects;
@@ -67,25 +69,32 @@
int zoneId = PRIMARY_AUDIO_ZONE;
int groupId = mCarAudioService.getVolumeGroupIdForAudioContext(zoneId, suggestedContext);
+ boolean isMuted = isMuted(zoneId, groupId);
- if (Log.isLoggable(TAG_AUDIO, VERBOSE)) {
- Slog.v(TAG_AUDIO, "onVolumeAdjustment: "
+ if (Slogf.isLoggable(TAG_AUDIO, VERBOSE)) {
+ Slogf.v(TAG_AUDIO, "onVolumeAdjustment: "
+ AudioManager.adjustToString(adjustment) + " suggested audio context: "
+ CarAudioContext.toString(suggestedContext) + " suggested volume group: "
- + groupId);
+ + groupId + " is muted " + isMuted);
}
final int currentVolume = mCarAudioService.getGroupVolume(zoneId, groupId);
final int flags = AudioManager.FLAG_FROM_KEY | AudioManager.FLAG_SHOW_UI;
+ int minGain = mCarAudioService.getGroupMinVolume(zoneId, groupId);
switch (adjustment) {
case AudioManager.ADJUST_LOWER:
- int minValue = Math.max(currentVolume - 1,
- mCarAudioService.getGroupMinVolume(zoneId, groupId));
+ int minValue = Math.max(currentVolume - 1, minGain);
+ if (isMuted) {
+ minValue = minGain;
+ }
mCarAudioService.setGroupVolume(zoneId, groupId, minValue, flags);
break;
case AudioManager.ADJUST_RAISE:
int maxValue = Math.min(currentVolume + 1,
mCarAudioService.getGroupMaxVolume(zoneId, groupId));
+ if (isMuted) {
+ maxValue = minGain;
+ }
mCarAudioService.setGroupVolume(zoneId, groupId, maxValue, flags);
break;
case AudioManager.ADJUST_MUTE:
@@ -93,7 +102,7 @@
setMute(adjustment == AudioManager.ADJUST_MUTE, groupId, flags);
break;
case AudioManager.ADJUST_TOGGLE_MUTE:
- toggleMute(groupId, flags);
+ setMute(!isMuted, groupId, flags);
break;
case AudioManager.ADJUST_SAME:
default:
@@ -101,13 +110,11 @@
}
}
- private void toggleMute(int groupId, int flags) {
+ private boolean isMuted(int zoneId, int groupId) {
if (mUseCarVolumeGroupMuting) {
- setMute(!mCarAudioService.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId), groupId,
- flags);
- return;
+ return mCarAudioService.isVolumeGroupMuted(zoneId, groupId);
}
- setMute(!mAudioManager.isMasterMute(), groupId, flags);
+ return mAudioManager.isMasterMute();
}
private void setMute(boolean mute, int groupId, int flags) {
diff --git a/service/src/com/android/car/audio/CarAudioService.java b/service/src/com/android/car/audio/CarAudioService.java
index 62be1fd..1673f61 100644
--- a/service/src/com/android/car/audio/CarAudioService.java
+++ b/service/src/com/android/car/audio/CarAudioService.java
@@ -69,7 +69,6 @@
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
-import android.view.KeyEvent;
import com.android.car.CarLocalServices;
import com.android.car.CarLog;
@@ -436,25 +435,8 @@
void setMasterMute(boolean mute, int flags) {
mAudioManager.setMasterMute(mute, flags);
- // Master Mute only appliers to primary zone
+ // Master Mute only applies to primary zone
callbackMasterMuteChange(PRIMARY_AUDIO_ZONE, flags);
-
- // When the master mute is turned ON, we want the playing app to get a "pause" command.
- // When the volume is unmuted, we want to resume playback.
- int keycode = mute ? KeyEvent.KEYCODE_MEDIA_PAUSE : KeyEvent.KEYCODE_MEDIA_PLAY;
-
- dispatchMediaKeyEvent(keycode);
- }
-
- private void dispatchMediaKeyEvent(int keycode) {
- long currentTime = SystemClock.uptimeMillis();
- KeyEvent keyDown = new KeyEvent(/* downTime= */ currentTime, /* eventTime= */ currentTime,
- KeyEvent.ACTION_DOWN, keycode, /* repeat= */ 0);
- mAudioManager.dispatchMediaKeyEvent(keyDown);
-
- KeyEvent keyUp = new KeyEvent(/* downTime= */ currentTime, /* eventTime= */ currentTime,
- KeyEvent.ACTION_UP, keycode, /* repeat= */ 0);
- mAudioManager.dispatchMediaKeyEvent(keyUp);
}
void callbackMasterMuteChange(int zoneId, int flags) {
@@ -550,9 +532,9 @@
AudioManager.GET_DEVICES_INPUTS);
}
+ @GuardedBy("mImplLock")
private SparseArray<CarAudioZone> loadCarAudioConfigurationLocked(
- List<CarAudioDeviceInfo> carAudioDeviceInfos) {
- AudioDeviceInfo[] inputDevices = getAllInputDevices();
+ List<CarAudioDeviceInfo> carAudioDeviceInfos, AudioDeviceInfo[] inputDevices) {
try (InputStream inputStream = new FileInputStream(mCarAudioConfigurationPath)) {
CarAudioZonesHelper zonesHelper = new CarAudioZonesHelper(mCarAudioSettings,
inputStream, carAudioDeviceInfos, inputDevices, mUseCarVolumeGroupMuting);
@@ -564,8 +546,9 @@
}
}
+ @GuardedBy("mImplLock")
private SparseArray<CarAudioZone> loadVolumeGroupConfigurationWithAudioControlLocked(
- List<CarAudioDeviceInfo> carAudioDeviceInfos) {
+ List<CarAudioDeviceInfo> carAudioDeviceInfos, AudioDeviceInfo[] inputDevices) {
AudioControlWrapper audioControlWrapper = getAudioControlWrapperLocked();
if (!(audioControlWrapper instanceof AudioControlWrapperV1)) {
throw new IllegalStateException(
@@ -574,20 +557,22 @@
}
CarAudioZonesHelperLegacy legacyHelper = new CarAudioZonesHelperLegacy(mContext,
R.xml.car_volume_groups, carAudioDeviceInfos,
- (AudioControlWrapperV1) audioControlWrapper, mCarAudioSettings);
+ (AudioControlWrapperV1) audioControlWrapper, mCarAudioSettings, inputDevices);
return legacyHelper.loadAudioZones();
}
@GuardedBy("mImplLock")
private void loadCarAudioZonesLocked() {
List<CarAudioDeviceInfo> carAudioDeviceInfos = generateCarAudioDeviceInfos();
+ AudioDeviceInfo[] inputDevices = getAllInputDevices();
mCarAudioConfigurationPath = getAudioConfigurationPath();
if (mCarAudioConfigurationPath != null) {
- mCarAudioZones = loadCarAudioConfigurationLocked(carAudioDeviceInfos);
+ mCarAudioZones = loadCarAudioConfigurationLocked(carAudioDeviceInfos, inputDevices);
} else {
- mCarAudioZones = loadVolumeGroupConfigurationWithAudioControlLocked(
- carAudioDeviceInfos);
+ mCarAudioZones =
+ loadVolumeGroupConfigurationWithAudioControlLocked(carAudioDeviceInfos,
+ inputDevices);
}
CarAudioZonesValidator.validate(mCarAudioZones);
diff --git a/service/src/com/android/car/audio/CarAudioUtils.java b/service/src/com/android/car/audio/CarAudioUtils.java
index b787efb..ad74587 100644
--- a/service/src/com/android/car/audio/CarAudioUtils.java
+++ b/service/src/com/android/car/audio/CarAudioUtils.java
@@ -16,6 +16,10 @@
package com.android.car.audio;
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+
+import android.media.AudioDeviceInfo;
+
final class CarAudioUtils {
private CarAudioUtils() {
}
@@ -23,4 +27,8 @@
static boolean hasExpired(long startTimeMs, long currentTimeMs, int timeoutMs) {
return (currentTimeMs - startTimeMs) > timeoutMs;
}
+
+ static boolean isMicrophoneInputDevice(AudioDeviceInfo device) {
+ return device.getType() == TYPE_BUILTIN_MIC;
+ }
}
diff --git a/service/src/com/android/car/audio/CarAudioZonesHelper.java b/service/src/com/android/car/audio/CarAudioZonesHelper.java
index 7956ea2..2e25325 100644
--- a/service/src/com/android/car/audio/CarAudioZonesHelper.java
+++ b/service/src/com/android/car/audio/CarAudioZonesHelper.java
@@ -17,10 +17,14 @@
import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static com.android.car.audio.CarAudioUtils.isMicrophoneInputDevice;
+
import android.annotation.NonNull;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.Xml;
@@ -35,8 +39,6 @@
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -73,7 +75,7 @@
private static final Map<String, Integer> CONTEXT_NAME_MAP;
static {
- CONTEXT_NAME_MAP = new HashMap<>(CarAudioContext.CONTEXTS.length);
+ CONTEXT_NAME_MAP = new ArrayMap<>(CarAudioContext.CONTEXTS.length);
CONTEXT_NAME_MAP.put("music", CarAudioContext.MUSIC);
CONTEXT_NAME_MAP.put("navigation", CarAudioContext.NAVIGATION);
CONTEXT_NAME_MAP.put("voice_command", CarAudioContext.VOICE_COMMAND);
@@ -128,11 +130,11 @@
private final CarAudioSettings mCarAudioSettings;
private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo;
- private final Map<String, AudioDeviceInfo> mAddressToInputAudioDeviceInfo;
+ private final Map<String, AudioDeviceInfo> mAddressToInputAudioDeviceInfoForAllInputDevices;
private final InputStream mInputStream;
private final SparseIntArray mZoneIdToOccupantZoneIdMapping;
private final Set<Integer> mAudioZoneIds;
- private final Set<String> mInputAudioDevices;
+ private final Set<String> mAssignedInputAudioDevices;
private final boolean mUseCarVolumeGroupMute;
private int mNextSecondaryZoneId;
@@ -152,12 +154,12 @@
Objects.requireNonNull(inputDeviceInfo);
mAddressToCarAudioDeviceInfo = CarAudioZonesHelper.generateAddressToInfoMap(
carAudioDeviceInfos);
- mAddressToInputAudioDeviceInfo =
+ mAddressToInputAudioDeviceInfoForAllInputDevices =
CarAudioZonesHelper.generateAddressToInputAudioDeviceInfoMap(inputDeviceInfo);
mNextSecondaryZoneId = PRIMARY_AUDIO_ZONE + 1;
mZoneIdToOccupantZoneIdMapping = new SparseIntArray();
- mAudioZoneIds = new HashSet<>();
- mInputAudioDevices = new HashSet<>();
+ mAudioZoneIds = new ArraySet<>();
+ mAssignedInputAudioDevices = new ArraySet<>();
mUseCarVolumeGroupMute = useCarVolumeGroupMute;
}
@@ -178,8 +180,8 @@
private static Map<String, AudioDeviceInfo> generateAddressToInputAudioDeviceInfoMap(
@NonNull AudioDeviceInfo[] inputAudioDeviceInfos) {
- HashMap<String, AudioDeviceInfo> deviceAddressToInputDeviceMap =
- new HashMap<>(inputAudioDeviceInfos.length);
+ Map<String, AudioDeviceInfo> deviceAddressToInputDeviceMap =
+ new ArrayMap<>(inputAudioDeviceInfos.length);
for (int i = 0; i < inputAudioDeviceInfos.length; ++i) {
AudioDeviceInfo device = inputAudioDeviceInfos[i];
if (device.isSource()) {
@@ -238,9 +240,20 @@
}
verifyPrimaryZonePresent(carAudioZones);
+ addRemainingMicrophonesToPrimaryZone(carAudioZones);
return carAudioZones;
}
+ private void addRemainingMicrophonesToPrimaryZone(SparseArray<CarAudioZone> carAudioZones) {
+ CarAudioZone primaryAudioZone = carAudioZones.get(PRIMARY_AUDIO_ZONE);
+ for (AudioDeviceInfo info : mAddressToInputAudioDeviceInfoForAllInputDevices.values()) {
+ if (!mAssignedInputAudioDevices.contains(info.getAddress())
+ && isMicrophoneInputDevice(info)) {
+ primaryAudioZone.addInputAudioDevice(new AudioDeviceAttributes(info));
+ }
+ }
+ }
+
private void verifyOnlyOnePrimaryZone(CarAudioZone newZone, SparseArray<CarAudioZone> zones) {
if (newZone.getId() == PRIMARY_AUDIO_ZONE && zones.contains(PRIMARY_AUDIO_ZONE)) {
throw new RuntimeException("More than one zone parsed with primary audio zone ID: "
@@ -347,7 +360,8 @@
String audioDeviceAddress =
parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
validateInputAudioDeviceAddress(audioDeviceAddress);
- AudioDeviceInfo info = mAddressToInputAudioDeviceInfo.get(audioDeviceAddress);
+ AudioDeviceInfo info =
+ mAddressToInputAudioDeviceInfoForAllInputDevices.get(audioDeviceAddress);
Preconditions.checkArgument(info != null,
"%s %s of %s does not exist, add input device to"
+ " audio_policy_configuration.xml.",
@@ -364,11 +378,11 @@
Preconditions.checkArgument(!audioDeviceAddress.isEmpty(),
"%s %s attribute can not be empty.",
TAG_INPUT_DEVICE, ATTR_DEVICE_ADDRESS);
- if (mInputAudioDevices.contains(audioDeviceAddress)) {
+ if (mAssignedInputAudioDevices.contains(audioDeviceAddress)) {
throw new IllegalArgumentException(TAG_INPUT_DEVICE + " " + audioDeviceAddress
+ " repeats, " + TAG_INPUT_DEVICES + " can not repeat.");
}
- mInputAudioDevices.add(audioDeviceAddress);
+ mAssignedInputAudioDevices.add(audioDeviceAddress);
}
private void validateOccupantZoneIdIsUnique(int occupantZoneId) {
diff --git a/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java b/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
index 6bbd6c9..7ce2538 100644
--- a/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
+++ b/service/src/com/android/car/audio/CarAudioZonesHelperLegacy.java
@@ -17,6 +17,7 @@
import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static com.android.car.audio.CarAudioUtils.isMicrophoneInputDevice;
import static com.android.car.audio.CarAudioZonesHelper.LEGACY_CONTEXTS;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DEPRECATED_CODE;
@@ -25,6 +26,8 @@
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
import android.util.AttributeSet;
import android.util.Slog;
import android.util.SparseArray;
@@ -62,15 +65,21 @@
private final SparseIntArray mLegacyAudioContextToBus;
private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfo;
private final CarAudioSettings mCarAudioSettings;
+ private final AudioDeviceInfo[] mInputDevices;
- CarAudioZonesHelperLegacy(@NonNull Context context, @XmlRes int xmlConfiguration,
+ CarAudioZonesHelperLegacy(@NonNull Context context, @XmlRes int xmlConfiguration,
@NonNull List<CarAudioDeviceInfo> carAudioDeviceInfos,
@NonNull AudioControlWrapperV1 audioControlWrapper,
- @NonNull CarAudioSettings carAudioSettings) {
- Objects.requireNonNull(context);
- Objects.requireNonNull(carAudioDeviceInfos);
- Objects.requireNonNull(audioControlWrapper);
- mCarAudioSettings = Objects.requireNonNull(carAudioSettings);
+ @NonNull CarAudioSettings carAudioSettings,
+ AudioDeviceInfo[] inputDevices) {
+ Objects.requireNonNull(context, "Context must not be null.");
+ Objects.requireNonNull(carAudioDeviceInfos,
+ "Car Audio Device Info must not be null.");
+ Objects.requireNonNull(audioControlWrapper,
+ "Car Audio Control must not be null.");
+ Objects.requireNonNull(inputDevices, "Input Devices must not be null.");
+ mCarAudioSettings = Objects.requireNonNull(carAudioSettings,
+ "Car Audio Settings can not be null.");
mContext = context;
mXmlConfiguration = xmlConfiguration;
mBusToCarAudioDeviceInfo =
@@ -78,6 +87,7 @@
mLegacyAudioContextToBus =
loadBusesForLegacyContexts(audioControlWrapper);
+ mInputDevices = inputDevices;
}
/* Loads mapping from {@link CarAudioContext} values to bus numbers
@@ -135,7 +145,9 @@
zone.addVolumeGroup(volumeGroup);
}
SparseArray<CarAudioZone> carAudioZones = new SparseArray<>();
+ addMicrophonesToPrimaryZone(zone);
carAudioZones.put(PRIMARY_AUDIO_ZONE, zone);
+
return carAudioZones;
}
@@ -221,6 +233,15 @@
return contexts;
}
+ private void addMicrophonesToPrimaryZone(CarAudioZone primaryAudioZone) {
+ for (int index = 0; index < mInputDevices.length; index++) {
+ AudioDeviceInfo info = mInputDevices[index];
+ if (isMicrophoneInputDevice(info)) {
+ primaryAudioZone.addInputAudioDevice(new AudioDeviceAttributes(info));
+ }
+ }
+ }
+
/**
* Parse device address. Expected format is BUS%d_%s, address, usage hint
*
diff --git a/service/src/com/android/car/audio/CarAudioZonesValidator.java b/service/src/com/android/car/audio/CarAudioZonesValidator.java
index 5fe495d..6b7bbb9 100644
--- a/service/src/com/android/car/audio/CarAudioZonesValidator.java
+++ b/service/src/com/android/car/audio/CarAudioZonesValidator.java
@@ -16,9 +16,16 @@
package com.android.car.audio;
+import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+
+import android.media.AudioDeviceAttributes;
import android.util.SparseArray;
+import com.android.internal.util.Preconditions;
+
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
final class CarAudioZonesValidator {
@@ -29,6 +36,20 @@
validateAtLeastOneZoneDefined(carAudioZones);
validateVolumeGroupsForEachZone(carAudioZones);
validateEachAddressAppearsAtMostOnce(carAudioZones);
+ validatePrimaryZoneHasInputDevice(carAudioZones);
+ }
+
+ private static void validatePrimaryZoneHasInputDevice(SparseArray<CarAudioZone> carAudioZones) {
+ CarAudioZone primaryZone = carAudioZones.get(PRIMARY_AUDIO_ZONE);
+ List<AudioDeviceAttributes> devices = primaryZone.getInputAudioDevices();
+ Preconditions.checkCollectionNotEmpty(devices, "Primary Zone Input Devices");
+ for (int index = 0; index < devices.size(); index++) {
+ AudioDeviceAttributes device = devices.get(index);
+ if (device.getType() == TYPE_BUILTIN_MIC) {
+ return;
+ }
+ }
+ throw new RuntimeException("Primary Zone must have at least one microphone input device");
}
private static void validateAtLeastOneZoneDefined(SparseArray<CarAudioZone> carAudioZones) {
diff --git a/service/src/com/android/car/audio/CarDuckingUtils.java b/service/src/com/android/car/audio/CarDuckingUtils.java
index f386d62..e0998fc 100644
--- a/service/src/com/android/car/audio/CarDuckingUtils.java
+++ b/service/src/com/android/car/audio/CarDuckingUtils.java
@@ -126,6 +126,7 @@
static List<String> getAddressesToDuck(int[] usages, CarAudioZone zone) {
Set<Integer> uniqueContexts = CarAudioContext.getUniqueContextsForUsages(usages);
+ uniqueContexts.remove(INVALID);
Set<Integer> contextsToDuck = getContextsToDuck(uniqueContexts);
Set<String> addressesToDuck = getAddressesForContexts(contextsToDuck, zone);
diff --git a/service/src/com/android/car/audio/CarVolumeGroup.java b/service/src/com/android/car/audio/CarVolumeGroup.java
index 9c40725..8e98fea 100644
--- a/service/src/com/android/car/audio/CarVolumeGroup.java
+++ b/service/src/com/android/car/audio/CarVolumeGroup.java
@@ -150,30 +150,43 @@
int getCurrentGainIndex() {
synchronized (mLock) {
- return mCurrentGainIndex;
+ if (mIsMuted) {
+ return getIndexForGain(mMinGain);
+ }
+ return getCurrentGainIndexLocked();
}
}
+ private int getCurrentGainIndexLocked() {
+ return mCurrentGainIndex;
+ }
+
/**
* Sets the gain on this group, gain will be set on all devices within volume group.
*/
void setCurrentGainIndex(int gainIndex) {
- int gainInMillibels = getGainForIndex(gainIndex);
Preconditions.checkArgument(isValidGainIndex(gainIndex),
- "Gain out of range (%d:%d) %d index %d", mMinGain, mMaxGain,
- gainInMillibels, gainIndex);
+ "Gain out of range (%d:%d) index %d", mMinGain, mMaxGain, gainIndex);
synchronized (mLock) {
- for (String address : mAddressToCarAudioDeviceInfo.keySet()) {
- CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
- info.setCurrentGain(gainInMillibels);
+ if (mIsMuted) {
+ setMuteLocked(false);
}
-
- mCurrentGainIndex = gainIndex;
-
- storeGainIndexForUserLocked(mCurrentGainIndex, mUserId);
+ setCurrentGainIndexLocked(gainIndex);
}
}
+ private void setCurrentGainIndexLocked(int gainIndex) {
+ int gainInMillibels = getGainForIndex(gainIndex);
+ for (String address : mAddressToCarAudioDeviceInfo.keySet()) {
+ CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
+ info.setCurrentGain(gainInMillibels);
+ }
+
+ mCurrentGainIndex = gainIndex;
+
+ storeGainIndexForUserLocked(mCurrentGainIndex, mUserId);
+ }
+
@Nullable
AudioDevicePort getAudioDevicePortForContext(int carAudioContext) {
final String address = mContextToAddress.get(carAudioContext);
@@ -233,18 +246,22 @@
updateUserIdLocked(userId);
//Update the current gain index
updateCurrentGainIndexLocked();
+ setCurrentGainIndexLocked(getCurrentGainIndexLocked());
//Reset devices with current gain index
updateGroupMuteLocked();
}
- setCurrentGainIndex(getCurrentGainIndex());
}
void setMute(boolean mute) {
synchronized (mLock) {
- mIsMuted = mute;
- if (mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) {
- mSettingsManager.storeVolumeGroupMuteForUser(mUserId, mZoneId, mId, mute);
- }
+ setMuteLocked(mute);
+ }
+ }
+
+ void setMuteLocked(boolean mute) {
+ mIsMuted = mute;
+ if (mSettingsManager.isPersistVolumeGroupMuteEnabled(mUserId)) {
+ mSettingsManager.storeVolumeGroupMuteForUser(mUserId, mZoneId, mId, mute);
}
}
diff --git a/service/src/com/android/car/cluster/ClusterHomeService.java b/service/src/com/android/car/cluster/ClusterHomeService.java
index b032d93..7592d60 100644
--- a/service/src/com/android/car/cluster/ClusterHomeService.java
+++ b/service/src/com/android/car/cluster/ClusterHomeService.java
@@ -29,8 +29,9 @@
import android.car.ICarOccupantZoneCallback;
import android.car.cluster.ClusterHomeManager;
import android.car.cluster.ClusterState;
-import android.car.cluster.IClusterHomeCallback;
import android.car.cluster.IClusterHomeService;
+import android.car.cluster.IClusterNavigationStateListener;
+import android.car.cluster.IClusterStateListener;
import android.car.cluster.navigation.NavigationState.NavigationStateProto;
import android.car.navigation.CarNavigationInstrumentCluster;
import android.content.ComponentName;
@@ -84,8 +85,12 @@
private Insets mInsets = Insets.NONE;
private int mUiType = ClusterHomeManager.UI_TYPE_CLUSTER_HOME;
private Intent mLastIntent;
+ private int mLastIntentUserId = UserHandle.USER_SYSTEM;
- private final RemoteCallbackList<IClusterHomeCallback> mClientCallbacks =
+ private final RemoteCallbackList<IClusterStateListener> mClientListeners =
+ new RemoteCallbackList<>();
+
+ private final RemoteCallbackList<IClusterNavigationStateListener> mClientNavigationListeners =
new RemoteCallbackList<>();
public ClusterHomeService(Context context, ClusterHalService clusterHalService,
@@ -126,6 +131,7 @@
private void initClusterDisplay() {
int clusterDisplayId = mOccupantZoneService.getDisplayIdForDriver(
CarOccupantZoneManager.DISPLAY_TYPE_INSTRUMENT_CLUSTER);
+ Slogf.d(TAG, "initClusterDisplay: displayId=%d", clusterDisplayId);
if (clusterDisplayId == Display.INVALID_DISPLAY) {
Slogf.i(TAG, "No cluster display is defined");
}
@@ -152,7 +158,7 @@
ActivityOptions activityOptions = ActivityOptions.makeBasic()
.setLaunchDisplayId(clusterDisplayId);
mFixedActivityService.startFixedActivityModeForDisplayAndUser(
- mLastIntent, activityOptions, clusterDisplayId, UserHandle.USER_SYSTEM);
+ mLastIntent, activityOptions, clusterDisplayId, mLastIntentUserId);
}
private final ICarOccupantZoneCallback mOccupantZoneCallback =
@@ -171,7 +177,8 @@
mOccupantZoneService.unregisterCallback(mOccupantZoneCallback);
mClusterHalService.setCallback(null);
mClusterNavigationService.setClusterServiceCallback(null);
- mClientCallbacks.kill();
+ mClientListeners.kill();
+ mClientNavigationListeners.kill();
}
@Override
@@ -214,16 +221,16 @@
private void sendDisplayState(int changes) {
ClusterState state = createClusterState();
- int n = mClientCallbacks.beginBroadcast();
+ int n = mClientListeners.beginBroadcast();
for (int i = 0; i < n; i++) {
- IClusterHomeCallback callback = mClientCallbacks.getBroadcastItem(i);
+ IClusterStateListener callback = mClientListeners.getBroadcastItem(i);
try {
callback.onClusterStateChanged(state, changes);
} catch (RemoteException ignores) {
// ignore
}
}
- mClientCallbacks.finishBroadcast();
+ mClientListeners.finishBroadcast();
}
// ClusterNavigationServiceCallback starts
@@ -235,16 +242,17 @@
}
private void sendNavigationState(byte[] protoBytes) {
- final int n = mClientCallbacks.beginBroadcast();
+ final int n = mClientNavigationListeners.beginBroadcast();
for (int i = 0; i < n; i++) {
- IClusterHomeCallback callback = mClientCallbacks.getBroadcastItem(i);
+ IClusterNavigationStateListener callback =
+ mClientNavigationListeners.getBroadcastItem(i);
try {
callback.onNavigationStateChanged(protoBytes);
} catch (RemoteException ignores) {
// ignore
}
}
- mClientCallbacks.finishBroadcast();
+ mClientNavigationListeners.finishBroadcast();
if (!mClusterHalService.isNavigationStateSupported()) {
Slogf.d(TAG, "No Cluster NavigationState HAL property");
@@ -292,6 +300,7 @@
@Override
public boolean startFixedActivityModeAsUser(Intent intent,
Bundle activityOptionsBundle, int userId) {
+ Slogf.d(TAG, "startFixedActivityModeAsUser: intent=%s, userId=%d", intent, userId);
enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
if (mClusterDisplayId == Display.INVALID_DISPLAY) {
@@ -302,6 +311,7 @@
ActivityOptions activityOptions = ActivityOptions.fromBundle(activityOptionsBundle);
activityOptions.setLaunchDisplayId(mClusterDisplayId);
mLastIntent = intent;
+ mLastIntentUserId = userId;
return mFixedActivityService.startFixedActivityModeForDisplayAndUser(
intent, activityOptions, mClusterDisplayId, userId);
}
@@ -319,19 +329,35 @@
}
@Override
- public void registerCallback(IClusterHomeCallback callback) {
+ public void registerClusterStateListener(IClusterStateListener listener) {
enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
- mClientCallbacks.register(callback);
+ mClientListeners.register(listener);
}
@Override
- public void unregisterCallback(IClusterHomeCallback callback) {
+ public void unregisterClusterStateListener(IClusterStateListener listener) {
enforcePermission(Car.PERMISSION_CAR_INSTRUMENT_CLUSTER_CONTROL);
if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
- mClientCallbacks.unregister(callback);
+ mClientListeners.unregister(listener);
+ }
+
+ @Override
+ public void registerClusterNavigationStateListener(IClusterNavigationStateListener listener) {
+ enforcePermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE);
+ if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
+
+ mClientNavigationListeners.register(listener);
+ }
+
+ @Override
+ public void unregisterClusterNavigationStateListener(IClusterNavigationStateListener listener) {
+ enforcePermission(Car.PERMISSION_CAR_MONITOR_CLUSTER_NAVIGATION_STATE);
+ if (!mServiceEnabled) throw new IllegalStateException("Service is not enabled");
+
+ mClientNavigationListeners.unregister(listener);
}
@Override
diff --git a/service/src/com/android/car/evs/CarEvsService.java b/service/src/com/android/car/evs/CarEvsService.java
index 1b19249..7af1adb 100644
--- a/service/src/com/android/car/evs/CarEvsService.java
+++ b/service/src/com/android/car/evs/CarEvsService.java
@@ -121,9 +121,9 @@
private static final int MSG_CHECK_ACTIVITY_REQUEST_TIMEOUT = 0;
// Service request priorities
- private static final int REQUEST_PRIORITY_HIGH = 0;
+ private static final int REQUEST_PRIORITY_LOW = 0;
private static final int REQUEST_PRIORITY_NORMAL = 1;
- private static final int REQUEST_PRIORITY_LOW = 2;
+ private static final int REQUEST_PRIORITY_HIGH = 2;
private static final class EvsHalEvent {
private long mTimestamp;
@@ -200,6 +200,9 @@
synchronized (mLock) {
if (requestActivityIfNecessaryLocked()) {
Slog.i(TAG_EVS, "Requested to launch the activity.");
+ } else {
+ // Ensure we stops streaming
+ mStateEngine.execute(REQUEST_PRIORITY_HIGH, SERVICE_STATE_INACTIVE);
}
}
}
@@ -409,7 +412,7 @@
break;
case SERVICE_STATE_REQUESTED:
- if (priority > mLastRequestPriority) {
+ if (priority < mLastRequestPriority) {
// A current service request has a lower priority than a previous
// service request.
Slog.e(TAG_EVS,
@@ -422,7 +425,7 @@
break;
case SERVICE_STATE_ACTIVE:
- if (priority > mLastRequestPriority) {
+ if (priority < mLastRequestPriority) {
// We decline a request because CarEvsService is busy with a higher priority
// client.
return ERROR_BUSY;
@@ -500,7 +503,7 @@
case SERVICE_STATE_ACTIVE:
// CarEvsManager will transfer an active video stream to a new client with a
// higher or equal priority.
- if (priority > mLastRequestPriority) {
+ if (priority < mLastRequestPriority) {
Slog.i(TAG_EVS, "Declines a service request with a lower priority.");
break;
}
@@ -591,6 +594,9 @@
return;
}
+ // Notify the client that the stream has ended.
+ notifyStreamStopped(callback);
+
unlinkToDeathStreamCallbackLocked();
mStreamCallback = null;
Slog.i(TAG_EVS, "Last stream client has been disconnected.");
@@ -625,6 +631,8 @@
return false;
}
+ // Request to launch an activity again after cleaning up
+ mStateEngine.execute(REQUEST_PRIORITY_HIGH, SERVICE_STATE_INACTIVE);
mStateEngine.execute(REQUEST_PRIORITY_HIGH, SERVICE_STATE_REQUESTED,
mLastEvsHalEvent.getServiceType());
return true;
@@ -708,8 +716,8 @@
synchronized (mLock) {
int targetState = on ? SERVICE_STATE_REQUESTED : SERVICE_STATE_INACTIVE;
- if (mStateEngine.execute(REQUEST_PRIORITY_HIGH, targetState, type) !=
- ERROR_NONE) {
+ if (mStateEngine.execute(REQUEST_PRIORITY_HIGH, targetState, type, /* token = */ null,
+ mStreamCallback) != ERROR_NONE) {
Slog.e(TAG_EVS, "Failed to execute a service request.");
}
@@ -1209,7 +1217,8 @@
// Request to stop the rearview activity when the gear is shifted from the reverse
// position to other positions.
if (mStateEngine.execute(REQUEST_PRIORITY_HIGH, SERVICE_STATE_INACTIVE,
- CarEvsManager.SERVICE_TYPE_REARVIEW) != ERROR_NONE) {
+ CarEvsManager.SERVICE_TYPE_REARVIEW, /* token = */ null, mStreamCallback)
+ != ERROR_NONE) {
Slog.d(TAG_EVS, "Failed to stop the rearview activity.");
}
}
@@ -1234,23 +1243,30 @@
}
}
- /** Processes a streaming event and propagates it to registered clients */
- private void processNewFrame(int id, @NonNull HardwareBuffer buffer) {
+ /**
+ * Processes a streaming event and propagates it to registered clients.
+ *
+ * @return True if this buffer is hold and used by the client, false otherwise.
+ */
+ private boolean processNewFrame(int id, @NonNull HardwareBuffer buffer) {
Objects.requireNonNull(buffer);
synchronized (mLock) {
- mBufferRecords.add(id);
if (mStreamCallback == null) {
- return;
+ return false;
}
try {
mStreamCallback.onNewFrame(new CarEvsBufferDescriptor(id, buffer));
+ mBufferRecords.add(id);
} catch (RemoteException e) {
// Likely the binder death incident
Slog.e(TAG_EVS, Log.getStackTraceString(e));
+ return false;
}
}
+
+ return true;
}
/** EVS stream event handler called after a native handler */
@@ -1261,7 +1277,11 @@
/** EVS frame handler called after a native handler */
private void postNativeFrameHandler(int id, HardwareBuffer buffer) {
- processNewFrame(id, buffer);
+ if (!processNewFrame(id, buffer)) {
+ // No client uses this buffer.
+ Slog.d(TAG_EVS, "Returns buffer " + id + " because no client uses it.");
+ nativeDoneWithFrame(mNativeEvsServiceObj, id);
+ }
}
/** EVS service death handler called after a native handler */
diff --git a/service/src/com/android/car/garagemode/Controller.java b/service/src/com/android/car/garagemode/Controller.java
index 81c8c10..fdf0c05 100644
--- a/service/src/com/android/car/garagemode/Controller.java
+++ b/service/src/com/android/car/garagemode/Controller.java
@@ -16,6 +16,8 @@
package com.android.car.garagemode;
+import static com.android.car.internal.testing.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
+
import android.app.job.JobScheduler;
import android.car.hardware.power.CarPowerManager;
import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
@@ -28,6 +30,7 @@
import com.android.car.CarLocalServices;
import com.android.car.CarLog;
+import com.android.car.internal.testing.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.systeminterface.SystemInterface;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.utils.Slogf;
@@ -116,6 +119,7 @@
/**
* Prints Garage Mode's status, including what jobs it is waiting for
*/
+ @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
void dump(PrintWriter writer) {
mGarageMode.dump(writer);
}
diff --git a/service/src/com/android/car/garagemode/GarageMode.java b/service/src/com/android/car/garagemode/GarageMode.java
index f315dca..5936391 100644
--- a/service/src/com/android/car/garagemode/GarageMode.java
+++ b/service/src/com/android/car/garagemode/GarageMode.java
@@ -16,6 +16,8 @@
package com.android.car.garagemode;
+import static com.android.car.internal.testing.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
+
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.app.job.JobSnapshot;
@@ -30,6 +32,7 @@
import com.android.car.CarLocalServices;
import com.android.car.CarLog;
import com.android.car.CarStatsLogHelper;
+import com.android.car.internal.testing.ExcludeFromCodeCoverageGeneratedReport;
import com.android.car.power.CarPowerManagementService;
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
@@ -242,6 +245,7 @@
}
}
+ @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
void dump(PrintWriter writer) {
if (!mGarageModeActive) {
return;
diff --git a/service/src/com/android/car/hal/CarPropertyUtils.java b/service/src/com/android/car/hal/CarPropertyUtils.java
index 9ac2745..146a4fb 100644
--- a/service/src/com/android/car/hal/CarPropertyUtils.java
+++ b/service/src/com/android/car/hal/CarPropertyUtils.java
@@ -260,6 +260,12 @@
int areaType = getVehicleAreaType(p.prop & VehicleArea.MASK);
Class<?> clazz = getJavaClass(p.prop & VehiclePropertyType.MASK);
+ float maxSampleRate = 0f;
+ float minSampleRate = 0f;
+ if (p.changeMode != CarPropertyConfig.VEHICLE_PROPERTY_CHANGE_MODE_STATIC) {
+ maxSampleRate = p.maxSampleRate;
+ minSampleRate = p.minSampleRate;
+ }
if (p.areaConfigs.isEmpty()) {
return CarPropertyConfig
.newBuilder(clazz, propertyId, areaType, /* capacity */ 1)
@@ -268,8 +274,8 @@
.setChangeMode(p.changeMode)
.setConfigArray(p.configArray)
.setConfigString(p.configString)
- .setMaxSampleRate(p.maxSampleRate)
- .setMinSampleRate(p.minSampleRate)
+ .setMaxSampleRate(maxSampleRate)
+ .setMinSampleRate(minSampleRate)
.build();
} else {
CarPropertyConfig.Builder builder = CarPropertyConfig
@@ -278,8 +284,8 @@
.setChangeMode(p.changeMode)
.setConfigArray(p.configArray)
.setConfigString(p.configString)
- .setMaxSampleRate(p.maxSampleRate)
- .setMinSampleRate(p.minSampleRate);
+ .setMaxSampleRate(maxSampleRate)
+ .setMinSampleRate(minSampleRate);
for (VehicleAreaConfig area : p.areaConfigs) {
if (classMatched(Integer.class, clazz)) {
diff --git a/service/src/com/android/car/hal/TimeHalService.java b/service/src/com/android/car/hal/TimeHalService.java
new file mode 100644
index 0000000..a166850
--- /dev/null
+++ b/service/src/com/android/car/hal/TimeHalService.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2021 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.hal;
+
+import static android.hardware.automotive.vehicle.V2_0.VehicleProperty.EPOCH_TIME;
+
+import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.automotive.vehicle.V2_0.VehicleArea;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropertyStatus;
+import android.util.IndentingPrintWriter;
+
+import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
+
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.List;
+
+/** Writes the Android System time to EPOCH_TIME in the VHAL, if supported. */
+public final class TimeHalService extends HalServiceBase {
+
+ private static final int[] SUPPORTED_PROPERTIES = new int[]{EPOCH_TIME};
+
+ private final Context mContext;
+
+ private final VehicleHal mHal;
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_TIME_CHANGED.equals(intent.getAction())) {
+ updateProperty(System.currentTimeMillis());
+ }
+ }
+ };
+
+ private boolean mReceiverRegistered;
+
+ @Nullable
+ private Instant mLastAndroidTimeReported;
+
+ private boolean mAndroidTimeSupported;
+
+ TimeHalService(Context context, VehicleHal hal) {
+ mContext = requireNonNull(context);
+ mHal = requireNonNull(hal);
+ }
+
+ @Override
+ public void init() {
+ if (!mAndroidTimeSupported) {
+ return;
+ }
+
+ updateProperty(System.currentTimeMillis());
+
+ IntentFilter filter = new IntentFilter(Intent.ACTION_TIME_CHANGED);
+ mContext.registerReceiver(mReceiver, filter);
+ mReceiverRegistered = true;
+ }
+
+ @Override
+ public void release() {
+ if (mReceiverRegistered) {
+ mContext.unregisterReceiver(mReceiver);
+ mReceiverRegistered = false;
+ }
+
+ mAndroidTimeSupported = false;
+ mLastAndroidTimeReported = null;
+ }
+
+ @Override
+ public int[] getAllSupportedProperties() {
+ return SUPPORTED_PROPERTIES;
+ }
+
+ @Override
+ public void takeProperties(Collection<VehiclePropConfig> properties) {
+ for (VehiclePropConfig property : properties) {
+ switch (property.prop) {
+ case EPOCH_TIME:
+ mAndroidTimeSupported = true;
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onHalEvents(List<VehiclePropValue> values) {
+ }
+
+ public boolean isAndroidTimeSupported() {
+ return mAndroidTimeSupported;
+ }
+
+ private void updateProperty(long timeMillis) {
+ VehiclePropValue propValue = new VehiclePropValue();
+ propValue.prop = EPOCH_TIME;
+ propValue.areaId = VehicleArea.GLOBAL;
+ propValue.status = VehiclePropertyStatus.AVAILABLE;
+ propValue.timestamp = timeMillis;
+ propValue.value.int64Values.add(timeMillis);
+
+ mHal.set(propValue);
+ mLastAndroidTimeReported = Instant.ofEpochMilli(timeMillis);
+ }
+
+ @Override
+ @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
+ public void dump(PrintWriter printWriter) {
+ IndentingPrintWriter writer = new IndentingPrintWriter(printWriter);
+ writer.println("*ExternalTime HAL*");
+ writer.increaseIndent();
+ writer.printf(
+ "mLastAndroidTimeReported: %d millis",
+ mLastAndroidTimeReported.toEpochMilli());
+ writer.decreaseIndent();
+ writer.flush();
+ }
+}
diff --git a/service/src/com/android/car/hal/VehicleHal.java b/service/src/com/android/car/hal/VehicleHal.java
index 5de5a81..6b67b8d 100644
--- a/service/src/com/android/car/hal/VehicleHal.java
+++ b/service/src/com/android/car/hal/VehicleHal.java
@@ -19,6 +19,7 @@
import static com.android.car.CarServiceUtils.toByteArray;
import static com.android.car.CarServiceUtils.toFloatArray;
import static com.android.car.CarServiceUtils.toIntArray;
+import static com.android.car.CarServiceUtils.toLongArray;
import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
import static java.lang.Integer.toHexString;
@@ -39,6 +40,7 @@
import android.hardware.automotive.vehicle.V2_0.VehiclePropertyAccess;
import android.hardware.automotive.vehicle.V2_0.VehiclePropertyChangeMode;
import android.hardware.automotive.vehicle.V2_0.VehiclePropertyType;
+import android.os.Handler;
import android.os.HandlerThread;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
@@ -86,8 +88,8 @@
public static final int NO_AREA = -1;
public static final float NO_SAMPLE_RATE = -1;
- private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
- VehicleHal.class.getSimpleName());
+ private final HandlerThread mHandlerThread;
+ private final Handler mHandler;
private final PowerHalService mPowerHal;
private final PropertyHalService mPropertyHal;
private final InputHalService mInputHal;
@@ -96,6 +98,7 @@
private final DiagnosticHalService mDiagnosticHal;
private final ClusterHalService mClusterHalService;
private final EvsHalService mEvsHal;
+ private final TimeHalService mTimeHalService;
private final Object mLock = new Object();
@@ -124,6 +127,9 @@
* both passed as parameters.
*/
public VehicleHal(Context context, IVehicle vehicle) {
+ mHandlerThread = CarServiceUtils.getHandlerThread(
+ VehicleHal.class.getSimpleName());
+ mHandler = new Handler(mHandlerThread.getLooper());
mPowerHal = new PowerHalService(this);
mPropertyHal = new PropertyHalService(this);
mInputHal = new InputHalService(this);
@@ -132,6 +138,8 @@
mDiagnosticHal = new DiagnosticHalService(this);
mClusterHalService = new ClusterHalService(this);
mEvsHal = new EvsHalService(this);
+ mTimeHalService = new TimeHalService(context, this);
+ //TODO(b/202396546): Dedupe this assignment with the other one in constructor below
mAllServices.addAll(Arrays.asList(mPowerHal,
mInputHal,
mDiagnosticHal,
@@ -139,6 +147,7 @@
mUserHal,
mClusterHalService,
mEvsHal,
+ mTimeHalService,
mPropertyHal)); // mPropertyHal should be the last.
mHalClient = new HalClient(vehicle, mHandlerThread.getLooper(),
/* callback= */ this);
@@ -156,7 +165,11 @@
UserHalService userHal,
DiagnosticHalService diagnosticHal,
ClusterHalService clusterHalService,
- HalClient halClient) {
+ TimeHalService timeHalService,
+ HalClient halClient,
+ HandlerThread handlerThread) {
+ mHandlerThread = handlerThread;
+ mHandler = new Handler(mHandlerThread.getLooper());
mPowerHal = powerHal;
mPropertyHal = propertyHal;
mInputHal = inputHal;
@@ -165,12 +178,14 @@
mDiagnosticHal = diagnosticHal;
mClusterHalService = clusterHalService;
mEvsHal = new EvsHalService(this);
+ mTimeHalService = timeHalService;
mAllServices.addAll(Arrays.asList(mPowerHal,
mInputHal,
mDiagnosticHal,
mVmsHal,
mUserHal,
mEvsHal,
+ mTimeHalService,
mPropertyHal));
mHalClient = halClient;
}
@@ -315,6 +330,10 @@
return mEvsHal;
}
+ public TimeHalService getTimeHalService() {
+ return mTimeHalService;
+ }
+
private void assertServiceOwnerLocked(HalServiceBase service, int property) {
if (service != mPropertyHandlers.get(property)) {
throw new IllegalArgumentException("Property 0x" + toHexString(property)
@@ -510,18 +529,25 @@
VehiclePropValue propValue;
propValue = mHalClient.getValue(requestedPropValue);
- if (clazz == Integer.class || clazz == int.class) {
+ if (clazz == Long.class || clazz == long.class) {
+ return (T) propValue.value.int64Values.get(0);
+ } else if (clazz == Integer.class || clazz == int.class) {
return (T) propValue.value.int32Values.get(0);
} else if (clazz == Boolean.class || clazz == boolean.class) {
return (T) Boolean.valueOf(propValue.value.int32Values.get(0) == 1);
} else if (clazz == Float.class || clazz == float.class) {
return (T) propValue.value.floatValues.get(0);
+ } else if (clazz == Long[].class) {
+ Long[] longArray = new Long[propValue.value.int64Values.size()];
+ return (T) propValue.value.int32Values.toArray(longArray);
} else if (clazz == Integer[].class) {
Integer[] intArray = new Integer[propValue.value.int32Values.size()];
return (T) propValue.value.int32Values.toArray(intArray);
} else if (clazz == Float[].class) {
Float[] floatArray = new Float[propValue.value.floatValues.size()];
return (T) propValue.value.floatValues.toArray(floatArray);
+ } else if (clazz == long[].class) {
+ return (T) toLongArray(propValue.value.int64Values);
} else if (clazz == int[].class) {
return (T) toIntArray(propValue.value.int32Values);
} else if (clazz == float[].class) {
@@ -581,6 +607,7 @@
private final ArraySet<HalServiceBase> mServicesToDispatch = new ArraySet<>();
+ // should be posted to the mHandlerThread
@Override
public void onPropertyEvent(ArrayList<VehiclePropValue> propValues) {
synchronized (mLock) {
@@ -614,6 +641,7 @@
// No need to handle on-property-set events in HAL service yet.
}
+ // should be posted to the mHandlerThread
@Override
public void onPropertySetError(@CarPropertyManager.CarSetPropertyErrorCode int errorCode,
int propId, int areaId) {
@@ -800,7 +828,7 @@
}
// update timestamp
v.timestamp = SystemClock.elapsedRealtimeNanos() + TimeUnit.SECONDS.toNanos(delayTime);
- onPropertyEvent(Lists.newArrayList(v));
+ mHandler.post(() -> onPropertyEvent(Lists.newArrayList(v)));
}
/**
@@ -838,7 +866,7 @@
// Avoid the fake events be covered by real Event
v.timestamp = SystemClock.elapsedRealtimeNanos()
+ TimeUnit.SECONDS.toNanos(timeDurationInSec);
- onPropertyEvent(Lists.newArrayList(v));
+ mHandler.post(() -> onPropertyEvent(Lists.newArrayList(v)));
}
}
}, /* delay= */0, period);
diff --git a/service/src/com/android/car/pm/CarPackageManagerService.java b/service/src/com/android/car/pm/CarPackageManagerService.java
index 4c262b5..e732a50 100644
--- a/service/src/com/android/car/pm/CarPackageManagerService.java
+++ b/service/src/com/android/car/pm/CarPackageManagerService.java
@@ -55,12 +55,17 @@
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.ParcelFileDescriptor;
import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.LocalLog;
@@ -83,9 +88,13 @@
import com.android.car.user.CarUserService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
import com.google.android.collect.Sets;
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
@@ -108,6 +117,7 @@
private static final String PACKAGE_DELIMITER = ",";
private static final String PACKAGE_ACTIVITY_DELIMITER = "/";
private static final int LOG_SIZE = 20;
+ private static final String[] WINDOW_DUMP_ARGUMENTS = new String[]{"windows"};
private static final String PROPERTY_RO_DRIVING_SAFETY_REGION =
"ro.android.car.drivingsafetyregion";
@@ -117,6 +127,7 @@
private final PackageManager mPackageManager;
private final ActivityManager mActivityManager;
private final DisplayManager mDisplayManager;
+ private final IBinder mWindowManagerBinder;
private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
getClass().getSimpleName());
@@ -137,6 +148,9 @@
private final List<String> mAllowedAppInstallSources;
+ @GuardedBy("mLock")
+ private final SparseArray<ComponentName> mTopActivityWithDialogPerDisplay = new SparseArray<>();
+
/**
* Hold policy set from policy service or client.
* Key: packageName of policy service
@@ -164,6 +178,8 @@
private final boolean mEnableActivityBlocking;
private final ComponentName mActivityBlockingActivity;
+ private final boolean mPreventTemplatedAppsFromShowingDialog;
+ private final String mTemplateActivityClassName;
private final ActivityLaunchListener mActivityLaunchListener = new ActivityLaunchListener();
@@ -252,14 +268,21 @@
mPackageManager = mContext.getPackageManager();
mActivityManager = mContext.getSystemService(ActivityManager.class);
mDisplayManager = mContext.getSystemService(DisplayManager.class);
+ mWindowManagerBinder = ServiceManager.getService(Context.WINDOW_SERVICE);
Resources res = context.getResources();
mEnableActivityBlocking = res.getBoolean(R.bool.enableActivityBlockingForSafety);
String blockingActivity = res.getString(R.string.activityBlockingActivity);
mActivityBlockingActivity = ComponentName.unflattenFromString(blockingActivity);
+ if (mEnableActivityBlocking && mActivityBlockingActivity == null) {
+ Slogf.wtf(TAG, "mActivityBlockingActivity can't be null when enabled");
+ }
mAllowedAppInstallSources = Arrays.asList(
res.getStringArray(R.array.allowedAppInstallSources));
mVendorServiceController = new VendorServiceController(
mContext, mHandler.getLooper());
+ mPreventTemplatedAppsFromShowingDialog =
+ res.getBoolean(R.bool.config_preventTemplatedAppsFromShowingDialog);
+ mTemplateActivityClassName = res.getString(R.string.config_template_activity_class_name);
}
@@ -414,6 +437,15 @@
return true;
}
+ for (int i = mTopActivityWithDialogPerDisplay.size() - 1; i >= 0; i--) {
+ ComponentName activityWithDialog = mTopActivityWithDialogPerDisplay.get(
+ mTopActivityWithDialogPerDisplay.keyAt(i));
+ if (activityWithDialog.getClassName().equals(className)
+ && activityWithDialog.getPackageName().equals(packageName)) {
+ return false;
+ }
+ }
+
if (searchFromClientPolicyBlocklistsLocked(packageName)) {
return false;
}
@@ -1166,6 +1198,9 @@
synchronized (mLock) {
writer.println("*CarPackageManagerService*");
writer.println("mEnableActivityBlocking:" + mEnableActivityBlocking);
+ writer.println("mPreventTemplatedAppsFromShowingDialog:"
+ + mPreventTemplatedAppsFromShowingDialog);
+ writer.println("mTemplateActivityClassName:" + mTemplateActivityClassName);
List<String> restrictions = new ArrayList<>(mUxRestrictionsListeners.size());
for (int i = 0; i < mUxRestrictionsListeners.size(); i++) {
int displayId = mUxRestrictionsListeners.keyAt(i);
@@ -1275,6 +1310,15 @@
}
private void blockTopActivityIfNecessary(TopTaskInfoContainer topTask) {
+ synchronized (mLock) {
+ if (!Objects.equals(mActivityBlockingActivity, topTask.topActivity)
+ && mTopActivityWithDialogPerDisplay.contains(topTask.displayId)
+ && !topTask.topActivity.equals(
+ mTopActivityWithDialogPerDisplay.get(topTask.displayId))) {
+ // Clear top activity-with-dialog if the activity has changed on this display.
+ mTopActivityWithDialogPerDisplay.remove(topTask.displayId);
+ }
+ }
if (isUxRestrictedOnDisplay(topTask.displayId)) {
doBlockTopActivityIfNotAllowed(topTask);
}
@@ -1284,10 +1328,7 @@
if (topTask.topActivity == null) {
return;
}
-
- boolean allowed = isActivityDistractionOptimized(
- topTask.topActivity.getPackageName(),
- topTask.topActivity.getClassName());
+ boolean allowed = isActivityAllowed(topTask);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Slog.d(TAG, "new activity:" + topTask.toString() + " allowed:" + allowed);
}
@@ -1320,9 +1361,10 @@
boolean isRootDO = false;
if (taskRootActivity != null) {
- ComponentName componentName = ComponentName.unflattenFromString(taskRootActivity);
+ ComponentName taskRootComponentName =
+ ComponentName.unflattenFromString(taskRootActivity);
isRootDO = isActivityDistractionOptimized(
- componentName.getPackageName(), componentName.getClassName());
+ taskRootComponentName.getPackageName(), taskRootComponentName.getClassName());
}
Intent newActivityIntent = createBlockingActivityIntent(
@@ -1339,6 +1381,82 @@
mSystemActivityMonitoringService.blockActivity(topTask, newActivityIntent);
}
+ private boolean isActivityAllowed(TopTaskInfoContainer topTaskInfoContainer) {
+ ComponentName activityName = topTaskInfoContainer.topActivity;
+ boolean isDistractionOptimized = isActivityDistractionOptimized(
+ activityName.getPackageName(),
+ activityName.getClassName());
+ if (!isDistractionOptimized) {
+ return false;
+ }
+ return !(mPreventTemplatedAppsFromShowingDialog
+ && isTemplateActivity(activityName)
+ && isActivityShowingADialogOnDisplay(activityName, topTaskInfoContainer.displayId));
+ }
+
+ private boolean isTemplateActivity(ComponentName activityName) {
+ // TODO(b/191263486): Finalise on how to detect the templated activities.
+ return activityName.getClassName().equals(mTemplateActivityClassName);
+ }
+
+ private boolean isActivityShowingADialogOnDisplay(ComponentName activityName, int displayId) {
+ String output = dumpWindows();
+ List<WindowDumpParser.Window> appWindows =
+ WindowDumpParser.getParsedAppWindows(output, activityName.getPackageName());
+ // TODO(b/192354699): Handle case where an activity can have multiple instances on the same
+ // display.
+ int totalAppWindows = appWindows.size();
+ String firstActivityRecord = null;
+ int numTopActivityAppWindowsOnDisplay = 0;
+ for (int i = 0; i < totalAppWindows; i++) {
+ WindowDumpParser.Window appWindow = appWindows.get(i);
+ if (appWindow.getDisplayId() != displayId) {
+ continue;
+ }
+ if (TextUtils.isEmpty(appWindow.getActivityRecord())) {
+ continue;
+ }
+ if (firstActivityRecord == null) {
+ firstActivityRecord = appWindow.getActivityRecord();
+ }
+ if (firstActivityRecord.equals(appWindow.getActivityRecord())) {
+ numTopActivityAppWindowsOnDisplay++;
+ }
+ }
+ Slogf.d(TAG, "Top activity = " + activityName);
+ Slogf.d(TAG, "Number of app widows of top activity = " + numTopActivityAppWindowsOnDisplay);
+ boolean isShowingADialog = numTopActivityAppWindowsOnDisplay > 1;
+ synchronized (mLock) {
+ if (isShowingADialog) {
+ mTopActivityWithDialogPerDisplay.put(displayId, activityName);
+ } else {
+ mTopActivityWithDialogPerDisplay.remove(displayId);
+ }
+ }
+ return isShowingADialog;
+ }
+
+ private String dumpWindows() {
+ try {
+ ParcelFileDescriptor[] fileDescriptors = ParcelFileDescriptor.createSocketPair();
+ mWindowManagerBinder.dump(
+ fileDescriptors[0].getFileDescriptor(), WINDOW_DUMP_ARGUMENTS);
+ fileDescriptors[0].close();
+ StringBuilder outputBuilder = new StringBuilder();
+ BufferedReader reader = new BufferedReader(
+ new FileReader(fileDescriptors[1].getFileDescriptor()));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ outputBuilder.append(line).append("\n");
+ }
+ reader.close();
+ fileDescriptors[1].close();
+ return outputBuilder.toString();
+ } catch (IOException | RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
/**
* Creates an intent to start blocking activity.
*
@@ -1346,6 +1464,7 @@
* @param blockedActivity the activity being blocked
* @param blockedTaskId the blocked task id, which contains the blocked activity
* @param taskRootActivity root activity of the blocked task
+ * @param isRootDo denotes if the root activity is distraction optimised
* @return an intent to launch the blocking activity.
*/
private static Intent createBlockingActivityIntent(ComponentName blockingActivity,
@@ -1644,6 +1763,15 @@
}
/**
+ * Called when a window change event is received by the {@link CarSafetyAccessibilityService}.
+ */
+ @VisibleForTesting
+ void onWindowChangeEvent() {
+ Slogf.d(TAG, "onWindowChange event received");
+ mHandlerThread.getThreadHandler().post(() -> blockTopActivitiesIfNecessary());
+ }
+
+ /**
* Listens to the package install/uninstall events to know when to initiate parsing
* installed packages.
*/
diff --git a/service/src/com/android/car/pm/CarSafetyAccessibilityService.java b/service/src/com/android/car/pm/CarSafetyAccessibilityService.java
new file mode 100644
index 0000000..76a6246
--- /dev/null
+++ b/service/src/com/android/car/pm/CarSafetyAccessibilityService.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.pm;
+
+import android.accessibilityservice.AccessibilityService;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.car.CarLocalServices;
+
+/**
+ * An accessibility service to notify the Car Service of any change in the window state. The car
+ * safety related code can read the events sent from this service and take the necessary actions.
+ */
+public class CarSafetyAccessibilityService extends AccessibilityService {
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ CarPackageManagerService carPackageManagerService =
+ CarLocalServices.getService(CarPackageManagerService.class);
+ if (carPackageManagerService != null) {
+ carPackageManagerService.onWindowChangeEvent();
+ }
+ }
+
+ @Override
+ public void onInterrupt() {
+ }
+}
diff --git a/service/src/com/android/car/pm/TEST_MAPPING b/service/src/com/android/car/pm/TEST_MAPPING
index d37fe3d..1c32824 100644
--- a/service/src/com/android/car/pm/TEST_MAPPING
+++ b/service/src/com/android/car/pm/TEST_MAPPING
@@ -19,6 +19,12 @@
},
{
"include-filter": "com.android.car.pm.VendorServiceInfoTest"
+ },
+ {
+ "include-filter": "com.android.car.pm.CarSafetyAccessibilityServiceTest"
+ },
+ {
+ "include-filter": "com.android.car.pm.WindowDumpParserTest"
}
]
},
diff --git a/service/src/com/android/car/pm/WindowDumpParser.java b/service/src/com/android/car/pm/WindowDumpParser.java
new file mode 100644
index 0000000..8387ce7
--- /dev/null
+++ b/service/src/com/android/car/pm/WindowDumpParser.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2021 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.pm;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A utility class to parse the window dump.
+ */
+class WindowDumpParser {
+ private static final String WINDOW_TYPE_APPLICATION_STARTING = "APPLICATION_STARTING";
+
+ /**
+ * Parses the provided window dump and returns the list of windows only for a particular app.
+ *
+ * @param dump the window dump in string format as returned from `dumpsys window
+ * windows`.
+ * @param appPackageName the package name of the app, which the windows are being
+ * requested for.
+ * @return a list of parsed {@link Window} objects.
+ */
+ public static List<Window> getParsedAppWindows(String dump, String appPackageName) {
+ Pattern dumpSplitter = Pattern.compile("(Window #)|\\n\\n");
+ // \\n\\n to separate out the Global dump from the windows list.
+
+ Pattern windowDetailsPattern = Pattern.compile("\\d*.*\\n"
+ + ".*mDisplayId=(\\S*).*\\n"
+ + ".*package=(\\S*).*\\n"
+ + ".*ty=(\\S*)"
+ + "((.*ActivityRecord\\{(.*?)\\}.*\\n)|(.*\\n))*"
+ // (.*\\n) is required for skipping the lines before the line containing
+ // ActivityRecord{}.
+ );
+ List<Window> windows = new ArrayList<>();
+
+ String[] windowDumps = dumpSplitter.split(dump);
+ for (int i = 1; i < windowDumps.length - 1; i++) {
+ Matcher m = windowDetailsPattern.matcher(windowDumps[i]);
+ if (m.find()) {
+ // Only consider windows for the given appPackageName which are not the splash
+ // screen windows.
+ // TODO(b/192355798): Revisit this logic as window type can be changed.
+ if (Objects.equals(m.group(2), appPackageName)
+ && !Objects.equals(m.group(3), WINDOW_TYPE_APPLICATION_STARTING)) {
+ windows.add(new Window(
+ /* packageName = */ m.group(2),
+ /* displayId = */ Integer.parseInt(m.group(1)),
+ /* activityRecord = */ m.group(6)
+ ));
+ }
+ }
+ }
+ return windows;
+ }
+
+ /**
+ * A holder class that represents an app's window.
+ */
+ static class Window {
+ private final String mPackageName;
+ private final int mDisplayId;
+ private final String mActivityRecord;
+
+ Window(String packageName, int displayId, String activityRecord) {
+ mPackageName = packageName;
+ mDisplayId = displayId;
+ mActivityRecord = activityRecord;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
+ public String getActivityRecord() {
+ return mActivityRecord;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Window)) return false;
+ Window window = (Window) o;
+ return mDisplayId == window.mDisplayId
+ && mPackageName.equals(window.mPackageName)
+ && Objects.equals(mActivityRecord, window.mActivityRecord);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPackageName, mDisplayId, mActivityRecord);
+ }
+
+ @Override
+ public String toString() {
+ return "Window{"
+ + "mPackageName=" + mPackageName
+ + ", mDisplayId=" + mDisplayId
+ + ", mActivityRecord={" + mActivityRecord + "}"
+ + "}";
+ }
+ }
+}
diff --git a/service/src/com/android/car/power/PowerComponentHandler.java b/service/src/com/android/car/power/PowerComponentHandler.java
index dcf22e8..f6f1b3b 100644
--- a/service/src/com/android/car/power/PowerComponentHandler.java
+++ b/service/src/com/android/car/power/PowerComponentHandler.java
@@ -113,13 +113,8 @@
mComponentStates.put(component, false);
PowerComponentMediator mediator = factory.createPowerComponent(component);
String componentName = powerComponentToString(component);
- if (mediator == null) {
- Slogf.w(TAG, "Power component(%s) is not valid or doesn't need a mediator",
- componentName);
- continue;
- }
- if (!mediator.isComponentAvailable()) {
- Slogf.w(TAG, "Power component(%s) is not available", componentName);
+ if (mediator == null || !mediator.isComponentAvailable()) {
+ // We don't not associate a mediator with the component.
continue;
}
mPowerComponentMediators.put(component, mediator);
@@ -237,7 +232,6 @@
PowerComponentMediator mediator = mPowerComponentMediators.get(component);
if (mediator == null) {
- Slogf.w(TAG, "%s doesn't have a mediator", powerComponentToString(component));
return true;
}
diff --git a/service/src/com/android/car/telemetry/CarTelemetryService.java b/service/src/com/android/car/telemetry/CarTelemetryService.java
index 695e25e..fc108be 100644
--- a/service/src/com/android/car/telemetry/CarTelemetryService.java
+++ b/service/src/com/android/car/telemetry/CarTelemetryService.java
@@ -15,28 +15,43 @@
*/
package com.android.car.telemetry;
-import static android.car.telemetry.CarTelemetryManager.ERROR_NEWER_MANIFEST_EXISTS;
-import static android.car.telemetry.CarTelemetryManager.ERROR_NONE;
-import static android.car.telemetry.CarTelemetryManager.ERROR_PARSE_MANIFEST_FAILED;
-import static android.car.telemetry.CarTelemetryManager.ERROR_SAME_MANIFEST_EXISTS;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_NONE;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_PARSE_FAILED;
import android.annotation.NonNull;
+import android.app.StatsManager;
import android.car.Car;
-import android.car.telemetry.CarTelemetryManager.AddManifestError;
import android.car.telemetry.ICarTelemetryService;
import android.car.telemetry.ICarTelemetryServiceListener;
-import android.car.telemetry.ManifestKey;
+import android.car.telemetry.MetricsConfigKey;
import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
import android.util.IndentingPrintWriter;
-import android.util.Slog;
+import com.android.car.CarLocalServices;
+import com.android.car.CarLog;
+import com.android.car.CarPropertyService;
import com.android.car.CarServiceBase;
-import com.android.internal.annotations.GuardedBy;
+import com.android.car.CarServiceUtils;
+import com.android.car.systeminterface.SystemInterface;
+import com.android.car.telemetry.databroker.DataBroker;
+import com.android.car.telemetry.databroker.DataBrokerController;
+import com.android.car.telemetry.databroker.DataBrokerImpl;
+import com.android.car.telemetry.publisher.PublisherFactory;
+import com.android.car.telemetry.publisher.StatsManagerImpl;
+import com.android.car.telemetry.publisher.StatsManagerProxy;
+import com.android.car.telemetry.systemmonitor.SystemMonitor;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
import com.google.protobuf.InvalidProtocolBufferException;
-import java.util.HashMap;
-import java.util.Map;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
/**
* CarTelemetryService manages OEM telemetry collection, processing and communication
@@ -44,32 +59,55 @@
*/
public class CarTelemetryService extends ICarTelemetryService.Stub implements CarServiceBase {
- // TODO(b/189340793): Rename Manifest to MetricsConfig
-
private static final boolean DEBUG = false;
- private static final int DEFAULT_VERSION = 0;
- private static final String TAG = CarTelemetryService.class.getSimpleName();
+ public static final String TELEMETRY_DIR = "telemetry";
private final Context mContext;
- @GuardedBy("mLock")
- private final Map<String, Integer> mNameVersionMap = new HashMap<>();
- private final Object mLock = new Object();
+ private final CarPropertyService mCarPropertyService;
+ private final HandlerThread mTelemetryThread = CarServiceUtils.getHandlerThread(
+ CarTelemetryService.class.getSimpleName());
+ private final Handler mTelemetryHandler = new Handler(mTelemetryThread.getLooper());
- @GuardedBy("mLock")
private ICarTelemetryServiceListener mListener;
+ private DataBroker mDataBroker;
+ private DataBrokerController mDataBrokerController;
+ private MetricsConfigStore mMetricsConfigStore;
+ private PublisherFactory mPublisherFactory;
+ private ResultStore mResultStore;
+ private StatsManagerProxy mStatsManagerProxy;
+ private SystemMonitor mSystemMonitor;
- public CarTelemetryService(Context context) {
+ public CarTelemetryService(Context context, CarPropertyService carPropertyService) {
mContext = context;
+ mCarPropertyService = carPropertyService;
}
@Override
public void init() {
- // nothing to do
+ mTelemetryHandler.post(() -> {
+ SystemInterface systemInterface = CarLocalServices.getService(SystemInterface.class);
+ // full root directory path is /data/system/car/telemetry
+ File rootDirectory = new File(systemInterface.getSystemCarDir(), TELEMETRY_DIR);
+ // initialize all necessary components
+ mMetricsConfigStore = new MetricsConfigStore(rootDirectory);
+ mResultStore = new ResultStore(rootDirectory);
+ mStatsManagerProxy = new StatsManagerImpl(
+ mContext.getSystemService(StatsManager.class));
+ mPublisherFactory = new PublisherFactory(mCarPropertyService, mTelemetryHandler,
+ mStatsManagerProxy, rootDirectory);
+ mDataBroker = new DataBrokerImpl(mContext, mPublisherFactory, mResultStore);
+ mSystemMonitor = SystemMonitor.create(mContext, mTelemetryHandler);
+ // controller starts metrics collection after boot complete
+ mDataBrokerController = new DataBrokerController(mDataBroker, mTelemetryHandler,
+ mMetricsConfigStore, mSystemMonitor,
+ systemInterface.getSystemStateInterface());
+ });
}
@Override
public void release() {
- // nothing to do
+ // TODO(b/197969149): prevent threading issue, block main thread
+ mTelemetryHandler.post(() -> mResultStore.flushToDisk());
}
@Override
@@ -85,9 +123,12 @@
// TODO(b/184890506): verify that only a hardcoded app can set the listener
mContext.enforceCallingOrSelfPermission(
Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
- synchronized (mLock) {
- setListenerLocked(listener);
- }
+ mTelemetryHandler.post(() -> {
+ if (DEBUG) {
+ Slogf.d(CarLog.TAG_TELEMETRY, "Setting the listener for car telemetry service");
+ }
+ mListener = listener;
+ });
}
/**
@@ -96,150 +137,171 @@
@Override
public void clearListener() {
mContext.enforceCallingOrSelfPermission(
- Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
- synchronized (mLock) {
- clearListenerLocked();
- }
+ Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "clearListener");
+ mTelemetryHandler.post(() -> {
+ if (DEBUG) {
+ Slogf.d(CarLog.TAG_TELEMETRY, "Clearing the listener for car telemetry service");
+ }
+ mListener = null;
+ });
}
/**
- * Allows client to send telemetry manifests.
+ * Send a telemetry metrics config to the service. This method assumes
+ * {@link #setListener(ICarTelemetryServiceListener)} is called. Otherwise it does nothing.
*
- * @param key the unique key to identify the manifest.
- * @param config the serialized bytes of a Manifest object.
- * @return {@link AddManifestError} the error code.
+ * @param key the unique key to identify the MetricsConfig.
+ * @param config the serialized bytes of a MetricsConfig object.
*/
@Override
- public @AddManifestError int addManifest(@NonNull ManifestKey key, @NonNull byte[] config) {
+ public void addMetricsConfig(@NonNull MetricsConfigKey key, @NonNull byte[] config) {
mContext.enforceCallingOrSelfPermission(
- Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
- synchronized (mLock) {
- return addManifestLocked(key, config);
- }
+ Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "addMetricsConfig");
+ mTelemetryHandler.post(() -> {
+ if (mListener == null) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "ICarTelemetryServiceListener is not set");
+ return;
+ }
+ Slogf.d(CarLog.TAG_TELEMETRY, "Adding metrics config: " + key.getName()
+ + " to car telemetry service");
+ TelemetryProto.MetricsConfig metricsConfig = null;
+ int status;
+ try {
+ metricsConfig = TelemetryProto.MetricsConfig.parseFrom(config);
+ status = mMetricsConfigStore.addMetricsConfig(metricsConfig);
+ } catch (InvalidProtocolBufferException e) {
+ Slogf.e(CarLog.TAG_TELEMETRY, "Failed to parse MetricsConfig.", e);
+ status = ERROR_METRICS_CONFIG_PARSE_FAILED;
+ }
+ // If no error (config is added to MetricsConfigStore), remove legacy data and add
+ // config to data broker for metrics collection
+ if (status == ERROR_METRICS_CONFIG_NONE) {
+ mResultStore.removeResult(key);
+ mDataBroker.removeMetricsConfig(key);
+ mDataBroker.addMetricsConfig(key, metricsConfig);
+ // TODO(b/199410900): update logic once metrics configs have expiration dates
+ }
+ try {
+ mListener.onAddMetricsConfigStatus(key, status);
+ } catch (RemoteException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "error with ICarTelemetryServiceListener", e);
+ }
+ });
}
/**
- * Removes a manifest based on the key.
+ * Removes a metrics config based on the key. This will also remove outputs produced by the
+ * MetricsConfig.
+ *
+ * @param key the unique identifier of a MetricsConfig.
*/
@Override
- public boolean removeManifest(@NonNull ManifestKey key) {
+ public void removeMetricsConfig(@NonNull MetricsConfigKey key) {
mContext.enforceCallingOrSelfPermission(
- Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
- synchronized (mLock) {
- return removeManifestLocked(key);
- }
+ Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "removeMetricsConfig");
+ mTelemetryHandler.post(() -> {
+ Slogf.d(CarLog.TAG_TELEMETRY, "Removing metrics config " + key.getName()
+ + " from car telemetry service");
+ if (mMetricsConfigStore.removeMetricsConfig(key)) {
+ mDataBroker.removeMetricsConfig(key);
+ mResultStore.removeResult(key);
+ }
+ });
}
/**
- * Removes all manifests.
+ * Removes all MetricsConfigs. This will also remove all MetricsConfig outputs.
*/
@Override
- public void removeAllManifests() {
+ public void removeAllMetricsConfigs() {
mContext.enforceCallingOrSelfPermission(
- Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
- synchronized (mLock) {
- removeAllManifestsLocked();
- }
+ Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "removeAllMetricsConfigs");
+ mTelemetryHandler.post(() -> {
+ Slogf.d(CarLog.TAG_TELEMETRY,
+ "Removing all metrics config from car telemetry service");
+ mDataBroker.removeAllMetricsConfigs();
+ mMetricsConfigStore.removeAllMetricsConfigs();
+ mResultStore.removeAllResults();
+ });
}
/**
* Sends script results associated with the given key using the
- * {@link ICarTelemetryServiceListener}.
+ * {@link ICarTelemetryServiceListener}. This method assumes listener is set. Otherwise it
+ * does nothing.
+ *
+ * @param key the unique identifier of a MetricsConfig.
*/
@Override
- public void sendFinishedReports(@NonNull ManifestKey key) {
- // TODO(b/184087869): Implement
+ public void sendFinishedReports(@NonNull MetricsConfigKey key) {
mContext.enforceCallingOrSelfPermission(
- Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
- if (DEBUG) {
- Slog.d(TAG, "Flushing reports for a manifest");
- }
+ Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "sendFinishedReports");
+ mTelemetryHandler.post(() -> {
+ if (mListener == null) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "ICarTelemetryServiceListener is not set");
+ return;
+ }
+ if (DEBUG) {
+ Slogf.d(CarLog.TAG_TELEMETRY,
+ "Flushing reports for metrics config " + key.getName());
+ }
+ PersistableBundle result = mResultStore.getFinalResult(key.getName(), true);
+ TelemetryProto.TelemetryError error = mResultStore.getError(key.getName(), true);
+ if (result != null) {
+ sendFinalResult(key, result);
+ } else if (error != null) {
+ sendError(key, error);
+ } else {
+ Slogf.w(CarLog.TAG_TELEMETRY, "config " + key.getName()
+ + " did not produce any results");
+ }
+ });
}
/**
- * Sends all script results associated using the {@link ICarTelemetryServiceListener}.
+ * Sends all script results or errors using the {@link ICarTelemetryServiceListener}.
*/
@Override
public void sendAllFinishedReports() {
// TODO(b/184087869): Implement
mContext.enforceCallingOrSelfPermission(
- Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
+ Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "sendAllFinishedReports");
if (DEBUG) {
- Slog.d(TAG, "Flushing all reports");
+ Slogf.d(CarLog.TAG_TELEMETRY, "Flushing all reports");
}
}
- /**
- * Sends all errors using the {@link ICarTelemetryServiceListener}.
- */
- @Override
- public void sendScriptExecutionErrors() {
- // TODO(b/184087869): Implement
- mContext.enforceCallingOrSelfPermission(
- Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE, "setListener");
- if (DEBUG) {
- Slog.d(TAG, "Flushing script execution errors");
+ private void sendFinalResult(MetricsConfigKey key, PersistableBundle result) {
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ result.writeToStream(bos);
+ mListener.onResult(key, bos.toByteArray());
+ } catch (RemoteException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "error with ICarTelemetryServiceListener", e);
+ } catch (IOException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "failed to write bundle to output stream", e);
}
}
- @GuardedBy("mLock")
- private void setListenerLocked(@NonNull ICarTelemetryServiceListener listener) {
- if (DEBUG) {
- Slog.d(TAG, "Setting the listener for car telemetry service");
- }
- mListener = listener;
- }
-
- @GuardedBy("mLock")
- private void clearListenerLocked() {
- if (DEBUG) {
- Slog.d(TAG, "Clearing listener");
- }
- mListener = null;
- }
-
- @GuardedBy("mLock")
- private @AddManifestError int addManifestLocked(ManifestKey key, byte[] configProto) {
- if (DEBUG) {
- Slog.d(TAG, "Adding MetricsConfig to car telemetry service");
- }
- int currentVersion = mNameVersionMap.getOrDefault(key.getName(), DEFAULT_VERSION);
- if (currentVersion > key.getVersion()) {
- return ERROR_NEWER_MANIFEST_EXISTS;
- } else if (currentVersion == key.getVersion()) {
- return ERROR_SAME_MANIFEST_EXISTS;
- }
-
- TelemetryProto.MetricsConfig metricsConfig;
+ private void sendError(MetricsConfigKey key, TelemetryProto.TelemetryError error) {
try {
- metricsConfig = TelemetryProto.MetricsConfig.parseFrom(configProto);
- } catch (InvalidProtocolBufferException e) {
- Slog.e(TAG, "Failed to parse MetricsConfig.", e);
- return ERROR_PARSE_MANIFEST_FAILED;
+ mListener.onError(key, error.toByteArray());
+ } catch (RemoteException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "error with ICarTelemetryServiceListener", e);
}
- mNameVersionMap.put(key.getName(), key.getVersion());
-
- // TODO(b/186047142): Store the MetricsConfig to disk
- // TODO(b/186047142): Send metricsConfig to a script manager or a queue
- return ERROR_NONE;
}
- @GuardedBy("mLock")
- private boolean removeManifestLocked(@NonNull ManifestKey key) {
- Integer version = mNameVersionMap.remove(key.getName());
- if (version == null) {
- return false;
- }
- // TODO(b/186047142): Delete manifest from disk and remove it from queue
- return true;
+ @VisibleForTesting
+ Handler getTelemetryHandler() {
+ return mTelemetryHandler;
}
- @GuardedBy("mLock")
- private void removeAllManifestsLocked() {
- if (DEBUG) {
- Slog.d(TAG, "Removing all manifest from car telemetry service");
- }
- mNameVersionMap.clear();
- // TODO(b/186047142): Delete all manifests from disk & queue
+ @VisibleForTesting
+ ResultStore getResultStore() {
+ return mResultStore;
+ }
+
+ @VisibleForTesting
+ MetricsConfigStore getMetricsConfigStore() {
+ return mMetricsConfigStore;
}
}
diff --git a/service/src/com/android/car/telemetry/MetricsConfigStore.java b/service/src/com/android/car/telemetry/MetricsConfigStore.java
new file mode 100644
index 0000000..5a6e1ca
--- /dev/null
+++ b/service/src/com/android/car/telemetry/MetricsConfigStore.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2021 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.telemetry;
+
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_ALREADY_EXISTS;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_NONE;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_UNKNOWN;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_VERSION_TOO_OLD;
+
+import android.car.telemetry.MetricsConfigKey;
+import android.util.ArrayMap;
+
+import com.android.car.CarLog;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class is responsible for storing, retrieving, and deleting {@link
+ * TelemetryProto.MetricsConfig}. All of the methods are blocking so the class should only be
+ * accessed on the telemetry thread.
+ */
+public class MetricsConfigStore {
+ @VisibleForTesting
+ static final String METRICS_CONFIG_DIR = "metrics_configs";
+
+ private final File mConfigDirectory;
+ private Map<String, TelemetryProto.MetricsConfig> mActiveConfigs;
+
+ public MetricsConfigStore(File rootDirectory) {
+ mConfigDirectory = new File(rootDirectory, METRICS_CONFIG_DIR);
+ mConfigDirectory.mkdirs();
+ mActiveConfigs = new ArrayMap<>();
+ // TODO(b/197336485): Add expiration date check for MetricsConfig
+ for (File file : mConfigDirectory.listFiles()) {
+ try {
+ byte[] serializedConfig = Files.readAllBytes(file.toPath());
+ TelemetryProto.MetricsConfig config =
+ TelemetryProto.MetricsConfig.parseFrom(serializedConfig);
+ mActiveConfigs.put(config.getName(), config);
+ } catch (IOException e) {
+ // TODO(b/197336655): record failure
+ file.delete();
+ }
+ }
+ }
+
+ /**
+ * Returns all active {@link TelemetryProto.MetricsConfig} from disk.
+ */
+ public List<TelemetryProto.MetricsConfig> getActiveMetricsConfigs() {
+ return new ArrayList<>(mActiveConfigs.values());
+ }
+
+ /**
+ * Stores the MetricsConfig to disk if it is valid. It checks both config name and version for
+ * validity.
+ *
+ * @param metricsConfig the config to be persisted to disk.
+ * @return {@link android.car.telemetry.CarTelemetryManager.MetricsConfigError} status code.
+ */
+ public int addMetricsConfig(TelemetryProto.MetricsConfig metricsConfig) {
+ // TODO(b/197336485): Check expiration date for MetricsConfig
+ if (metricsConfig.getVersion() <= 0) {
+ return ERROR_METRICS_CONFIG_VERSION_TOO_OLD;
+ }
+ if (mActiveConfigs.containsKey(metricsConfig.getName())) {
+ int currentVersion = mActiveConfigs.get(metricsConfig.getName()).getVersion();
+ if (currentVersion > metricsConfig.getVersion()) {
+ return ERROR_METRICS_CONFIG_VERSION_TOO_OLD;
+ } else if (currentVersion == metricsConfig.getVersion()) {
+ return ERROR_METRICS_CONFIG_ALREADY_EXISTS;
+ }
+ }
+ mActiveConfigs.put(metricsConfig.getName(), metricsConfig);
+ try {
+ Files.write(
+ new File(mConfigDirectory, metricsConfig.getName()).toPath(),
+ metricsConfig.toByteArray());
+ } catch (IOException e) {
+ // TODO(b/197336655): record failure
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to write metrics config to disk", e);
+ return ERROR_METRICS_CONFIG_UNKNOWN;
+ }
+ return ERROR_METRICS_CONFIG_NONE;
+ }
+
+ /**
+ * Deletes the MetricsConfig from disk.
+ *
+ * @param key the unique identifier of the metrics config that should be deleted.
+ * @return true for successful removal, false otherwise.
+ */
+ public boolean removeMetricsConfig(MetricsConfigKey key) {
+ String metricsConfigName = key.getName();
+ if (!mActiveConfigs.containsKey(key.getName())
+ || mActiveConfigs.get(key.getName()).getVersion() != key.getVersion()) {
+ return false; // no match found, nothing to remove
+ }
+ mActiveConfigs.remove(metricsConfigName);
+ try {
+ return Files.deleteIfExists(Paths.get(
+ mConfigDirectory.getAbsolutePath(), metricsConfigName));
+ } catch (IOException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to remove MetricsConfig: " + key.getName(), e);
+ // TODO(b/197336655): record failure
+ }
+ return false;
+ }
+
+ /** Deletes all MetricsConfigs from disk. */
+ public void removeAllMetricsConfigs() {
+ mActiveConfigs.clear();
+ for (File file : mConfigDirectory.listFiles()) {
+ if (!file.delete()) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to remove MetricsConfig: " + file.getName());
+ }
+ }
+ }
+}
diff --git a/service/src/com/android/car/telemetry/ResultStore.java b/service/src/com/android/car/telemetry/ResultStore.java
new file mode 100644
index 0000000..1f4311b
--- /dev/null
+++ b/service/src/com/android/car/telemetry/ResultStore.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2021 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.telemetry;
+
+import android.car.telemetry.MetricsConfigKey;
+import android.os.PersistableBundle;
+import android.util.ArrayMap;
+
+import com.android.car.CarLog;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Disk storage for interim and final metrics statistics.
+ * All methods in this class should be invoked from the telemetry thread.
+ */
+public class ResultStore {
+
+ private static final long STALE_THRESHOLD_MILLIS =
+ TimeUnit.MILLISECONDS.convert(30, TimeUnit.DAYS);
+ @VisibleForTesting
+ static final String INTERIM_RESULT_DIR = "interim";
+ @VisibleForTesting
+ static final String ERROR_RESULT_DIR = "error";
+ @VisibleForTesting
+ static final String FINAL_RESULT_DIR = "final";
+
+ /** Map keys are MetricsConfig names, which are also the file names in disk. */
+ private final Map<String, InterimResult> mInterimResultCache = new ArrayMap<>();
+
+ private final File mInterimResultDirectory;
+ private final File mErrorResultDirectory;
+ private final File mFinalResultDirectory;
+
+ ResultStore(File rootDirectory) {
+ mInterimResultDirectory = new File(rootDirectory, INTERIM_RESULT_DIR);
+ mErrorResultDirectory = new File(rootDirectory, ERROR_RESULT_DIR);
+ mFinalResultDirectory = new File(rootDirectory, FINAL_RESULT_DIR);
+ mInterimResultDirectory.mkdirs();
+ mErrorResultDirectory.mkdirs();
+ mFinalResultDirectory.mkdirs();
+ // load results into memory to reduce the frequency of disk access
+ loadInterimResultsIntoMemory();
+ }
+
+ /** Reads interim results into memory for faster access. */
+ private void loadInterimResultsIntoMemory() {
+ for (File file : mInterimResultDirectory.listFiles()) {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ mInterimResultCache.put(
+ file.getName(),
+ new InterimResult(PersistableBundle.readFromStream(fis)));
+ } catch (IOException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to read result from disk.", e);
+ // TODO(b/197153560): record failure
+ }
+ }
+ }
+
+ /**
+ * Retrieves interim metrics for the given
+ * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
+ */
+ public PersistableBundle getInterimResult(String metricsConfigName) {
+ if (!mInterimResultCache.containsKey(metricsConfigName)) {
+ return null;
+ }
+ return mInterimResultCache.get(metricsConfigName).getBundle();
+ }
+
+ /**
+ * Stores interim metrics results in memory for the given
+ * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
+ */
+ public void putInterimResult(String metricsConfigName, PersistableBundle result) {
+ mInterimResultCache.put(metricsConfigName, new InterimResult(result, /* dirty = */ true));
+ }
+
+ /**
+ * Retrieves final metrics for the given
+ * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
+ *
+ * @param metricsConfigName name of the MetricsConfig.
+ * @param deleteResult if true, the final result will be deleted from disk.
+ * @return the final result as PersistableBundle if exists, null otherwise
+ */
+ public PersistableBundle getFinalResult(String metricsConfigName, boolean deleteResult) {
+ File file = new File(mFinalResultDirectory, metricsConfigName);
+ // if no final result exists for this metrics config, return immediately
+ if (!file.exists()) {
+ return null;
+ }
+ PersistableBundle result = null;
+ try (FileInputStream fis = new FileInputStream(file)) {
+ result = PersistableBundle.readFromStream(fis);
+ } catch (IOException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to get final result from disk.", e);
+ }
+ if (deleteResult) {
+ file.delete();
+ }
+ return result;
+ }
+
+ /**
+ * Stores final metrics in memory for the given
+ * {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}.
+ */
+ public void putFinalResult(String metricsConfigName, PersistableBundle result) {
+ writePersistableBundleToFile(mFinalResultDirectory, metricsConfigName, result);
+ deleteFileInDirectory(mInterimResultDirectory, metricsConfigName);
+ mInterimResultCache.remove(metricsConfigName);
+ }
+
+ /** Returns the error result produced by the metrics config if exists, null otherwise. */
+ public TelemetryProto.TelemetryError getError(
+ String metricsConfigName, boolean deleteResult) {
+ File file = new File(mErrorResultDirectory, metricsConfigName);
+ // if no error exists for this metrics config, return immediately
+ if (!file.exists()) {
+ return null;
+ }
+ TelemetryProto.TelemetryError result = null;
+ try {
+ byte[] serializedBytes = Files.readAllBytes(file.toPath());
+ result = TelemetryProto.TelemetryError.parseFrom(serializedBytes);
+ } catch (IOException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to get error result from disk.", e);
+ }
+ if (deleteResult) {
+ file.delete();
+ }
+ return result;
+ }
+
+ /** Stores the error object produced by the script. */
+ public void putError(String metricsConfigName, TelemetryProto.TelemetryError error) {
+ mInterimResultCache.remove(metricsConfigName);
+ try {
+ Files.write(
+ new File(mErrorResultDirectory, metricsConfigName).toPath(),
+ error.toByteArray());
+ deleteFileInDirectory(mInterimResultDirectory, metricsConfigName);
+ mInterimResultCache.remove(metricsConfigName);
+ } catch (IOException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to write data to file", e);
+ // TODO(b/197153560): record failure
+ }
+ }
+
+ /** Persists data to disk. */
+ public void flushToDisk() {
+ writeInterimResultsToFile();
+ deleteAllStaleData(mInterimResultDirectory, mFinalResultDirectory);
+ }
+
+ /**
+ * Deletes script result associated with the given config name. If result does not exist, this
+ * method does not do anything.
+ */
+ public void removeResult(MetricsConfigKey key) {
+ String metricsConfigName = key.getName();
+ mInterimResultCache.remove(metricsConfigName);
+ deleteFileInDirectory(mInterimResultDirectory, metricsConfigName);
+ deleteFileInDirectory(mFinalResultDirectory, metricsConfigName);
+ }
+
+ /** Deletes all interim and final results stored in disk. */
+ public void removeAllResults() {
+ mInterimResultCache.clear();
+ for (File interimResult : mInterimResultDirectory.listFiles()) {
+ interimResult.delete();
+ }
+ for (File finalResult : mFinalResultDirectory.listFiles()) {
+ finalResult.delete();
+ }
+ }
+
+ /** Writes dirty interim results to disk. */
+ private void writeInterimResultsToFile() {
+ mInterimResultCache.forEach((metricsConfigName, interimResult) -> {
+ // only write dirty data
+ if (!interimResult.isDirty()) {
+ return;
+ }
+ writePersistableBundleToFile(
+ mInterimResultDirectory, metricsConfigName, interimResult.getBundle());
+ });
+ }
+
+ /** Deletes data that are older than some threshold in the given directories. */
+ private void deleteAllStaleData(File... dirs) {
+ long currTimeMs = System.currentTimeMillis();
+ for (File dir : dirs) {
+ for (File file : dir.listFiles()) {
+ // delete stale data
+ if (file.lastModified() + STALE_THRESHOLD_MILLIS < currTimeMs) {
+ file.delete();
+ }
+ }
+ }
+ }
+
+ /**
+ * Converts a {@link PersistableBundle} into byte array and saves the results to a file.
+ */
+ private void writePersistableBundleToFile(
+ File dir, String metricsConfigName, PersistableBundle result) {
+ try (FileOutputStream os = new FileOutputStream(new File(dir, metricsConfigName))) {
+ result.writeToStream(os);
+ } catch (IOException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to write result to file", e);
+ // TODO(b/197153560): record failure
+ }
+ }
+
+ /** Deletes a the given file in the given directory if it exists. */
+ private void deleteFileInDirectory(File interimResultDirectory, String metricsConfigName) {
+ File file = new File(interimResultDirectory, metricsConfigName);
+ file.delete();
+ }
+
+ /** Wrapper around a result and whether the result should be written to disk. */
+ static final class InterimResult {
+ private final PersistableBundle mBundle;
+ private final boolean mDirty;
+
+ InterimResult(PersistableBundle bundle) {
+ mBundle = bundle;
+ mDirty = false;
+ }
+
+ InterimResult(PersistableBundle bundle, boolean dirty) {
+ mBundle = bundle;
+ mDirty = dirty;
+ }
+
+ PersistableBundle getBundle() {
+ return mBundle;
+ }
+
+ boolean isDirty() {
+ return mDirty;
+ }
+ }
+}
diff --git a/service/src/com/android/car/telemetry/ScriptExecutor.java b/service/src/com/android/car/telemetry/ScriptExecutor.java
deleted file mode 100644
index d791481..0000000
--- a/service/src/com/android/car/telemetry/ScriptExecutor.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2021 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.telemetry;
-
-import android.app.Service;
-import android.car.telemetry.IScriptExecutor;
-import android.car.telemetry.IScriptExecutorListener;
-import android.content.Intent;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.util.Log;
-
-import com.android.car.CarServiceUtils;
-
-/**
- * Executes Lua code in an isolated process with provided source code
- * and input arguments.
- */
-public final class ScriptExecutor extends Service {
-
- static {
- System.loadLibrary("scriptexecutorjni");
- }
-
- private static final String TAG = ScriptExecutor.class.getSimpleName();
-
- // Dedicated "worker" thread to handle all calls related to native code.
- private HandlerThread mNativeHandlerThread;
- // Handler associated with the native worker thread.
- private Handler mNativeHandler;
-
- private final class IScriptExecutorImpl extends IScriptExecutor.Stub {
- @Override
- public void invokeScript(String scriptBody, String functionName, Bundle publishedData,
- Bundle savedState, IScriptExecutorListener listener) {
- mNativeHandler.post(() ->
- nativeInvokeScript(mLuaEnginePtr, scriptBody, functionName, publishedData,
- savedState, listener));
- }
- }
- private IScriptExecutorImpl mScriptExecutorBinder;
-
- // Memory location of Lua Engine object which is allocated in native code.
- private long mLuaEnginePtr;
-
- @Override
- public void onCreate() {
- super.onCreate();
-
- mNativeHandlerThread = CarServiceUtils.getHandlerThread(
- ScriptExecutor.class.getSimpleName());
- // TODO(b/192284628): Remove this once start handling all recoverable errors via onError
- // callback.
- mNativeHandlerThread.setUncaughtExceptionHandler((t, e) -> Log.e(TAG, e.getMessage()));
- mNativeHandler = new Handler(mNativeHandlerThread.getLooper());
-
- mLuaEnginePtr = nativeInitLuaEngine();
- mScriptExecutorBinder = new IScriptExecutorImpl();
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- nativeDestroyLuaEngine(mLuaEnginePtr);
- mNativeHandlerThread.quit();
- }
-
- @Override
- public IBinder onBind(Intent intent) {
- return mScriptExecutorBinder;
- }
-
- /**
- * Initializes Lua Engine.
- *
- * <p>Returns memory location of Lua Engine.
- */
- private native long nativeInitLuaEngine();
-
- /**
- * Destroys LuaEngine at the provided memory address.
- */
- private native void nativeDestroyLuaEngine(long luaEnginePtr);
-
- /**
- * Calls provided Lua function.
- *
- * @param luaEnginePtr memory address of the stored LuaEngine instance.
- * @param scriptBody complete body of Lua script that also contains the function to be invoked.
- * @param functionName the name of the function to execute.
- * @param publishedData input data provided by the source which the function handles.
- * @param savedState key-value pairs preserved from the previous invocation of the function.
- * @param listener callback for the sandboxed environent to report back script execution results
- * and errors.
- */
- private native void nativeInvokeScript(long luaEnginePtr, String scriptBody,
- String functionName, Bundle publishedData, Bundle savedState,
- IScriptExecutorListener listener);
-}
diff --git a/service/src/com/android/car/telemetry/databroker/DataBroker.java b/service/src/com/android/car/telemetry/databroker/DataBroker.java
index 44f2f61..2e8900b 100644
--- a/service/src/com/android/car/telemetry/databroker/DataBroker.java
+++ b/service/src/com/android/car/telemetry/databroker/DataBroker.java
@@ -16,42 +16,74 @@
package com.android.car.telemetry.databroker;
+import android.car.telemetry.MetricsConfigKey;
+
import com.android.car.telemetry.TelemetryProto;
/** Interface for the data path. Handles data forwarding from publishers to subscribers */
public interface DataBroker {
/**
+ * Interface for receiving notification that script finished.
+ */
+ interface ScriptFinishedCallback {
+ /**
+ * Listens to script finished event.
+ *
+ * @param key that uniquely identifies the config whose script finished.
+ */
+ void onScriptFinished(MetricsConfigKey key);
+ }
+
+ /**
* Adds an active {@link com.android.car.telemetry.TelemetryProto.MetricsConfig} that is pending
* execution. When updating the MetricsConfig to a newer version, the caller must call
- * {@link #removeMetricsConfiguration(TelemetryProto.MetricsConfig)} first to clear the old
- * MetricsConfig.
+ * {@link #removeMetricsConfig(String)} first to clear the old MetricsConfig.
* TODO(b/191378559): Define behavior when metricsConfig contains invalid config
*
+ * @param key the unique identifier of the MetricsConfig.
* @param metricsConfig to be added and queued for execution.
- * @return true for success, false for failure.
*/
- boolean addMetricsConfiguration(TelemetryProto.MetricsConfig metricsConfig);
+ void addMetricsConfig(MetricsConfigKey key, TelemetryProto.MetricsConfig metricsConfig);
/**
* Removes a {@link com.android.car.telemetry.TelemetryProto.MetricsConfig} and all its
* relevant subscriptions.
*
- * @param metricsConfig to be removed from DataBroker.
- * @return true for success, false for failure.
+ * @param key the unique identifier of the MetricsConfig to be removed.
*/
- boolean removeMetricsConfiguration(TelemetryProto.MetricsConfig metricsConfig);
+ void removeMetricsConfig(MetricsConfigKey key);
+
+ /**
+ * Removes all {@link com.android.car.telemetry.TelemetryProto.MetricsConfig}s and
+ * subscriptions.
+ */
+ void removeAllMetricsConfigs();
+
+ /**
+ * Adds a {@link ScriptExecutionTask} to the priority queue. This method will schedule the
+ * next task if a task is not currently running.
+ */
+ void addTaskToQueue(ScriptExecutionTask task);
+
+ /**
+ * Checks system health state and executes a task if condition allows.
+ */
+ void scheduleNextTask();
/**
* Sets callback for notifying script finished.
*
* @param callback script finished callback.
*/
- void setOnScriptFinishedCallback(DataBrokerController.ScriptFinishedCallback callback);
+ void setOnScriptFinishedCallback(ScriptFinishedCallback callback);
/**
- * Invoked by controller to indicate system health state and which subscribers can be consumed.
- * A smaller priority number indicates higher priority. Range 1 - 100.
+ * Sets the priority which affects which subscribers can consume data. Invoked by controller to
+ * indicate system health state and which subscribers can be consumed. If controller does not
+ * set the priority, it will be defaulted to 1. A smaller priority number indicates higher
+ * priority. Range 0 - 100. A priority of 0 means the script should run regardless of system
+ * health conditions.
*/
void setTaskExecutionPriority(int priority);
}
diff --git a/service/src/com/android/car/telemetry/databroker/DataBrokerController.java b/service/src/com/android/car/telemetry/databroker/DataBrokerController.java
index 7663667..452a12a 100644
--- a/service/src/com/android/car/telemetry/databroker/DataBrokerController.java
+++ b/service/src/com/android/car/telemetry/databroker/DataBrokerController.java
@@ -16,60 +16,83 @@
package com.android.car.telemetry.databroker;
-import com.android.car.telemetry.TelemetryProto.MetricsConfig;
+import android.car.telemetry.MetricsConfigKey;
+import android.os.Handler;
+
+import com.android.car.systeminterface.SystemStateInterface;
+import com.android.car.telemetry.MetricsConfigStore;
+import com.android.car.telemetry.TelemetryProto;
import com.android.car.telemetry.systemmonitor.SystemMonitor;
import com.android.car.telemetry.systemmonitor.SystemMonitorEvent;
+import java.time.Duration;
+
/**
* DataBrokerController instantiates the DataBroker and manages what Publishers
* it can read from based current system states and policies.
*/
public class DataBrokerController {
+ /**
+ * Priorities range from 0 to 100, with 0 being the highest priority and 100 being the lowest.
+ * A {@link ScriptExecutionTask} must have equal or higher priority than the threshold in order
+ * to be executed.
+ * The following constants are chosen with the idea that subscribers with a priority of 0
+ * must be executed as soon as data is published regardless of system health conditions.
+ * Otherwise {@link ScriptExecutionTask}s are executed from the highest priority to the lowest
+ * subject to system health constraints from {@link SystemMonitor}.
+ */
public static final int TASK_PRIORITY_HI = 0;
public static final int TASK_PRIORITY_MED = 50;
public static final int TASK_PRIORITY_LOW = 100;
- private MetricsConfig mMetricsConfig;
private final DataBroker mDataBroker;
+ private final Handler mTelemetryHandler;
+ private final MetricsConfigStore mMetricsConfigStore;
private final SystemMonitor mSystemMonitor;
+ private final SystemStateInterface mSystemStateInterface;
- /**
- * Interface for receiving notification that script finished.
- */
- public interface ScriptFinishedCallback {
- /**
- * Listens to script finished event.
- *
- * @param configName the name of the config whose script finished.
- */
- void onScriptFinished(String configName);
- }
-
- public DataBrokerController(DataBroker dataBroker, SystemMonitor systemMonitor) {
+ public DataBrokerController(
+ DataBroker dataBroker,
+ Handler telemetryHandler,
+ MetricsConfigStore metricsConfigStore,
+ SystemMonitor systemMonitor,
+ SystemStateInterface systemStateInterface) {
mDataBroker = dataBroker;
- mDataBroker.setOnScriptFinishedCallback(this::onScriptFinished);
+ mTelemetryHandler = telemetryHandler;
+ mMetricsConfigStore = metricsConfigStore;
mSystemMonitor = systemMonitor;
+ mSystemStateInterface = systemStateInterface;
+
+ mDataBroker.setOnScriptFinishedCallback(this::onScriptFinished);
mSystemMonitor.setSystemMonitorCallback(this::onSystemMonitorEvent);
+ mSystemStateInterface.scheduleActionForBootCompleted(
+ this::startMetricsCollection, Duration.ZERO);
}
/**
- * Listens to new {@link MetricsConfig}.
- *
- * @param metricsConfig the metrics config.
+ * Starts collecting data. Once data is sent by publishers, DataBroker will arrange scripts to
+ * run. This method is called by some thread on executor service, therefore the work needs to
+ * be posted on the telemetry thread.
*/
- public void onNewMetricsConfig(MetricsConfig metricsConfig) {
- mMetricsConfig = metricsConfig;
- mDataBroker.addMetricsConfiguration(mMetricsConfig);
+ private void startMetricsCollection() {
+ mTelemetryHandler.post(() -> {
+ for (TelemetryProto.MetricsConfig config :
+ mMetricsConfigStore.getActiveMetricsConfigs()) {
+ mDataBroker.addMetricsConfig(
+ new MetricsConfigKey(config.getName(), config.getVersion()), config);
+ }
+ });
}
/**
* Listens to script finished event from {@link DataBroker}.
*
- * @param configName the name of the config of the finished script.
+ * @param key the unique identifier of the config whose script finished.
*/
- public void onScriptFinished(String configName) {
- // TODO(b/192008783): remove finished config from config store
+ public void onScriptFinished(MetricsConfigKey key) {
+ mMetricsConfigStore.removeMetricsConfig(key);
+ mDataBroker.removeMetricsConfig(key);
}
/**
@@ -86,7 +109,7 @@
|| event.getMemoryUsageLevel() == SystemMonitorEvent.USAGE_LEVEL_HI) {
mDataBroker.setTaskExecutionPriority(TASK_PRIORITY_HI);
} else if (event.getCpuUsageLevel() == SystemMonitorEvent.USAGE_LEVEL_MED
- || event.getMemoryUsageLevel() == SystemMonitorEvent.USAGE_LEVEL_MED) {
+ || event.getMemoryUsageLevel() == SystemMonitorEvent.USAGE_LEVEL_MED) {
mDataBroker.setTaskExecutionPriority(TASK_PRIORITY_MED);
} else {
mDataBroker.setTaskExecutionPriority(TASK_PRIORITY_LOW);
diff --git a/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java b/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
index 86d725e..c1ba29b 100644
--- a/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
+++ b/service/src/com/android/car/telemetry/databroker/DataBrokerImpl.java
@@ -16,55 +16,219 @@
package com.android.car.telemetry.databroker;
+import android.car.telemetry.MetricsConfigKey;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
import android.util.ArrayMap;
-import android.util.Slog;
import com.android.car.CarLog;
+import com.android.car.CarServiceUtils;
+import com.android.car.telemetry.CarTelemetryService;
+import com.android.car.telemetry.ResultStore;
import com.android.car.telemetry.TelemetryProto;
import com.android.car.telemetry.TelemetryProto.MetricsConfig;
import com.android.car.telemetry.publisher.AbstractPublisher;
import com.android.car.telemetry.publisher.PublisherFactory;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutor;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.PriorityBlockingQueue;
/**
* Implementation of the data path component of CarTelemetryService. Forwards the published data
* from publishers to consumers subject to the Controller's decision.
+ * All methods should be called from the telemetry thread unless otherwise specified as thread-safe.
*/
public class DataBrokerImpl implements DataBroker {
- // Maps MetricsConfig's name to its subscriptions. This map is useful when removing a
- // MetricsConfig.
- private final Map<String, List<DataSubscriber>> mSubscriptionMap = new ArrayMap<>();
+ private static final int MSG_HANDLE_TASK = 1;
+ private static final int MSG_BIND_TO_SCRIPT_EXECUTOR = 2;
- private DataBrokerController.ScriptFinishedCallback mScriptFinishedCallback;
+ /** Bind to script executor 5 times before entering disabled state. */
+ private static final int MAX_BIND_SCRIPT_EXECUTOR_ATTEMPTS = 5;
+
+ private static final String SCRIPT_EXECUTOR_PACKAGE = "com.android.car.scriptexecutor";
+ private static final String SCRIPT_EXECUTOR_CLASS =
+ "com.android.car.scriptexecutor.ScriptExecutor";
+
+ private final Context mContext;
private final PublisherFactory mPublisherFactory;
+ private final ResultStore mResultStore;
+ private final ScriptExecutorListener mScriptExecutorListener;
+ private final HandlerThread mTelemetryThread = CarServiceUtils.getHandlerThread(
+ CarTelemetryService.class.getSimpleName());
+ private final Handler mTelemetryHandler = new TaskHandler(mTelemetryThread.getLooper());
- public DataBrokerImpl(PublisherFactory publisherFactory) {
+ /** Thread-safe priority queue for scheduling tasks. */
+ private final PriorityBlockingQueue<ScriptExecutionTask> mTaskQueue =
+ new PriorityBlockingQueue<>();
+
+ /**
+ * Maps MetricsConfig's unique identifier to its subscriptions. This map is useful when
+ * removing a MetricsConfig.
+ */
+ private final Map<MetricsConfigKey, List<DataSubscriber>> mSubscriptionMap = new ArrayMap<>();
+
+ /**
+ * If something irrecoverable happened, DataBroker should enter into a disabled state to prevent
+ * doing futile work.
+ */
+ private boolean mDisabled = false;
+
+ /** Current number of attempts to bind to ScriptExecutor. */
+ private int mBindScriptExecutorAttempts = 0;
+
+ /** Priority of current system to determine if a {@link ScriptExecutionTask} can run. */
+ private int mPriority = 1;
+
+ /** Waiting period between attempts to bind script executor. Can be shortened for tests. */
+ @VisibleForTesting long mBindScriptExecutorDelayMillis = 3_000L;
+
+ /**
+ * {@link MetricsConfigKey} that uniquely identifies the current running {@link MetricsConfig}.
+ * A non-null value indicates ScriptExecutor is currently running this config, which means
+ * DataBroker should not make another ScriptExecutor binder call.
+ */
+ private MetricsConfigKey mCurrentMetricsConfigKey;
+ private IScriptExecutor mScriptExecutor;
+ private ScriptFinishedCallback mScriptFinishedCallback;
+
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mTelemetryHandler.post(() -> {
+ mScriptExecutor = IScriptExecutor.Stub.asInterface(service);
+ scheduleNextTask();
+ });
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ // TODO(b/198684473): clean up the state after script executor disconnects
+ mTelemetryHandler.post(() -> {
+ mScriptExecutor = null;
+ unbindScriptExecutor();
+ });
+ }
+ };
+
+ public DataBrokerImpl(
+ Context context, PublisherFactory publisherFactory, ResultStore resultStore) {
+ mContext = context;
mPublisherFactory = publisherFactory;
+ mResultStore = resultStore;
+ mScriptExecutorListener = new ScriptExecutorListener(this);
+ mPublisherFactory.setFailureListener(this::onPublisherFailure);
}
- // current task priority, used to determine which data can be processed
- private int mTaskExecutionPriority;
+ private void onPublisherFailure(
+ AbstractPublisher publisher, List<TelemetryProto.MetricsConfig> affectedConfigs,
+ Throwable error) {
+ // TODO(b/193680465): disable MetricsConfig and log the error
+ Slogf.w(CarLog.TAG_TELEMETRY, "publisher failed", error);
+ }
+
+ private void bindScriptExecutor() {
+ // do not re-bind if broker is in a disabled state or if script executor is nonnull
+ if (mDisabled || mScriptExecutor != null) {
+ return;
+ }
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(SCRIPT_EXECUTOR_PACKAGE, SCRIPT_EXECUTOR_CLASS));
+ boolean success = mContext.bindServiceAsUser(
+ intent,
+ mServiceConnection,
+ Context.BIND_AUTO_CREATE,
+ UserHandle.SYSTEM);
+ if (success) {
+ mBindScriptExecutorAttempts = 0; // reset
+ return;
+ }
+ unbindScriptExecutor();
+ mBindScriptExecutorAttempts++;
+ if (mBindScriptExecutorAttempts < MAX_BIND_SCRIPT_EXECUTOR_ATTEMPTS) {
+ Slogf.w(CarLog.TAG_TELEMETRY,
+ "failed to get valid connection to ScriptExecutor, retrying in "
+ + mBindScriptExecutorDelayMillis + "ms.");
+ mTelemetryHandler.sendEmptyMessageDelayed(MSG_BIND_TO_SCRIPT_EXECUTOR,
+ mBindScriptExecutorDelayMillis);
+ } else {
+ Slogf.w(CarLog.TAG_TELEMETRY, "failed to get valid connection to ScriptExecutor, "
+ + "disabling DataBroker");
+ disableBroker();
+ }
+ }
+
+ /**
+ * Unbinds {@link ScriptExecutor} to release the connection. This method should be called from
+ * the telemetry thread.
+ */
+ private void unbindScriptExecutor() {
+ // TODO(b/198648763): unbind from script executor when there is no work to do
+ mCurrentMetricsConfigKey = null;
+ try {
+ mContext.unbindService(mServiceConnection);
+ } catch (IllegalArgumentException e) {
+ // If ScriptExecutor is gone before unbinding, it will throw this exception
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to unbind from ScriptExecutor", e);
+ }
+ }
+
+ /**
+ * Enters into a disabled state because something irrecoverable happened.
+ * TODO(b/200841260): expose the state to the caller.
+ */
+ private void disableBroker() {
+ mDisabled = true;
+ // remove all MetricConfigs, disable all publishers, stop receiving data
+ for (MetricsConfigKey key : mSubscriptionMap.keySet()) {
+ // get the metrics config from the DataSubscriber and remove the metrics config
+ if (mSubscriptionMap.get(key).size() != 0) {
+ removeMetricsConfig(key);
+ }
+ }
+ mSubscriptionMap.clear();
+ }
@Override
- public boolean addMetricsConfiguration(MetricsConfig metricsConfig) {
- // if metricsConfig already exists, it should not be added again
- if (mSubscriptionMap.containsKey(metricsConfig.getName())) {
- return false;
+ public void addMetricsConfig(MetricsConfigKey key, MetricsConfig metricsConfig) {
+ // TODO(b/187743369): pass status back to caller
+ // if broker is disabled or metricsConfig already exists, do nothing
+ if (mDisabled || mSubscriptionMap.containsKey(key)) {
+ return;
}
// Create the subscribers for this metrics configuration
- List<DataSubscriber> dataSubscribers = new ArrayList<>();
+ List<DataSubscriber> dataSubscribers = new ArrayList<>(
+ metricsConfig.getSubscribersList().size());
for (TelemetryProto.Subscriber subscriber : metricsConfig.getSubscribersList()) {
// protobuf publisher to a concrete Publisher
AbstractPublisher publisher = mPublisherFactory.getPublisher(
subscriber.getPublisher().getPublisherCase());
-
// create DataSubscriber from TelemetryProto.Subscriber
- DataSubscriber dataSubscriber = new DataSubscriber(metricsConfig, subscriber);
+ DataSubscriber dataSubscriber = new DataSubscriber(
+ this,
+ metricsConfig,
+ subscriber);
dataSubscribers.add(dataSubscriber);
try {
@@ -72,21 +236,21 @@
// TODO(b/191378559): handle bad configs
publisher.addDataSubscriber(dataSubscriber);
} catch (IllegalArgumentException e) {
- Slog.w(CarLog.TAG_TELEMETRY, "Invalid config", e);
- return false;
+ Slogf.w(CarLog.TAG_TELEMETRY, "Invalid config", e);
+ return;
}
}
- mSubscriptionMap.put(metricsConfig.getName(), dataSubscribers);
- return true;
+ mSubscriptionMap.put(key, dataSubscribers);
}
@Override
- public boolean removeMetricsConfiguration(MetricsConfig metricsConfig) {
- if (!mSubscriptionMap.containsKey(metricsConfig.getName())) {
- return false;
+ public void removeMetricsConfig(MetricsConfigKey key) {
+ // TODO(b/187743369): pass status back to caller
+ if (!mSubscriptionMap.containsKey(key)) {
+ return;
}
// get the subscriptions associated with this MetricsConfig, remove it from the map
- List<DataSubscriber> dataSubscribers = mSubscriptionMap.remove(metricsConfig.getName());
+ List<DataSubscriber> dataSubscribers = mSubscriptionMap.remove(key);
// for each subscriber, remove it from publishers
for (DataSubscriber subscriber : dataSubscribers) {
AbstractPublisher publisher = mPublisherFactory.getPublisher(
@@ -95,25 +259,271 @@
publisher.removeDataSubscriber(subscriber);
} catch (IllegalArgumentException e) {
// It shouldn't happen, but if happens, let's just log it.
- Slog.w(CarLog.TAG_TELEMETRY, "Failed to remove subscriber from publisher", e);
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to remove subscriber from publisher", e);
}
- // TODO(b/187743369): remove related tasks from the queue
}
- return true;
+ // Remove all the tasks associated with this metrics config. The underlying impl uses the
+ // weakly consistent iterator, which is thread-safe but does not freeze the collection while
+ // iterating, so it may or may not reflect any updates since the iterator was created.
+ // But since adding & polling from queue should happen in the same thread, the task queue
+ // should not be changed while tasks are being iterated and removed.
+ mTaskQueue.removeIf(task -> task.isAssociatedWithMetricsConfig(key));
}
@Override
- public void setOnScriptFinishedCallback(DataBrokerController.ScriptFinishedCallback callback) {
+ public void removeAllMetricsConfigs() {
+ mPublisherFactory.removeAllDataSubscribers();
+ mSubscriptionMap.clear();
+ mTaskQueue.clear();
+ }
+
+ @Override
+ public void addTaskToQueue(ScriptExecutionTask task) {
+ if (mDisabled) {
+ return;
+ }
+ mTaskQueue.add(task);
+ scheduleNextTask();
+ }
+
+ /**
+ * This method can be called from any thread.
+ * It is possible for this method to be invoked from different threads at the same time, but
+ * it is not possible to schedule the same task twice, because the handler handles message
+ * in the order they come in, this means the task will be polled sequentially instead of
+ * concurrently. Every task that is scheduled and run will be distinct.
+ * TODO(b/187743369): If the threading behavior in DataSubscriber changes, ScriptExecutionTask
+ * will also have different threading behavior. Update javadoc when the behavior is decided.
+ */
+ @Override
+ public void scheduleNextTask() {
+ if (mDisabled || mTelemetryHandler.hasMessages(MSG_HANDLE_TASK)) {
+ return;
+ }
+ mTelemetryHandler.sendEmptyMessage(MSG_HANDLE_TASK);
+ }
+
+ @Override
+ public void setOnScriptFinishedCallback(ScriptFinishedCallback callback) {
+ if (mDisabled) {
+ return;
+ }
mScriptFinishedCallback = callback;
}
@Override
public void setTaskExecutionPriority(int priority) {
- mTaskExecutionPriority = priority;
+ if (mDisabled) {
+ return;
+ }
+ mPriority = priority;
+ scheduleNextTask(); // when priority updates, schedule a task which checks task queue
}
@VisibleForTesting
- Map<String, List<DataSubscriber>> getSubscriptionMap() {
- return mSubscriptionMap;
+ Map<MetricsConfigKey, List<DataSubscriber>> getSubscriptionMap() {
+ return new ArrayMap<>((ArrayMap<MetricsConfigKey, List<DataSubscriber>>) mSubscriptionMap);
+ }
+
+ @VisibleForTesting
+ Handler getTelemetryHandler() {
+ return mTelemetryHandler;
+ }
+
+ @VisibleForTesting
+ PriorityBlockingQueue<ScriptExecutionTask> getTaskQueue() {
+ return mTaskQueue;
+ }
+
+ /**
+ * Polls and runs a task from the head of the priority queue if the queue is nonempty and the
+ * head of the queue has priority higher than or equal to the current priority. A higher
+ * priority is denoted by a lower priority number, so head of the queue should have equal or
+ * lower priority number to be polled.
+ */
+ private void pollAndExecuteTask() {
+ // check databroker state is ready to run script
+ if (mDisabled || mCurrentMetricsConfigKey != null) {
+ return;
+ }
+ // check task is valid and ready to be run
+ ScriptExecutionTask task = mTaskQueue.peek();
+ if (task == null || task.getPriority() > mPriority) {
+ return;
+ }
+ // if script executor is null, bind service
+ if (mScriptExecutor == null) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "script executor is null, binding to script executor");
+ // upon successful binding, a task will be scheduled to run if there are any
+ mTelemetryHandler.sendEmptyMessage(MSG_BIND_TO_SCRIPT_EXECUTOR);
+ return;
+ }
+ mTaskQueue.poll(); // remove task from queue
+ // update current config key because a script is currently running
+ mCurrentMetricsConfigKey = new MetricsConfigKey(task.getMetricsConfig().getName(),
+ task.getMetricsConfig().getVersion());
+ try {
+ if (task.isLargeData()) {
+ Slogf.d(CarLog.TAG_TELEMETRY, "invoking script executor for large input");
+ invokeScriptForLargeInput(task);
+ } else {
+ Slogf.d(CarLog.TAG_TELEMETRY, "invoking script executor");
+ mScriptExecutor.invokeScript(
+ task.getMetricsConfig().getScript(),
+ task.getHandlerName(),
+ task.getData(),
+ mResultStore.getInterimResult(mCurrentMetricsConfigKey.getName()),
+ mScriptExecutorListener);
+ }
+ } catch (RemoteException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "remote exception occurred invoking script", e);
+ unbindScriptExecutor();
+ addTaskToQueue(task); // will trigger scheduleNextTask() and re-binding scriptexecutor
+ } catch (IOException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Either unable to create pipe or failed to pipe data"
+ + " to ScriptExecutor. Skipping the published data", e);
+ mCurrentMetricsConfigKey = null;
+ scheduleNextTask(); // drop this task and schedule the next one
+ }
+ }
+
+ /**
+ * Sets up pipes, invokes ScriptExecutor#invokeScriptForLargeInput() API, and writes the
+ * script input to the pipe.
+ *
+ * @param task containing all the necessary parameters for ScriptExecutor API.
+ * @throws IOException if cannot create pipe or cannot write the bundle to pipe.
+ * @throws RemoteException if ScriptExecutor failed.
+ */
+ private void invokeScriptForLargeInput(ScriptExecutionTask task)
+ throws IOException, RemoteException {
+ ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
+ ParcelFileDescriptor readFd = fds[0];
+ ParcelFileDescriptor writeFd = fds[1];
+ try {
+ mScriptExecutor.invokeScriptForLargeInput(
+ task.getMetricsConfig().getScript(),
+ task.getHandlerName(),
+ readFd,
+ mResultStore.getInterimResult(mCurrentMetricsConfigKey.getName()),
+ mScriptExecutorListener);
+ } catch (RemoteException e) {
+ closeQuietly(readFd);
+ closeQuietly(writeFd);
+ throw e;
+ }
+ closeQuietly(readFd);
+
+ Slogf.d(CarLog.TAG_TELEMETRY, "writing large script data to pipe");
+ try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeFd)) {
+ task.getData().writeToStream(outputStream);
+ }
+ }
+
+ /** Quietly closes Java Closeables, ignoring IOException. */
+ private void closeQuietly(Closeable closeable) {
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+
+ /** Stores final metrics and schedules the next task. */
+ private void onScriptFinished(PersistableBundle result) {
+ mTelemetryHandler.post(() -> {
+ mResultStore.putFinalResult(mCurrentMetricsConfigKey.getName(), result);
+ mScriptFinishedCallback.onScriptFinished(mCurrentMetricsConfigKey);
+ mCurrentMetricsConfigKey = null;
+ scheduleNextTask();
+ });
+ }
+
+ /** Stores interim metrics and schedules the next task. */
+ private void onScriptSuccess(PersistableBundle stateToPersist) {
+ mTelemetryHandler.post(() -> {
+ mResultStore.putInterimResult(mCurrentMetricsConfigKey.getName(), stateToPersist);
+ mCurrentMetricsConfigKey = null;
+ scheduleNextTask();
+ });
+ }
+
+ /** Stores telemetry error and schedules the next task. */
+ private void onScriptError(int errorType, String message, String stackTrace) {
+ mTelemetryHandler.post(() -> {
+ TelemetryProto.TelemetryError.Builder error = TelemetryProto.TelemetryError.newBuilder()
+ .setErrorType(TelemetryProto.TelemetryError.ErrorType.forNumber(errorType))
+ .setMessage(message);
+ if (stackTrace != null) {
+ error.setStackTrace(stackTrace);
+ }
+ mResultStore.putError(mCurrentMetricsConfigKey.getName(), error.build());
+ mCurrentMetricsConfigKey = null;
+ scheduleNextTask();
+ });
+ }
+
+ /** Listens for script execution status. Methods are called on the binder thread. */
+ private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
+ private final WeakReference<DataBrokerImpl> mWeakDataBroker;
+
+ private ScriptExecutorListener(DataBrokerImpl dataBroker) {
+ mWeakDataBroker = new WeakReference<>(dataBroker);
+ }
+
+ @Override
+ public void onScriptFinished(PersistableBundle result) {
+ DataBrokerImpl dataBroker = mWeakDataBroker.get();
+ if (dataBroker == null) {
+ return;
+ }
+ dataBroker.onScriptFinished(result);
+ }
+
+ @Override
+ public void onSuccess(PersistableBundle stateToPersist) {
+ DataBrokerImpl dataBroker = mWeakDataBroker.get();
+ if (dataBroker == null) {
+ return;
+ }
+ dataBroker.onScriptSuccess(stateToPersist);
+ }
+
+ @Override
+ public void onError(int errorType, String message, String stackTrace) {
+ DataBrokerImpl dataBroker = mWeakDataBroker.get();
+ if (dataBroker == null) {
+ return;
+ }
+ dataBroker.onScriptError(errorType, message, stackTrace);
+ }
+ }
+
+ /** Callback handler to handle scheduling and rescheduling of {@link ScriptExecutionTask}s. */
+ class TaskHandler extends Handler {
+ TaskHandler(Looper looper) {
+ super(looper);
+ }
+
+ /**
+ * Handles a message depending on the message ID.
+ * If the msg ID is MSG_HANDLE_TASK, it polls a task from the priority queue and executing a
+ * {@link ScriptExecutionTask}. There are multiple places where this message is sent: when
+ * priority updates, when a new task is added to the priority queue, and when a task
+ * finishes running.
+ */
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_HANDLE_TASK:
+ pollAndExecuteTask(); // run the next task
+ break;
+ case MSG_BIND_TO_SCRIPT_EXECUTOR:
+ bindScriptExecutor();
+ break;
+ default:
+ Slogf.w(CarLog.TAG_TELEMETRY, "TaskHandler received unknown message.");
+ }
+ }
}
}
diff --git a/service/src/com/android/car/telemetry/databroker/DataSubscriber.java b/service/src/com/android/car/telemetry/databroker/DataSubscriber.java
index 0c713c5..9ab7604 100644
--- a/service/src/com/android/car/telemetry/databroker/DataSubscriber.java
+++ b/service/src/com/android/car/telemetry/databroker/DataSubscriber.java
@@ -16,31 +16,84 @@
package com.android.car.telemetry.databroker;
-import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.os.SystemClock;
import com.android.car.telemetry.TelemetryProto;
+import java.util.Objects;
+
/**
- * Subscriber class that receive published data and schedules tasks for execution on the data.
+ * Subscriber class that receives published data and schedules tasks for execution.
+ * All methods of this class must be accessed on telemetry thread.
*/
public class DataSubscriber {
+
+ private final DataBroker mDataBroker;
+ private final TelemetryProto.MetricsConfig mMetricsConfig;
private final TelemetryProto.Subscriber mSubscriber;
- public DataSubscriber(TelemetryProto.MetricsConfig metricsConfig,
+ public DataSubscriber(
+ DataBroker dataBroker,
+ TelemetryProto.MetricsConfig metricsConfig,
TelemetryProto.Subscriber subscriber) {
+ mDataBroker = dataBroker;
+ mMetricsConfig = metricsConfig;
mSubscriber = subscriber;
}
+ /** Returns the handler function name for this subscriber. */
+ public String getHandlerName() {
+ return mSubscriber.getHandler();
+ }
+
/**
- * Returns the publisher param {@link com.android.car.telemetry.TelemetryProto.Publisher} that
+ * Returns the publisher param {@link TelemetryProto.Publisher} that
* contains the data source and the config.
*/
public TelemetryProto.Publisher getPublisherParam() {
return mSubscriber.getPublisher();
}
- /** Pushes data to the subscriber. */
- public void push(Bundle data) {
- // TODO(b/187743369): implement
+ /**
+ * Creates a {@link ScriptExecutionTask} and pushes it to the priority queue where the task
+ * will be pending execution.
+ */
+ public void push(PersistableBundle data) {
+ ScriptExecutionTask task = new ScriptExecutionTask(
+ this, data, SystemClock.elapsedRealtime());
+ mDataBroker.addTaskToQueue(task);
+ }
+
+ /** Returns the {@link TelemetryProto.MetricsConfig}. */
+ public TelemetryProto.MetricsConfig getMetricsConfig() {
+ return mMetricsConfig;
+ }
+
+ /** Returns the {@link TelemetryProto.Subscriber}. */
+ public TelemetryProto.Subscriber getSubscriber() {
+ return mSubscriber;
+ }
+
+ /** Returns the priority of subscriber. */
+ public int getPriority() {
+ return mSubscriber.getPriority();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof DataSubscriber)) {
+ return false;
+ }
+ DataSubscriber other = (DataSubscriber) o;
+ return mMetricsConfig.getName().equals(other.getMetricsConfig().getName())
+ && mMetricsConfig.getVersion() == other.getMetricsConfig().getVersion()
+ && mSubscriber.getHandler().equals(other.getSubscriber().getHandler());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mMetricsConfig.getName(), mMetricsConfig.getVersion(),
+ mSubscriber.getHandler());
}
}
diff --git a/service/src/com/android/car/telemetry/databroker/ScriptExecutionTask.java b/service/src/com/android/car/telemetry/databroker/ScriptExecutionTask.java
new file mode 100644
index 0000000..18d0512
--- /dev/null
+++ b/service/src/com/android/car/telemetry/databroker/ScriptExecutionTask.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2021 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.telemetry.databroker;
+
+import android.car.telemetry.MetricsConfigKey;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.TelemetryProto;
+
+/**
+ * A wrapper class containing all the necessary information to invoke the ScriptExecutor API. It
+ * is enqueued into the priority queue where it pends execution by {@link DataBroker}.
+ * It implements the {@link Comparable} interface so it has a natural ordering by priority and
+ * creation timestamp in the priority queue.
+ * The object can be accessed from any thread. See {@link DataSubscriber} for thread-safety.
+ */
+public class ScriptExecutionTask implements Comparable<ScriptExecutionTask> {
+ // Key to the calculated bundle size, which doesn't contain implementation-dependent overhead
+ // and other allocations. It's approximately 10% smaller than the actual size of binder parcel.
+ public static final String APPROX_BUNDLE_SIZE_BYTES_KEY = "approx_bundle_size_bytes";
+
+ /**
+ * Binder transaction size limit is 1MB for all binders per process, so for large script input
+ * file pipe will be used to transfer the data to script executor instead of binder call. This
+ * is the input size threshold above which piping is used.
+ */
+ private static final int LARGE_SCRIPT_INPUT_SIZE_BYTES = 20 * 1024; // 20 kb
+
+ private final long mTimestampMillis;
+ private final DataSubscriber mSubscriber;
+ private final PersistableBundle mData;
+ private final boolean mIsLargeData;
+
+ ScriptExecutionTask(DataSubscriber subscriber, PersistableBundle data,
+ long elapsedRealtimeMillis) {
+ mTimestampMillis = elapsedRealtimeMillis;
+ mSubscriber = subscriber;
+ mData = data;
+ if (mData.containsKey(APPROX_BUNDLE_SIZE_BYTES_KEY)) {
+ mIsLargeData =
+ mData.getInt(APPROX_BUNDLE_SIZE_BYTES_KEY) > LARGE_SCRIPT_INPUT_SIZE_BYTES;
+ mData.remove(APPROX_BUNDLE_SIZE_BYTES_KEY);
+ } else {
+ Parcel parcel = Parcel.obtain();
+ parcel.writePersistableBundle(mData);
+ mIsLargeData = parcel.dataSize() > LARGE_SCRIPT_INPUT_SIZE_BYTES;
+ parcel.recycle();
+ }
+ }
+
+ /** Returns the priority of the task. */
+ public int getPriority() {
+ return mSubscriber.getPriority();
+ }
+
+ /** Returns the creation timestamp of the task. */
+ public long getCreationTimestampMillis() {
+ return mTimestampMillis;
+ }
+
+ public TelemetryProto.MetricsConfig getMetricsConfig() {
+ return mSubscriber.getMetricsConfig();
+ }
+
+ public String getHandlerName() {
+ return mSubscriber.getHandlerName();
+ }
+
+ public PersistableBundle getData() {
+ return mData;
+ }
+
+ /**
+ * Indicates whether the task is associated with MetricsConfig specified by its key.
+ */
+ public boolean isAssociatedWithMetricsConfig(MetricsConfigKey key) {
+ return mSubscriber.getMetricsConfig().getName().equals(key.getName())
+ && mSubscriber.getMetricsConfig().getVersion() == key.getVersion();
+ }
+
+ /**
+ * Returns the script input data size in bytes.
+ */
+ public boolean isLargeData() {
+ return mIsLargeData;
+ }
+
+ @Override
+ public int compareTo(ScriptExecutionTask other) {
+ if (getPriority() < other.getPriority()) {
+ return -1;
+ } else if (getPriority() > other.getPriority()) {
+ return 1;
+ }
+ // if equal priority, compare creation timestamps
+ if (getCreationTimestampMillis() < other.getCreationTimestampMillis()) {
+ return -1;
+ }
+ return 1;
+ }
+}
diff --git a/service/src/com/android/car/telemetry/proto/Android.bp b/service/src/com/android/car/telemetry/proto/Android.bp
index 8ffcaa2..89e09c5 100644
--- a/service/src/com/android/car/telemetry/proto/Android.bp
+++ b/service/src/com/android/car/telemetry/proto/Android.bp
@@ -16,11 +16,16 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
-java_library_static {
+java_library {
name: "cartelemetry-protos",
- host_supported: true,
proto: {
type: "lite",
},
- srcs: ["*.proto"],
+ srcs: [
+ ":cartelemetry-cardata-proto-srcs",
+ "atoms.proto",
+ "stats_log.proto",
+ "statsd_config.proto",
+ "telemetry.proto",
+ ],
}
diff --git a/service/src/com/android/car/telemetry/proto/atoms.proto b/service/src/com/android/car/telemetry/proto/atoms.proto
new file mode 100644
index 0000000..ccb9021
--- /dev/null
+++ b/service/src/com/android/car/telemetry/proto/atoms.proto
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+// Partial clone of frameworks/proto_logging/stats/atoms.proto. CarTelemetryService only uses
+// small number of atoms.
+
+syntax = "proto2";
+
+package android.car.telemetry.statsd;
+option java_package = "com.android.car.telemetry";
+option java_outer_classname = "AtomsProto";
+
+message Atom {
+ oneof pushed {
+ ActivityForegroundStateChanged activity_foreground_state_changed = 42;
+ AppStartMemoryStateCaptured app_start_memory_state_captured = 55;
+ }
+
+ // Pulled events will start at field 10000.
+ oneof pulled {
+ ProcessMemoryState process_memory_state = 10013;
+ }
+}
+
+message AppStartMemoryStateCaptured {
+ // The uid if available. -1 means not available.
+ optional int32 uid = 1;
+ optional string process_name = 2;
+ optional string activity_name = 3;
+ optional int64 page_fault = 4;
+ optional int64 page_major_fault = 5;
+ optional int64 rss_in_bytes = 6;
+ optional int64 cache_in_bytes = 7;
+ optional int64 swap_in_bytes = 8;
+}
+
+message ProcessMemoryState {
+ optional int32 uid = 1;
+ optional string process_name = 2;
+ optional int32 oom_adj_score = 3;
+ optional int64 page_fault = 4;
+ optional int64 page_major_fault = 5;
+ optional int64 rss_in_bytes = 6;
+ optional int64 cache_in_bytes = 7;
+ optional int64 swap_in_bytes = 8;
+}
+
+message ActivityForegroundStateChanged {
+ optional int32 uid = 1;
+ optional string pkg_name = 2;
+ optional string class_name = 3;
+ enum State {
+ BACKGROUND = 0;
+ FOREGROUND = 1;
+ }
+ optional State state = 4;
+}
diff --git a/service/src/com/android/car/telemetry/proto/stats_log.proto b/service/src/com/android/car/telemetry/proto/stats_log.proto
new file mode 100644
index 0000000..ec76cb8
--- /dev/null
+++ b/service/src/com/android/car/telemetry/proto/stats_log.proto
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+// Partial clone packages/modules/StatsD/statsd/src/stats_log.proto. Unused messages are not copied
+// here.
+
+syntax = "proto2";
+
+package android.car.telemetry.statsd;
+
+option java_package = "com.android.car.telemetry";
+option java_outer_classname = "StatsLogProto";
+
+import "packages/services/Car/service/src/com/android/car/telemetry/proto/atoms.proto";
+
+message DimensionsValueTuple {
+ repeated DimensionsValue dimensions_value = 1;
+}
+
+message DimensionsValue {
+ optional int32 field = 1;
+
+ oneof value {
+ string value_str = 2;
+ int32 value_int = 3;
+ int64 value_long = 4;
+ bool value_bool = 5;
+ float value_float = 6;
+ DimensionsValueTuple value_tuple = 7;
+ uint64 value_str_hash = 8;
+ }
+}
+
+message EventMetricData {
+ optional int64 elapsed_timestamp_nanos = 1;
+ optional Atom atom = 2;
+ reserved 3, 4;
+}
+
+message GaugeBucketInfo {
+ repeated Atom atom = 3;
+ repeated int64 elapsed_timestamp_nanos = 4;
+ reserved 1, 2, 5, 6, 7, 8, 9;
+}
+
+message GaugeMetricData {
+ repeated GaugeBucketInfo bucket_info = 3;
+ repeated DimensionsValue dimension_leaf_values_in_what = 4;
+ reserved 1, 2, 5, 6;
+}
+
+message StatsLogReport {
+ optional int64 metric_id = 1;
+
+ message EventMetricDataWrapper {
+ repeated EventMetricData data = 1;
+ }
+
+ message GaugeMetricDataWrapper {
+ repeated GaugeMetricData data = 1;
+ reserved 2;
+ }
+
+ oneof data {
+ EventMetricDataWrapper event_metrics = 4;
+ GaugeMetricDataWrapper gauge_metrics = 8;
+ }
+
+ optional DimensionsValue dimensions_path_in_what = 11;
+
+ optional bool is_active = 14;
+
+ reserved 2, 3, 5, 6, 7, 9, 10, 12, 13, 15, 16;
+}
+
+message ConfigMetricsReport {
+ repeated StatsLogReport metrics = 1;
+
+ repeated string strings = 9;
+
+ reserved 2, 3, 4, 5, 6, 7, 8;
+}
+
+message ConfigMetricsReportList {
+ repeated ConfigMetricsReport reports = 2;
+
+ reserved 1, 10;
+}
+
+message StatsdStatsReport {
+ message ConfigStats {
+ optional int32 uid = 1;
+ optional int64 id = 2;
+ optional bool is_valid = 9;
+ }
+
+ repeated ConfigStats config_stats = 3;
+
+ reserved 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19;
+}
diff --git a/service/src/com/android/car/telemetry/proto/statsd_config.proto b/service/src/com/android/car/telemetry/proto/statsd_config.proto
new file mode 100644
index 0000000..15ea9d5
--- /dev/null
+++ b/service/src/com/android/car/telemetry/proto/statsd_config.proto
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+// Partial clone of packages/modules/StatsD/statsd/src/statsd_config.proto. The service only
+// uses some parameters when configuring StatsD.
+
+syntax = "proto2";
+
+package android.car.telemetry.statsd;
+
+option java_package = "com.android.car.telemetry";
+option java_outer_classname = "StatsdConfigProto";
+
+enum TimeUnit {
+ TIME_UNIT_UNSPECIFIED = 0;
+ FIVE_MINUTES = 2;
+ TEN_MINUTES = 3;
+ THIRTY_MINUTES = 4;
+ ONE_HOUR = 5;
+ reserved 1, 6, 7, 8, 9, 10, 1000;
+}
+
+message FieldMatcher {
+ optional int32 field = 1;
+
+ repeated FieldMatcher child = 3;
+
+ reserved 2;
+}
+
+message SimpleAtomMatcher {
+ optional int32 atom_id = 1;
+
+ reserved 2;
+}
+
+message AtomMatcher {
+ optional int64 id = 1;
+
+ oneof contents {
+ SimpleAtomMatcher simple_atom_matcher = 2;
+ }
+
+ reserved 3;
+}
+
+message FieldFilter {
+ optional bool include_all = 1 [default = false];
+ optional FieldMatcher fields = 2;
+}
+
+message EventMetric {
+ optional int64 id = 1;
+ optional int64 what = 2;
+ optional int64 condition = 3;
+
+ reserved 4;
+ reserved 100;
+ reserved 101;
+}
+
+message GaugeMetric {
+ optional int64 id = 1;
+
+ optional int64 what = 2;
+
+ optional FieldFilter gauge_fields_filter = 3;
+
+ optional FieldMatcher dimensions_in_what = 5;
+
+ optional TimeUnit bucket = 6;
+
+ enum SamplingType {
+ RANDOM_ONE_SAMPLE = 1;
+ CONDITION_CHANGE_TO_TRUE = 3;
+ FIRST_N_SAMPLES = 4;
+ reserved 2;
+ }
+ optional SamplingType sampling_type = 9 [default = RANDOM_ONE_SAMPLE];
+
+ optional int64 max_num_gauge_atoms_per_bucket = 11 [default = 10];
+
+ optional int32 max_pull_delay_sec = 13 [default = 30];
+
+ reserved 4, 7, 8, 10, 12, 14;
+ reserved 100;
+ reserved 101;
+}
+
+message PullAtomPackages {
+ optional int32 atom_id = 1;
+
+ repeated string packages = 2;
+}
+
+message StatsdConfig {
+ optional int64 id = 1;
+
+ repeated EventMetric event_metric = 2;
+
+ repeated GaugeMetric gauge_metric = 5;
+
+ repeated AtomMatcher atom_matcher = 7;
+
+ repeated string allowed_log_source = 12;
+
+ optional int64 ttl_in_seconds = 15;
+
+ optional bool hash_strings_in_metric_report = 16 [default = true];
+
+ optional bool persist_locally = 20 [default = false];
+
+ repeated PullAtomPackages pull_atom_packages = 23;
+
+ repeated int32 whitelisted_atom_ids = 24;
+
+ reserved 3, 4, 6, 8, 9, 10, 11, 13, 14, 17, 18, 19, 21, 22, 25;
+
+ // Do not use.
+ reserved 1000, 1001;
+}
diff --git a/service/src/com/android/car/telemetry/proto/telemetry.proto b/service/src/com/android/car/telemetry/proto/telemetry.proto
index 5787d62..4106098 100644
--- a/service/src/com/android/car/telemetry/proto/telemetry.proto
+++ b/service/src/com/android/car/telemetry/proto/telemetry.proto
@@ -16,7 +16,7 @@
syntax = "proto2";
-package com.android.car.telemetry;
+package android.car.telemetry;
option java_package = "com.android.car.telemetry";
option java_outer_classname = "TelemetryProto";
@@ -30,16 +30,22 @@
message MetricsConfig {
// Required.
// The name of the configuration. Must be unique within a device.
+ //
+ // Changing the name of the config should be done carefully, by first removing the config
+ // with the old name, and creating a new config with a new name.
+ // The name is used to for persisting the configs and other internal state, not removing the
+ // configs with old name will result dangling data in the system.
+ //
// Only alphanumeric and _ characters are allowed. Minimum length is 3 chars.
optional string name = 1;
// Required.
- // Version number of the configuration.
+ // Version number of the configuration. Must be more than 0.
optional int32 version = 2;
// Required.
- // A script that is executed in devices. Must contain all the handler functions
- // defined in the listeners below.
+ // A script that is executed at the device side. Must contain all the handler
+ // functions defined in the listeners below.
// The script functions must be `pure` functions.
optional string script = 3;
@@ -60,19 +66,92 @@
optional float read_rate = 2;
}
+// Parameters for cartelemetryd publisher.
+// See packages/services/Car/cpp/telemetry for CarData proto and docs.
+message CarTelemetrydPublisher {
+ // Required.
+ // CarData id to subscribe to.
+ // See packages/services/Car/cpp/telemetry/proto/CarData.proto for all the
+ // messages and IDs.
+ optional int32 id = 1;
+}
+
+// Publisher for various system metrics and stats.
+// It pushes metrics to the subscribers periodically or when garage mode starts.
+message StatsPublisher {
+ enum SystemMetric {
+ UNDEFINED = 0; // default value, not used
+ // Collects all the app start events with the initial used RSS/CACHE/SWAP memory.
+ APP_START_MEMORY_STATE_CAPTURED = 1;
+ // Collects memory state of processes in 5-minute buckets (1 memory measurement per bucket).
+ PROCESS_MEMORY_STATE = 2;
+ // Collects activity foreground/background transition events.
+ ACTIVITY_FOREGROUND_STATE_CHANGED = 3;
+ }
+
+ // Required.
+ // System metric for the publisher.
+ optional SystemMetric system_metric = 1;
+}
+
// Specifies data publisher and its parameters.
message Publisher {
oneof publisher {
VehiclePropertyPublisher vehicle_property = 1;
+ CarTelemetrydPublisher cartelemetryd = 2;
+ StatsPublisher stats = 3;
}
}
// Specifies publisher with its parameters and the handler function that's invoked
// when data is received. The format of the data depends on the Publisher.
message Subscriber {
- // Name of the script function that's invoked when this subscriber is triggered.
+ // Required.
+ // Name of the function that handles the published data. Must be defined
+ // in the script.
optional string handler = 1;
- // Publisher and its parameters.
+ // Required.
+ // Publisher definition.
optional Publisher publisher = 2;
+
+ // Required.
+ // Priority of the subscriber, which affects the order of when it receives data.
+ // Ranges from 0 to 100. 0 being highest priority and 100 being lowest.
+ optional int32 priority = 3;
+}
+
+// A message that encapsulates an error that's produced when collecting metrics.
+// Any changes here should also be reflected on
+// p/s/C/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl
+// p/s/C/package/ScriptExecutor/src/ScriptExecutorListener.h
+message TelemetryError {
+ enum ErrorType {
+ // Not used.
+ UNSPECIFIED = 0;
+
+ // Used when an error occurs in the ScriptExecutor code.
+ SCRIPT_EXECUTOR_ERROR = 1;
+
+ // Used when an error occurs while executing the Lua script (such as errors returned by
+ // lua_pcall)
+ LUA_RUNTIME_ERROR = 2;
+
+ // Used to log errors by a script itself, for instance, when a script received inputs outside
+ // of expected range.
+ LUA_SCRIPT_ERROR = 3;
+ }
+
+ // Required.
+ // A type that indicates the category of the error.
+ optional ErrorType error_type = 1;
+
+ // Required.
+ // Human readable message explaining the error or how to fix it.
+ optional string message = 2;
+
+ // Optional.
+ // If there is an exception, there will be stack trace. However this information is not always
+ // available.
+ optional string stack_trace = 3;
}
diff --git a/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java b/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java
index f58a6fa..088859b 100644
--- a/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java
+++ b/service/src/com/android/car/telemetry/publisher/AbstractPublisher.java
@@ -16,11 +16,10 @@
package com.android.car.telemetry.publisher;
+import com.android.car.telemetry.TelemetryProto;
import com.android.car.telemetry.databroker.DataSubscriber;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
+import java.util.List;
/**
* Abstract class for publishers. It is 1-1 with data source and manages sending data to
@@ -30,54 +29,62 @@
* configuration. Single publisher instance can send data as several
* {@link com.android.car.telemetry.TelemetryProto.Publisher} to subscribers.
*
- * <p>Not thread-safe.
+ * <p>The methods must be called from the telemetry thread.
*/
public abstract class AbstractPublisher {
- private final HashSet<DataSubscriber> mDataSubscribers = new HashSet<>();
+ private final PublisherFailureListener mFailureListener;
+
+ /**
+ * Listener for publisher failures, such as failing to connect to a underlying service or
+ * invalid Publisher configuration. When publishers fail, the affected configs should be
+ * disabled, because the associated scripts cannot receive data from the failed publishers.
+ */
+ public interface PublisherFailureListener {
+ /** Called by publishers when they fail. */
+ void onPublisherFailure(
+ AbstractPublisher publisher,
+ List<TelemetryProto.MetricsConfig> affectedConfigs, Throwable error);
+ }
+
+ AbstractPublisher(PublisherFailureListener failureListener) {
+ mFailureListener = failureListener;
+ }
/**
* Adds a subscriber that listens for data produced by this publisher.
*
+ * <p>DataBroker may call this method when a new {@code MetricsConfig} is added,
+ * {@code CarTelemetryService} is restarted or the device is restarted.
+ *
* @param subscriber a subscriber to receive data
- * @throws IllegalArgumentException if an invalid subscriber was provided.
+ * @throws IllegalArgumentException if the subscriber is invalid.
+ * @throws IllegalStateException if there are internal errors.
*/
- public void addDataSubscriber(DataSubscriber subscriber) {
- // This method can throw exceptions if data is invalid - do now swap these 2 lines.
- onDataSubscriberAdded(subscriber);
- mDataSubscribers.add(subscriber);
- }
+ public abstract void addDataSubscriber(DataSubscriber subscriber);
/**
* Removes the subscriber from the publisher. Publisher stops if necessary.
*
- * @throws IllegalArgumentException if the subscriber was not found.
+ * <p>It does nothing if subscriber is not found.
*/
- public void removeDataSubscriber(DataSubscriber subscriber) {
- if (!mDataSubscribers.remove(subscriber)) {
- throw new IllegalArgumentException("Failed to remove, subscriber not found");
- }
- onDataSubscribersRemoved(Collections.singletonList(subscriber));
- }
+ public abstract void removeDataSubscriber(DataSubscriber subscriber);
/**
* Removes all the subscribers from the publisher. The publisher may stop.
+ *
+ * <p>This method also cleans-up internal publisher and the data source persisted state.
*/
- public void removeAllDataSubscribers() {
- onDataSubscribersRemoved(mDataSubscribers);
- mDataSubscribers.clear();
- }
+ public abstract void removeAllDataSubscribers();
+
+ /** Returns true if the publisher already has this data subscriber. */
+ public abstract boolean hasDataSubscriber(DataSubscriber subscriber);
/**
- * Called when a new subscriber is added to the publisher.
- *
- * @throws IllegalArgumentException if the invalid subscriber was provided.
+ * Notifies the failure Listener that this publisher failed. See
+ * {@link PublisherFailureListener} for details.
*/
- protected abstract void onDataSubscriberAdded(DataSubscriber subscriber);
-
- /** Called when subscribers are removed from the publisher. */
- protected abstract void onDataSubscribersRemoved(Collection<DataSubscriber> subscribers);
-
- protected HashSet<DataSubscriber> getDataSubscribers() {
- return mDataSubscribers;
+ protected void onPublisherFailure(
+ List<TelemetryProto.MetricsConfig> affectedConfigs, Throwable error) {
+ mFailureListener.onPublisherFailure(this, affectedConfigs, error);
}
}
diff --git a/service/src/com/android/car/telemetry/publisher/CarTelemetrydPublisher.java b/service/src/com/android/car/telemetry/publisher/CarTelemetrydPublisher.java
new file mode 100644
index 0000000..282838d
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/CarTelemetrydPublisher.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher;
+
+import android.automotive.telemetry.internal.CarDataInternal;
+import android.automotive.telemetry.internal.ICarDataListener;
+import android.automotive.telemetry.internal.ICarTelemetryInternal;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+import com.android.automotive.telemetry.CarDataProto;
+import com.android.car.CarLog;
+import com.android.car.telemetry.TelemetryProto;
+import com.android.car.telemetry.databroker.DataSubscriber;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.server.utils.Slogf;
+
+import java.util.ArrayList;
+import java.util.stream.Collectors;
+
+/**
+ * Publisher for cartelemtryd service (aka ICarTelemetry).
+ *
+ * <p>When a subscriber is added, the publisher binds to ICarTelemetryInternal and starts listening
+ * for incoming CarData. The matching CarData will be pushed to the subscriber. It unbinds itself
+ * from ICarTelemetryInternal if there are no subscribers.
+ *
+ * <p>See {@code packages/services/Car/cpp/telemetry/cartelemetryd} to learn more about the service.
+ */
+public class CarTelemetrydPublisher extends AbstractPublisher {
+ private static final boolean DEBUG = false; // STOPSHIP if true
+ private static final String SERVICE_NAME = ICarTelemetryInternal.DESCRIPTOR + "/default";
+ private static final int BINDER_FLAGS = 0;
+
+ private ICarTelemetryInternal mCarTelemetryInternal;
+
+ private final ArrayList<DataSubscriber> mSubscribers = new ArrayList<>();
+
+ // All the methods in this class are expected to be called on this handler's thread.
+ private final Handler mTelemetryHandler;
+
+ private final ICarDataListener mListener = new ICarDataListener.Stub() {
+ @Override
+ public void onCarDataReceived(final CarDataInternal[] dataList) throws RemoteException {
+ if (DEBUG) {
+ Slogf.d(CarLog.TAG_TELEMETRY,
+ "Received " + dataList.length + " CarData from cartelemetryd");
+ }
+ // TODO(b/189142577): Create custom Handler and post message to improve performance
+ mTelemetryHandler.post(() -> onCarDataListReceived(dataList));
+ }
+ };
+
+ CarTelemetrydPublisher(PublisherFailureListener failureListener, Handler telemetryHandler) {
+ super(failureListener);
+ this.mTelemetryHandler = telemetryHandler;
+ }
+
+ /** Called when binder for ICarTelemetry service is died. */
+ private void onBinderDied() {
+ // TODO(b/189142577): Create custom Handler and post message to improve performance
+ mTelemetryHandler.post(() -> {
+ if (mCarTelemetryInternal != null) {
+ mCarTelemetryInternal.asBinder().unlinkToDeath(this::onBinderDied, BINDER_FLAGS);
+ mCarTelemetryInternal = null;
+ }
+ onPublisherFailure(
+ getMetricsConfigs(),
+ new IllegalStateException("ICarTelemetryInternal binder died"));
+ });
+ }
+
+ /** Connects to ICarTelemetryInternal service and starts listening for CarData. */
+ private void connectToCarTelemetryd() {
+ if (mCarTelemetryInternal != null) {
+ return;
+ }
+ IBinder binder = ServiceManager.checkService(SERVICE_NAME);
+ if (binder == null) {
+ onPublisherFailure(
+ getMetricsConfigs(),
+ new IllegalStateException(
+ "Failed to connect to the ICarTelemetryInternal: service is not "
+ + "ready"));
+ return;
+ }
+ try {
+ binder.linkToDeath(this::onBinderDied, BINDER_FLAGS);
+ } catch (RemoteException e) {
+ onPublisherFailure(
+ getMetricsConfigs(),
+ new IllegalStateException(
+ "Failed to connect to the ICarTelemetryInternal: linkToDeath failed",
+ e));
+ return;
+ }
+ mCarTelemetryInternal = ICarTelemetryInternal.Stub.asInterface(binder);
+ try {
+ mCarTelemetryInternal.setListener(mListener);
+ } catch (RemoteException e) {
+ binder.unlinkToDeath(this::onBinderDied, BINDER_FLAGS);
+ mCarTelemetryInternal = null;
+ onPublisherFailure(
+ getMetricsConfigs(),
+ new IllegalStateException(
+ "Failed to connect to the ICarTelemetryInternal: Cannot set CarData "
+ + "listener", e));
+ }
+ }
+
+ private ArrayList<TelemetryProto.MetricsConfig> getMetricsConfigs() {
+ return new ArrayList<>(mSubscribers.stream().map(DataSubscriber::getMetricsConfig).collect(
+ Collectors.toSet()));
+ }
+
+ /**
+ * Disconnects from ICarTelemetryInternal service.
+ *
+ * @throws IllegalStateException if fails to clear the listener.
+ */
+ private void disconnectFromCarTelemetryd() {
+ if (mCarTelemetryInternal == null) {
+ return; // already disconnected
+ }
+ try {
+ mCarTelemetryInternal.clearListener();
+ } catch (RemoteException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to remove ICarTelemetryInternal listener", e);
+ }
+ mCarTelemetryInternal.asBinder().unlinkToDeath(this::onBinderDied, BINDER_FLAGS);
+ mCarTelemetryInternal = null;
+ }
+
+ @VisibleForTesting
+ boolean isConnectedToCarTelemetryd() {
+ return mCarTelemetryInternal != null;
+ }
+
+ @Override
+ public void addDataSubscriber(DataSubscriber subscriber) {
+ TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+ Preconditions.checkArgument(
+ publisherParam.getPublisherCase()
+ == TelemetryProto.Publisher.PublisherCase.CARTELEMETRYD,
+ "Subscribers only with CarTelemetryd publisher are supported by this class.");
+ int carDataId = publisherParam.getCartelemetryd().getId();
+ CarDataProto.CarData.PushedCase carDataCase =
+ CarDataProto.CarData.PushedCase.forNumber(carDataId);
+ Preconditions.checkArgument(
+ carDataCase != null
+ && carDataCase != CarDataProto.CarData.PushedCase.PUSHED_NOT_SET,
+ "Invalid CarData ID " + carDataId
+ + ". Please see CarData.proto for the list of available IDs.");
+
+ mSubscribers.add(subscriber);
+
+ connectToCarTelemetryd();
+
+ Slogf.d(CarLog.TAG_TELEMETRY, "Subscribing to CarDat.id=%d", carDataId);
+ }
+
+ @Override
+ public void removeDataSubscriber(DataSubscriber subscriber) {
+ mSubscribers.remove(subscriber);
+ if (mSubscribers.isEmpty()) {
+ disconnectFromCarTelemetryd();
+ }
+ }
+
+ @Override
+ public void removeAllDataSubscribers() {
+ mSubscribers.clear();
+ disconnectFromCarTelemetryd();
+ }
+
+ @Override
+ public boolean hasDataSubscriber(DataSubscriber subscriber) {
+ return mSubscribers.contains(subscriber);
+ }
+
+ /**
+ * Called when publisher receives new car data list. It's executed on the telemetry thread.
+ */
+ private void onCarDataListReceived(CarDataInternal[] dataList) {
+ // TODO(b/189142577): implement
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/HashUtils.java b/service/src/com/android/car/telemetry/publisher/HashUtils.java
new file mode 100644
index 0000000..98cc702
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/HashUtils.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher;
+
+import android.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Utility class for computing hash code.
+ *
+ * <p>Most of the methods are copied from {@code external/guava/}.
+ */
+public class HashUtils {
+ private static final long M = 0xC6A4A7935BD1E995L;
+ private static final int R = 47;
+ private static final long SEED = 0xDECAFCAFFEL;
+
+ /**
+ * Returns the hash code of the given string using SHA-256 algorithm. Returns only the first
+ * 8 bytes if the hash code, as SHA-256 is uniformly distributed.
+ */
+ public static long sha256(@NonNull String data) {
+ try {
+ return asLong(MessageDigest.getInstance("SHA-256").digest(data.getBytes()));
+ } catch (NoSuchAlgorithmException e) {
+ // unreachable
+ throw new RuntimeException("SHA-256 algorithm not found.", e);
+ }
+ }
+
+ /**
+ * Returns the Murmur2 hash of the provided string.
+ *
+ * <p> This algorithm works the same way as Hash64() in
+ * packages/modules/StatsD/statsd/src/hash.h
+ *
+ * @param str the string to be hashed.
+ * @return hash of the string.
+ */
+ public static long murmur2Hash64(String str) {
+ final byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
+ ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
+
+ long h = SEED ^ (buf.remaining() * M);
+ while (buf.remaining() >= 8) {
+ long k = buf.getLong();
+ k *= M;
+ k ^= k >>> R;
+ k *= M;
+ h ^= k;
+ h *= M;
+ }
+
+ if (buf.hasRemaining()) {
+ for (int i = 0; buf.hasRemaining(); i += 8) {
+ h ^= (buf.get() & 0xFFL) << i;
+ }
+ h *= M;
+ }
+
+ h ^= h >>> R;
+ h *= M;
+ h ^= h >>> R;
+
+ return h;
+ }
+
+ /**
+ * Returns the first eight bytes of {@code hashCode}, converted to a {@code long} value in
+ * little-endian order.
+ *
+ * <p>Copied from Guava's {@code HashCode#asLong()}.
+ *
+ * @throws IllegalStateException if {@code hashCode bytes < 8}
+ */
+ private static long asLong(byte[] hashCode) {
+ Preconditions.checkState(hashCode.length >= 8, "requires >= 8 bytes (it only has %s bytes)",
+ hashCode.length);
+ long retVal = (hashCode[0] & 0xFF);
+ for (int i = 1; i < Math.min(hashCode.length, 8); i++) {
+ retVal |= (hashCode[i] & 0xFFL) << (i * 8);
+ }
+ return retVal;
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/PublisherFactory.java b/service/src/com/android/car/telemetry/publisher/PublisherFactory.java
index 8d0975a..0c210b6 100644
--- a/service/src/com/android/car/telemetry/publisher/PublisherFactory.java
+++ b/service/src/com/android/car/telemetry/publisher/PublisherFactory.java
@@ -16,39 +16,97 @@
package com.android.car.telemetry.publisher;
+import android.os.Handler;
+
import com.android.car.CarPropertyService;
import com.android.car.telemetry.TelemetryProto;
+import java.io.File;
+
/**
- * Factory class for Publishers. It's expected to have a single factory instance.
+ * Lazy factory class for Publishers. It's expected to have a single factory instance.
+ * Must be called from the telemetry thread.
*
- * <p>Thread-safe.
+ * <p>It doesn't instantiate all the publishers right away, as in some cases some publishers are
+ * not needed.
+ *
+ * <p>Methods in this class must be called on telemetry thread unless specified as thread-safe.
*/
public class PublisherFactory {
private final Object mLock = new Object();
private final CarPropertyService mCarPropertyService;
+ private final File mRootDirectory;
+ private final Handler mTelemetryHandler;
+ private final StatsManagerProxy mStatsManager;
private VehiclePropertyPublisher mVehiclePropertyPublisher;
+ private CarTelemetrydPublisher mCarTelemetrydPublisher;
+ private StatsPublisher mStatsPublisher;
- public PublisherFactory(CarPropertyService carPropertyService) {
+ private AbstractPublisher.PublisherFailureListener mFailureListener;
+
+ public PublisherFactory(
+ CarPropertyService carPropertyService,
+ Handler handler,
+ StatsManagerProxy statsManager,
+ File rootDirectory) {
mCarPropertyService = carPropertyService;
+ mTelemetryHandler = handler;
+ mStatsManager = statsManager;
+ mRootDirectory = rootDirectory;
}
- /** Returns publisher by given type. */
- public AbstractPublisher getPublisher(
- TelemetryProto.Publisher.PublisherCase type) {
+ /** Returns the publisher by given type. This method is thread-safe. */
+ public AbstractPublisher getPublisher(TelemetryProto.Publisher.PublisherCase type) {
// No need to optimize locks, as this method is infrequently called.
synchronized (mLock) {
switch (type.getNumber()) {
case TelemetryProto.Publisher.VEHICLE_PROPERTY_FIELD_NUMBER:
if (mVehiclePropertyPublisher == null) {
mVehiclePropertyPublisher = new VehiclePropertyPublisher(
- mCarPropertyService);
+ mCarPropertyService, mFailureListener, mTelemetryHandler);
}
return mVehiclePropertyPublisher;
+ case TelemetryProto.Publisher.CARTELEMETRYD_FIELD_NUMBER:
+ if (mCarTelemetrydPublisher == null) {
+ mCarTelemetrydPublisher = new CarTelemetrydPublisher(
+ mFailureListener, mTelemetryHandler);
+ }
+ return mCarTelemetrydPublisher;
+ case TelemetryProto.Publisher.STATS_FIELD_NUMBER:
+ if (mStatsPublisher == null) {
+ mStatsPublisher = new StatsPublisher(
+ mFailureListener, mStatsManager, mRootDirectory, mTelemetryHandler);
+ }
+ return mStatsPublisher;
default:
throw new IllegalArgumentException(
"Publisher type " + type + " is not supported");
}
}
}
+
+ /**
+ * Removes all {@link com.android.car.telemetry.databroker.DataSubscriber} from all publishers.
+ */
+ public void removeAllDataSubscribers() {
+ if (mVehiclePropertyPublisher != null) {
+ mVehiclePropertyPublisher.removeAllDataSubscribers();
+ }
+ if (mCarTelemetrydPublisher != null) {
+ mCarTelemetrydPublisher.removeAllDataSubscribers();
+ }
+ if (mStatsPublisher != null) {
+ mStatsPublisher.removeAllDataSubscribers();
+ }
+ }
+
+ /**
+ * Sets the publisher failure listener for all the publishers. This is expected to be called
+ * before {@link #getPublisher} method, because the listener is set after
+ * {@code PublisherFactory} initialized. This is not the best approach, but it suits for this
+ * case.
+ */
+ public void setFailureListener(AbstractPublisher.PublisherFailureListener listener) {
+ mFailureListener = listener;
+ }
}
diff --git a/service/src/com/android/car/telemetry/publisher/StatsManagerImpl.java b/service/src/com/android/car/telemetry/publisher/StatsManagerImpl.java
new file mode 100644
index 0000000..ef54b3c
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/StatsManagerImpl.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher;
+
+import android.app.StatsManager;
+import android.app.StatsManager.StatsUnavailableException;
+
+/** Implementation for {@link StatsManagerProxy} */
+public class StatsManagerImpl implements StatsManagerProxy {
+ private final StatsManager mStatsManager;
+
+ public StatsManagerImpl(StatsManager statsManager) {
+ mStatsManager = statsManager;
+ }
+
+ @Override
+ public byte[] getReports(long configKey) throws StatsUnavailableException {
+ return mStatsManager.getReports(configKey);
+ }
+
+ @Override
+ public void addConfig(long configKey, byte[] data) throws StatsUnavailableException {
+ mStatsManager.addConfig(configKey, data);
+ }
+
+ @Override
+ public void removeConfig(long configKey) throws StatsUnavailableException {
+ mStatsManager.removeConfig(configKey);
+ }
+
+ @Override
+ public byte[] getStatsMetadata() throws StatsUnavailableException {
+ return mStatsManager.getStatsMetadata();
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/StatsManagerProxy.java b/service/src/com/android/car/telemetry/publisher/StatsManagerProxy.java
new file mode 100644
index 0000000..5507e28
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/StatsManagerProxy.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher;
+
+import android.app.StatsManager;
+import android.app.StatsManager.StatsUnavailableException;
+
+/** Proxy for {@link StatsManager}, as it's marked as final and can't be used in tests. */
+public interface StatsManagerProxy {
+ /** See {@link StatsManager#getReports(long)}. */
+ byte[] getReports(long configKey) throws StatsUnavailableException;
+
+ /** See {@link StatsManager#addConfig(long, byte[])}. */
+ void addConfig(long configKey, byte[] data) throws StatsUnavailableException;
+
+ /** See {@link StatsManager#removeConfig(long)}. */
+ void removeConfig(long configKey) throws StatsUnavailableException;
+
+ /** See {@link StatsManager#getStatsMetadata()}. */
+ byte[] getStatsMetadata() throws StatsUnavailableException;
+}
diff --git a/service/src/com/android/car/telemetry/publisher/StatsPublisher.java b/service/src/com/android/car/telemetry/publisher/StatsPublisher.java
new file mode 100644
index 0000000..be335e9
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/StatsPublisher.java
@@ -0,0 +1,537 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher;
+
+import static com.android.car.telemetry.AtomsProto.Atom.ACTIVITY_FOREGROUND_STATE_CHANGED_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.Atom.APP_START_MEMORY_STATE_CAPTURED_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.Atom.PROCESS_MEMORY_STATE_FIELD_NUMBER;
+
+import android.app.StatsManager.StatsUnavailableException;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.util.LongSparseArray;
+
+import com.android.car.CarLog;
+import com.android.car.telemetry.AtomsProto;
+import com.android.car.telemetry.StatsLogProto;
+import com.android.car.telemetry.StatsdConfigProto;
+import com.android.car.telemetry.StatsdConfigProto.StatsdConfig;
+import com.android.car.telemetry.TelemetryProto;
+import com.android.car.telemetry.TelemetryProto.Publisher.PublisherCase;
+import com.android.car.telemetry.databroker.DataSubscriber;
+import com.android.car.telemetry.publisher.statsconverters.ConfigMetricsReportListConverter;
+import com.android.car.telemetry.publisher.statsconverters.StatsConversionException;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+import com.android.server.utils.Slogf;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Publisher for {@link TelemetryProto.StatsPublisher}.
+ *
+ * <p>The publisher adds subscriber configurations in StatsD and they persist between reboots and
+ * CarTelemetryService restarts. Please use {@link #removeAllDataSubscribers} to clean-up these
+ * configs from StatsD store.
+ */
+public class StatsPublisher extends AbstractPublisher {
+ // These IDs are used in StatsdConfig and ConfigMetricsReport.
+ @VisibleForTesting
+ static final long APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID = 1;
+ @VisibleForTesting
+ static final long APP_START_MEMORY_STATE_CAPTURED_EVENT_METRIC_ID = 2;
+ @VisibleForTesting
+ static final long PROCESS_MEMORY_STATE_MATCHER_ID = 3;
+ @VisibleForTesting
+ static final long PROCESS_MEMORY_STATE_GAUGE_METRIC_ID = 4;
+ @VisibleForTesting
+ static final long ACTIVITY_FOREGROUND_STATE_CHANGED_ATOM_MATCHER_ID = 5;
+ @VisibleForTesting
+ static final long ACTIVITY_FOREGROUND_STATE_CHANGED_EVENT_METRIC_ID = 6;
+
+ // The file that contains stats config key and stats config version
+ @VisibleForTesting
+ static final String SAVED_STATS_CONFIGS_FILE = "stats_config_keys_versions";
+
+ // TODO(b/202115033): Flatten the load spike by pulling reports for each MetricsConfigs
+ // using separate periodical timers.
+ private static final Duration PULL_REPORTS_PERIOD = Duration.ofMinutes(10);
+
+ private static final String BUNDLE_CONFIG_KEY_PREFIX = "statsd-publisher-config-id-";
+ private static final String BUNDLE_CONFIG_VERSION_PREFIX = "statsd-publisher-config-version-";
+
+ @VisibleForTesting
+ static final StatsdConfigProto.FieldMatcher PROCESS_MEMORY_STATE_FIELDS_MATCHER =
+ StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(
+ PROCESS_MEMORY_STATE_FIELD_NUMBER)
+ .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(
+ AtomsProto.ProcessMemoryState.OOM_ADJ_SCORE_FIELD_NUMBER))
+ .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(
+ AtomsProto.ProcessMemoryState.PAGE_FAULT_FIELD_NUMBER))
+ .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(
+ AtomsProto.ProcessMemoryState.PAGE_MAJOR_FAULT_FIELD_NUMBER))
+ .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(
+ AtomsProto.ProcessMemoryState.RSS_IN_BYTES_FIELD_NUMBER))
+ .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(
+ AtomsProto.ProcessMemoryState.CACHE_IN_BYTES_FIELD_NUMBER))
+ .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(
+ AtomsProto.ProcessMemoryState.SWAP_IN_BYTES_FIELD_NUMBER))
+ .build();
+
+ private final StatsManagerProxy mStatsManager;
+ private final File mSavedStatsConfigsFile;
+ private final Handler mTelemetryHandler;
+
+ // True if the publisher is periodically pulling reports from StatsD.
+ private boolean mIsPullingReports = false;
+
+ /** Assign the method to {@link Runnable}, otherwise the handler fails to remove it. */
+ private final Runnable mPullReportsPeriodically = this::pullReportsPeriodically;
+
+ // LongSparseArray is memory optimized, but they can be bit slower for more
+ // than 100 items. We're expecting much less number of subscribers, so these data structures
+ // are ok.
+ // Maps config_key to the set of DataSubscriber.
+ private final LongSparseArray<DataSubscriber> mConfigKeyToSubscribers = new LongSparseArray<>();
+
+ private final PersistableBundle mSavedStatsConfigs;
+
+ StatsPublisher(
+ PublisherFailureListener failureListener,
+ StatsManagerProxy statsManager,
+ File rootDirectory,
+ Handler telemetryHandler) {
+ super(failureListener);
+ mStatsManager = statsManager;
+ mTelemetryHandler = telemetryHandler;
+ mSavedStatsConfigsFile = new File(rootDirectory, SAVED_STATS_CONFIGS_FILE);
+ mSavedStatsConfigs = loadBundle();
+ }
+
+ /** Loads the PersistableBundle containing stats config keys and versions from disk. */
+ private PersistableBundle loadBundle() {
+ try (FileInputStream fileInputStream = new FileInputStream(mSavedStatsConfigsFile)) {
+ return PersistableBundle.readFromStream(fileInputStream);
+ } catch (FileNotFoundException e) {
+ return new PersistableBundle();
+ } catch (IOException e) {
+ // TODO(b/199947533): handle failure
+ Slogf.e(CarLog.TAG_TELEMETRY,
+ "Failed to read file " + mSavedStatsConfigsFile.getAbsolutePath(), e);
+ return new PersistableBundle();
+ }
+ }
+
+ /** Writes the PersistableBundle containing stats config keys and versions to disk. */
+ private void saveBundle() {
+ if (mSavedStatsConfigs.size() == 0) {
+ mSavedStatsConfigsFile.delete();
+ return;
+ }
+ try (FileOutputStream fileOutputStream = new FileOutputStream(mSavedStatsConfigsFile)) {
+ mSavedStatsConfigs.writeToStream(fileOutputStream);
+ } catch (IOException e) {
+ // TODO(b/199947533): handle failure
+ Slogf.e(CarLog.TAG_TELEMETRY,
+ "Cannot write to " + mSavedStatsConfigsFile.getAbsolutePath()
+ + ". Added stats config info is lost.", e);
+ }
+ }
+
+ @Override
+ public void addDataSubscriber(DataSubscriber subscriber) {
+ TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+ Preconditions.checkArgument(
+ publisherParam.getPublisherCase() == PublisherCase.STATS,
+ "Subscribers only with StatsPublisher are supported by this class.");
+
+ long configKey = buildConfigKey(subscriber);
+ mConfigKeyToSubscribers.put(configKey, subscriber);
+ addStatsConfig(configKey, subscriber);
+ if (!mIsPullingReports) {
+ Slogf.d(CarLog.TAG_TELEMETRY, "Stats report will be pulled in "
+ + PULL_REPORTS_PERIOD.toMinutes() + " minutes.");
+ mIsPullingReports = true;
+ mTelemetryHandler.postDelayed(
+ mPullReportsPeriodically, PULL_REPORTS_PERIOD.toMillis());
+ }
+ }
+
+ private void processReport(long configKey, StatsLogProto.ConfigMetricsReportList report) {
+ Slogf.i(CarLog.TAG_TELEMETRY, "Received reports: " + report.getReportsCount());
+ if (report.getReportsCount() == 0) {
+ return;
+ }
+ DataSubscriber subscriber = mConfigKeyToSubscribers.get(configKey);
+ if (subscriber == null) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "No subscribers found for config " + configKey);
+ return;
+ }
+ Map<Long, PersistableBundle> metricBundles = null;
+ try {
+ metricBundles = ConfigMetricsReportListConverter.convert(report);
+ } catch (StatsConversionException ex) {
+ Slogf.e(CarLog.TAG_TELEMETRY, "Stats conversion exception for config " + configKey, ex);
+ return;
+ }
+ Long metricId;
+ switch (subscriber.getPublisherParam().getStats().getSystemMetric()) {
+ case APP_START_MEMORY_STATE_CAPTURED:
+ metricId = APP_START_MEMORY_STATE_CAPTURED_EVENT_METRIC_ID;
+ break;
+ case PROCESS_MEMORY_STATE:
+ metricId = PROCESS_MEMORY_STATE_GAUGE_METRIC_ID;
+ break;
+ case ACTIVITY_FOREGROUND_STATE_CHANGED:
+ metricId = ACTIVITY_FOREGROUND_STATE_CHANGED_EVENT_METRIC_ID;
+ break;
+ default:
+ return;
+ }
+ if (!metricBundles.containsKey(metricId)) {
+ Slogf.w(CarLog.TAG_TELEMETRY,
+ "No reports for metric id " + metricId + " for config " + configKey);
+ return;
+ }
+ subscriber.push(metricBundles.get(metricId));
+ }
+
+ private void processStatsMetadata(StatsLogProto.StatsdStatsReport statsReport) {
+ int myUid = Process.myUid();
+ // configKey and StatsdConfig.id are the same, see this#addStatsConfig().
+ HashSet<Long> activeConfigKeys = new HashSet<>(getActiveConfigKeys());
+ HashSet<TelemetryProto.MetricsConfig> failedConfigs = new HashSet<>();
+ for (int i = 0; i < statsReport.getConfigStatsCount(); i++) {
+ StatsLogProto.StatsdStatsReport.ConfigStats stats = statsReport.getConfigStats(i);
+ if (stats.getUid() != myUid || !activeConfigKeys.contains(stats.getId())) {
+ continue;
+ }
+ if (!stats.getIsValid()) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Config key " + stats.getId() + " is invalid.");
+ failedConfigs.add(mConfigKeyToSubscribers.get(stats.getId()).getMetricsConfig());
+ }
+ }
+ if (!failedConfigs.isEmpty()) {
+ // Notify DataBroker so it can disable invalid MetricsConfigs.
+ onPublisherFailure(
+ new ArrayList<>(failedConfigs),
+ new IllegalStateException("Found invalid configs"));
+ }
+ }
+
+ private void pullReportsPeriodically() {
+ if (mIsPullingReports) {
+ Slogf.d(CarLog.TAG_TELEMETRY, "Stats report will be pulled in "
+ + PULL_REPORTS_PERIOD.toMinutes() + " minutes.");
+ mTelemetryHandler.postDelayed(mPullReportsPeriodically, PULL_REPORTS_PERIOD.toMillis());
+ }
+
+ try {
+ // TODO(b/202131100): Get the active list of configs using
+ // StatsManager#setActiveConfigsChangedOperation()
+ processStatsMetadata(
+ StatsLogProto.StatsdStatsReport.parseFrom(mStatsManager.getStatsMetadata()));
+
+ for (long configKey : getActiveConfigKeys()) {
+ processReport(configKey, StatsLogProto.ConfigMetricsReportList.parseFrom(
+ mStatsManager.getReports(configKey)));
+ }
+ } catch (InvalidProtocolBufferException | StatsUnavailableException e) {
+ // If the StatsD is not available, retry in the next pullReportsPeriodically call.
+ Slogf.w(CarLog.TAG_TELEMETRY, e);
+ }
+ }
+
+ private List<Long> getActiveConfigKeys() {
+ ArrayList<Long> result = new ArrayList<>();
+ for (String key : mSavedStatsConfigs.keySet()) {
+ // filter out all the config versions
+ if (!key.startsWith(BUNDLE_CONFIG_KEY_PREFIX)) {
+ continue;
+ }
+ // the remaining values are config keys
+ result.add(mSavedStatsConfigs.getLong(key));
+ }
+ return result;
+ }
+
+ /**
+ * Removes the subscriber from the publisher and removes StatsdConfig from StatsD service.
+ * If StatsdConfig is present in Statsd, it removes it even if the subscriber is not present
+ * in the publisher (it happens when subscriber was added before and CarTelemetryService was
+ * restarted and lost publisher state).
+ */
+ @Override
+ public void removeDataSubscriber(DataSubscriber subscriber) {
+ TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+ if (publisherParam.getPublisherCase() != PublisherCase.STATS) {
+ Slogf.w(CarLog.TAG_TELEMETRY,
+ "Expected STATS publisher, but received "
+ + publisherParam.getPublisherCase().name());
+ return;
+ }
+ long configKey = removeStatsConfig(subscriber);
+ mConfigKeyToSubscribers.remove(configKey);
+ if (mConfigKeyToSubscribers.size() == 0) {
+ mIsPullingReports = false;
+ mTelemetryHandler.removeCallbacks(mPullReportsPeriodically);
+ }
+ }
+
+ /**
+ * Removes all the subscribers from the publisher removes StatsdConfigs from StatsD service.
+ */
+ @Override
+ public void removeAllDataSubscribers() {
+ for (String key : mSavedStatsConfigs.keySet()) {
+ // filter out all the config versions
+ if (!key.startsWith(BUNDLE_CONFIG_KEY_PREFIX)) {
+ continue;
+ }
+ // the remaining values are config keys
+ long configKey = mSavedStatsConfigs.getLong(key);
+ try {
+ mStatsManager.removeConfig(configKey);
+ String bundleVersion = buildBundleConfigVersionKey(configKey);
+ mSavedStatsConfigs.remove(key);
+ mSavedStatsConfigs.remove(bundleVersion);
+ } catch (StatsUnavailableException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to remove config " + configKey
+ + ". Ignoring the failure. Will retry removing again when"
+ + " removeAllDataSubscribers() is called.", e);
+ // If it cannot remove statsd config, it's less likely it can delete it even if
+ // retry. So we will just ignore the failures. The next call of this method
+ // will ry deleting StatsD configs again.
+ }
+ }
+ saveBundle();
+ mSavedStatsConfigs.clear();
+ mIsPullingReports = false;
+ mTelemetryHandler.removeCallbacks(mPullReportsPeriodically);
+ }
+
+ /**
+ * Returns true if the publisher has the subscriber.
+ */
+ @Override
+ public boolean hasDataSubscriber(DataSubscriber subscriber) {
+ TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+ if (publisherParam.getPublisherCase() != PublisherCase.STATS) {
+ return false;
+ }
+ long configKey = buildConfigKey(subscriber);
+ return mConfigKeyToSubscribers.indexOfKey(configKey) >= 0;
+ }
+
+ /** Returns all the {@link TelemetryProto.MetricsConfig} associated with added subscribers. */
+ private List<TelemetryProto.MetricsConfig> getMetricsConfigs() {
+ HashSet<TelemetryProto.MetricsConfig> uniqueConfigs = new HashSet<>();
+ for (int i = 0; i < mConfigKeyToSubscribers.size(); i++) {
+ uniqueConfigs.add(mConfigKeyToSubscribers.valueAt(i).getMetricsConfig());
+ }
+ return new ArrayList<>(uniqueConfigs);
+ }
+
+ /**
+ * Returns the key for PersistableBundle to store/retrieve configKey associated with the
+ * subscriber.
+ */
+ private static String buildBundleConfigKey(DataSubscriber subscriber) {
+ return BUNDLE_CONFIG_KEY_PREFIX + subscriber.getMetricsConfig().getName() + "-"
+ + subscriber.getSubscriber().getHandler();
+ }
+
+ /**
+ * Returns the key for PersistableBundle to store/retrieve {@link TelemetryProto.MetricsConfig}
+ * version associated with the configKey (which is generated per DataSubscriber).
+ */
+ private static String buildBundleConfigVersionKey(long configKey) {
+ return BUNDLE_CONFIG_VERSION_PREFIX + configKey;
+ }
+
+ /**
+ * This method can be called even if StatsdConfig was added to StatsD service before. It stores
+ * previously added config_keys in the persistable bundle and only updates StatsD when
+ * the MetricsConfig (of CarTelemetryService) has a new version.
+ */
+ private void addStatsConfig(long configKey, DataSubscriber subscriber) {
+ // Store MetricsConfig (of CarTelemetryService) version per handler_function.
+ String bundleVersion = buildBundleConfigVersionKey(configKey);
+ if (mSavedStatsConfigs.getInt(bundleVersion) != 0) {
+ int currentVersion = mSavedStatsConfigs.getInt(bundleVersion);
+ if (currentVersion >= subscriber.getMetricsConfig().getVersion()) {
+ // It's trying to add current or older MetricsConfig version, just ignore it.
+ return;
+ } // if the subscriber's MetricsConfig version is newer, it will replace the old one.
+ }
+ String bundleConfigKey = buildBundleConfigKey(subscriber);
+ StatsdConfig config = buildStatsdConfig(subscriber, configKey);
+ try {
+ // It doesn't throw exception if the StatsdConfig is invalid. But it shouldn't happen,
+ // as we generate well-tested StatsdConfig in this service.
+ mStatsManager.addConfig(configKey, config.toByteArray());
+ mSavedStatsConfigs.putInt(bundleVersion, subscriber.getMetricsConfig().getVersion());
+ mSavedStatsConfigs.putLong(bundleConfigKey, configKey);
+ saveBundle();
+ } catch (StatsUnavailableException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to add config" + configKey, e);
+ // We will notify the failure immediately, as we're expecting StatsManager to be stable.
+ onPublisherFailure(
+ getMetricsConfigs(),
+ new IllegalStateException("Failed to add config " + configKey, e));
+ }
+ }
+
+ /** Removes StatsdConfig and returns configKey. */
+ private long removeStatsConfig(DataSubscriber subscriber) {
+ String bundleConfigKey = buildBundleConfigKey(subscriber);
+ long configKey = buildConfigKey(subscriber);
+ // Store MetricsConfig (of CarTelemetryService) version per handler_function.
+ String bundleVersion = buildBundleConfigVersionKey(configKey);
+ try {
+ mStatsManager.removeConfig(configKey);
+ mSavedStatsConfigs.remove(bundleVersion);
+ mSavedStatsConfigs.remove(bundleConfigKey);
+ saveBundle();
+ } catch (StatsUnavailableException e) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to remove config " + configKey
+ + ". Ignoring the failure. Will retry removing again when"
+ + " removeAllDataSubscribers() is called.", e);
+ // If it cannot remove statsd config, it's less likely it can delete it even if we
+ // retry. So we will just ignore the failures. The next call of this method will
+ // try deleting StatsD configs again.
+ }
+ return configKey;
+ }
+
+ /**
+ * Builds StatsdConfig id (aka config_key) using subscriber handler name.
+ *
+ * <p>StatsD uses ConfigKey struct to uniquely identify StatsdConfigs. StatsD ConfigKey consists
+ * of two parts: client uid and config_key number. The StatsdConfig is added to StatsD from
+ * CarService - which has uid=1000. Currently there is no client under uid=1000 and there will
+ * not be config_key collision.
+ */
+ private static long buildConfigKey(DataSubscriber subscriber) {
+ // Not to be confused with statsd metric, this one is a global CarTelemetry metric name.
+ String metricConfigName = subscriber.getMetricsConfig().getName();
+ String handlerFnName = subscriber.getSubscriber().getHandler();
+ return HashUtils.sha256(metricConfigName + "-" + handlerFnName);
+ }
+
+ /** Builds {@link StatsdConfig} proto for given subscriber. */
+ @VisibleForTesting
+ static StatsdConfig buildStatsdConfig(DataSubscriber subscriber, long configId) {
+ TelemetryProto.StatsPublisher.SystemMetric metric =
+ subscriber.getPublisherParam().getStats().getSystemMetric();
+ StatsdConfig.Builder builder = StatsdConfig.newBuilder()
+ // This id is not used in StatsD, but let's make it the same as config_key
+ // just in case.
+ .setId(configId)
+ .addAllowedLogSource("AID_SYSTEM");
+
+ switch (metric) {
+ case APP_START_MEMORY_STATE_CAPTURED:
+ return buildAppStartMemoryStateStatsdConfig(builder);
+ case PROCESS_MEMORY_STATE:
+ return buildProcessMemoryStateStatsdConfig(builder);
+ case ACTIVITY_FOREGROUND_STATE_CHANGED:
+ return buildActivityForegroundStateStatsdConfig(builder);
+ default:
+ throw new IllegalArgumentException("Unsupported metric " + metric.name());
+ }
+ }
+
+ private static StatsdConfig buildAppStartMemoryStateStatsdConfig(StatsdConfig.Builder builder) {
+ return builder
+ .addAtomMatcher(StatsdConfigProto.AtomMatcher.newBuilder()
+ // The id must be unique within StatsdConfig/matchers
+ .setId(APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID)
+ .setSimpleAtomMatcher(StatsdConfigProto.SimpleAtomMatcher.newBuilder()
+ .setAtomId(APP_START_MEMORY_STATE_CAPTURED_FIELD_NUMBER)))
+ .addEventMetric(StatsdConfigProto.EventMetric.newBuilder()
+ // The id must be unique within StatsdConfig/metrics
+ .setId(APP_START_MEMORY_STATE_CAPTURED_EVENT_METRIC_ID)
+ .setWhat(APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID))
+ .build();
+ }
+
+ private static StatsdConfig buildProcessMemoryStateStatsdConfig(StatsdConfig.Builder builder) {
+ return builder
+ .addAtomMatcher(StatsdConfigProto.AtomMatcher.newBuilder()
+ // The id must be unique within StatsdConfig/matchers
+ .setId(PROCESS_MEMORY_STATE_MATCHER_ID)
+ .setSimpleAtomMatcher(StatsdConfigProto.SimpleAtomMatcher.newBuilder()
+ .setAtomId(PROCESS_MEMORY_STATE_FIELD_NUMBER)))
+ .addGaugeMetric(StatsdConfigProto.GaugeMetric.newBuilder()
+ // The id must be unique within StatsdConfig/metrics
+ .setId(PROCESS_MEMORY_STATE_GAUGE_METRIC_ID)
+ .setWhat(PROCESS_MEMORY_STATE_MATCHER_ID)
+ .setDimensionsInWhat(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(PROCESS_MEMORY_STATE_FIELD_NUMBER)
+ .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(1)) // ProcessMemoryState.uid
+ .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(2)) // ProcessMemoryState.process_name
+ )
+ .setGaugeFieldsFilter(StatsdConfigProto.FieldFilter.newBuilder()
+ .setFields(PROCESS_MEMORY_STATE_FIELDS_MATCHER)
+ ) // setGaugeFieldsFilter
+ .setSamplingType(
+ StatsdConfigProto.GaugeMetric.SamplingType.RANDOM_ONE_SAMPLE)
+ .setBucket(StatsdConfigProto.TimeUnit.FIVE_MINUTES)
+ )
+ .addPullAtomPackages(StatsdConfigProto.PullAtomPackages.newBuilder()
+ .setAtomId(PROCESS_MEMORY_STATE_FIELD_NUMBER)
+ .addPackages("AID_SYSTEM"))
+ .build();
+ }
+
+ private static StatsdConfig buildActivityForegroundStateStatsdConfig(
+ StatsdConfig.Builder builder) {
+ return builder
+ .addAtomMatcher(StatsdConfigProto.AtomMatcher.newBuilder()
+ // The id must be unique within StatsdConfig/matchers
+ .setId(ACTIVITY_FOREGROUND_STATE_CHANGED_ATOM_MATCHER_ID)
+ .setSimpleAtomMatcher(StatsdConfigProto.SimpleAtomMatcher.newBuilder()
+ .setAtomId(ACTIVITY_FOREGROUND_STATE_CHANGED_FIELD_NUMBER)))
+ .addEventMetric(StatsdConfigProto.EventMetric.newBuilder()
+ // The id must be unique within StatsdConfig/metrics
+ .setId(ACTIVITY_FOREGROUND_STATE_CHANGED_EVENT_METRIC_ID)
+ .setWhat(ACTIVITY_FOREGROUND_STATE_CHANGED_ATOM_MATCHER_ID))
+ .build();
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java b/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java
index ed0b19b..777883d 100644
--- a/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java
+++ b/service/src/com/android/car/telemetry/publisher/VehiclePropertyPublisher.java
@@ -20,24 +20,27 @@
import android.car.hardware.CarPropertyConfig;
import android.car.hardware.property.CarPropertyEvent;
import android.car.hardware.property.ICarPropertyEventListener;
-import android.os.Bundle;
+import android.os.Handler;
+import android.os.PersistableBundle;
import android.os.RemoteException;
-import android.util.Slog;
+import android.util.ArraySet;
import android.util.SparseArray;
import com.android.car.CarLog;
import com.android.car.CarPropertyService;
import com.android.car.telemetry.TelemetryProto;
+import com.android.car.telemetry.TelemetryProto.Publisher.PublisherCase;
import com.android.car.telemetry.databroker.DataSubscriber;
import com.android.internal.util.Preconditions;
+import com.android.server.utils.Slogf;
-import java.util.Collection;
import java.util.List;
/**
* Publisher for Vehicle Property changes, aka {@code CarPropertyService}.
*
- * <p>TODO(b/187525360): Add car property listener logic
+ * <p> When a subscriber is added, it registers a car property change listener for the
+ * property id of the subscriber and starts pushing the change events to the subscriber.
*/
public class VehiclePropertyPublisher extends AbstractPublisher {
private static final boolean DEBUG = false; // STOPSHIP if true
@@ -46,14 +49,24 @@
public static final String CAR_PROPERTY_EVENT_KEY = "car_property_event";
private final CarPropertyService mCarPropertyService;
+ private final Handler mTelemetryHandler;
+
+ // The class only reads, no need to synchronize this object.
+ // Maps property_id to CarPropertyConfig.
private final SparseArray<CarPropertyConfig> mCarPropertyList;
+ // SparseArray and ArraySet are memory optimized, but they can be bit slower for more
+ // than 100 items. We're expecting much less number of subscribers, so these DS are ok.
+ // Maps property_id to the set of DataSubscriber.
+ private final SparseArray<ArraySet<DataSubscriber>> mCarPropertyToSubscribers =
+ new SparseArray<>();
+
private final ICarPropertyEventListener mCarPropertyEventListener =
new ICarPropertyEventListener.Stub() {
@Override
public void onEvent(List<CarPropertyEvent> events) throws RemoteException {
if (DEBUG) {
- Slog.d(CarLog.TAG_TELEMETRY,
+ Slogf.d(CarLog.TAG_TELEMETRY,
"Received " + events.size() + " vehicle property events");
}
for (CarPropertyEvent event : events) {
@@ -62,17 +75,21 @@
}
};
- public VehiclePropertyPublisher(CarPropertyService carPropertyService) {
+ public VehiclePropertyPublisher(CarPropertyService carPropertyService,
+ PublisherFailureListener failureListener, Handler handler) {
+ super(failureListener);
mCarPropertyService = carPropertyService;
+ mTelemetryHandler = handler;
// Load car property list once, as the list doesn't change runtime.
- mCarPropertyList = new SparseArray<>();
- for (CarPropertyConfig property : mCarPropertyService.getPropertyList()) {
+ List<CarPropertyConfig> propertyList = mCarPropertyService.getPropertyList();
+ mCarPropertyList = new SparseArray<>(propertyList.size());
+ for (CarPropertyConfig property : propertyList) {
mCarPropertyList.append(property.getPropertyId(), property);
}
}
@Override
- protected void onDataSubscriberAdded(DataSubscriber subscriber) {
+ public void addDataSubscriber(DataSubscriber subscriber) {
TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
Preconditions.checkArgument(
publisherParam.getPublisherCase()
@@ -88,31 +105,80 @@
|| config.getAccess()
== CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ_WRITE,
"No access. Cannot read " + VehiclePropertyIds.toString(propertyId) + ".");
- mCarPropertyService.registerListener(
- propertyId,
- publisherParam.getVehicleProperty().getReadRate(),
- mCarPropertyEventListener);
+
+ ArraySet<DataSubscriber> subscribers = mCarPropertyToSubscribers.get(propertyId);
+ if (subscribers == null) {
+ subscribers = new ArraySet<>();
+ mCarPropertyToSubscribers.put(propertyId, subscribers);
+ // Register the listener only once per propertyId.
+ mCarPropertyService.registerListener(
+ propertyId,
+ publisherParam.getVehicleProperty().getReadRate(),
+ mCarPropertyEventListener);
+ }
+ subscribers.add(subscriber);
}
@Override
- protected void onDataSubscribersRemoved(Collection<DataSubscriber> subscribers) {
- // TODO(b/190230611): Remove car property listener
+ public void removeDataSubscriber(DataSubscriber subscriber) {
+ TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+ if (publisherParam.getPublisherCase() != PublisherCase.VEHICLE_PROPERTY) {
+ Slogf.w(CarLog.TAG_TELEMETRY,
+ "Expected VEHICLE_PROPERTY publisher, but received "
+ + publisherParam.getPublisherCase().name());
+ return;
+ }
+ int propertyId = publisherParam.getVehicleProperty().getVehiclePropertyId();
+
+ ArraySet<DataSubscriber> subscribers = mCarPropertyToSubscribers.get(propertyId);
+ if (subscribers == null) {
+ return;
+ }
+ subscribers.remove(subscriber);
+ if (subscribers.isEmpty()) {
+ mCarPropertyToSubscribers.remove(propertyId);
+ // Doesn't throw exception as listener is not null. mCarPropertyService and
+ // local mCarPropertyToSubscribers will not get out of sync.
+ mCarPropertyService.unregisterListener(propertyId, mCarPropertyEventListener);
+ }
+ }
+
+ @Override
+ public void removeAllDataSubscribers() {
+ for (int i = 0; i < mCarPropertyToSubscribers.size(); i++) {
+ int propertyId = mCarPropertyToSubscribers.keyAt(i);
+ // Doesn't throw exception as listener is not null. mCarPropertyService and
+ // local mCarPropertyToSubscribers will not get out of sync.
+ mCarPropertyService.unregisterListener(propertyId, mCarPropertyEventListener);
+ }
+ mCarPropertyToSubscribers.clear();
+ }
+
+ @Override
+ public boolean hasDataSubscriber(DataSubscriber subscriber) {
+ TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
+ if (publisherParam.getPublisherCase() != PublisherCase.VEHICLE_PROPERTY) {
+ return false;
+ }
+ int propertyId = publisherParam.getVehicleProperty().getVehiclePropertyId();
+ ArraySet<DataSubscriber> subscribers = mCarPropertyToSubscribers.get(propertyId);
+ return subscribers != null && subscribers.contains(subscriber);
}
/**
- * Called when publisher receives new events. It's called on CarPropertyService's worker
- * thread.
+ * Called when publisher receives new event. It's executed on a CarPropertyService's
+ * worker thread.
*/
private void onVehicleEvent(CarPropertyEvent event) {
- Bundle bundle = new Bundle();
- bundle.putParcelable(CAR_PROPERTY_EVENT_KEY, event);
- for (DataSubscriber subscriber : getDataSubscribers()) {
- TelemetryProto.Publisher publisherParam = subscriber.getPublisherParam();
- if (event.getCarPropertyValue().getPropertyId()
- != publisherParam.getVehicleProperty().getVehiclePropertyId()) {
- continue;
+ // move the work from CarPropertyService's worker thread to the telemetry thread
+ mTelemetryHandler.post(() -> {
+ // TODO(b/197269115): convert CarPropertyEvent into PersistableBundle
+ PersistableBundle bundle = new PersistableBundle();
+ ArraySet<DataSubscriber> subscribersClone = new ArraySet<>(
+ mCarPropertyToSubscribers.get(event.getCarPropertyValue().getPropertyId()));
+ for (DataSubscriber subscriber : subscribersClone) {
+ subscriber.push(bundle);
}
- subscriber.push(bundle);
- }
+ });
}
}
diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/AbstractAtomConverter.java b/service/src/com/android/car/telemetry/publisher/statsconverters/AbstractAtomConverter.java
new file mode 100644
index 0000000..e201f1d
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/statsconverters/AbstractAtomConverter.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import static com.android.car.telemetry.databroker.ScriptExecutionTask.APPROX_BUNDLE_SIZE_BYTES_KEY;
+
+import static java.nio.charset.StandardCharsets.UTF_16;
+
+import android.os.PersistableBundle;
+import android.util.SparseArray;
+
+import com.android.car.telemetry.AtomsProto.Atom;
+import com.android.car.telemetry.StatsLogProto.DimensionsValue;
+
+import com.google.protobuf.MessageLite;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Base class for converters from StatsD atom list to {@link PersistableBundle}. PersistableBundle
+ * will be sent to {@code ScriptExecutor} to be consumed by scripts, thus its structure is simple
+ * and lightweight.
+ *
+ * <p> Resulting fields in the PersistableBundle will be arrays of primitive values: int, long,
+ * double, boolean and String. The keys to the arrays will be the same as atom field names in the
+ * proto definitions.
+ *
+ * <p> Example resulting PersistableBundle format:
+ *
+ * {
+ * "uid": [1000, 10000, 11000],
+ * "process_name": ["A", "B", "C"],
+ * "rss_in_bytes": [11111L, 222222L, 3333333L],
+ * ...
+ * }
+ *
+ * @param <T> the atom data type.
+ */
+public abstract class AbstractAtomConverter<T extends MessageLite> {
+ AbstractAtomConverter() {}
+
+ /**
+ * Gets the parser config that's a mapping of the field id to {@link AtomFieldAccessor} for atom
+ * data of type T.
+ *
+ * @return the atom fields parser config.
+ */
+ abstract SparseArray<AtomFieldAccessor<T>> getAtomFieldAccessorMap();
+
+ /**
+ * Gets atom data of type T from atom proto.
+ *
+ * @param atom the proto that contains the atom data.
+ * @return atom data.
+ */
+ abstract T getAtomData(Atom atom);
+
+ /**
+ * Gets the name of the atom data class as string.
+ *
+ * @return atom data class name string.
+ */
+ abstract String getAtomDataClassName();
+
+ /**
+ * Converts the atom fields to be set as fields in the returned {@link PersistableBundle}.
+ *
+ * <p> Atom list is parsed into field value arrays.
+ *
+ * <p> Dimension values are extracted, dehashed if necessary, and parsed into arrays.
+ *
+ * <p> The resulting primitive arrays are put into the returned {@link PersistableBundle}
+ * with the atom field names as keys.
+ *
+ * @param atoms list of atoms with data type T to be converted to PersistableBundle formats.
+ * @param dimensionsFieldsIds list of ids for the atom fields that are encoded in dimensions.
+ * @param dimensionsValuesList dimension value groups matching mDimensionsFieldsIds.
+ * @param hashToStringMap hash mapping used to de-hash hash string type dimension values.
+ * @return {@link PersistableBundle} with the converted atom fields arrays.
+ * @throws StatsConversionException if atom field mismatch or can't convert dimension value.
+ */
+ PersistableBundle convert(
+ List<Atom> atoms,
+ List<Integer> dimensionsFieldsIds,
+ List<List<DimensionsValue>> dimensionsValuesList,
+ Map<Long, String> hashToStringMap) throws StatsConversionException {
+ PersistableBundle bundle = new PersistableBundle();
+ SparseArray<AtomFieldAccessor<T>> parserConfig = getAtomFieldAccessorMap();
+ int bundleByteSize = 0;
+ // For each field, if set, add the values from all atoms to list and convert
+ for (int i = 0; i < parserConfig.size(); ++i) {
+ AtomFieldAccessor<T> atomFieldAccessor = parserConfig.valueAt(i);
+ // All atoms are expected to have the same fields set
+ // If the first atom does not have a field, that field is skipped
+ if (atomFieldAccessor.hasField(getAtomData(atoms.get(0)))) {
+ List<Object> valueList = new ArrayList<>(atoms.size());
+ for (Atom atom : atoms) {
+ T atomData = getAtomData(atom);
+ if (!atomFieldAccessor.hasField(atomData)) {
+ throw new StatsConversionException(
+ "Atom field inconsistency in atom list. "
+ + "A field is unset for atom of type "
+ + getAtomDataClassName());
+ }
+ valueList.add(atomFieldAccessor.getField(atomData));
+ }
+ bundleByteSize += setPersistableBundleArrayField(
+ atomFieldAccessor.getFieldName(), valueList, bundle);
+ }
+ }
+ // Check if there are dimension fields needing conversion
+ if (dimensionsFieldsIds == null || dimensionsValuesList == null) {
+ bundle.putInt(APPROX_BUNDLE_SIZE_BYTES_KEY, bundleByteSize);
+ return bundle;
+ }
+ // Create conversions for fields encoded in dimension fields
+ // Atom fields encoded in dimension values are not set, thus not extracted above
+ for (int i = 0; i < dimensionsFieldsIds.size(); ++i) {
+ Integer fieldId = dimensionsFieldsIds.get(i);
+ List<Object> valueList = new ArrayList<>();
+ for (List<DimensionsValue> dvList : dimensionsValuesList) {
+ valueList.add(extractDimensionsValue(dvList.get(i), hashToStringMap));
+ }
+ bundleByteSize += setPersistableBundleArrayField(
+ getAtomFieldAccessorMap().get(fieldId).getFieldName(),
+ valueList,
+ bundle);
+ }
+ bundle.putInt(APPROX_BUNDLE_SIZE_BYTES_KEY, bundleByteSize);
+ return bundle;
+ }
+
+ /**
+ * Extracts the dimension value from the provided {@link DimensionsValue}.
+ *
+ * @param dv the {@link DimensionsValue} to extract value from.
+ * @param hashToStringMap the mapping used to translate hash code to string.
+ * @return extracted value object.
+ * @throws StatsConversionException if it's not possible to extract dimension value.
+ */
+ private static Object extractDimensionsValue(
+ DimensionsValue dv,
+ Map<Long, String> hashToStringMap) throws StatsConversionException {
+ switch (dv.getValueCase()) {
+ case VALUE_STR:
+ return dv.getValueStr();
+ case VALUE_INT:
+ return dv.getValueInt();
+ case VALUE_LONG:
+ return dv.getValueLong();
+ case VALUE_BOOL:
+ return dv.getValueBool();
+ case VALUE_FLOAT:
+ return dv.getValueFloat();
+ case VALUE_STR_HASH:
+ if (hashToStringMap == null) {
+ throw new StatsConversionException(
+ "Could not extract dimension value, no hash to string map found.");
+ }
+ return hashToStringMap.get(dv.getValueStrHash());
+ default:
+ throw new StatsConversionException(
+ "Could not extract dimension value, value not set or type not supported.");
+ }
+ }
+
+ /**
+ * Sets array fields in the {@link PersistableBundle}.
+ *
+ * @param name key value for the bundle, corresponds to atom field name.
+ * @param objList the list to be converted to {@link PersistableBundle} compatible array.
+ * @param bundle the {@link PersistableBundle} to put the arrays to.
+ * @return bytes written to PersistableBundle.
+ */
+ private static int setPersistableBundleArrayField(
+ String name,
+ List<Object> objList,
+ PersistableBundle bundle) {
+ Object e = objList.get(0); // All elements of the list are the same type.
+ int len = objList.size();
+ if (e instanceof Integer) {
+ int[] intArray = new int[objList.size()];
+ for (int i = 0; i < objList.size(); ++i) {
+ intArray[i] = (Integer) objList.get(i);
+ }
+ bundle.putIntArray(name, intArray);
+ return len * Integer.BYTES;
+ } else if (e instanceof Long) {
+ long[] longArray = new long[objList.size()];
+ for (int i = 0; i < objList.size(); ++i) {
+ longArray[i] = (Long) objList.get(i);
+ }
+ bundle.putLongArray(name, longArray);
+ return len * Long.BYTES;
+ } else if (e instanceof String) {
+ String[] strArray = objList.toArray(new String[0]);
+ bundle.putStringArray(name, strArray);
+ int bytes = 0;
+ for (String str : strArray) {
+ bytes += str.getBytes(UTF_16).length;
+ }
+ return bytes;
+ } else if (e instanceof Boolean) {
+ boolean[] boolArray = new boolean[objList.size()];
+ for (int i = 0; i < objList.size(); ++i) {
+ boolArray[i] = (Boolean) objList.get(i);
+ }
+ bundle.putBooleanArray(name, boolArray);
+ return len; // Java boolean is 1 byte
+ } else if (e instanceof Double) {
+ double[] doubleArray = new double[objList.size()];
+ for (int i = 0; i < objList.size(); ++i) {
+ doubleArray[i] = (Double) objList.get(i);
+ }
+ bundle.putDoubleArray(name, doubleArray);
+ return len * Double.BYTES;
+ } else if (e instanceof Float) {
+ double[] doubleArray = new double[objList.size()];
+ for (int i = 0; i < objList.size(); ++i) {
+ doubleArray[i] = ((Float) objList.get(i)).doubleValue();
+ }
+ bundle.putDoubleArray(name, doubleArray);
+ return len * Double.BYTES;
+ }
+ return 0;
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/ActivityForegroundStateChangedConverter.java b/service/src/com/android/car/telemetry/publisher/statsconverters/ActivityForegroundStateChangedConverter.java
new file mode 100644
index 0000000..dc7da9f
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/statsconverters/ActivityForegroundStateChangedConverter.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import android.util.SparseArray;
+
+import com.android.car.telemetry.AtomsProto.ActivityForegroundStateChanged;
+import com.android.car.telemetry.AtomsProto.Atom;
+
+/**
+ * Atom data converter for atoms of type {@link ActivityForegroundStateChanged}.
+ */
+public class ActivityForegroundStateChangedConverter
+ extends AbstractAtomConverter<ActivityForegroundStateChanged> {
+ private static final SparseArray<
+ AtomFieldAccessor<ActivityForegroundStateChanged>> sAtomFieldAccessorMap =
+ new SparseArray<>();
+ static {
+ sAtomFieldAccessorMap.append(1, new AtomFieldAccessor<>(
+ "uid",
+ a -> a.hasUid(),
+ a -> a.getUid()
+ ));
+ sAtomFieldAccessorMap.append(2, new AtomFieldAccessor<>(
+ "pkg_name",
+ a -> a.hasPkgName(),
+ a -> a.getPkgName()
+ ));
+ sAtomFieldAccessorMap.append(3, new AtomFieldAccessor<>(
+ "class_name",
+ a -> a.hasClassName(),
+ a -> a.getClassName()
+ ));
+ sAtomFieldAccessorMap.append(4, new AtomFieldAccessor<>(
+ "state",
+ a -> a.hasState(),
+ a -> a.getState().getNumber()
+ ));
+ }
+
+ ActivityForegroundStateChangedConverter() {
+ super();
+ }
+
+ @Override
+ SparseArray<AtomFieldAccessor<ActivityForegroundStateChanged>> getAtomFieldAccessorMap() {
+ return sAtomFieldAccessorMap;
+ }
+
+ @Override
+ ActivityForegroundStateChanged getAtomData(Atom atom) {
+ return atom.getActivityForegroundStateChanged();
+ }
+
+ @Override
+ String getAtomDataClassName() {
+ return ActivityForegroundStateChanged.class.getSimpleName();
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/AppStartMemoryStateCapturedConverter.java b/service/src/com/android/car/telemetry/publisher/statsconverters/AppStartMemoryStateCapturedConverter.java
new file mode 100644
index 0000000..b8c554f
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/statsconverters/AppStartMemoryStateCapturedConverter.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import android.util.SparseArray;
+
+import com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured;
+import com.android.car.telemetry.AtomsProto.Atom;
+
+/**
+ * Atom data converter for atoms of type {@link AppStartMemoryStateCaptured}.
+ */
+public class AppStartMemoryStateCapturedConverter
+ extends AbstractAtomConverter<AppStartMemoryStateCaptured> {
+ private static final SparseArray<
+ AtomFieldAccessor<AppStartMemoryStateCaptured>> sAtomFieldAccessorMap =
+ new SparseArray<>();
+ static {
+ sAtomFieldAccessorMap.append(1, new AtomFieldAccessor<>(
+ "uid",
+ a -> a.hasUid(),
+ a -> a.getUid()
+ ));
+ sAtomFieldAccessorMap.append(2, new AtomFieldAccessor<>(
+ "process_name",
+ a -> a.hasProcessName(),
+ a -> a.getProcessName()
+ ));
+ sAtomFieldAccessorMap.append(3, new AtomFieldAccessor<>(
+ "activity_name",
+ a -> a.hasActivityName(),
+ a -> a.getActivityName()
+ ));
+ sAtomFieldAccessorMap.append(4, new AtomFieldAccessor<>(
+ "page_fault",
+ a -> a.hasPageFault(),
+ a -> a.getPageFault()
+ ));
+ sAtomFieldAccessorMap.append(5, new AtomFieldAccessor<>(
+ "page_major_fault",
+ a -> a.hasPageMajorFault(),
+ a -> a.getPageMajorFault()
+ ));
+ sAtomFieldAccessorMap.append(6, new AtomFieldAccessor<>(
+ "rss_in_bytes",
+ a -> a.hasRssInBytes(),
+ a -> a.getRssInBytes()
+ ));
+ sAtomFieldAccessorMap.append(7, new AtomFieldAccessor<>(
+ "cache_in_bytes",
+ a -> a.hasCacheInBytes(),
+ a -> a.getCacheInBytes()
+ ));
+ sAtomFieldAccessorMap.append(8, new AtomFieldAccessor<>(
+ "swap_in_bytes",
+ a -> a.hasSwapInBytes(),
+ a -> a.getSwapInBytes()
+ ));
+ }
+
+ AppStartMemoryStateCapturedConverter() {
+ super();
+ }
+
+ @Override
+ SparseArray<AtomFieldAccessor<AppStartMemoryStateCaptured>> getAtomFieldAccessorMap() {
+ return sAtomFieldAccessorMap;
+ }
+
+ @Override
+ AppStartMemoryStateCaptured getAtomData(Atom atom) {
+ return atom.getAppStartMemoryStateCaptured();
+ }
+
+ @Override
+ String getAtomDataClassName() {
+ return AppStartMemoryStateCaptured.class.getSimpleName();
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/AtomFieldAccessor.java b/service/src/com/android/car/telemetry/publisher/statsconverters/AtomFieldAccessor.java
new file mode 100644
index 0000000..9df25ec
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/statsconverters/AtomFieldAccessor.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import com.google.protobuf.MessageLite;
+
+import java.util.function.Function;
+
+/**
+ * Class that contains metadata and actions for a field of atom data type T.
+ *
+ * @param <T> the atom data type.
+ */
+public class AtomFieldAccessor<T extends MessageLite> {
+ private final String mFieldName;
+ private final Function<T, Boolean> mHasField;
+ private final Function<T, Object> mGetField;
+
+ AtomFieldAccessor(
+ String fieldName,
+ Function<T, Boolean> hasField,
+ Function<T, Object> getField) {
+ mFieldName = fieldName;
+ mHasField = hasField;
+ mGetField = getField;
+ }
+
+ /**
+ * Gets the field name.
+ *
+ * @return field name as string.
+ */
+ String getFieldName() {
+ return mFieldName;
+ }
+
+ /**
+ * Checks if the field is set for the provided atom data of type T.
+ *
+ * @param atomData the atom data in which to check if the field is set.
+ * @return whether the field is set.
+ */
+ Boolean hasField(T atomData) {
+ return mHasField.apply(atomData);
+ }
+
+ /**
+ * Gets the field value for atom data of type T.
+ *
+ * @param atomData the atom data for which to get the field value from.
+ * @return the field value Object.
+ */
+ Object getField(T atomData) {
+ return mGetField.apply(atomData);
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/AtomListConverter.java b/service/src/com/android/car/telemetry/publisher/statsconverters/AtomListConverter.java
new file mode 100644
index 0000000..a854b3a
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/statsconverters/AtomListConverter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.AtomsProto.Atom;
+import com.android.car.telemetry.StatsLogProto.DimensionsValue;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class for converting atom data to {@link PersistableBundle} compatible format. Uses specific
+ * atom data converters that extend the {@link AbstractAtomConverter}.
+ */
+public class AtomListConverter {
+ // Map of pushed atom cases to corresponding atom converter.
+ private static Map<Atom.PushedCase, AbstractAtomConverter> sPushedCaseConverters = Map.of(
+ Atom.PushedCase.APP_START_MEMORY_STATE_CAPTURED,
+ new AppStartMemoryStateCapturedConverter(),
+ Atom.PushedCase.ACTIVITY_FOREGROUND_STATE_CHANGED,
+ new ActivityForegroundStateChangedConverter());
+
+ // Map of pulled atom cases to corresponding atom converter.
+ private static Map<Atom.PulledCase, AbstractAtomConverter> sPulledCaseConverters = Map.of(
+ Atom.PulledCase.PROCESS_MEMORY_STATE, new ProcessMemoryStateConverter());
+
+ /**
+ * Converts a list of atoms to separate the atoms fields values into arrays to be put into the
+ * {@link PersistableBundle}.
+ * The list of atoms must contain atoms of same type.
+ * Only fields with types allowed in {@link PersistableBundle} are added to the bundle.
+ *
+ * @param atoms list of {@link Atom} of the same type.
+ * @param dimensionsFieldsIds field ids for fields that are encoded in {@link DimensionsValue}.
+ * @param dimensionsValuesList dimension value groups matching mDimensionsFieldsIds.
+ * @param hashToStringMap hash to string mapping for decoding the some dimension values.
+ * @return {@link PersistableBundle} that holds the converted atom fields.
+ * @throws StatsConversionException if atom field mismatch or can't convert dimension value.
+ */
+ static PersistableBundle convert(
+ List<Atom> atoms,
+ List<Integer> dimensionsFieldsIds,
+ List<List<DimensionsValue>> dimensionsValuesList,
+ Map<Long, String> hashToStringMap) throws StatsConversionException {
+ // The atoms are either pushed or pulled type atoms.
+ if (atoms.get(0).getPushedCase() != Atom.PushedCase.PUSHED_NOT_SET) {
+ return sPushedCaseConverters.get(atoms.get(0).getPushedCase()).convert(
+ atoms, dimensionsFieldsIds, dimensionsValuesList, hashToStringMap);
+ } else {
+ return sPulledCaseConverters.get(atoms.get(0).getPulledCase()).convert(
+ atoms, dimensionsFieldsIds, dimensionsValuesList, hashToStringMap);
+ }
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/ConfigMetricsReportListConverter.java b/service/src/com/android/car/telemetry/publisher/statsconverters/ConfigMetricsReportListConverter.java
new file mode 100644
index 0000000..8e6f2b0
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/statsconverters/ConfigMetricsReportListConverter.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.StatsLogProto;
+import com.android.car.telemetry.publisher.HashUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Class for converting metrics report list data to {@link PersistableBundle} compatible format.
+ */
+public class ConfigMetricsReportListConverter {
+ /**
+ * Converts metrics report list to map of metric_id to {@link PersistableBundle} format where
+ * each PersistableBundle containing arrays of metric fields data. Multiple reports with the
+ * same metrics are combined on the metrics.
+ *
+ * Example:
+ * Given a ConfigMetricsReportList like this:
+ * {
+ * reports: {
+ * metrics: {
+ * metric_id: 1234
+ * event_metrics: {
+ * data: {...}
+ * }
+ * metric_id: 2345
+ * event_metrics: {
+ * data: {...}
+ * }
+ * metric_id: 3456
+ * gauge_metrics: {
+ * data: {...}
+ * }
+ * }
+ * metrics: {
+ * metric_id: 3456
+ * gauge_metrics: {
+ * data: {...}
+ * }
+ * }
+ * }
+ * }
+ * Will result in a map of this form (note metric 3456 is combined from the two reports):
+ * {
+ * "1234" : {...} // PersistableBundle containing metric 1234's data
+ * "2345" : {...}
+ * "3456" : {...}
+ * }
+ *
+ * @param reportList the {@link StatsLogProto.ConfigMetricsReportList} to be converted.
+ * @return a {@link PersistableBundle} containing mapping of metric id to metric data.
+ * @throws StatsConversionException if atom field mismatch or can't convert dimension value.
+ */
+ public static Map<Long, PersistableBundle> convert(
+ StatsLogProto.ConfigMetricsReportList reportList) throws StatsConversionException {
+ // Map metric id to StatsLogReport list so that separate reports can be combined.
+ Map<Long, List<StatsLogProto.StatsLogReport>> metricsStatsReportMap = new HashMap<>();
+ Set<String> stringsSet = new HashSet<>();
+ // ConfigMetricsReportList is for one config. Normally only 1 report exists unless
+ // the previous report did not upload after shutdown, then at most 2 reports can exist.
+ for (StatsLogProto.ConfigMetricsReport report : reportList.getReportsList()) {
+ stringsSet.addAll(report.getStringsList());
+ // Each statsReport is for a different metric in the report.
+ for (StatsLogProto.StatsLogReport statsReport : report.getMetricsList()) {
+ Long metricId = statsReport.getMetricId();
+ if (!metricsStatsReportMap.containsKey(metricId)) {
+ metricsStatsReportMap.put(
+ metricId, new ArrayList<StatsLogProto.StatsLogReport>());
+ }
+ metricsStatsReportMap.get(metricId).add(statsReport);
+ }
+ }
+ Map<Long, PersistableBundle> metricIdBundleMap = new HashMap<>();
+ // For each metric extract the metric data list from the combined stats reports,
+ // convert to bundle data.
+ for (Map.Entry<Long, List<StatsLogProto.StatsLogReport>>
+ entry : metricsStatsReportMap.entrySet()) {
+ PersistableBundle statsReportBundle = null;
+ Long metricId = entry.getKey();
+ List<StatsLogProto.StatsLogReport> statsReportList = entry.getValue();
+ switch (statsReportList.get(0).getDataCase()) {
+ case EVENT_METRICS:
+ List<StatsLogProto.EventMetricData> eventDataList = new ArrayList<>();
+ for (StatsLogProto.StatsLogReport statsReport : statsReportList) {
+ eventDataList.addAll(statsReport.getEventMetrics().getDataList());
+ }
+ statsReportBundle =
+ EventMetricDataConverter.convertEventDataList(eventDataList);
+ break;
+ case GAUGE_METRICS:
+ List<StatsLogProto.GaugeMetricData> gaugeDataList = new ArrayList<>();
+ for (StatsLogProto.StatsLogReport statsReport : statsReportList) {
+ gaugeDataList.addAll(statsReport.getGaugeMetrics().getDataList());
+ }
+ statsReportBundle = GaugeMetricDataConverter.convertGaugeDataList(
+ gaugeDataList,
+ extractDimensionFieldsIds(
+ statsReportList.get(0).getDimensionsPathInWhat()),
+ createDimensionHashToStringMap(stringsSet));
+ break;
+ default:
+ break;
+ }
+ if (statsReportBundle != null) {
+ metricIdBundleMap.put(metricId, statsReportBundle);
+ }
+ }
+ return metricIdBundleMap;
+ }
+
+ /**
+ * Creates a hash to string mapping for decoding {@link StatsLogProto.DimensionsValue}.
+ *
+ * <p> The mapping is created using murmur2 hashing algorithm.
+ *
+ * @param dimensionStrings the strings that were encoded in dimension values.
+ * @return hash to string mapping.
+ */
+ private static Map<Long, String> createDimensionHashToStringMap(Set<String> dimensionStrings) {
+ Map<Long, String> hashToStringMap = new HashMap<>();
+ for (String str : dimensionStrings) {
+ Long hash = HashUtils.murmur2Hash64(str);
+ hashToStringMap.put(hash, str);
+ }
+ return hashToStringMap;
+ }
+
+ /**
+ * Extracts the field ids of atom fields that were encoded in the dimension values.
+ *
+ * @param dimensionsPath the root level DimensionsValue. Contains field ids instead of values.
+ * @return list of atom field ids.
+ */
+ private static List<Integer> extractDimensionFieldsIds(
+ StatsLogProto.DimensionsValue dimensionsPath) {
+ List<Integer> dimensionsFieldsIds = new ArrayList<>();
+ StatsLogProto.DimensionsValueTuple dimensionTuple = dimensionsPath.getValueTuple();
+ for (StatsLogProto.DimensionsValue dv : dimensionTuple.getDimensionsValueList()) {
+ dimensionsFieldsIds.add(dv.getField());
+ }
+ return dimensionsFieldsIds;
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/EventMetricDataConverter.java b/service/src/com/android/car/telemetry/publisher/statsconverters/EventMetricDataConverter.java
new file mode 100644
index 0000000..f0803d6
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/statsconverters/EventMetricDataConverter.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import static com.android.car.telemetry.databroker.ScriptExecutionTask.APPROX_BUNDLE_SIZE_BYTES_KEY;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.AtomsProto;
+import com.android.car.telemetry.StatsLogProto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class for converting event metric data to {@link PersistableBundle} compatible format.
+ */
+public class EventMetricDataConverter {
+ static final String ELAPSED_TIME_NANOS = "elapsed_timestamp_nanos";
+
+ /**
+ * Converts a list of {@link StatsLogProto.EventMetricData} to {@link PersistableBundle}
+ * format such that along with the elapsed time array each field of the atom has an associated
+ * array containing the field's data in order received, matching the elapsed time array order.
+ *
+ * Example:
+ * {
+ * elapsed_timestamp_nanos: [32948395739, 45623453646, ...]
+ * uid: [1000, 1100, ...]
+ * ...
+ * }
+ * @param eventDataList the list of {@link StatsLogProto.EventMetricData} to be converted.
+ * @return {@link PersistableBundle} that holds the converted values.
+ * @throws StatsConversionException if atom field mismatch or can't convert dimension value.
+ */
+ static PersistableBundle convertEventDataList(
+ List<StatsLogProto.EventMetricData> eventDataList)
+ throws StatsConversionException {
+ long[] elapsedTimes = new long[eventDataList.size()];
+ List<AtomsProto.Atom> atoms = new ArrayList<>(eventDataList.size());
+ for (int i = 0; i < eventDataList.size(); ++i) {
+ elapsedTimes[i] = eventDataList.get(i).getElapsedTimestampNanos();
+ atoms.add(eventDataList.get(i).getAtom());
+ }
+ PersistableBundle bundle = AtomListConverter.convert(atoms, null, null, null);
+ bundle.putLongArray(ELAPSED_TIME_NANOS, elapsedTimes);
+ int bundleSize = bundle.getInt(APPROX_BUNDLE_SIZE_BYTES_KEY)
+ + elapsedTimes.length * Long.BYTES;
+ bundle.putInt(APPROX_BUNDLE_SIZE_BYTES_KEY, bundleSize);
+ return bundle;
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/GaugeMetricDataConverter.java b/service/src/com/android/car/telemetry/publisher/statsconverters/GaugeMetricDataConverter.java
new file mode 100644
index 0000000..e82a6ce
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/statsconverters/GaugeMetricDataConverter.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import static com.android.car.telemetry.databroker.ScriptExecutionTask.APPROX_BUNDLE_SIZE_BYTES_KEY;
+
+import android.os.PersistableBundle;
+
+import com.android.car.telemetry.AtomsProto;
+import com.android.car.telemetry.StatsLogProto.DimensionsValue;
+import com.android.car.telemetry.StatsLogProto.GaugeBucketInfo;
+import com.android.car.telemetry.StatsLogProto.GaugeMetricData;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class for converting gauge metric data to {@link PersistableBundle} compatible format.
+ */
+public class GaugeMetricDataConverter {
+ static final String ELAPSED_TIME_NANOS = "elapsed_timestamp_nanos";
+
+ /**
+ * Converts a list of {@link StatsLogProto.GaugeMetricData} to {@link PersistableBundle}
+ * format such that along with the elapsed time array each field of the atom has an associated
+ * array containing the field's data in order received, matching the elapsed time array order.
+ * The atoms are extracted out of the each bucket while preserving the order they had in the
+ * bucket.
+ *
+ * Example:
+ * {
+ * elapsed_timestamp_nanos: [32948395739, 45623453646, ...]
+ * uid: [1000, 1100, ...]
+ * ...
+ * }
+ *
+ * @param gaugeDataList the list of {@link StatsLogProto.GaugeMetricData} to be converted.
+ * @param dimensionsFieldsIds field ids for fields that are encoded in {@link DimensionsValue}.
+ * @param hashToStringMap hash to string mapping for decoding the some dimension values.
+ * @return {@link PersistableBundle} that holds the converted values.
+ * @throws StatsConversionException if atom field mismatch or can't convert dimension value.
+ */
+ static PersistableBundle convertGaugeDataList(
+ List<GaugeMetricData> gaugeDataList,
+ List<Integer> dimensionsFieldsIds,
+ Map<Long, String> hashToStringMap) throws StatsConversionException {
+ List<Long> elapsedTimes = new ArrayList<>();
+ List<AtomsProto.Atom> atoms = new ArrayList<>();
+ // This list contains dimensionsValues for each atom, matching in index and list size.
+ List<List<DimensionsValue>> dimensionsValuesList = new ArrayList<>();
+ for (GaugeMetricData gaugeData : gaugeDataList) {
+ // The dimensionsValue is same for all stoms in the same GaugeMetricData.
+ List<DimensionsValue> dimensionsValues = gaugeData.getDimensionLeafValuesInWhatList();
+ for (GaugeBucketInfo bi : gaugeData.getBucketInfoList()) {
+ elapsedTimes.addAll(bi.getElapsedTimestampNanosList());
+ for (AtomsProto.Atom atom : bi.getAtomList()) {
+ atoms.add(atom);
+ dimensionsValuesList.add(dimensionsValues);
+ }
+ }
+ }
+ PersistableBundle bundle = AtomListConverter.convert(
+ atoms,
+ dimensionsFieldsIds,
+ dimensionsValuesList,
+ hashToStringMap);
+ long[] elapsedTimesArray = new long[elapsedTimes.size()];
+ for (int i = 0; i < elapsedTimes.size(); ++i) {
+ elapsedTimesArray[i] = elapsedTimes.get(i);
+ }
+ bundle.putLongArray(ELAPSED_TIME_NANOS, elapsedTimesArray);
+ int bundleSize = bundle.getInt(APPROX_BUNDLE_SIZE_BYTES_KEY)
+ + elapsedTimesArray.length * Long.BYTES;
+ bundle.putInt(APPROX_BUNDLE_SIZE_BYTES_KEY, bundleSize);
+ return bundle;
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemoryStateConverter.java b/service/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemoryStateConverter.java
new file mode 100644
index 0000000..95b0b1d
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemoryStateConverter.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import android.util.SparseArray;
+
+import com.android.car.telemetry.AtomsProto.Atom;
+import com.android.car.telemetry.AtomsProto.ProcessMemoryState;
+
+/**
+ * Atom data converter for atoms of type {@link ProcessMemoryState}.
+ */
+public class ProcessMemoryStateConverter extends AbstractAtomConverter<ProcessMemoryState> {
+ private static final SparseArray<AtomFieldAccessor<ProcessMemoryState>> sAtomFieldAccessorMap =
+ new SparseArray<>();
+ static {
+ sAtomFieldAccessorMap.append(1, new AtomFieldAccessor<>(
+ "uid",
+ a -> a.hasUid(),
+ a -> a.getUid()
+ ));
+ sAtomFieldAccessorMap.append(2, new AtomFieldAccessor<>(
+ "process_name",
+ a -> a.hasProcessName(),
+ a -> a.getProcessName()
+ ));
+ sAtomFieldAccessorMap.append(3, new AtomFieldAccessor<>(
+ "oom_adj_score",
+ a -> a.hasOomAdjScore(),
+ a -> a.getOomAdjScore()
+ ));
+ sAtomFieldAccessorMap.append(4, new AtomFieldAccessor<>(
+ "page_fault",
+ a -> a.hasPageFault(),
+ a -> a.getPageFault()
+ ));
+ sAtomFieldAccessorMap.append(5, new AtomFieldAccessor<>(
+ "page_major_fault",
+ a -> a.hasPageMajorFault(),
+ a -> a.getPageMajorFault()
+ ));
+ sAtomFieldAccessorMap.append(6, new AtomFieldAccessor<>(
+ "rss_in_bytes",
+ a -> a.hasRssInBytes(),
+ a -> a.getRssInBytes()
+ ));
+ sAtomFieldAccessorMap.append(7, new AtomFieldAccessor<>(
+ "cache_in_bytes",
+ a -> a.hasCacheInBytes(),
+ a -> a.getCacheInBytes()
+ ));
+ sAtomFieldAccessorMap.append(8, new AtomFieldAccessor<>(
+ "swap_in_bytes",
+ a -> a.hasSwapInBytes(),
+ a -> a.getSwapInBytes()
+ ));
+ }
+
+ ProcessMemoryStateConverter() {
+ super();
+ }
+
+ @Override
+ SparseArray<AtomFieldAccessor<ProcessMemoryState>> getAtomFieldAccessorMap() {
+ return sAtomFieldAccessorMap;
+ }
+
+ @Override
+ ProcessMemoryState getAtomData(Atom atom) {
+ return atom.getProcessMemoryState();
+ }
+
+ @Override
+ String getAtomDataClassName() {
+ return ProcessMemoryState.class.getSimpleName();
+ }
+}
diff --git a/service/src/com/android/car/telemetry/publisher/statsconverters/StatsConversionException.java b/service/src/com/android/car/telemetry/publisher/statsconverters/StatsConversionException.java
new file mode 100644
index 0000000..45f5bb5
--- /dev/null
+++ b/service/src/com/android/car/telemetry/publisher/statsconverters/StatsConversionException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+/**
+ * Class for exceptions that occur when converting statsd data.
+ */
+public class StatsConversionException extends Exception {
+ StatsConversionException(String errorMsg) {
+ super(errorMsg);
+ }
+}
diff --git a/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutor.aidl b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutor.aidl
new file mode 100644
index 0000000..3101634
--- /dev/null
+++ b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutor.aidl
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2021, 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.telemetry.scriptexecutorinterface;
+
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
+
+/**
+ * An internal API provided by isolated Script Executor process
+ * for executing Lua scripts in a sandboxed environment
+ */
+oneway interface IScriptExecutor {
+ /**
+ * Executes a specified function in a provided Lua script with given input arguments.
+ *
+ * @param scriptBody complete body of Lua script that also contains the function to be invoked
+ * @param functionName the name of the function to execute
+ * @param publishedData input data provided by the source which the function handles
+ * @param savedState key-value pairs preserved from the previous invocation of the function
+ * @param listener callback for the sandboxed environment to report back script execution results,
+ * errors, and logs
+ */
+ void invokeScript(String scriptBody,
+ String functionName,
+ in PersistableBundle publishedData,
+ in @nullable PersistableBundle savedState,
+ in IScriptExecutorListener listener);
+
+ /**
+ * Executes a specified function in a provided Lua script with given input arguments.
+ * This is a specialized version of invokeScript API above for a case when publishedData input
+ * could be potentially large and overflow Binder's buffer.
+ *
+ * @param scriptBody complete body of Lua script that also contains the function to be invoked
+ * @param functionName the name of the function to execute
+ * @param publishedDataFileDescriptor file descriptor which is be used to open a pipe to read
+ * large amount of input data. The input data is then handled by the provided Lua function.
+ * @param savedState key-value pairs preserved from the previous invocation of the function
+ * @param listener callback for the sandboxed environment to report back script execution results,
+ * errors, and logs
+ */
+ void invokeScriptForLargeInput(String scriptBody,
+ String functionName,
+ in ParcelFileDescriptor publishedDataFileDescriptor,
+ in @nullable PersistableBundle savedState,
+ in IScriptExecutorListener listener);
+}
diff --git a/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl
new file mode 100644
index 0000000..99fed2d
--- /dev/null
+++ b/service/src/com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorListener.aidl
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2021, 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.telemetry.scriptexecutorinterface;
+
+import android.os.PersistableBundle;
+
+/**
+ * Listener for {@code IScriptExecutor#invokeScript}.
+ *
+ * An invocation of a script by Script Executor will result in a call of only one
+ * of the three methods below. If a script fully completes its objective, onScriptFinished
+ * is called. If a script's invocation completes normally, onSuccess is called.
+ * onError is called if any error happens before or during script execution and we
+ * should abandon this run of the script.
+ */
+interface IScriptExecutorListener {
+ /**
+ * Called by ScriptExecutor when the script declares itself as "finished".
+ *
+ * @param result final results of the script that will be uploaded.
+ */
+ void onScriptFinished(in PersistableBundle result);
+
+ /**
+ * Called by ScriptExecutor when a function completes successfully and also provides
+ * optional state that the script wants CarTelemetryService to persist.
+ *
+ * @param stateToPersist key-value pairs to persist
+ */
+ void onSuccess(in @nullable PersistableBundle stateToPersist);
+
+ /**
+ * Called by ScriptExecutor to report errors that prevented the script
+ * from running or completing execution successfully.
+ *
+ * @param errorType type of the error message as defined in this aidl file.
+ * @param messsage the human-readable message containing information helpful for analysis or debugging.
+ * @param stackTrace the stack trace of the error if available.
+ */
+ void onError(int errorType, String message, @nullable String stackTrace);
+
+ /**
+ * Any changes to the following ERROR_TYPE_* constants must be also reflected in the following files:
+ * p/s/C/package/ScriptExecutor/src/ScriptExecutorListener.h
+ * p/s/C/service/src/com/android/car/telemetry/proto/telemetry.proto
+ */
+
+ /**
+ * Default error type.
+ */
+ const int ERROR_TYPE_UNSPECIFIED = 0;
+
+ /**
+ * Used when an error occurs in the ScriptExecutor code.
+ */
+ const int ERROR_TYPE_SCRIPT_EXECUTOR_ERROR = 1;
+
+ /**
+ * Used when an error occurs while executing the Lua script (such as
+ * errors returned by lua_pcall)
+ */
+ const int ERROR_TYPE_LUA_RUNTIME_ERROR = 2;
+
+ /**
+ * Used to log errors by a script itself, for instance, when a script received
+ * inputs outside of expected range.
+ */
+ const int ERROR_TYPE_LUA_SCRIPT_ERROR = 3;
+}
+
diff --git a/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java b/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
index 65bc45b..28888f5 100644
--- a/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
+++ b/service/src/com/android/car/telemetry/systemmonitor/SystemMonitor.java
@@ -16,13 +16,45 @@
package com.android.car.telemetry.systemmonitor;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityManager.MemoryInfo;
+import android.content.Context;
+import android.os.Handler;
+
+import com.android.car.CarLog;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
/**
* SystemMonitor monitors system states and report to listeners when there are
* important changes.
+ * All methods in this class should be invoked from the telemetry thread.
*/
public class SystemMonitor {
- private SystemMonitorCallback mCallback;
+ private static final int NUM_LOADAVG_VALS = 3;
+ private static final float HI_CPU_LOAD_PER_CORE_BASE_LEVEL = 1.0f;
+ private static final float MED_CPU_LOAD_PER_CORE_BASE_LEVEL = 0.5f;
+ private static final float HI_MEM_LOAD_BASE_LEVEL = 0.95f;
+ private static final float MED_MEM_LOAD_BASE_LEVEL = 0.80f;
+ private static final String LOADAVG_PATH = "/proc/loadavg";
+
+ private static final int POLL_INTERVAL_MILLIS = 60000;
+
+ private final Handler mTelemetryHandler;
+
+ private final Context mContext;
+ private final ActivityManager mActivityManager;
+ private final String mLoadavgPath;
+ private final Runnable mSystemLoadRunnable = this::getSystemLoadRepeated;
+
+ @Nullable private SystemMonitorCallback mCallback;
+ private boolean mSystemMonitorRunning = false;
/**
* Interface for receiving notifications about system monitor changes.
@@ -37,11 +69,162 @@
}
/**
- * Sets the callback to notify of system state changes.
+ * Creates a SystemMonitor instance set with default loadavg path.
+ *
+ * @param context the context this is running in.
+ * @param workerHandler a handler for running monitoring jobs.
+ * @return SystemMonitor instance.
+ */
+ public static SystemMonitor create(Context context, Handler workerHandler) {
+ return new SystemMonitor(context, workerHandler, LOADAVG_PATH);
+ }
+
+ @VisibleForTesting
+ SystemMonitor(Context context, Handler telemetryHandler, String loadavgPath) {
+ mContext = context;
+ mTelemetryHandler = telemetryHandler;
+ mActivityManager = (ActivityManager)
+ mContext.getSystemService(Context.ACTIVITY_SERVICE);
+ mLoadavgPath = loadavgPath;
+ }
+
+ /**
+ * Sets the {@link SystemMonitorCallback} to notify of system state changes.
*
* @param callback the callback to nofify state changes on.
*/
public void setSystemMonitorCallback(SystemMonitorCallback callback) {
mCallback = callback;
+ if (!mSystemMonitorRunning) {
+ startSystemLoadMonitoring();
+ }
+ }
+
+ /**
+ * Unsets the {@link SystemMonitorCallback}.
+ */
+ public void unsetSystemMonitorCallback() {
+ mTelemetryHandler.removeCallbacks(mSystemLoadRunnable);
+ mSystemMonitorRunning = false;
+ mCallback = null;
+ }
+
+ /**
+ * Gets the loadavg data from /proc/loadavg, getting the first 3 averages,
+ * which are 1-min, 5-min and 15-min moving averages respectively.
+ *
+ * Requires Selinux permissions 'open', 'read, 'getattr' to proc_loadavg,
+ * which is set in Car/car_product/sepolicy/private/carservice_app.te.
+ *
+ * @return the {@link CpuLoadavg}.
+ */
+ @VisibleForTesting
+ @Nullable
+ CpuLoadavg getCpuLoad() {
+ try (BufferedReader reader = new BufferedReader(new FileReader(mLoadavgPath))) {
+ String line = reader.readLine();
+ String[] vals = line.split("\\s+", NUM_LOADAVG_VALS + 1);
+ if (vals.length < NUM_LOADAVG_VALS) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Loadavg wrong format");
+ return null;
+ }
+ CpuLoadavg cpuLoadavg = new CpuLoadavg();
+ cpuLoadavg.mOneMinuteVal = Float.parseFloat(vals[0]);
+ cpuLoadavg.mFiveMinutesVal = Float.parseFloat(vals[1]);
+ cpuLoadavg.mFifteenMinutesVal = Float.parseFloat(vals[2]);
+ return cpuLoadavg;
+ } catch (IOException | NumberFormatException ex) {
+ Slogf.w(CarLog.TAG_TELEMETRY, "Failed to read loadavg file.", ex);
+ return null;
+ }
+ }
+
+ /**
+ * Gets the {@link ActivityManager.MemoryInfo} for system memory pressure.
+ *
+ * Of the MemoryInfo fields, we will only be using availMem and totalMem,
+ * since lowMemory and threshold are likely deprecated.
+ *
+ * @return {@link MemoryInfo} for the system.
+ */
+ private MemoryInfo getMemoryLoad() {
+ MemoryInfo mi = new ActivityManager.MemoryInfo();
+ mActivityManager.getMemoryInfo(mi);
+ return mi;
+ }
+
+ /**
+ * Sets the CPU usage level for a {@link SystemMonitorEvent}.
+ *
+ * @param event the {@link SystemMonitorEvent}.
+ * @param cpuLoadPerCore the CPU load average per CPU core.
+ */
+ @VisibleForTesting
+ void setEventCpuUsageLevel(SystemMonitorEvent event, double cpuLoadPerCore) {
+ if (cpuLoadPerCore > HI_CPU_LOAD_PER_CORE_BASE_LEVEL) {
+ event.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
+ } else if (cpuLoadPerCore > MED_CPU_LOAD_PER_CORE_BASE_LEVEL
+ && cpuLoadPerCore <= HI_CPU_LOAD_PER_CORE_BASE_LEVEL) {
+ event.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
+ } else {
+ event.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+ }
+ }
+
+ /**
+ * Sets the memory usage level for a {@link SystemMonitorEvent}.
+ *
+ * @param event the {@link SystemMonitorEvent}.
+ * @param memLoadRatio ratio of used memory to total memory.
+ */
+ @VisibleForTesting
+ void setEventMemUsageLevel(SystemMonitorEvent event, double memLoadRatio) {
+ if (memLoadRatio > HI_MEM_LOAD_BASE_LEVEL) {
+ event.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
+ } else if (memLoadRatio > MED_MEM_LOAD_BASE_LEVEL
+ && memLoadRatio <= HI_MEM_LOAD_BASE_LEVEL) {
+ event.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
+ } else {
+ event.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+ }
+ }
+
+ /**
+ * The Runnable to repeatedly getting system load data with some interval.
+ */
+ private void getSystemLoadRepeated() {
+ try {
+ CpuLoadavg cpuLoadAvg = getCpuLoad();
+ if (cpuLoadAvg == null) {
+ return;
+ }
+ int numProcessors = Runtime.getRuntime().availableProcessors();
+
+ MemoryInfo memInfo = getMemoryLoad();
+
+ SystemMonitorEvent event = new SystemMonitorEvent();
+ setEventCpuUsageLevel(event, cpuLoadAvg.mOneMinuteVal / numProcessors);
+ setEventMemUsageLevel(event, 1 - (double) memInfo.availMem / memInfo.totalMem);
+
+ mCallback.onSystemMonitorEvent(event);
+ } finally {
+ if (mSystemMonitorRunning) {
+ mTelemetryHandler.postDelayed(mSystemLoadRunnable, POLL_INTERVAL_MILLIS);
+ }
+ }
+ }
+
+ /**
+ * Starts system load monitoring.
+ */
+ private void startSystemLoadMonitoring() {
+ mTelemetryHandler.post(mSystemLoadRunnable);
+ mSystemMonitorRunning = true;
+ }
+
+ static final class CpuLoadavg {
+ float mOneMinuteVal;
+ float mFiveMinutesVal;
+ float mFifteenMinutesVal;
}
}
diff --git a/service/src/com/android/car/user/AppLifecycleListener.java b/service/src/com/android/car/user/AppLifecycleListener.java
new file mode 100644
index 0000000..153ca85
--- /dev/null
+++ b/service/src/com/android/car/user/AppLifecycleListener.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 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.user;
+
+import android.os.IBinder.DeathRecipient;
+import android.os.RemoteException;
+
+import com.android.car.CarLog;
+import com.android.internal.os.IResultReceiver;
+import com.android.server.utils.Slogf;
+
+import java.io.PrintWriter;
+
+/**
+ * Helper DTO to hold info about an app-based {@code UserLifecycleListener}
+ */
+final class AppLifecycleListener {
+
+ private static final String TAG = CarLog.tagFor(AppLifecycleListener.class);
+
+ private final DeathRecipient mDeathRecipient;
+
+ public final int uid;
+ public final String packageName;
+ public final IResultReceiver receiver;
+
+ AppLifecycleListener(int uid, String packageName, IResultReceiver receiver,
+ BinderDeathCallback binderDeathCallback) {
+ this.uid = uid;
+ this.packageName = packageName;
+ this.receiver = receiver;
+
+ mDeathRecipient = () -> binderDeathCallback.onBinderDeath(this);
+ Slogf.v(TAG, "linking death recipient %s", mDeathRecipient);
+ try {
+ receiver.asBinder().linkToDeath(mDeathRecipient, /* flags= */ 0);
+ } catch (RemoteException e) {
+ Slogf.wtf(TAG, "Cannot listen to death of %s", mDeathRecipient);
+ }
+ }
+
+ void onDestroy() {
+ Slogf.v(TAG, "onDestroy(): unlinking death recipient %s", mDeathRecipient);
+ receiver.asBinder().unlinkToDeath(mDeathRecipient, /* flags= */ 0);
+ }
+
+ void dump(PrintWriter writer) {
+ writer.printf("uid=%d, pkg=%s\n", uid, packageName);
+ }
+
+ String toShortString() {
+ return uid + "-" + packageName;
+ }
+
+ @Override
+ public String toString() {
+ return "AppLifecycleListener[uid=" + uid + ", pkg=" + packageName + "]";
+ }
+
+ interface BinderDeathCallback {
+ void onBinderDeath(AppLifecycleListener listener);
+ }
+}
diff --git a/service/src/com/android/car/user/CarUserService.java b/service/src/com/android/car/user/CarUserService.java
index 2adbe79..d535c92 100644
--- a/service/src/com/android/car/user/CarUserService.java
+++ b/service/src/com/android/car/user/CarUserService.java
@@ -77,6 +77,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.os.Trace;
@@ -85,11 +86,11 @@
import android.provider.Settings;
import android.sysprop.CarProperties;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.EventLog;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
-import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.TimingsTraceLog;
import android.view.Display;
@@ -195,16 +196,19 @@
private final Handler mHandler;
/**
- * List of listeners to be notified on new user activities events.
- * This collection should be accessed and manipulated by mHandlerThread only.
+ * Internal listeners to be notified on new user activities events.
+ *
+ * <p>This collection should be accessed and manipulated by {@code mHandlerThread} only.
*/
private final List<UserLifecycleListener> mUserLifecycleListeners = new ArrayList<>();
/**
- * List of lifecycle listeners by uid.
- * This collection should be accessed and manipulated by mHandlerThread only.
+ * App listeners to be notified on new user activities events.
+ *
+ * <p>This collection should be accessed and manipulated by {@code mHandlerThread} only.
*/
- private final SparseArray<IResultReceiver> mAppLifecycleListeners = new SparseArray<>();
+ private final ArrayMap<IBinder, AppLifecycleListener> mAppLifecycleListeners =
+ new ArrayMap<>();
/**
* User Id for the user switch in process, if any.
@@ -359,8 +363,7 @@
checkHasDumpPermissionGranted("dump()");
writer.println("*CarUserService*");
- String indent = " ";
- handleDumpListeners(writer, indent);
+ handleDumpListeners(writer);
writer.printf("User switch UI receiver %s\n", mUserSwitchUiReceiver);
synchronized (mLockUser) {
writer.println("User0Unlocked: " + mUser0Unlocked);
@@ -380,9 +383,10 @@
List<UserInfo> allDrivers = getAllDrivers();
int driversSize = allDrivers.size();
writer.println("NumberOfDrivers: " + driversSize);
+ writer.increaseIndent();
for (int i = 0; i < driversSize; i++) {
int driverId = allDrivers.get(i).id;
- writer.print(indent + "#" + i + ": id=" + driverId);
+ writer.printf("#%d: id=%d", i, driverId);
List<UserInfo> passengers = getPassengers(driverId);
int passengersSize = passengers.size();
writer.print(" NumberPassengers: " + passengersSize);
@@ -398,38 +402,42 @@
}
writer.println();
}
+ writer.decreaseIndent();
writer.printf("EnablePassengerSupport: %s\n", mEnablePassengerSupport);
writer.printf("User HAL timeout: %dms\n", mHalTimeoutMs);
writer.printf("Initial user: %s\n", mInitialUser);
writer.println("Relevant overlayable properties");
Resources res = mContext.getResources();
- writer.printf("%sowner_name=%s\n", indent,
- res.getString(com.android.internal.R.string.owner_name));
- writer.printf("%sdefault_guest_name=%s\n", indent,
- res.getString(R.string.default_guest_name));
+ writer.increaseIndent();
+ writer.printf("owner_name=%s\n", res.getString(com.android.internal.R.string.owner_name));
+ writer.printf("default_guest_name=%s\n", res.getString(R.string.default_guest_name));
+ writer.decreaseIndent();
writer.printf("User switch in process=%d\n", mUserIdForUserSwitchInProcess);
writer.printf("Request Id for the user switch in process=%d\n ",
mRequestIdForUserSwitchInProcess);
writer.printf("System UI package name=%s\n", getSystemUiPackageName());
writer.println("Relevant Global settings");
- dumpGlobalProperty(writer, indent, CarSettings.Global.LAST_ACTIVE_USER_ID);
- dumpGlobalProperty(writer, indent, CarSettings.Global.LAST_ACTIVE_PERSISTENT_USER_ID);
+ writer.increaseIndent();
+ dumpGlobalProperty(writer, CarSettings.Global.LAST_ACTIVE_USER_ID);
+ dumpGlobalProperty(writer, CarSettings.Global.LAST_ACTIVE_PERSISTENT_USER_ID);
+ writer.decreaseIndent();
mInitialUserSetter.dump(writer);
}
- private void dumpGlobalProperty(PrintWriter writer, String indent, String property) {
+ private void dumpGlobalProperty(IndentingPrintWriter writer, String property) {
String value = Settings.Global.getString(mContext.getContentResolver(), property);
- writer.printf("%s%s=%s\n", indent, property, value);
+ writer.printf("%s=%s\n", property, value);
}
- private void handleDumpListeners(@NonNull PrintWriter writer, String indent) {
+ private void handleDumpListeners(IndentingPrintWriter writer) {
+ writer.increaseIndent();
CountDownLatch latch = new CountDownLatch(1);
mHandler.post(() -> {
handleDumpServiceLifecycleListeners(writer);
- handleDumpAppLifecycleListeners(writer, indent);
+ handleDumpAppLifecycleListeners(writer);
latch.countDown();
});
int timeout = 5;
@@ -442,9 +450,10 @@
Thread.currentThread().interrupt();
writer.println("Interrupted waiting for handler thread to dump app and user listeners");
}
+ writer.decreaseIndent();
}
- private void handleDumpServiceLifecycleListeners(@NonNull PrintWriter writer) {
+ private void handleDumpServiceLifecycleListeners(PrintWriter writer) {
if (mUserLifecycleListeners.isEmpty()) {
writer.println("No lifecycle listeners for internal services");
return;
@@ -452,24 +461,24 @@
int size = mUserLifecycleListeners.size();
writer.printf("%d lifecycle listener%s for services\n", size, size == 1 ? "" : "s");
String indent = " ";
- for (UserLifecycleListener listener : mUserLifecycleListeners) {
+ for (int i = 0; i < size; i++) {
+ UserLifecycleListener listener = mUserLifecycleListeners.get(i);
writer.printf("%s%s\n", indent, FunctionalUtils.getLambdaName(listener));
}
}
- private void handleDumpAppLifecycleListeners(@NonNull PrintWriter writer, String indent) {
+ private void handleDumpAppLifecycleListeners(IndentingPrintWriter writer) {
int size = mAppLifecycleListeners.size();
if (size == 0) {
writer.println("No lifecycle listeners for apps");
return;
}
- writer.printf("%d lifecycle listener%s for apps \n", size, size == 1 ? "" : "s");
+ writer.printf("%d lifecycle listener%s for apps\n", size, size == 1 ? "" : "s");
+ writer.increaseIndent();
for (int i = 0; i < size; i++) {
- int uid = mAppLifecycleListeners.keyAt(i);
- IResultReceiver listener = mAppLifecycleListeners.valueAt(i);
- writer.printf("%suid: %d listener: %s\n", indent, uid,
- FunctionalUtils.getLambdaName(listener));
+ mAppLifecycleListeners.valueAt(i).dump(writer);
}
+ writer.decreaseIndent();
}
/**
@@ -684,30 +693,45 @@
}
@Override
- public void setLifecycleListenerForUid(IResultReceiver listener) {
+ public void setLifecycleListenerForApp(String packageName, IResultReceiver receiver) {
int uid = Binder.getCallingUid();
- EventLog.writeEvent(EventLogTags.CAR_USER_SVC_SET_LIFECYCLE_LISTENER, uid);
- checkInteractAcrossUsersPermission("setLifecycleListenerForUid" + uid);
+ EventLog.writeEvent(EventLogTags.CAR_USER_SVC_SET_LIFECYCLE_LISTENER, uid, packageName);
+ checkInteractAcrossUsersPermission("setLifecycleListenerForApp-" + uid + "-" + packageName);
- try {
- listener.asBinder().linkToDeath(() -> onListenerDeath(uid), 0);
- } catch (RemoteException e) {
- Slog.wtf(TAG, "Cannot listen to death of " + uid);
- }
- mHandler.post(() -> mAppLifecycleListeners.append(uid, listener));
+ IBinder receiverBinder = receiver.asBinder();
+ AppLifecycleListener listener = new AppLifecycleListener(uid, packageName, receiver,
+ (l) -> onListenerDeath(l));
+ Slogf.d(TAG, "Adding %s (using binder %s)", listener, receiverBinder);
+ mHandler.post(() -> mAppLifecycleListeners.put(receiverBinder, listener));
}
- private void onListenerDeath(int uid) {
- Slog.i(TAG, "Removing listeners for uid " + uid + " on binder death");
- mHandler.post(() -> mAppLifecycleListeners.remove(uid));
+ private void onListenerDeath(AppLifecycleListener listener) {
+ Slogf.i(TAG, "Removing listener %s on binder death", listener);
+ mHandler.post(() -> mAppLifecycleListeners.remove(listener.receiver.asBinder()));
}
@Override
- public void resetLifecycleListenerForUid() {
+ public void resetLifecycleListenerForApp(IResultReceiver receiver) {
int uid = Binder.getCallingUid();
- EventLog.writeEvent(EventLogTags.CAR_USER_SVC_RESET_LIFECYCLE_LISTENER, uid);
- checkInteractAcrossUsersPermission("resetLifecycleListenerForUid-" + uid);
- mHandler.post(() -> mAppLifecycleListeners.remove(uid));
+ checkInteractAcrossUsersPermission("resetLifecycleListenerForApp-" + uid);
+ IBinder receiverBinder = receiver.asBinder();
+ mHandler.post(() -> {
+ AppLifecycleListener listener = mAppLifecycleListeners.get(receiverBinder);
+ if (listener == null) {
+ Slogf.e(TAG, "resetLifecycleListenerForApp(uid=%d): no listener for receiver", uid);
+ return;
+ }
+ if (listener.uid != uid) {
+ Slogf.e(TAG, "resetLifecycleListenerForApp(): uid mismatch (called by %d) for "
+ + "listener %s", uid, listener);
+ }
+ EventLog.writeEvent(EventLogTags.CAR_USER_SVC_RESET_LIFECYCLE_LISTENER, uid,
+ listener.packageName);
+ Slogf.d(TAG, "Removing %s (using binder %s)", listener, receiverBinder);
+ mAppLifecycleListeners.remove(receiverBinder);
+
+ listener.onDestroy();
+ });
}
/**
@@ -2102,23 +2126,17 @@
private void handleNotifyAppUserLifecycleListeners(UserLifecycleEvent event) {
int listenersSize = mAppLifecycleListeners.size();
if (listenersSize == 0) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Slog.d(TAG, "No app listener to be notified of " + event);
- }
+ Slogf.d(TAG, "No app listener to be notified of %s", event);
return;
}
// Must use a different TimingsTraceLog because it's another thread
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Slog.d(TAG, "Notifying " + listenersSize + " app listeners of " + event);
- }
+ Slogf.d(TAG, "Notifying %d app listeners of %s", listenersSize, event);
int userId = event.getUserId();
TimingsTraceLog t = new TimingsTraceLog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
int eventType = event.getEventType();
t.traceBegin("notify-app-listeners-user-" + userId + "-event-" + eventType);
for (int i = 0; i < listenersSize; i++) {
- int uid = mAppLifecycleListeners.keyAt(i);
-
- IResultReceiver listener = mAppLifecycleListeners.valueAt(i);
+ AppLifecycleListener listener = mAppLifecycleListeners.valueAt(i);
Bundle data = new Bundle();
data.putInt(CarUserManager.BUNDLE_PARAM_ACTION, eventType);
@@ -2126,17 +2144,14 @@
if (fromUserId != UserHandle.USER_NULL) {
data.putInt(CarUserManager.BUNDLE_PARAM_PREVIOUS_USER_ID, fromUserId);
}
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Slog.d(TAG, "Notifying listener for uid " + uid);
- }
+ Slogf.d(TAG, "Notifying listener %s", listener);
EventLog.writeEvent(EventLogTags.CAR_USER_SVC_NOTIFY_APP_LIFECYCLE_LISTENER,
- uid, eventType, fromUserId, userId);
+ listener.uid, listener.packageName, eventType, fromUserId, userId);
try {
- t.traceBegin("notify-app-listener-uid-" + uid);
- listener.send(userId, data);
+ t.traceBegin("notify-app-listener-" + listener.toShortString());
+ listener.receiver.send(userId, data);
} catch (RemoteException e) {
- Slog.e(TAG, "Error calling lifecycle listener", e);
+ Slogf.e(TAG, e, "Error calling lifecycle listener %s", listener);
} finally {
t.traceEnd();
}
diff --git a/service/src/com/android/car/user/InitialUserSetter.java b/service/src/com/android/car/user/InitialUserSetter.java
index 29406db..69fdfcf 100644
--- a/service/src/com/android/car/user/InitialUserSetter.java
+++ b/service/src/com/android/car/user/InitialUserSetter.java
@@ -241,7 +241,13 @@
*/
@NonNull
public Builder setUserLocales(@Nullable String userLocales) {
- mUserLocales = userLocales;
+ // This string can come from a binder IPC call where empty string is the default value
+ // for the auto-generated code. So, need to check for that.
+ if (userLocales != null && userLocales.trim().isEmpty()) {
+ mUserLocales = null;
+ } else {
+ mUserLocales = userLocales;
+ }
return this;
}
diff --git a/service/src/com/android/car/watchdog/CarWatchdogService.java b/service/src/com/android/car/watchdog/CarWatchdogService.java
index fccaa8b..be888d3 100644
--- a/service/src/com/android/car/watchdog/CarWatchdogService.java
+++ b/service/src/com/android/car/watchdog/CarWatchdogService.java
@@ -28,10 +28,15 @@
import android.automotive.watchdog.internal.PackageIoOveruseStats;
import android.automotive.watchdog.internal.PowerCycle;
import android.automotive.watchdog.internal.StateType;
+import android.automotive.watchdog.internal.UserPackageIoUsageStats;
import android.automotive.watchdog.internal.UserState;
import android.car.Car;
import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
+import android.car.hardware.power.CarPowerPolicy;
+import android.car.hardware.power.CarPowerPolicyFilter;
+import android.car.hardware.power.ICarPowerPolicyListener;
import android.car.hardware.power.ICarPowerStateListener;
+import android.car.hardware.power.PowerComponent;
import android.car.watchdog.CarWatchdogManager;
import android.car.watchdog.ICarWatchdogService;
import android.car.watchdog.ICarWatchdogServiceCallback;
@@ -64,6 +69,7 @@
import com.android.server.utils.Slogf;
import java.lang.ref.WeakReference;
+import java.time.Instant;
import java.util.List;
/**
@@ -76,10 +82,23 @@
"com.android.server.jobscheduler.GARAGE_MODE_ON";
static final String ACTION_GARAGE_MODE_OFF =
"com.android.server.jobscheduler.GARAGE_MODE_OFF";
+ static final int MISSING_ARG_VALUE = -1;
+ static final TimeSourceInterface SYSTEM_INSTANCE = new TimeSourceInterface() {
+ @Override
+ public Instant now() {
+ return Instant.now();
+ }
+
+ @Override
+ public String toString() {
+ return "System time instance";
+ }
+ };
private final Context mContext;
private final ICarWatchdogServiceForSystemImpl mWatchdogServiceForSystem;
private final PackageInfoHandler mPackageInfoHandler;
+ private final WatchdogStorage mWatchdogStorage;
private final WatchdogProcessHandler mWatchdogProcessHandler;
private final WatchdogPerfHandler mWatchdogPerfHandler;
private final CarWatchdogDaemonHelper mCarWatchdogDaemonHelper;
@@ -93,41 +112,98 @@
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
- boolean isGarageMode = false;
- if (action.equals(ACTION_GARAGE_MODE_ON)) {
- isGarageMode = true;
- } else if (!action.equals(ACTION_GARAGE_MODE_OFF)) {
- return;
- }
- try {
- mCarWatchdogDaemonHelper.notifySystemStateChange(StateType.GARAGE_MODE,
- isGarageMode ? GarageMode.GARAGE_MODE_ON : GarageMode.GARAGE_MODE_OFF,
- /* arg2= */ -1);
- if (DEBUG) {
- Slogf.d(TAG, "Notified car watchdog daemon of garage mode(%s)",
- isGarageMode ? "ON" : "OFF");
- }
- } catch (RemoteException | RuntimeException e) {
- Slogf.w(TAG, "Notifying garage mode state change failed: %s", e);
+ switch (action) {
+ case ACTION_GARAGE_MODE_ON:
+ case ACTION_GARAGE_MODE_OFF:
+ int garageMode;
+ synchronized (mLock) {
+ garageMode = mCurrentGarageMode = action.equals(ACTION_GARAGE_MODE_ON)
+ ? GarageMode.GARAGE_MODE_ON : GarageMode.GARAGE_MODE_OFF;
+ }
+ mWatchdogPerfHandler.onGarageModeChange(garageMode);
+ if (garageMode == GarageMode.GARAGE_MODE_ON) {
+ mWatchdogStorage.shrinkDatabase();
+ }
+ notifyGarageModeChange(garageMode);
+ return;
+ case Intent.ACTION_USER_REMOVED:
+ UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
+ mWatchdogPerfHandler.deleteUser(user.getIdentifier());
+ return;
}
}
};
+ private final ICarPowerStateListener mCarPowerStateListener =
+ new ICarPowerStateListener.Stub() {
+ @Override
+ public void onStateChanged(int state) {
+ CarPowerManagementService powerService =
+ CarLocalServices.getService(CarPowerManagementService.class);
+ if (powerService == null) {
+ return;
+ }
+ int powerCycle = carPowerStateToPowerCycle(powerService.getPowerState());
+ switch (powerCycle) {
+ case PowerCycle.POWER_CYCLE_SHUTDOWN_ENTER:
+ mWatchdogPerfHandler.writeToDatabase();
+ break;
+ // ON covers resume.
+ case PowerCycle.POWER_CYCLE_RESUME:
+ // There might be outdated & incorrect info. We should reset them before
+ // starting to do health check.
+ mWatchdogProcessHandler.prepareHealthCheck();
+ break;
+ default:
+ return;
+ }
+ notifyPowerCycleChange(powerCycle);
+ }
+ };
+
+ private final ICarPowerPolicyListener mCarDisplayPowerPolicyListener =
+ new ICarPowerPolicyListener.Stub() {
+ @Override
+ public void onPolicyChanged(CarPowerPolicy appliedPolicy,
+ CarPowerPolicy accumulatedPolicy) {
+ boolean isDisplayEnabled =
+ appliedPolicy.isComponentEnabled(PowerComponent.DISPLAY);
+ boolean didStateChange = false;
+ synchronized (mLock) {
+ didStateChange = mIsDisplayEnabled != isDisplayEnabled;
+ mIsDisplayEnabled = isDisplayEnabled;
+ }
+ if (didStateChange) {
+ mWatchdogPerfHandler.onDisplayStateChanged(isDisplayEnabled);
+ }
+ }
+ };
+
private final Object mLock = new Object();
@GuardedBy("mLock")
private boolean mReadyToRespond;
@GuardedBy("mLock")
private boolean mIsConnected;
+ @GuardedBy("mLock")
+ private @GarageMode int mCurrentGarageMode;
+ @GuardedBy("mLock")
+ private boolean mIsDisplayEnabled;
public CarWatchdogService(Context context) {
+ this(context, new WatchdogStorage(context));
+ }
+
+ @VisibleForTesting
+ CarWatchdogService(Context context, WatchdogStorage watchdogStorage) {
mContext = context;
+ mWatchdogStorage = watchdogStorage;
mPackageInfoHandler = new PackageInfoHandler(mContext.getPackageManager());
mCarWatchdogDaemonHelper = new CarWatchdogDaemonHelper(TAG_WATCHDOG);
mWatchdogServiceForSystem = new ICarWatchdogServiceForSystemImpl(this);
mWatchdogProcessHandler = new WatchdogProcessHandler(mWatchdogServiceForSystem,
mCarWatchdogDaemonHelper);
mWatchdogPerfHandler = new WatchdogPerfHandler(mContext, mCarWatchdogDaemonHelper,
- mPackageInfoHandler);
+ mPackageInfoHandler, mWatchdogStorage);
mConnectionListener = (isConnected) -> {
mWatchdogPerfHandler.onDaemonConnectionChange(isConnected);
synchronized (mLock) {
@@ -135,17 +211,19 @@
}
registerToDaemon();
};
+ mCurrentGarageMode = GarageMode.GARAGE_MODE_OFF;
+ mIsDisplayEnabled = true;
}
@Override
public void init() {
mWatchdogProcessHandler.init();
- subscribePowerCycleChange();
+ mWatchdogPerfHandler.init();
+ subscribePowerManagementService();
subscribeUserStateChange();
subscribeBroadcastReceiver();
mCarWatchdogDaemonHelper.addOnConnectionChangeListener(mConnectionListener);
mCarWatchdogDaemonHelper.connect();
- mWatchdogPerfHandler.init();
// To make sure the main handler is ready for responding to car watchdog daemon, registering
// to the daemon is done through the main handler. Once the registration is completed, we
// can assume that the main handler is not too busy handling other stuffs.
@@ -158,16 +236,23 @@
@Override
public void release() {
mContext.unregisterReceiver(mBroadcastReceiver);
+ unsubscribePowerManagementService();
mWatchdogPerfHandler.release();
+ mWatchdogStorage.release();
unregisterFromDaemon();
mCarWatchdogDaemonHelper.disconnect();
}
@Override
public void dump(IndentingPrintWriter writer) {
- writer.println("*CarWatchdogService*");
+ writer.println("*" + getClass().getSimpleName() + "*");
+ writer.increaseIndent();
+ synchronized (mLock) {
+ writer.println("Current garage mode: " + toGarageModeString(mCurrentGarageMode));
+ }
mWatchdogProcessHandler.dump(writer);
mWatchdogPerfHandler.dump(writer);
+ writer.decreaseIndent();
}
/**
@@ -199,11 +284,6 @@
mWatchdogProcessHandler.tellClientAlive(client, sessionId);
}
- @VisibleForTesting
- int getClientCount(int timeout) {
- return mWatchdogProcessHandler.getClientCount(timeout);
- }
-
/** Returns {@link android.car.watchdog.ResourceOveruseStats} for the calling package. */
@Override
@NonNull
@@ -326,6 +406,85 @@
return mWatchdogPerfHandler.getResourceOveruseConfigurations(resourceOveruseFlag);
}
+ /**
+ * Enables/disables the watchdog daemon client health check process.
+ */
+ public void controlProcessHealthCheck(boolean disable) {
+ ICarImpl.assertPermission(mContext, Car.PERMISSION_USE_CAR_WATCHDOG);
+ mWatchdogProcessHandler.controlProcessHealthCheck(disable);
+ }
+
+ @VisibleForTesting
+ int getClientCount(int timeout) {
+ return mWatchdogProcessHandler.getClientCount(timeout);
+ }
+
+ @VisibleForTesting
+ void setTimeSource(TimeSourceInterface timeSource) {
+ mWatchdogPerfHandler.setTimeSource(timeSource);
+ }
+
+ @VisibleForTesting
+ void setOveruseHandlingDelay(long millis) {
+ mWatchdogPerfHandler.setOveruseHandlingDelay(millis);
+ }
+
+ @VisibleForTesting
+ void setRecurringOveruseThreshold(int threshold) {
+ mWatchdogPerfHandler.setRecurringOveruseThreshold(threshold);
+ }
+
+ private void notifyAllUserStates() {
+ UserManager userManager = UserManager.get(mContext);
+ List<UserInfo> users = userManager.getUsers();
+ try {
+ // TODO(b/152780162): reduce the number of RPC calls(isUserRunning).
+ for (int i = 0; i < users.size(); ++i) {
+ UserInfo info = users.get(i);
+ int userState = userManager.isUserRunning(info.id)
+ ? UserState.USER_STATE_STARTED
+ : UserState.USER_STATE_STOPPED;
+ mCarWatchdogDaemonHelper.notifySystemStateChange(StateType.USER_STATE, info.id,
+ userState);
+ mWatchdogProcessHandler.updateUserState(info.id,
+ userState == UserState.USER_STATE_STOPPED);
+ }
+ if (DEBUG) {
+ Slogf.d(TAG, "Notified car watchdog daemon of user states");
+ }
+ } catch (RemoteException | RuntimeException e) {
+ Slogf.w(TAG, "Notifying latest user states failed: %s", e);
+ }
+ }
+
+ private void notifyPowerCycleChange(@PowerCycle int powerCycle) {
+ if (powerCycle == PowerCycle.NUM_POWER_CYLES) {
+ Slogf.e(TAG, "Skipping notifying invalid power cycle (%d)", powerCycle);
+ return;
+ }
+ try {
+ mCarWatchdogDaemonHelper.notifySystemStateChange(
+ StateType.POWER_CYCLE, powerCycle, MISSING_ARG_VALUE);
+ if (DEBUG) {
+ Slogf.d(TAG, "Notified car watchdog daemon of power cycle(%d)", powerCycle);
+ }
+ } catch (RemoteException | RuntimeException e) {
+ Slogf.w(TAG, e, "Notifying power cycle change to %d failed", powerCycle);
+ }
+ }
+
+ private void notifyGarageModeChange(@GarageMode int garageMode) {
+ try {
+ mCarWatchdogDaemonHelper.notifySystemStateChange(
+ StateType.GARAGE_MODE, garageMode, MISSING_ARG_VALUE);
+ if (DEBUG) {
+ Slogf.d(TAG, "Notified car watchdog daemon of garage mode(%d)", garageMode);
+ }
+ } catch (RemoteException | RuntimeException e) {
+ Slogf.w(TAG, e, "Notifying garage mode change to %d failed", garageMode);
+ }
+ }
+
private void postRegisterToDaemonMessage() {
CarServiceUtils.runOnMain(() -> {
synchronized (mLock) {
@@ -349,24 +508,27 @@
} catch (RemoteException | RuntimeException e) {
Slogf.w(TAG, "Cannot register to car watchdog daemon: %s", e);
}
- UserManager userManager = UserManager.get(mContext);
- List<UserInfo> users = userManager.getUsers();
- try {
- // TODO(b/152780162): reduce the number of RPC calls(isUserRunning).
- for (UserInfo info : users) {
- int userState = userManager.isUserRunning(info.id)
- ? UserState.USER_STATE_STARTED : UserState.USER_STATE_STOPPED;
- mCarWatchdogDaemonHelper.notifySystemStateChange(StateType.USER_STATE, info.id,
- userState);
- if (userState == UserState.USER_STATE_STOPPED) {
- mWatchdogProcessHandler.updateUserState(info.id, /*isStopped=*/true);
- } else {
- mWatchdogProcessHandler.updateUserState(info.id, /*isStopped=*/false);
- }
+ notifyAllUserStates();
+ CarPowerManagementService powerService =
+ CarLocalServices.getService(CarPowerManagementService.class);
+ if (powerService != null) {
+ int powerState = powerService.getPowerState();
+ int powerCycle = carPowerStateToPowerCycle(powerState);
+ if (powerCycle != PowerCycle.NUM_POWER_CYLES) {
+ notifyPowerCycleChange(powerCycle);
+ } else {
+ Slogf.i(TAG, "Skipping notifying %d power state", powerState);
}
- } catch (RemoteException | RuntimeException e) {
- Slogf.w(TAG, "Notifying system state change failed: %s", e);
}
+ int garageMode;
+ synchronized (mLock) {
+ // To avoid race condition, fetch {@link mCurrentGarageMode} just before
+ // the {@link notifyGarageModeChange} call. For instance, if {@code mCurrentGarageMode}
+ // changes before the above {@link notifyPowerCycleChange} call returns,
+ // the {@link garageMode}'s value will be out of date.
+ garageMode = mCurrentGarageMode;
+ }
+ notifyGarageModeChange(garageMode);
}
private void unregisterFromDaemon() {
@@ -380,46 +542,28 @@
}
}
- private void subscribePowerCycleChange() {
+ private void subscribePowerManagementService() {
CarPowerManagementService powerService =
CarLocalServices.getService(CarPowerManagementService.class);
if (powerService == null) {
Slogf.w(TAG, "Cannot get CarPowerManagementService");
return;
}
- powerService.registerListener(new ICarPowerStateListener.Stub() {
- @Override
- public void onStateChanged(int state) {
- int powerCycle;
- switch (state) {
- // SHUTDOWN_PREPARE covers suspend and shutdown.
- case CarPowerStateListener.SHUTDOWN_PREPARE:
- powerCycle = PowerCycle.POWER_CYCLE_SHUTDOWN_PREPARE;
- break;
- case CarPowerStateListener.SHUTDOWN_ENTER:
- case CarPowerStateListener.SUSPEND_ENTER:
- powerCycle = PowerCycle.POWER_CYCLE_SHUTDOWN_ENTER;
- // ON covers resume.
- case CarPowerStateListener.ON:
- powerCycle = PowerCycle.POWER_CYCLE_RESUME;
- // There might be outdated & incorrect info. We should reset them before
- // starting to do health check.
- mWatchdogProcessHandler.prepareHealthCheck();
- break;
- default:
- return;
- }
- try {
- mCarWatchdogDaemonHelper.notifySystemStateChange(StateType.POWER_CYCLE,
- powerCycle, /* arg2= */ -1);
- if (DEBUG) {
- Slogf.d(TAG, "Notified car watchdog daemon of power cycle(%d)", powerCycle);
- }
- } catch (RemoteException | RuntimeException e) {
- Slogf.w(TAG, "Notifying power cycle state change failed: %s", e);
- }
- }
- });
+ powerService.registerListener(mCarPowerStateListener);
+ powerService.addPowerPolicyListener(
+ new CarPowerPolicyFilter.Builder().setComponents(PowerComponent.DISPLAY).build(),
+ mCarDisplayPowerPolicyListener);
+ }
+
+ private void unsubscribePowerManagementService() {
+ CarPowerManagementService powerService =
+ CarLocalServices.getService(CarPowerManagementService.class);
+ if (powerService == null) {
+ Slogf.w(TAG, "Cannot get CarPowerManagementService");
+ return;
+ }
+ powerService.unregisterListener(mCarPowerStateListener);
+ powerService.removePowerPolicyListener(mCarDisplayPowerPolicyListener);
}
private void subscribeUserStateChange() {
@@ -434,12 +578,12 @@
String userStateDesc;
switch (event.getEventType()) {
case USER_LIFECYCLE_EVENT_TYPE_STARTING:
- mWatchdogProcessHandler.updateUserState(userId, /*isStopped=*/false);
+ mWatchdogProcessHandler.updateUserState(userId, /*isStopped=*/ false);
userState = UserState.USER_STATE_STARTED;
userStateDesc = "STARTING";
break;
case USER_LIFECYCLE_EVENT_TYPE_STOPPED:
- mWatchdogProcessHandler.updateUserState(userId, /*isStopped=*/true);
+ mWatchdogProcessHandler.updateUserState(userId, /*isStopped=*/ true);
userState = UserState.USER_STATE_STOPPED;
userStateDesc = "STOPPING";
break;
@@ -463,10 +607,36 @@
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_GARAGE_MODE_ON);
filter.addAction(ACTION_GARAGE_MODE_OFF);
+ filter.addAction(Intent.ACTION_USER_REMOVED);
mContext.registerReceiverForAllUsers(mBroadcastReceiver, filter, null, null);
}
+ private static @PowerCycle int carPowerStateToPowerCycle(int powerState) {
+ switch (powerState) {
+ // SHUTDOWN_PREPARE covers suspend and shutdown.
+ case CarPowerStateListener.SHUTDOWN_PREPARE:
+ return PowerCycle.POWER_CYCLE_SHUTDOWN_PREPARE;
+ case CarPowerStateListener.SHUTDOWN_ENTER:
+ case CarPowerStateListener.SUSPEND_ENTER:
+ return PowerCycle.POWER_CYCLE_SHUTDOWN_ENTER;
+ // ON covers resume.
+ case CarPowerStateListener.ON:
+ return PowerCycle.POWER_CYCLE_RESUME;
+ }
+ return PowerCycle.NUM_POWER_CYLES;
+ }
+
+ private static String toGarageModeString(@GarageMode int garageMode) {
+ switch (garageMode) {
+ case GarageMode.GARAGE_MODE_OFF:
+ return "GARAGE_MODE_OFF";
+ case GarageMode.GARAGE_MODE_ON:
+ return "GARAGE_MODE_ON";
+ }
+ return "INVALID";
+ }
+
private static final class ICarWatchdogServiceForSystemImpl
extends ICarWatchdogServiceForSystem.Stub {
private final WeakReference<CarWatchdogService> mService;
@@ -532,5 +702,15 @@
}
service.mWatchdogPerfHandler.resetResourceOveruseStats(new ArraySet<>(packageNames));
}
+
+ @Override
+ public List<UserPackageIoUsageStats> getTodayIoUsageStats() {
+ CarWatchdogService service = mService.get();
+ if (service == null) {
+ Slogf.w(TAG, "CarWatchdogService is not available");
+ return null;
+ }
+ return service.mWatchdogPerfHandler.getTodayIoUsageStats();
+ }
}
}
diff --git a/service/src/com/android/car/watchdog/OveruseConfigurationCache.java b/service/src/com/android/car/watchdog/OveruseConfigurationCache.java
new file mode 100644
index 0000000..1402ef6
--- /dev/null
+++ b/service/src/com/android/car/watchdog/OveruseConfigurationCache.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2021 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.watchdog;
+
+import static com.android.car.watchdog.WatchdogPerfHandler.INTERNAL_APPLICATION_CATEGORY_TYPE_MAPS;
+import static com.android.car.watchdog.WatchdogPerfHandler.INTERNAL_APPLICATION_CATEGORY_TYPE_MEDIA;
+
+import android.automotive.watchdog.PerStateBytes;
+import android.automotive.watchdog.internal.ApplicationCategoryType;
+import android.automotive.watchdog.internal.ComponentType;
+import android.automotive.watchdog.internal.IoOveruseConfiguration;
+import android.automotive.watchdog.internal.PackageMetadata;
+import android.automotive.watchdog.internal.PerStateIoOveruseThreshold;
+import android.automotive.watchdog.internal.ResourceOveruseConfiguration;
+import android.automotive.watchdog.internal.ResourceSpecificConfiguration;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.function.BiFunction;
+
+/**
+ * Cache to store overuse configurations in memory.
+ *
+ * <p>It assumes that the error checking and loading/merging initial configs are done prior to
+ * setting the cache.
+ */
+public final class OveruseConfigurationCache {
+ static final PerStateBytes DEFAULT_THRESHOLD =
+ constructPerStateBytes(Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE);
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final ArraySet<String> mSafeToKillSystemPackages = new ArraySet<>();
+ @GuardedBy("mLock")
+ private final ArraySet<String> mSafeToKillVendorPackages = new ArraySet<>();
+ @GuardedBy("mLock")
+ private final List<String> mVendorPackagePrefixes = new ArrayList<>();
+ @GuardedBy("mLock")
+ private final SparseArray<ArraySet<String>> mPackagesByAppCategoryType = new SparseArray<>();
+ @GuardedBy("mLock")
+ private final SparseArray<PerStateBytes> mGenericIoThresholdsByComponent = new SparseArray<>();
+ @GuardedBy("mLock")
+ private final ArrayMap<String, PerStateBytes> mIoThresholdsBySystemPackages = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private final ArrayMap<String, PerStateBytes> mIoThresholdsByVendorPackages = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private final SparseArray<PerStateBytes> mIoThresholdsByAppCategoryType = new SparseArray<>();
+
+ /** Dumps the contents of the cache. */
+ public void dump(IndentingPrintWriter writer) {
+ writer.println("*" + getClass().getSimpleName() + "*");
+ writer.increaseIndent();
+ synchronized (mLock) {
+ writer.println("mSafeToKillSystemPackages: " + mSafeToKillSystemPackages);
+ writer.println("mSafeToKillVendorPackages: " + mSafeToKillVendorPackages);
+ writer.println("mVendorPackagePrefixes: " + mVendorPackagePrefixes);
+ writer.println("mPackagesByAppCategoryType: ");
+ writer.increaseIndent();
+ for (int i = 0; i < mPackagesByAppCategoryType.size(); ++i) {
+ writer.print("App category: "
+ + toApplicationCategoryTypeString(mPackagesByAppCategoryType.keyAt(i)));
+ writer.println(", Packages: " + mPackagesByAppCategoryType.valueAt(i));
+ }
+ writer.decreaseIndent();
+ writer.println("mGenericIoThresholdsByComponent: ");
+ writer.increaseIndent();
+ for (int i = 0; i < mGenericIoThresholdsByComponent.size(); ++i) {
+ writer.print("Component type: "
+ + toComponentTypeString(mGenericIoThresholdsByComponent.keyAt(i)));
+ writer.print(", Threshold: ");
+ dumpPerStateBytes(mGenericIoThresholdsByComponent.valueAt(i), writer);
+ }
+ writer.decreaseIndent();
+ writer.println("mIoThresholdsBySystemPackages: ");
+ writer.increaseIndent();
+ for (int i = 0; i < mIoThresholdsBySystemPackages.size(); ++i) {
+ writer.print("Package name: " + mIoThresholdsBySystemPackages.keyAt(i));
+ writer.print(", Threshold: ");
+ dumpPerStateBytes(mIoThresholdsBySystemPackages.valueAt(i), writer);
+ }
+ writer.decreaseIndent();
+ writer.println("mIoThresholdsByVendorPackages: ");
+ writer.increaseIndent();
+ for (int i = 0; i < mIoThresholdsByVendorPackages.size(); ++i) {
+ writer.print("Package name: " + mIoThresholdsByVendorPackages.keyAt(i));
+ writer.print(", Threshold: ");
+ dumpPerStateBytes(mIoThresholdsByVendorPackages.valueAt(i), writer);
+ }
+ writer.decreaseIndent();
+ writer.println("mIoThresholdsByAppCategoryType: ");
+ writer.increaseIndent();
+ for (int i = 0; i < mIoThresholdsByAppCategoryType.size(); ++i) {
+ writer.print("App category: "
+ + toApplicationCategoryTypeString(mIoThresholdsByAppCategoryType.keyAt(i)));
+ writer.print(", Threshold: ");
+ dumpPerStateBytes(mIoThresholdsByAppCategoryType.valueAt(i), writer);
+ }
+ writer.decreaseIndent();
+ }
+ writer.decreaseIndent();
+ }
+
+ /** Overwrites the configurations in the cache. */
+ public void set(List<ResourceOveruseConfiguration> configs) {
+ synchronized (mLock) {
+ clearLocked();
+ for (int i = 0; i < configs.size(); i++) {
+ ResourceOveruseConfiguration config = configs.get(i);
+ switch (config.componentType) {
+ case ComponentType.SYSTEM:
+ mSafeToKillSystemPackages.addAll(config.safeToKillPackages);
+ break;
+ case ComponentType.VENDOR:
+ mSafeToKillVendorPackages.addAll(config.safeToKillPackages);
+ mVendorPackagePrefixes.addAll(config.vendorPackagePrefixes);
+ for (int j = 0; j < config.packageMetadata.size(); ++j) {
+ PackageMetadata meta = config.packageMetadata.get(j);
+ ArraySet<String> packages =
+ mPackagesByAppCategoryType.get(meta.appCategoryType);
+ if (packages == null) {
+ packages = new ArraySet<>();
+ }
+ packages.add(meta.packageName);
+ mPackagesByAppCategoryType.append(meta.appCategoryType, packages);
+ }
+ break;
+ default:
+ // All third-party apps are killable.
+ break;
+ }
+ for (int j = 0; j < config.resourceSpecificConfigurations.size(); ++j) {
+ if (config.resourceSpecificConfigurations.get(j).getTag()
+ == ResourceSpecificConfiguration.ioOveruseConfiguration) {
+ setIoThresholdsLocked(config.componentType,
+ config.resourceSpecificConfigurations.get(j)
+ .getIoOveruseConfiguration());
+ }
+ }
+ }
+ }
+ }
+
+ /** Returns the threshold for the given package and component type. */
+ public PerStateBytes fetchThreshold(String genericPackageName,
+ @ComponentType int componentType) {
+ synchronized (mLock) {
+ PerStateBytes threshold = null;
+ switch (componentType) {
+ case ComponentType.SYSTEM:
+ threshold = mIoThresholdsBySystemPackages.get(genericPackageName);
+ if (threshold != null) {
+ return copyPerStateBytes(threshold);
+ }
+ break;
+ case ComponentType.VENDOR:
+ threshold = mIoThresholdsByVendorPackages.get(genericPackageName);
+ if (threshold != null) {
+ return copyPerStateBytes(threshold);
+ }
+ break;
+ }
+ threshold = fetchAppCategorySpecificThresholdLocked(genericPackageName);
+ if (threshold != null) {
+ return copyPerStateBytes(threshold);
+ }
+ threshold = mGenericIoThresholdsByComponent.get(componentType);
+ return threshold != null ? copyPerStateBytes(threshold)
+ : copyPerStateBytes(DEFAULT_THRESHOLD);
+ }
+ }
+
+ /** Returns whether or not the given package is safe-to-kill on resource overuse. */
+ public boolean isSafeToKill(String genericPackageName, @ComponentType int componentType,
+ List<String> sharedPackages) {
+ synchronized (mLock) {
+ BiFunction<List<String>, Set<String>, Boolean> isSafeToKillAnyPackage =
+ (packages, safeToKillPackages) -> {
+ if (packages == null) {
+ return false;
+ }
+ for (int i = 0; i < packages.size(); i++) {
+ if (safeToKillPackages.contains(packages.get(i))) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ switch (componentType) {
+ case ComponentType.SYSTEM:
+ if (mSafeToKillSystemPackages.contains(genericPackageName)) {
+ return true;
+ }
+ return isSafeToKillAnyPackage.apply(sharedPackages, mSafeToKillSystemPackages);
+ case ComponentType.VENDOR:
+ if (mSafeToKillVendorPackages.contains(genericPackageName)) {
+ return true;
+ }
+ /*
+ * Packages under the vendor shared UID may contain system packages because when
+ * CarWatchdogService derives the shared component type it attributes system
+ * packages as vendor packages when there is at least one vendor package.
+ */
+ return isSafeToKillAnyPackage.apply(sharedPackages, mSafeToKillSystemPackages)
+ || isSafeToKillAnyPackage.apply(sharedPackages,
+ mSafeToKillVendorPackages);
+ default:
+ // Third-party apps are always killable
+ return true;
+ }
+ }
+ }
+
+ /** Returns the list of vendor package prefixes. */
+ public List<String> getVendorPackagePrefixes() {
+ synchronized (mLock) {
+ return new ArrayList<>(mVendorPackagePrefixes);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void clearLocked() {
+ mSafeToKillSystemPackages.clear();
+ mSafeToKillVendorPackages.clear();
+ mVendorPackagePrefixes.clear();
+ mPackagesByAppCategoryType.clear();
+ mGenericIoThresholdsByComponent.clear();
+ mIoThresholdsBySystemPackages.clear();
+ mIoThresholdsByVendorPackages.clear();
+ mIoThresholdsByAppCategoryType.clear();
+ }
+
+ @GuardedBy("mLock")
+ private void setIoThresholdsLocked(int componentType, IoOveruseConfiguration ioConfig) {
+ mGenericIoThresholdsByComponent.append(componentType,
+ ioConfig.componentLevelThresholds.perStateWriteBytes);
+ switch (componentType) {
+ case ComponentType.SYSTEM:
+ populateThresholdsByPackagesLocked(
+ ioConfig.packageSpecificThresholds, mIoThresholdsBySystemPackages);
+ break;
+ case ComponentType.VENDOR:
+ populateThresholdsByPackagesLocked(
+ ioConfig.packageSpecificThresholds, mIoThresholdsByVendorPackages);
+ setIoThresholdsByAppCategoryTypeLocked(ioConfig.categorySpecificThresholds);
+ break;
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void setIoThresholdsByAppCategoryTypeLocked(
+ List<PerStateIoOveruseThreshold> thresholds) {
+ for (int i = 0; i < thresholds.size(); ++i) {
+ PerStateIoOveruseThreshold threshold = thresholds.get(i);
+ switch(threshold.name) {
+ case INTERNAL_APPLICATION_CATEGORY_TYPE_MAPS:
+ mIoThresholdsByAppCategoryType.append(
+ ApplicationCategoryType.MAPS, threshold.perStateWriteBytes);
+ break;
+ case INTERNAL_APPLICATION_CATEGORY_TYPE_MEDIA:
+ mIoThresholdsByAppCategoryType.append(ApplicationCategoryType.MEDIA,
+ threshold.perStateWriteBytes);
+ break;
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void populateThresholdsByPackagesLocked(List<PerStateIoOveruseThreshold> thresholds,
+ ArrayMap<String, PerStateBytes> thresholdsByPackages) {
+ for (int i = 0; i < thresholds.size(); ++i) {
+ thresholdsByPackages.put(
+ thresholds.get(i).name, thresholds.get(i).perStateWriteBytes);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private PerStateBytes fetchAppCategorySpecificThresholdLocked(String genericPackageName) {
+ for (int i = 0; i < mPackagesByAppCategoryType.size(); ++i) {
+ if (mPackagesByAppCategoryType.valueAt(i).contains(genericPackageName)) {
+ return mIoThresholdsByAppCategoryType.get(mPackagesByAppCategoryType.keyAt(i));
+ }
+ }
+ return null;
+ }
+
+ private static String toApplicationCategoryTypeString(@ApplicationCategoryType int type) {
+ switch (type) {
+ case ApplicationCategoryType.MAPS:
+ return "ApplicationCategoryType.MAPS";
+ case ApplicationCategoryType.MEDIA:
+ return "ApplicationCategoryType.MEDIA";
+ case ApplicationCategoryType.OTHERS:
+ return "ApplicationCategoryType.OTHERS";
+ default:
+ return "Invalid ApplicationCategoryType";
+ }
+ }
+
+ private static String toComponentTypeString(@ComponentType int type) {
+ switch (type) {
+ case ComponentType.SYSTEM:
+ return "ComponentType.SYSTEM";
+ case ComponentType.VENDOR:
+ return "ComponentType.VENDOR";
+ case ComponentType.THIRD_PARTY:
+ return "ComponentType.THIRD_PARTY";
+ default:
+ return "ComponentType.UNKNOWN";
+ }
+ }
+
+ private static void dumpPerStateBytes(PerStateBytes perStateBytes,
+ IndentingPrintWriter writer) {
+ if (perStateBytes == null) {
+ writer.println("{NULL}");
+ return;
+ }
+ writer.println("{Foreground bytes: " + perStateBytes.foregroundBytes
+ + ", Background bytes: " + perStateBytes.backgroundBytes + ", Garage mode bytes: "
+ + perStateBytes.garageModeBytes + '}');
+ }
+
+ private static PerStateBytes constructPerStateBytes(long fgBytes, long bgBytes, long gmBytes) {
+ return new PerStateBytes() {{
+ foregroundBytes = fgBytes;
+ backgroundBytes = bgBytes;
+ garageModeBytes = gmBytes;
+ }};
+ }
+
+ private static PerStateBytes copyPerStateBytes(PerStateBytes perStateBytes) {
+ return new PerStateBytes() {{
+ foregroundBytes = perStateBytes.foregroundBytes;
+ backgroundBytes = perStateBytes.backgroundBytes;
+ garageModeBytes = perStateBytes.garageModeBytes;
+ }};
+ }
+}
diff --git a/service/src/com/android/car/watchdog/PackageInfoHandler.java b/service/src/com/android/car/watchdog/PackageInfoHandler.java
index f04f2b9..359b515 100644
--- a/service/src/com/android/car/watchdog/PackageInfoHandler.java
+++ b/service/src/com/android/car/watchdog/PackageInfoHandler.java
@@ -16,6 +16,7 @@
package com.android.car.watchdog;
+import android.annotation.Nullable;
import android.automotive.watchdog.internal.ApplicationCategoryType;
import android.automotive.watchdog.internal.ComponentType;
import android.automotive.watchdog.internal.PackageIdentifier;
@@ -25,8 +26,11 @@
import android.content.pm.PackageManager;
import android.os.Process;
import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.IntArray;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import com.android.car.CarLog;
import com.android.internal.annotations.GuardedBy;
@@ -38,13 +42,21 @@
/** Handles package info resolving */
public final class PackageInfoHandler {
+ public static final String SHARED_PACKAGE_PREFIX = "shared:";
+
private static final String TAG = CarLog.tagFor(PackageInfoHandler.class);
private final PackageManager mPackageManager;
private final Object mLock = new Object();
- /* Cache of uid to package name mapping. */
@GuardedBy("mLock")
- private final SparseArray<String> mPackageNamesByUid = new SparseArray<>();
+ private final SparseArray<String> mGenericPackageNameByUid = new SparseArray<>();
+ @GuardedBy("mLock")
+ private final SparseArray<List<String>> mPackagesBySharedUid = new SparseArray<>();
+ @GuardedBy("mLock")
+ private final ArrayMap<String, String> mGenericPackageNameByPackage = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private final SparseArray<ArraySet<String>> mGenericPackageNamesByComponentType =
+ new SparseArray<>();
@GuardedBy("mLock")
private List<String> mVendorPackagePrefixes = new ArrayList<>();
@@ -53,149 +65,252 @@
}
/**
- * Returns package names for the given UIDs.
+ * Returns the generic package names for the given UIDs.
*
- * Some UIDs may not have package names. This may occur when a UID is being removed and the
+ * <p>Some UIDs may not have names. This may occur when a UID is being removed and the
* internal data structures are not up-to-date. The caller should handle it.
*/
- public SparseArray<String> getPackageNamesForUids(int[] uids) {
+ public SparseArray<String> getNamesForUids(int[] uids) {
IntArray unmappedUids = new IntArray(uids.length);
- SparseArray<String> packageNamesByUid = new SparseArray<>();
+ SparseArray<String> genericPackageNameByUid = new SparseArray<>();
synchronized (mLock) {
for (int uid : uids) {
- String packageName = mPackageNamesByUid.get(uid, null);
- if (packageName != null) {
- packageNamesByUid.append(uid, packageName);
+ String genericPackageName = mGenericPackageNameByUid.get(uid, null);
+ if (genericPackageName != null) {
+ genericPackageNameByUid.append(uid, genericPackageName);
} else {
unmappedUids.add(uid);
}
}
}
if (unmappedUids.size() == 0) {
- return packageNamesByUid;
+ return genericPackageNameByUid;
}
- String[] packageNames = mPackageManager.getNamesForUids(unmappedUids.toArray());
+ String[] genericPackageNames = mPackageManager.getNamesForUids(unmappedUids.toArray());
synchronized (mLock) {
for (int i = 0; i < unmappedUids.size(); ++i) {
- if (packageNames[i] == null || packageNames[i].isEmpty()) {
+ if (genericPackageNames[i] == null || genericPackageNames[i].isEmpty()) {
continue;
}
- mPackageNamesByUid.append(unmappedUids.get(i), packageNames[i]);
- packageNamesByUid.append(unmappedUids.get(i), packageNames[i]);
+ int uid = unmappedUids.get(i);
+ String genericPackageName = genericPackageNames[i];
+ mGenericPackageNameByUid.append(uid, genericPackageName);
+ genericPackageNameByUid.append(uid, genericPackageName);
+ mGenericPackageNameByPackage.put(genericPackageName, genericPackageName);
+ if (!genericPackageName.startsWith(SHARED_PACKAGE_PREFIX)) {
+ continue;
+ }
+ populateSharedPackagesLocked(uid, genericPackageName);
}
}
- return packageNamesByUid;
+ return genericPackageNameByUid;
}
/**
- * Returns package infos for the given UIDs.
+ * Returns the generic package name for the user package.
*
- * Some UIDs may not have package infos. This may occur when a UID is being removed and the
+ * <p>Returns null when no generic package name is found.
+ */
+ @Nullable
+ public String getNameForUserPackage(String packageName, int userId) {
+ synchronized (mLock) {
+ String genericPackageName = mGenericPackageNameByPackage.get(packageName);
+ if (genericPackageName != null) {
+ return genericPackageName;
+ }
+ }
+ try {
+ return getNameForPackage(
+ mPackageManager.getPackageInfoAsUser(packageName, /* flags= */ 0, userId));
+ } catch (PackageManager.NameNotFoundException e) {
+ Slogf.e(TAG, "Package '%s' not found for user %d: %s", packageName, userId, e);
+ }
+ return null;
+ }
+
+ /** Returns the packages owned by the shared UID */
+ public List<String> getPackagesForUid(int uid, String genericPackageName) {
+ synchronized (mLock) {
+ /* When fetching the packages under a shared UID update the internal DS. This will help
+ * capture any recently installed packages.
+ */
+ populateSharedPackagesLocked(uid, genericPackageName);
+ return mPackagesBySharedUid.get(uid);
+ }
+ }
+
+ /** Returns the generic package name for the given package info. */
+ public String getNameForPackage(android.content.pm.PackageInfo packageInfo) {
+ synchronized (mLock) {
+ String genericPackageName = mGenericPackageNameByPackage.get(packageInfo.packageName);
+ if (genericPackageName != null) {
+ return genericPackageName;
+ }
+ if (packageInfo.sharedUserId != null) {
+ populateSharedPackagesLocked(packageInfo.applicationInfo.uid,
+ SHARED_PACKAGE_PREFIX + packageInfo.sharedUserId);
+ return SHARED_PACKAGE_PREFIX + packageInfo.sharedUserId;
+ }
+ mGenericPackageNameByPackage.put(packageInfo.packageName, packageInfo.packageName);
+ return packageInfo.packageName;
+ }
+ }
+
+ /**
+ * Returns the internal package infos for the given UIDs.
+ *
+ * <p>Some UIDs may not have package infos. This may occur when a UID is being removed and the
* internal data structures are not up-to-date. The caller should handle it.
*/
public List<PackageInfo> getPackageInfosForUids(int[] uids,
List<String> vendorPackagePrefixes) {
- synchronized (mLock) {
- /*
- * Vendor package prefixes don't change frequently because it changes only when the
- * vendor configuration is updated. Thus caching this locally during this call should
- * keep the cache up-to-date because the daemon issues this call frequently.
- */
- mVendorPackagePrefixes = vendorPackagePrefixes;
- }
- SparseArray<String> packageNamesByUid = getPackageNamesForUids(uids);
- ArrayList<PackageInfo> packageInfos = new ArrayList<>(packageNamesByUid.size());
- for (int i = 0; i < packageNamesByUid.size(); ++i) {
- packageInfos.add(getPackageInfo(packageNamesByUid.keyAt(i),
- packageNamesByUid.valueAt(i)));
+ setVendorPackagePrefixes(vendorPackagePrefixes);
+ SparseArray<String> genericPackageNameByUid = getNamesForUids(uids);
+ ArrayList<PackageInfo> packageInfos = new ArrayList<>(genericPackageNameByUid.size());
+ for (int i = 0; i < genericPackageNameByUid.size(); ++i) {
+ packageInfos.add(getPackageInfo(genericPackageNameByUid.keyAt(i),
+ genericPackageNameByUid.valueAt(i)));
}
return packageInfos;
}
- private PackageInfo getPackageInfo(int uid, String packageName) {
+ /** Returns component type for the given uid and package name. */
+ public @ComponentType int getComponentType(int uid, String genericPackageName) {
+ synchronized (mLock) {
+ for (int i = 0; i < mGenericPackageNamesByComponentType.size(); ++i) {
+ if (mGenericPackageNamesByComponentType.valueAt(i).contains(genericPackageName)) {
+ return mGenericPackageNamesByComponentType.keyAt(i);
+ }
+ }
+ }
+ int componentType = ComponentType.UNKNOWN;
+ if (genericPackageName.startsWith(SHARED_PACKAGE_PREFIX)) {
+ synchronized (mLock) {
+ if (!mPackagesBySharedUid.contains(uid)) {
+ populateSharedPackagesLocked(uid, genericPackageName);
+ }
+ List<String> packages = mPackagesBySharedUid.get(uid);
+ if (packages != null) {
+ componentType = getSharedComponentTypeInternal(
+ UserHandle.getUserHandleForUid(uid), packages, genericPackageName);
+ }
+ }
+ } else {
+ componentType = getUserPackageComponentType(
+ UserHandle.getUserHandleForUid(uid), genericPackageName);
+ }
+ if (componentType != ComponentType.UNKNOWN) {
+ cachePackageComponentType(genericPackageName, componentType);
+ }
+ return componentType;
+ }
+
+ @GuardedBy("mLock")
+ private void populateSharedPackagesLocked(int uid, String genericPackageName) {
+ String[] packages = mPackageManager.getPackagesForUid(uid);
+ for (String pkg : packages) {
+ mGenericPackageNameByPackage.put(pkg, genericPackageName);
+ }
+ mPackagesBySharedUid.put(uid, Arrays.asList(packages));
+ }
+
+ private PackageInfo getPackageInfo(int uid, String genericPackageName) {
PackageInfo packageInfo = new PackageInfo();
packageInfo.packageIdentifier = new PackageIdentifier();
packageInfo.packageIdentifier.uid = uid;
- packageInfo.packageIdentifier.name = packageName;
+ packageInfo.packageIdentifier.name = genericPackageName;
packageInfo.sharedUidPackages = new ArrayList<>();
- packageInfo.componentType = ComponentType.UNKNOWN;
+ packageInfo.componentType = getComponentType(uid, genericPackageName);
/* Application category type mapping is handled on the daemon side. */
packageInfo.appCategoryType = ApplicationCategoryType.OTHERS;
- int userId = UserHandle.getUserId(uid);
int appId = UserHandle.getAppId(uid);
packageInfo.uidType = appId >= Process.FIRST_APPLICATION_UID ? UidType.APPLICATION :
UidType.NATIVE;
- if (packageName.startsWith("shared:")) {
- String[] sharedUidPackages = mPackageManager.getPackagesForUid(uid);
- if (sharedUidPackages == null) {
- return packageInfo;
- }
- boolean seenVendor = false;
- boolean seenSystem = false;
- boolean seenThirdParty = false;
- /*
- * A shared UID has multiple packages associated with it and these packages may be
- * mapped to different component types. Thus map the shared UID to the most restrictive
- * component type.
- */
- for (int i = 0; i < sharedUidPackages.length; ++i) {
- int componentType = getPackageComponentType(userId, sharedUidPackages[i]);
- switch(componentType) {
- case ComponentType.VENDOR:
- seenVendor = true;
- break;
- case ComponentType.SYSTEM:
- seenSystem = true;
- break;
- case ComponentType.THIRD_PARTY:
- seenThirdParty = true;
- break;
- default:
- Slogf.w(TAG, "Unknown component type %d for package '%s'", componentType,
- sharedUidPackages[i]);
+ if (genericPackageName.startsWith(SHARED_PACKAGE_PREFIX)) {
+ synchronized (mLock) {
+ List<String> packages = mPackagesBySharedUid.get(uid);
+ if (packages != null) {
+ packageInfo.sharedUidPackages = new ArrayList<>(packages);
}
}
- packageInfo.sharedUidPackages = Arrays.asList(sharedUidPackages);
- if (seenVendor) {
- packageInfo.componentType = ComponentType.VENDOR;
- } else if (seenSystem) {
- packageInfo.componentType = ComponentType.SYSTEM;
- } else if (seenThirdParty) {
- packageInfo.componentType = ComponentType.THIRD_PARTY;
- }
- } else {
- packageInfo.componentType = getPackageComponentType(
- userId, packageName);
}
return packageInfo;
}
- private int getPackageComponentType(int userId, String packageName) {
- try {
- ApplicationInfo info = mPackageManager.getApplicationInfoAsUser(packageName,
- /* flags= */ 0, userId);
- return getComponentType(packageName, info);
- } catch (PackageManager.NameNotFoundException e) {
- Slogf.e(TAG, "Package '%s' not found for user %d: %s", packageName, userId, e);
+ /**
+ * Returns the most restrictive component type shared by the given application infos.
+ *
+ * A shared UID has multiple packages associated with it and these packages may be
+ * mapped to different component types. Thus map the shared UID to the most restrictive
+ * component type.
+ */
+ public @ComponentType int getSharedComponentType(
+ List<ApplicationInfo> applicationInfos, String genericPackageName) {
+ SparseBooleanArray seenComponents = new SparseBooleanArray();
+ for (int i = 0; i < applicationInfos.size(); ++i) {
+ int type = getComponentType(applicationInfos.get(i));
+ seenComponents.put(type, true);
+ }
+ if (seenComponents.get(ComponentType.VENDOR)) {
+ return ComponentType.VENDOR;
+ } else if (seenComponents.get(ComponentType.SYSTEM)) {
+ synchronized (mLock) {
+ for (int i = 0; i < mVendorPackagePrefixes.size(); ++i) {
+ if (genericPackageName.startsWith(mVendorPackagePrefixes.get(i))) {
+ return ComponentType.VENDOR;
+ }
+ }
+ }
+ return ComponentType.SYSTEM;
+ } else if (seenComponents.get(ComponentType.THIRD_PARTY)) {
+ return ComponentType.THIRD_PARTY;
}
return ComponentType.UNKNOWN;
}
- /** Returns the component type for the given package and its application info. */
- public int getComponentType(String packageName, ApplicationInfo info) {
- if ((info.privateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0
- || (info.privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0
- || (info.privateFlags & ApplicationInfo.PRIVATE_FLAG_ODM) != 0) {
+ private @ComponentType int getUserPackageComponentType(
+ UserHandle userHandle, String packageName) {
+ try {
+ ApplicationInfo info = mPackageManager.getApplicationInfoAsUser(
+ packageName, /* flags= */ 0, userHandle);
+ return getComponentType(info);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slogf.e(TAG, e, "Package '%s' not found for user %d", packageName,
+ userHandle.getIdentifier());
+ }
+ return ComponentType.UNKNOWN;
+ }
+
+ private @ComponentType int getSharedComponentTypeInternal(
+ UserHandle userHandle, List<String> packages, String genericPackageName) {
+ List<ApplicationInfo> applicationInfos = new ArrayList<>();
+ for (int i = 0; i < packages.size(); ++i) {
+ try {
+ applicationInfos.add(mPackageManager.getApplicationInfoAsUser(
+ packages.get(i), /* flags= */ 0, userHandle));
+ } catch (PackageManager.NameNotFoundException e) {
+ Slogf.w(TAG, "Package '%s' not found for user %d", packages.get(i),
+ userHandle.getIdentifier());
+ }
+ }
+ return getSharedComponentType(applicationInfos, genericPackageName);
+ }
+
+ /** Returns the component type for the given application info. */
+ public int getComponentType(ApplicationInfo applicationInfo) {
+ if ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0
+ || (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0
+ || (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_ODM) != 0) {
return ComponentType.VENDOR;
}
- if ((info.flags & ApplicationInfo.FLAG_SYSTEM) != 0
- || (info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
- || (info.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRODUCT) != 0
- || (info.privateFlags & ApplicationInfo.PRIVATE_FLAG_SYSTEM_EXT) != 0) {
+ if ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0
+ || (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
+ || (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRODUCT) != 0
+ || (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_SYSTEM_EXT) != 0) {
synchronized (mLock) {
- for (String prefix : mVendorPackagePrefixes) {
- if (packageName.startsWith(prefix)) {
+ for (int i = 0; i < mVendorPackagePrefixes.size(); ++i) {
+ if (applicationInfo.packageName.startsWith(mVendorPackagePrefixes.get(i))) {
return ComponentType.VENDOR;
}
}
@@ -204,4 +319,29 @@
}
return ComponentType.THIRD_PARTY;
}
+
+ void setVendorPackagePrefixes(List<String> vendorPackagePrefixes) {
+ synchronized (mLock) {
+ if (mVendorPackagePrefixes.equals(vendorPackagePrefixes)) {
+ return;
+ }
+ mVendorPackagePrefixes = vendorPackagePrefixes;
+ // When the vendor package prefixes change due to config update, the component types
+ // for these packages also change. Ergo, clear the component type cache, so the
+ // component types can be inferred again.
+ mGenericPackageNamesByComponentType.clear();
+ }
+ }
+
+ private void cachePackageComponentType(String genericPackageName,
+ @ComponentType int componentType) {
+ synchronized (mLock) {
+ ArraySet<String> packages = mGenericPackageNamesByComponentType.get(componentType);
+ if (packages == null) {
+ packages = new ArraySet<>();
+ }
+ packages.add(genericPackageName);
+ mGenericPackageNamesByComponentType.append(componentType, packages);
+ }
+ }
}
diff --git a/service/src/com/android/car/watchdog/TimeSourceInterface.java b/service/src/com/android/car/watchdog/TimeSourceInterface.java
new file mode 100644
index 0000000..1bd6508
--- /dev/null
+++ b/service/src/com/android/car/watchdog/TimeSourceInterface.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2021 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.watchdog;
+
+import java.time.Instant;
+
+/**
+ * Provider for the current value of "now" for users of {@code java.time}.
+ */
+public interface TimeSourceInterface {
+ /** Returns the current instant from the time source implementation. */
+ Instant now();
+}
diff --git a/service/src/com/android/car/watchdog/WatchdogPerfHandler.java b/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
index 8d15e0f..50a4d24 100644
--- a/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
+++ b/service/src/com/android/car/watchdog/WatchdogPerfHandler.java
@@ -16,34 +16,54 @@
package com.android.car.watchdog;
-import static android.automotive.watchdog.internal.ResourceOveruseActionType.KILLED;
import static android.automotive.watchdog.internal.ResourceOveruseActionType.KILLED_RECURRING_OVERUSE;
import static android.automotive.watchdog.internal.ResourceOveruseActionType.NOT_KILLED;
-import static android.automotive.watchdog.internal.ResourceOveruseActionType.NOT_KILLED_USER_OPTED;
import static android.car.watchdog.CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO;
+import static android.car.watchdog.CarWatchdogManager.STATS_PERIOD_CURRENT_DAY;
+import static android.car.watchdog.CarWatchdogManager.STATS_PERIOD_PAST_15_DAYS;
+import static android.car.watchdog.CarWatchdogManager.STATS_PERIOD_PAST_30_DAYS;
+import static android.car.watchdog.CarWatchdogManager.STATS_PERIOD_PAST_3_DAYS;
+import static android.car.watchdog.CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS;
import static android.car.watchdog.PackageKillableState.KILLABLE_STATE_NEVER;
import static android.car.watchdog.PackageKillableState.KILLABLE_STATE_NO;
import static android.car.watchdog.PackageKillableState.KILLABLE_STATE_YES;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_IO_OVERUSE_STATS_REPORTED;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__KILL_REASON__KILLED_ON_IO_OVERUSE;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__GARAGE_MODE;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_INTERACTION_MODE;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__UID_STATE__UNKNOWN_UID_STATE;
import static com.android.car.watchdog.CarWatchdogService.DEBUG;
+import static com.android.car.watchdog.CarWatchdogService.SYSTEM_INSTANCE;
import static com.android.car.watchdog.CarWatchdogService.TAG;
-import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+import static com.android.car.watchdog.PackageInfoHandler.SHARED_PACKAGE_PREFIX;
+import static com.android.car.watchdog.WatchdogStorage.STATS_TEMPORAL_UNIT;
+import static com.android.car.watchdog.WatchdogStorage.ZONE_OFFSET;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.ActivityThread;
import android.automotive.watchdog.ResourceType;
import android.automotive.watchdog.internal.ApplicationCategoryType;
import android.automotive.watchdog.internal.ComponentType;
+import android.automotive.watchdog.internal.GarageMode;
+import android.automotive.watchdog.internal.IoUsageStats;
import android.automotive.watchdog.internal.PackageIdentifier;
import android.automotive.watchdog.internal.PackageIoOveruseStats;
import android.automotive.watchdog.internal.PackageMetadata;
import android.automotive.watchdog.internal.PackageResourceOveruseAction;
import android.automotive.watchdog.internal.PerStateIoOveruseThreshold;
import android.automotive.watchdog.internal.ResourceSpecificConfiguration;
+import android.automotive.watchdog.internal.UserPackageIoUsageStats;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.ICarUxRestrictionsChangeListener;
import android.car.watchdog.CarWatchdogManager;
import android.car.watchdog.IResourceOveruseListener;
import android.car.watchdog.IoOveruseAlertThreshold;
@@ -56,10 +76,10 @@
import android.car.watchdog.ResourceOveruseStats;
import android.car.watchdoglib.CarWatchdogDaemonHelper;
import android.content.Context;
+import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
@@ -73,15 +93,23 @@
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.SparseArray;
+import android.view.Display;
+import com.android.car.CarLocalServices;
+import com.android.car.CarServiceUtils;
+import com.android.car.CarStatsLog;
+import com.android.car.CarUxRestrictionsManagerService;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.server.utils.Slogf;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -97,18 +125,46 @@
public static final String INTERNAL_APPLICATION_CATEGORY_TYPE_MEDIA = "MEDIA";
public static final String INTERNAL_APPLICATION_CATEGORY_TYPE_UNKNOWN = "UNKNOWN";
+ private static final long OVERUSE_HANDLING_DELAY_MILLS = 10_000;
private static final long MAX_WAIT_TIME_MILLS = 3_000;
+ // TODO(b/195425666): Define this constant as a resource overlay config with two values:
+ // 1. Recurring overuse period - Period to calculate the recurring overuse.
+ // 2. Recurring overuse threshold - Total overuses for recurring behavior.
+ private static final int RECURRING_OVERUSE_THRESHOLD = 2;
+
+ /**
+ * Don't distract the user by sending user notifications/dialogs, killing foreground
+ * applications, repeatedly killing persistent background services, or disabling any
+ * application.
+ */
+ private static final int UX_STATE_NO_DISTRACTION = 1;
+ /** The user can safely receive user notifications or dialogs. */
+ private static final int UX_STATE_USER_NOTIFICATION = 2;
+ /**
+ * Any application or service can be safely killed/disabled. User notifications can be sent
+ * only to the notification center.
+ */
+ private static final int UX_STATE_NO_INTERACTION = 3;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"UX_STATE_"}, value = {
+ UX_STATE_NO_DISTRACTION,
+ UX_STATE_USER_NOTIFICATION,
+ UX_STATE_NO_INTERACTION
+ })
+ private @interface UxStateType{}
+
private final Context mContext;
private final CarWatchdogDaemonHelper mCarWatchdogDaemonHelper;
private final PackageInfoHandler mPackageInfoHandler;
private final Handler mMainHandler;
+ private final Handler mServiceHandler;
+ private final WatchdogStorage mWatchdogStorage;
+ private final OveruseConfigurationCache mOveruseConfigurationCache;
private final Object mLock = new Object();
- /*
- * Cache of added resource overuse listeners by uid.
- */
@GuardedBy("mLock")
- private final Map<String, PackageResourceUsage> mUsageByUserPackage = new ArrayMap<>();
+ private final ArrayMap<String, PackageResourceUsage> mUsageByUserPackage = new ArrayMap<>();
@GuardedBy("mLock")
private final List<PackageResourceOveruseAction> mOveruseActionsByUserPackage =
new ArrayList<>();
@@ -118,54 +174,90 @@
@GuardedBy("mLock")
private final SparseArray<ArrayList<ResourceOveruseListenerInfo>>
mOveruseSystemListenerInfosByUid = new SparseArray<>();
- /* Set of safe-to-kill system and vendor packages. */
+ /** Default killable state for packages. Updated only for {@link UserHandle.ALL} user handle. */
@GuardedBy("mLock")
- public final Set<String> mSafeToKillPackages = new ArraySet<>();
- /* Default killable state for packages when not updated by the user. */
+ private final ArraySet<String> mDefaultNotKillableGenericPackages = new ArraySet<>();
+ /** Keys in {@link mUsageByUserPackage} for user notification on resource overuse. */
@GuardedBy("mLock")
- public final Set<String> mDefaultNotKillablePackages = new ArraySet<>();
+ private final ArraySet<String> mUserNotifiablePackages = new ArraySet<>();
+ /**
+ * Keys in {@link mUsageByUserPackage} that should be killed/disabled due to resource overuse.
+ */
@GuardedBy("mLock")
- private ZonedDateTime mLastStatsReportUTC;
+ private final ArraySet<String> mActionableUserPackages = new ArraySet<>();
+ @GuardedBy("mLock")
+ private ZonedDateTime mLatestStatsReportDate;
@GuardedBy("mLock")
private List<android.automotive.watchdog.internal.ResourceOveruseConfiguration>
mPendingSetResourceOveruseConfigurationsRequest = null;
@GuardedBy("mLock")
- boolean mIsConnectedToDaemon;
+ private boolean mIsConnectedToDaemon;
+ @GuardedBy("mLock")
+ private boolean mIsWrittenToDatabase;
+ @GuardedBy("mLock")
+ private @UxStateType int mCurrentUxState;
+ @GuardedBy("mLock")
+ private CarUxRestrictions mCurrentUxRestrictions;
+ @GuardedBy("mLock")
+ private @GarageMode int mCurrentGarageMode;
+ @GuardedBy("mLock")
+ private TimeSourceInterface mTimeSource;
+ @GuardedBy("mLock")
+ private long mOveruseHandlingDelayMills;
+ @GuardedBy("mLock")
+ private long mRecurringOveruseThreshold;
+
+ private final ICarUxRestrictionsChangeListener mCarUxRestrictionsChangeListener =
+ new ICarUxRestrictionsChangeListener.Stub() {
+ @Override
+ public void onUxRestrictionsChanged(CarUxRestrictions restrictions) {
+ synchronized (mLock) {
+ mCurrentUxRestrictions = new CarUxRestrictions(restrictions);
+ applyCurrentUxRestrictionsLocked();
+ }
+ }
+ };
public WatchdogPerfHandler(Context context, CarWatchdogDaemonHelper daemonHelper,
- PackageInfoHandler packageInfoHandler) {
+ PackageInfoHandler packageInfoHandler, WatchdogStorage watchdogStorage) {
mContext = context;
mCarWatchdogDaemonHelper = daemonHelper;
mPackageInfoHandler = packageInfoHandler;
mMainHandler = new Handler(Looper.getMainLooper());
- mLastStatsReportUTC = ZonedDateTime.now(ZoneOffset.UTC);
+ mServiceHandler = new Handler(CarServiceUtils.getHandlerThread(
+ CarWatchdogService.class.getSimpleName()).getLooper());
+ mWatchdogStorage = watchdogStorage;
+ mOveruseConfigurationCache = new OveruseConfigurationCache();
+ mTimeSource = SYSTEM_INSTANCE;
+ mOveruseHandlingDelayMills = OVERUSE_HANDLING_DELAY_MILLS;
+ mCurrentUxState = UX_STATE_NO_DISTRACTION;
+ mCurrentGarageMode = GarageMode.GARAGE_MODE_OFF;
+ mRecurringOveruseThreshold = RECURRING_OVERUSE_THRESHOLD;
}
/** Initializes the handler. */
public void init() {
- /*
- * TODO(b/183947162): Opt-in to receive package change broadcast and handle package enabled
- * state changes.
- *
- * TODO(b/185287136): Persist in-memory data:
- * 1. Read the current day's I/O overuse stats from database and push them
- * to the daemon.
- * 2. Fetch the safe-to-kill from daemon on initialization and update mSafeToKillPackages.
- */
- synchronized (mLock) {
- checkAndHandleDateChangeLocked();
- }
+ /* First database read is expensive, so post it on a separate handler thread. */
+ mServiceHandler.post(() -> {
+ readFromDatabase();
+ synchronized (mLock) {
+ checkAndHandleDateChangeLocked();
+ mIsWrittenToDatabase = false;
+ }});
+
+ CarLocalServices.getService(CarUxRestrictionsManagerService.class)
+ .registerUxRestrictionsChangeListener(
+ mCarUxRestrictionsChangeListener, Display.DEFAULT_DISPLAY);
+
if (DEBUG) {
Slogf.d(TAG, "WatchdogPerfHandler is initialized");
}
}
- /** Releases the handler */
+ /** Releases resources. */
public void release() {
- /* TODO(b/185287136): Write daily usage to SQLite DB storage. */
- if (DEBUG) {
- Slogf.d(TAG, "WatchdogPerfHandler is released");
- }
+ CarLocalServices.getService(CarUxRestrictionsManagerService.class)
+ .unregisterUxRestrictionsChangeListener(mCarUxRestrictionsChangeListener);
}
/** Dumps its state. */
@@ -173,25 +265,59 @@
/*
* TODO(b/183436216): Implement this method.
*/
+ mOveruseConfigurationCache.dump(writer);
}
/** Retries any pending requests on re-connecting to the daemon */
public void onDaemonConnectionChange(boolean isConnected) {
+ boolean hasPendingRequest;
synchronized (mLock) {
mIsConnectedToDaemon = isConnected;
+ hasPendingRequest = mPendingSetResourceOveruseConfigurationsRequest != null;
}
if (isConnected) {
- /*
- * Retry pending set resource overuse configuration request before processing any new
- * set/get requests. Thus notify the waiting requests only after the retry completes.
- */
- retryPendingSetResourceOveruseConfigurations();
+ if (hasPendingRequest) {
+ /*
+ * Retry pending set resource overuse configuration request before processing any
+ * new set/get requests. Thus notify the waiting requests only after the retry
+ * completes.
+ */
+ retryPendingSetResourceOveruseConfigurations();
+ } else {
+ /* Start fetch/sync configs only when there are no pending set requests because the
+ * above retry starts fetch/sync configs on success. If the retry fails, the daemon
+ * has crashed and shouldn't start fetchAndSyncResourceOveruseConfigurations.
+ */
+ mMainHandler.post(this::fetchAndSyncResourceOveruseConfigurations);
+ }
}
synchronized (mLock) {
mLock.notifyAll();
}
}
+ /** Updates the current UX state based on the display state. */
+ public void onDisplayStateChanged(boolean isEnabled) {
+ synchronized (mLock) {
+ if (isEnabled) {
+ mCurrentUxState = UX_STATE_NO_DISTRACTION;
+ applyCurrentUxRestrictionsLocked();
+ } else {
+ mCurrentUxState = UX_STATE_NO_INTERACTION;
+ performOveruseHandlingLocked();
+ }
+ }
+ }
+
+ /** Handles garage mode change. */
+ public void onGarageModeChange(@GarageMode int garageMode) {
+ synchronized (mLock) {
+ mCurrentGarageMode = garageMode;
+ mCurrentUxState = UX_STATE_NO_INTERACTION;
+ performOveruseHandlingLocked();
+ }
+ }
+
/** Returns resource overuse stats for the calling package. */
@NonNull
public ResourceOveruseStats getResourceOveruseStats(
@@ -207,20 +333,20 @@
int callingUid = Binder.getCallingUid();
int callingUserId = UserHandle.getUserId(callingUid);
UserHandle callingUserHandle = UserHandle.of(callingUserId);
- String callingPackageName =
- mPackageInfoHandler.getPackageNamesForUids(new int[]{callingUid})
+ String genericPackageName =
+ mPackageInfoHandler.getNamesForUids(new int[]{callingUid})
.get(callingUid, null);
- if (callingPackageName == null) {
+ if (genericPackageName == null) {
Slogf.w(TAG, "Failed to fetch package info for uid %d", callingUid);
return new ResourceOveruseStats.Builder("", callingUserHandle).build();
}
ResourceOveruseStats.Builder statsBuilder =
- new ResourceOveruseStats.Builder(callingPackageName, callingUserHandle);
- statsBuilder.setIoOveruseStats(getIoOveruseStats(callingUserId, callingPackageName,
- /* minimumBytesWritten= */ 0, maxStatsPeriod));
+ new ResourceOveruseStats.Builder(genericPackageName, callingUserHandle);
+ statsBuilder.setIoOveruseStats(
+ getIoOveruseStatsForPeriod(callingUserId, genericPackageName, maxStatsPeriod));
if (DEBUG) {
Slogf.d(TAG, "Returning all resource overuse stats for calling uid %d [user %d and "
- + "package '%s']", callingUid, callingUserId, callingPackageName);
+ + "package '%s']", callingUid, callingUserId, genericPackageName);
}
return statsBuilder.build();
}
@@ -240,14 +366,17 @@
"Must provide resource I/O overuse flag");
long minimumBytesWritten = getMinimumBytesWritten(minimumStatsFlag);
List<ResourceOveruseStats> allStats = new ArrayList<>();
- for (PackageResourceUsage usage : mUsageByUserPackage.values()) {
- ResourceOveruseStats.Builder statsBuilder = usage.getResourceOveruseStatsBuilder();
- IoOveruseStats ioOveruseStats = getIoOveruseStats(usage.userId, usage.packageName,
- minimumBytesWritten, maxStatsPeriod);
- if (ioOveruseStats == null) {
- continue;
+ synchronized (mLock) {
+ for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+ PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
+ ResourceOveruseStats.Builder statsBuilder = usage.getResourceOveruseStatsBuilder();
+ IoOveruseStats ioOveruseStats =
+ getIoOveruseStatsLocked(usage, minimumBytesWritten, maxStatsPeriod);
+ if (ioOveruseStats == null) {
+ continue;
+ }
+ allStats.add(statsBuilder.setIoOveruseStats(ioOveruseStats).build());
}
- allStats.add(statsBuilder.setIoOveruseStats(ioOveruseStats).build());
}
if (DEBUG) {
Slogf.d(TAG, "Returning all resource overuse stats");
@@ -263,7 +392,7 @@
@CarWatchdogManager.StatsPeriod int maxStatsPeriod) {
Objects.requireNonNull(packageName, "Package name must be non-null");
Objects.requireNonNull(userHandle, "User handle must be non-null");
- Preconditions.checkArgument((userHandle != UserHandle.ALL),
+ Preconditions.checkArgument(!userHandle.equals(UserHandle.ALL),
"Must provide the user handle for a specific user");
Preconditions.checkArgument((resourceOveruseFlag > 0),
"Must provide valid resource overuse flag");
@@ -272,13 +401,19 @@
// When more resource types are added, make this as optional.
Preconditions.checkArgument((resourceOveruseFlag & FLAG_RESOURCE_OVERUSE_IO) != 0,
"Must provide resource I/O overuse flag");
+ String genericPackageName =
+ mPackageInfoHandler.getNameForUserPackage(packageName, userHandle.getIdentifier());
+ if (genericPackageName == null) {
+ throw new IllegalArgumentException("Package '" + packageName + "' not found");
+ }
ResourceOveruseStats.Builder statsBuilder =
- new ResourceOveruseStats.Builder(packageName, userHandle);
- statsBuilder.setIoOveruseStats(getIoOveruseStats(userHandle.getIdentifier(), packageName,
- /* minimumBytesWritten= */ 0, maxStatsPeriod));
+ new ResourceOveruseStats.Builder(genericPackageName, userHandle);
+ statsBuilder.setIoOveruseStats(getIoOveruseStatsForPeriod(userHandle.getIdentifier(),
+ genericPackageName, maxStatsPeriod));
if (DEBUG) {
- Slogf.d(TAG, "Returning resource overuse stats for user %d, package '%s'",
- userHandle.getIdentifier(), packageName);
+ Slogf.d(TAG, "Returning resource overuse stats for user %d, package '%s', "
+ + "generic package '%s'", userHandle.getIdentifier(), packageName,
+ genericPackageName);
}
return statsBuilder.build();
}
@@ -330,31 +465,17 @@
boolean isKillable) {
Objects.requireNonNull(packageName, "Package name must be non-null");
Objects.requireNonNull(userHandle, "User handle must be non-null");
- if (userHandle == UserHandle.ALL) {
- synchronized (mLock) {
- for (PackageResourceUsage usage : mUsageByUserPackage.values()) {
- if (!usage.packageName.equals(packageName)) {
- continue;
- }
- if (!usage.setKillableState(isKillable)) {
- Slogf.e(TAG, "Cannot set killable state for package '%s'", packageName);
- throw new IllegalArgumentException(
- "Package killable state is not updatable");
- }
- }
- if (!isKillable) {
- mDefaultNotKillablePackages.add(packageName);
- } else {
- mDefaultNotKillablePackages.remove(packageName);
- }
- }
- if (DEBUG) {
- Slogf.d(TAG, "Successfully set killable package state for all users");
- }
+
+ if (userHandle.equals(UserHandle.ALL)) {
+ setPackageKillableStateForAllUsers(packageName, isKillable);
return;
}
int userId = userHandle.getIdentifier();
- String key = getUserPackageUniqueId(userId, packageName);
+ String genericPackageName = mPackageInfoHandler.getNameForUserPackage(packageName, userId);
+ if (genericPackageName == null) {
+ throw new IllegalArgumentException("Package '" + packageName + "' not found");
+ }
+ String key = getUserPackageUniqueId(userId, genericPackageName);
synchronized (mLock) {
/*
* When the queried package is not cached in {@link mUsageByUserPackage}, the set API
@@ -366,11 +487,14 @@
* state when pushing the latest stats. Ergo, the invalid killable state doesn't have
* any effect.
*/
- PackageResourceUsage usage = mUsageByUserPackage.getOrDefault(key,
- new PackageResourceUsage(userId, packageName));
- if (!usage.setKillableState(isKillable)) {
+ PackageResourceUsage usage = mUsageByUserPackage.get(key);
+ if (usage == null) {
+ usage = new PackageResourceUsage(userId, genericPackageName,
+ getDefaultKillableStateLocked(genericPackageName));
+ }
+ if (!usage.verifyAndSetKillableState(isKillable)) {
Slogf.e(TAG, "User %d cannot set killable state for package '%s'",
- userHandle.getIdentifier(), packageName);
+ userHandle.getIdentifier(), genericPackageName);
throw new IllegalArgumentException("Package killable state is not updatable");
}
mUsageByUserPackage.put(key, usage);
@@ -380,12 +504,47 @@
}
}
+ private void setPackageKillableStateForAllUsers(String packageName, boolean isKillable) {
+ int[] userIds = getAliveUserIds();
+ String genericPackageName = null;
+ synchronized (mLock) {
+ for (int i = 0; i < userIds.length; ++i) {
+ int userId = userIds[i];
+ String name = mPackageInfoHandler.getNameForUserPackage(packageName, userId);
+ if (name == null) {
+ continue;
+ }
+ genericPackageName = name;
+ String key = getUserPackageUniqueId(userId, genericPackageName);
+ PackageResourceUsage usage = mUsageByUserPackage.get(key);
+ if (usage == null) {
+ continue;
+ }
+ if (!usage.verifyAndSetKillableState(isKillable)) {
+ Slogf.e(TAG, "Cannot set killable state for package '%s'", packageName);
+ throw new IllegalArgumentException(
+ "Package killable state is not updatable");
+ }
+ }
+ if (genericPackageName != null) {
+ if (!isKillable) {
+ mDefaultNotKillableGenericPackages.add(genericPackageName);
+ } else {
+ mDefaultNotKillableGenericPackages.remove(genericPackageName);
+ }
+ }
+ }
+ if (DEBUG) {
+ Slogf.d(TAG, "Successfully set killable package state for all users");
+ }
+ }
+
/** Returns the list of package killable states on resource overuse for the user. */
@NonNull
public List<PackageKillableState> getPackageKillableStatesAsUser(UserHandle userHandle) {
Objects.requireNonNull(userHandle, "User handle must be non-null");
PackageManager pm = mContext.getPackageManager();
- if (userHandle != UserHandle.ALL) {
+ if (!userHandle.equals(UserHandle.ALL)) {
if (DEBUG) {
Slogf.d(TAG, "Returning all package killable states for user %d",
userHandle.getIdentifier());
@@ -393,10 +552,10 @@
return getPackageKillableStatesForUserId(userHandle.getIdentifier(), pm);
}
List<PackageKillableState> packageKillableStates = new ArrayList<>();
- UserManager userManager = UserManager.get(mContext);
- List<UserInfo> userInfos = userManager.getAliveUsers();
- for (UserInfo userInfo : userInfos) {
- packageKillableStates.addAll(getPackageKillableStatesForUserId(userInfo.id, pm));
+ int[] userIds = getAliveUserIds();
+ for (int i = 0; i < userIds.length; ++i) {
+ packageKillableStates.addAll(
+ getPackageKillableStatesForUserId(userIds[i], pm));
}
if (DEBUG) {
Slogf.d(TAG, "Returning all package killable states for all users");
@@ -406,24 +565,55 @@
private List<PackageKillableState> getPackageKillableStatesForUserId(int userId,
PackageManager pm) {
- List<PackageInfo> packageInfos = pm.getInstalledPackagesAsUser(/* flags= */0, userId);
+ List<PackageInfo> packageInfos = pm.getInstalledPackagesAsUser(/* flags= */ 0, userId);
List<PackageKillableState> states = new ArrayList<>();
synchronized (mLock) {
+ ArrayMap<String, List<ApplicationInfo>> applicationInfosBySharedPackage =
+ new ArrayMap<>();
for (int i = 0; i < packageInfos.size(); ++i) {
PackageInfo packageInfo = packageInfos.get(i);
- String key = getUserPackageUniqueId(userId, packageInfo.packageName);
- PackageResourceUsage usage = mUsageByUserPackage.getOrDefault(key,
- new PackageResourceUsage(userId, packageInfo.packageName));
- int killableState = usage.syncAndFetchKillableStateLocked(
- mPackageInfoHandler.getComponentType(packageInfo.packageName,
- packageInfo.applicationInfo));
- mUsageByUserPackage.put(key, usage);
- states.add(
- new PackageKillableState(packageInfo.packageName, userId, killableState));
+ String genericPackageName = mPackageInfoHandler.getNameForPackage(packageInfo);
+ if (packageInfo.sharedUserId == null) {
+ int componentType = mPackageInfoHandler.getComponentType(
+ packageInfo.applicationInfo);
+ int killableState = getPackageKillableStateForUserPackageLocked(
+ userId, genericPackageName, componentType,
+ mOveruseConfigurationCache.isSafeToKill(
+ genericPackageName, componentType, /* sharedPackages= */null));
+ states.add(new PackageKillableState(packageInfo.packageName, userId,
+ killableState));
+ continue;
+ }
+ List<ApplicationInfo> applicationInfos =
+ applicationInfosBySharedPackage.get(genericPackageName);
+ if (applicationInfos == null) {
+ applicationInfos = new ArrayList<>();
+ }
+ applicationInfos.add(packageInfo.applicationInfo);
+ applicationInfosBySharedPackage.put(genericPackageName, applicationInfos);
+ }
+ for (Map.Entry<String, List<ApplicationInfo>> entry :
+ applicationInfosBySharedPackage.entrySet()) {
+ String genericPackageName = entry.getKey();
+ List<ApplicationInfo> applicationInfos = entry.getValue();
+ int componentType = mPackageInfoHandler.getSharedComponentType(
+ applicationInfos, genericPackageName);
+ List<String> packageNames = new ArrayList<>(applicationInfos.size());
+ for (int i = 0; i < applicationInfos.size(); ++i) {
+ packageNames.add(applicationInfos.get(i).packageName);
+ }
+ int killableState = getPackageKillableStateForUserPackageLocked(
+ userId, genericPackageName, componentType,
+ mOveruseConfigurationCache.isSafeToKill(
+ genericPackageName, componentType, packageNames));
+ for (int i = 0; i < applicationInfos.size(); ++i) {
+ states.add(new PackageKillableState(
+ applicationInfos.get(i).packageName, userId, killableState));
+ }
}
}
if (DEBUG) {
- Slogf.d(TAG, "Returning the package killable states for a user package");
+ Slogf.d(TAG, "Returning the package killable states for user packages");
}
return states;
}
@@ -439,28 +629,11 @@
"Must provide at least one configuration");
Preconditions.checkArgument((resourceOveruseFlag > 0),
"Must provide valid resource overuse flag");
- Set<Integer> seenComponentTypes = new ArraySet<>();
+ checkResourceOveruseConfigs(configurations, resourceOveruseFlag);
List<android.automotive.watchdog.internal.ResourceOveruseConfiguration> internalConfigs =
new ArrayList<>();
- for (ResourceOveruseConfiguration config : configurations) {
- /*
- * TODO(b/185287136): Make sure the validation done here matches the validation done in
- * the daemon so set requests retried at a later time will complete successfully.
- */
- int componentType = config.getComponentType();
- if (toComponentTypeStr(componentType).equals("UNKNOWN")) {
- throw new IllegalArgumentException("Invalid component type in the configuration");
- }
- if (seenComponentTypes.contains(componentType)) {
- throw new IllegalArgumentException(
- "Cannot provide duplicate configurations for the same component type");
- }
- if ((resourceOveruseFlag & FLAG_RESOURCE_OVERUSE_IO) != 0
- && config.getIoOveruseConfiguration() == null) {
- throw new IllegalArgumentException("Must provide I/O overuse configuration");
- }
- seenComponentTypes.add(config.getComponentType());
- internalConfigs.add(toInternalResourceOveruseConfiguration(config,
+ for (int i = 0; i < configurations.size(); ++i) {
+ internalConfigs.add(toInternalResourceOveruseConfiguration(configurations.get(i),
resourceOveruseFlag));
}
synchronized (mLock) {
@@ -497,9 +670,9 @@
throw new IllegalStateException(e);
}
List<ResourceOveruseConfiguration> configs = new ArrayList<>();
- for (android.automotive.watchdog.internal.ResourceOveruseConfiguration internalConfig
- : internalConfigs) {
- configs.add(toResourceOveruseConfiguration(internalConfig, resourceOveruseFlag));
+ for (int i = 0; i < internalConfigs.size(); ++i) {
+ configs.add(
+ toResourceOveruseConfiguration(internalConfigs.get(i), resourceOveruseFlag));
}
if (DEBUG) {
Slogf.d(TAG, "Returning the resource overuse configuration");
@@ -513,17 +686,18 @@
for (int i = 0; i < packageIoOveruseStats.size(); ++i) {
uids[i] = packageIoOveruseStats.get(i).uid;
}
- SparseArray<String> packageNamesByUid = mPackageInfoHandler.getPackageNamesForUids(uids);
+ SparseArray<String> genericPackageNamesByUid = mPackageInfoHandler.getNamesForUids(uids);
+ ArraySet<String> overusingUserPackageKeys = new ArraySet<>();
synchronized (mLock) {
checkAndHandleDateChangeLocked();
- for (PackageIoOveruseStats stats : packageIoOveruseStats) {
- String packageName = packageNamesByUid.get(stats.uid);
- if (packageName == null) {
+ for (int i = 0; i < packageIoOveruseStats.size(); ++i) {
+ PackageIoOveruseStats stats = packageIoOveruseStats.get(i);
+ String genericPackageName = genericPackageNamesByUid.get(stats.uid);
+ if (genericPackageName == null) {
continue;
}
- int userId = UserHandle.getUserId(stats.uid);
- PackageResourceUsage usage = cacheAndFetchUsageLocked(userId, packageName,
- stats.ioOveruseStats);
+ PackageResourceUsage usage = cacheAndFetchUsageLocked(stats.uid, genericPackageName,
+ stats.ioOveruseStats, stats.forgivenWriteBytes);
if (stats.shouldNotify) {
/*
* Packages that exceed the warn threshold percentage should be notified as well
@@ -535,125 +709,311 @@
usage.getIoOveruseStats()).build();
notifyResourceOveruseStatsLocked(stats.uid, resourceOveruseStats);
}
-
if (!usage.ioUsage.exceedsThreshold()) {
continue;
}
+ overusingUserPackageKeys.add(usage.getUniqueId());
PackageResourceOveruseAction overuseAction = new PackageResourceOveruseAction();
overuseAction.packageIdentifier = new PackageIdentifier();
- overuseAction.packageIdentifier.name = packageName;
- overuseAction.packageIdentifier.uid = stats.uid;
+ overuseAction.packageIdentifier.name = genericPackageName;
+ overuseAction.packageIdentifier.uid = usage.getUid();
overuseAction.resourceTypes = new int[]{ ResourceType.IO };
overuseAction.resourceOveruseActionType = NOT_KILLED;
- /*
- * No action required on I/O overuse on one of the following cases:
- * #1 The package is not safe to kill as it is critical for system stability.
- * #2 The package has no recurring overuse behavior and the user opted to not
- * kill the package so honor the user's decision.
- */
+ mOveruseActionsByUserPackage.add(overuseAction);
int killableState = usage.getKillableState();
if (killableState == KILLABLE_STATE_NEVER) {
- mOveruseActionsByUserPackage.add(overuseAction);
continue;
}
- boolean hasRecurringOveruse = isRecurringOveruseLocked(usage);
- if (!hasRecurringOveruse && killableState == KILLABLE_STATE_NO) {
- overuseAction.resourceOveruseActionType = NOT_KILLED_USER_OPTED;
- mOveruseActionsByUserPackage.add(overuseAction);
- continue;
+ if (isRecurringOveruseLocked(usage)) {
+ String id = usage.getUniqueId();
+ mActionableUserPackages.add(id);
+ mUserNotifiablePackages.add(id);
}
- try {
- int oldEnabledState = -1;
- IPackageManager packageManager = ActivityThread.getPackageManager();
- if (!hasRecurringOveruse) {
- oldEnabledState = packageManager.getApplicationEnabledSetting(packageName,
- userId);
-
- if (oldEnabledState == COMPONENT_ENABLED_STATE_DISABLED
- || oldEnabledState == COMPONENT_ENABLED_STATE_DISABLED_USER
- || oldEnabledState == COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
- mOveruseActionsByUserPackage.add(overuseAction);
- continue;
- }
- }
-
- packageManager.setApplicationEnabledSetting(packageName,
- COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, /* flags= */ 0, userId,
- mContext.getPackageName());
-
- overuseAction.resourceOveruseActionType = hasRecurringOveruse
- ? KILLED_RECURRING_OVERUSE : KILLED;
- if (!hasRecurringOveruse) {
- usage.oldEnabledState = oldEnabledState;
- }
- } catch (RemoteException e) {
- Slogf.e(TAG, "Failed to disable application enabled setting for user %d, "
- + "package '%s'", userId, packageName);
- }
- mOveruseActionsByUserPackage.add(overuseAction);
}
if (!mOveruseActionsByUserPackage.isEmpty()) {
- mMainHandler.sendMessage(obtainMessage(
- WatchdogPerfHandler::notifyActionsTakenOnOveruse, this));
+ mMainHandler.post(this::notifyActionsTakenOnOveruse);
}
+ if (mCurrentUxState != UX_STATE_NO_DISTRACTION
+ && (!mActionableUserPackages.isEmpty() || !mUserNotifiablePackages.isEmpty())) {
+ mMainHandler.postDelayed(() -> {
+ synchronized (mLock) {
+ performOveruseHandlingLocked();
+ }}, mOveruseHandlingDelayMills);
+ }
+ }
+ if (!overusingUserPackageKeys.isEmpty()) {
+ pushIoOveruseMetrics(overusingUserPackageKeys);
}
if (DEBUG) {
Slogf.d(TAG, "Processed latest I/O overuse stats");
}
}
- /** Notify daemon about the actions take on resource overuse */
- public void notifyActionsTakenOnOveruse() {
- List<PackageResourceOveruseAction> actions;
+ /** Resets the resource overuse settings and stats for the given generic package names. */
+ public void resetResourceOveruseStats(Set<String> genericPackageNames) {
+ IPackageManager packageManager = ActivityThread.getPackageManager();
synchronized (mLock) {
- if (mOveruseActionsByUserPackage.isEmpty()) {
- return;
- }
- actions = new ArrayList<>(mOveruseActionsByUserPackage);
- mOveruseActionsByUserPackage.clear();
- }
- try {
- mCarWatchdogDaemonHelper.actionTakenOnResourceOveruse(actions);
- } catch (RemoteException | RuntimeException e) {
- Slogf.w(TAG, e, "Failed to notify car watchdog daemon of actions taken on resource "
- + "overuse");
- }
- if (DEBUG) {
- Slogf.d(TAG, "Notified car watchdog daemon of actions taken on resource overuse");
- }
- }
-
- /** Resets the resource overuse stats for the given package. */
- public void resetResourceOveruseStats(Set<String> packageNames) {
- synchronized (mLock) {
- for (PackageResourceUsage usage : mUsageByUserPackage.values()) {
- if (packageNames.contains(usage.packageName)) {
- usage.resetStats();
- /*
- * TODO(b/185287136): When the stats are persisted in local DB, reset the stats
- * for this package from local DB.
- */
+ for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+ PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
+ if (!genericPackageNames.contains(usage.genericPackageName)) {
+ continue;
+ }
+ usage.resetStats();
+ usage.verifyAndSetKillableState(/* isKillable= */ true);
+ mWatchdogStorage.deleteUserPackage(usage.userId, usage.genericPackageName);
+ mActionableUserPackages.remove(usage.getUniqueId());
+ Slogf.i(TAG,
+ "Reset resource overuse settings and stats for user '%d' package '%s'",
+ usage.userId, usage.genericPackageName);
+ try {
+ if (packageManager.getApplicationEnabledSetting(usage.genericPackageName,
+ usage.userId) != COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
+ continue;
+ }
+ packageManager.setApplicationEnabledSetting(usage.genericPackageName,
+ COMPONENT_ENABLED_STATE_ENABLED, /* flags= */ 0, usage.userId,
+ mContext.getPackageName());
+ Slogf.i(TAG, "Enabled user '%d' package '%s'", usage.userId,
+ usage.genericPackageName);
+ } catch (RemoteException | IllegalArgumentException e) {
+ Slogf.e(TAG, e, "Failed to verify and enable user %d, package '%s'",
+ usage.userId, usage.genericPackageName);
}
}
}
}
+ /** Returns today's I/O usage stats for all packages collected during the previous boot. */
+ public List<UserPackageIoUsageStats> getTodayIoUsageStats() {
+ List<UserPackageIoUsageStats> userPackageIoUsageStats = new ArrayList<>();
+ List<WatchdogStorage.IoUsageStatsEntry> entries = mWatchdogStorage.getTodayIoUsageStats();
+ for (int i = 0; i < entries.size(); ++i) {
+ WatchdogStorage.IoUsageStatsEntry entry = entries.get(i);
+ UserPackageIoUsageStats stats = new UserPackageIoUsageStats();
+ stats.userId = entry.userId;
+ stats.packageName = entry.packageName;
+ stats.ioUsageStats = new IoUsageStats();
+ android.automotive.watchdog.IoOveruseStats internalIoUsage =
+ entry.ioUsage.getInternalIoOveruseStats();
+ stats.ioUsageStats.writtenBytes = internalIoUsage.writtenBytes;
+ stats.ioUsageStats.forgivenWriteBytes = entry.ioUsage.getForgivenWriteBytes();
+ stats.ioUsageStats.totalOveruses = internalIoUsage.totalOveruses;
+ userPackageIoUsageStats.add(stats);
+ }
+ return userPackageIoUsageStats;
+ }
+
+ /** Deletes all data for specific user. */
+ public void deleteUser(@UserIdInt int userId) {
+ synchronized (mLock) {
+ for (int i = mUsageByUserPackage.size() - 1; i >= 0; --i) {
+ if (userId == mUsageByUserPackage.valueAt(i).userId) {
+ mUsageByUserPackage.removeAt(i);
+ }
+ }
+ mWatchdogStorage.syncUsers(getAliveUserIds());
+ }
+ if (DEBUG) {
+ Slogf.d(TAG, "Resource usage for user id: %d was deleted.", userId);
+ }
+ }
+
+ /** Sets the time source. */
+ public void setTimeSource(TimeSourceInterface timeSource) {
+ synchronized (mLock) {
+ mTimeSource = timeSource;
+ }
+ }
+
+ /**
+ * Sets the delay to handle resource overuse after the package is notified of resource overuse.
+ */
+ public void setOveruseHandlingDelay(long millis) {
+ synchronized (mLock) {
+ mOveruseHandlingDelayMills = millis;
+ }
+ }
+
+ /** Sets the threshold for recurring overuse behavior. */
+ public void setRecurringOveruseThreshold(int threshold) {
+ synchronized (mLock) {
+ mRecurringOveruseThreshold = threshold;
+ }
+ }
+
+ /** Fetches and syncs the resource overuse configurations from watchdog daemon. */
+ private void fetchAndSyncResourceOveruseConfigurations() {
+ List<android.automotive.watchdog.internal.ResourceOveruseConfiguration> internalConfigs;
+ try {
+ internalConfigs = mCarWatchdogDaemonHelper.getResourceOveruseConfigurations();
+ } catch (RemoteException | RuntimeException e) {
+ Slogf.w(TAG, e, "Failed to fetch resource overuse configurations");
+ return;
+ }
+ if (internalConfigs.isEmpty()) {
+ Slogf.e(TAG, "Fetched resource overuse configurations are empty");
+ return;
+ }
+ mOveruseConfigurationCache.set(internalConfigs);
+ mPackageInfoHandler.setVendorPackagePrefixes(
+ mOveruseConfigurationCache.getVendorPackagePrefixes());
+ if (DEBUG) {
+ Slogf.d(TAG, "Fetched and synced resource overuse configs.");
+ }
+ }
+
+ private void readFromDatabase() {
+ mWatchdogStorage.syncUsers(getAliveUserIds());
+ List<WatchdogStorage.UserPackageSettingsEntry> settingsEntries =
+ mWatchdogStorage.getUserPackageSettings();
+ Slogf.i(TAG, "Read %d user package settings from database", settingsEntries.size());
+ List<WatchdogStorage.IoUsageStatsEntry> ioStatsEntries =
+ mWatchdogStorage.getTodayIoUsageStats();
+ Slogf.i(TAG, "Read %d I/O usage stats from database", ioStatsEntries.size());
+ synchronized (mLock) {
+ for (int i = 0; i < settingsEntries.size(); ++i) {
+ WatchdogStorage.UserPackageSettingsEntry entry = settingsEntries.get(i);
+ if (entry.userId == UserHandle.USER_ALL) {
+ if (entry.killableState != KILLABLE_STATE_YES) {
+ mDefaultNotKillableGenericPackages.add(entry.packageName);
+ }
+ continue;
+ }
+ String key = getUserPackageUniqueId(entry.userId, entry.packageName);
+ PackageResourceUsage usage = mUsageByUserPackage.get(key);
+ if (usage == null) {
+ usage = new PackageResourceUsage(entry.userId, entry.packageName,
+ getDefaultKillableStateLocked(entry.packageName));
+ }
+ usage.setKillableState(entry.killableState);
+ mUsageByUserPackage.put(key, usage);
+ }
+ ZonedDateTime curReportDate =
+ mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+ for (int i = 0; i < ioStatsEntries.size(); ++i) {
+ WatchdogStorage.IoUsageStatsEntry entry = ioStatsEntries.get(i);
+ String key = getUserPackageUniqueId(entry.userId, entry.packageName);
+ PackageResourceUsage usage = mUsageByUserPackage.get(key);
+ if (usage == null) {
+ usage = new PackageResourceUsage(entry.userId, entry.packageName,
+ getDefaultKillableStateLocked(entry.packageName));
+ }
+ /* Overwrite in memory cache as the stats will be merged on the daemon side and
+ * pushed on the next latestIoOveruseStats call. This is tolerable because the next
+ * push should happen soon.
+ */
+ usage.ioUsage.overwrite(entry.ioUsage);
+ mUsageByUserPackage.put(key, usage);
+ }
+ if (!ioStatsEntries.isEmpty()) {
+ /* When mLatestStatsReportDate is null, the latest stats push from daemon hasn't
+ * happened yet. Thus the cached stats contains only the stats read from database.
+ */
+ mIsWrittenToDatabase = mLatestStatsReportDate == null;
+ mLatestStatsReportDate = curReportDate;
+ }
+ }
+ }
+
+ /** Writes user package settings and stats to database. */
+ public void writeToDatabase() {
+ synchronized (mLock) {
+ if (mIsWrittenToDatabase) {
+ return;
+ }
+ List<WatchdogStorage.UserPackageSettingsEntry> entries =
+ new ArrayList<>(mUsageByUserPackage.size());
+ for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+ PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
+ entries.add(new WatchdogStorage.UserPackageSettingsEntry(
+ usage.userId, usage.genericPackageName, usage.getKillableState()));
+ }
+ for (String packageName : mDefaultNotKillableGenericPackages) {
+ entries.add(new WatchdogStorage.UserPackageSettingsEntry(
+ UserHandle.USER_ALL, packageName, KILLABLE_STATE_NO));
+ }
+ if (!mWatchdogStorage.saveUserPackageSettings(entries)) {
+ Slogf.e(TAG, "Failed to write user package settings to database");
+ } else {
+ Slogf.i(TAG, "Successfully saved %d user package settings to database",
+ entries.size());
+ }
+ writeStatsLocked();
+ mIsWrittenToDatabase = true;
+ }
+ }
+
+ @GuardedBy("mLock")
+ private @KillableState int getDefaultKillableStateLocked(String genericPackageName) {
+ return mDefaultNotKillableGenericPackages.contains(genericPackageName)
+ ? KILLABLE_STATE_NO : KILLABLE_STATE_YES;
+ }
+
+ @GuardedBy("mLock")
+ private void writeStatsLocked() {
+ List<WatchdogStorage.IoUsageStatsEntry> entries =
+ new ArrayList<>(mUsageByUserPackage.size());
+ for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+ PackageResourceUsage usage = mUsageByUserPackage.valueAt(i);
+ if (!usage.ioUsage.hasUsage()) {
+ continue;
+ }
+ entries.add(new WatchdogStorage.IoUsageStatsEntry(
+ usage.userId, usage.genericPackageName, usage.ioUsage));
+ }
+ if (!mWatchdogStorage.saveIoUsageStats(entries)) {
+ Slogf.e(TAG, "Failed to write %d I/O overuse stats to database", entries.size());
+ } else {
+ Slogf.i(TAG, "Successfully saved %d I/O overuse stats to database", entries.size());
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void applyCurrentUxRestrictionsLocked() {
+ if (mCurrentUxRestrictions == null
+ || mCurrentUxRestrictions.isRequiresDistractionOptimization()) {
+ mCurrentUxState = UX_STATE_NO_DISTRACTION;
+ return;
+ }
+ if (mCurrentUxState == UX_STATE_NO_INTERACTION) {
+ return;
+ }
+ mCurrentUxState = UX_STATE_USER_NOTIFICATION;
+ performOveruseHandlingLocked();
+ }
+
+ @GuardedBy("mLock")
+ private int getPackageKillableStateForUserPackageLocked(
+ int userId, String genericPackageName, int componentType, boolean isSafeToKill) {
+ String key = getUserPackageUniqueId(userId, genericPackageName);
+ PackageResourceUsage usage = mUsageByUserPackage.get(key);
+ int defaultKillableState = getDefaultKillableStateLocked(genericPackageName);
+ if (usage == null) {
+ usage = new PackageResourceUsage(userId, genericPackageName, defaultKillableState);
+ }
+ int killableState = usage.syncAndFetchKillableState(
+ componentType, isSafeToKill, defaultKillableState);
+ mUsageByUserPackage.put(key, usage);
+ return killableState;
+ }
+
+ @GuardedBy("mLock")
private void notifyResourceOveruseStatsLocked(int uid,
ResourceOveruseStats resourceOveruseStats) {
- String packageName = resourceOveruseStats.getPackageName();
+ String genericPackageName = resourceOveruseStats.getPackageName();
ArrayList<ResourceOveruseListenerInfo> listenerInfos = mOveruseListenerInfosByUid.get(uid);
if (listenerInfos != null) {
- for (ResourceOveruseListenerInfo listenerInfo : listenerInfos) {
- listenerInfo.notifyListener(FLAG_RESOURCE_OVERUSE_IO, uid, packageName,
- resourceOveruseStats);
+ for (int i = 0; i < listenerInfos.size(); ++i) {
+ listenerInfos.get(i).notifyListener(
+ FLAG_RESOURCE_OVERUSE_IO, uid, genericPackageName, resourceOveruseStats);
}
}
for (int i = 0; i < mOveruseSystemListenerInfosByUid.size(); ++i) {
ArrayList<ResourceOveruseListenerInfo> systemListenerInfos =
mOveruseSystemListenerInfosByUid.valueAt(i);
- for (ResourceOveruseListenerInfo listenerInfo : systemListenerInfos) {
- listenerInfo.notifyListener(FLAG_RESOURCE_OVERUSE_IO, uid, packageName,
- resourceOveruseStats);
+ for (int j = 0; j < systemListenerInfos.size(); ++j) {
+ systemListenerInfos.get(j).notifyListener(
+ FLAG_RESOURCE_OVERUSE_IO, uid, genericPackageName, resourceOveruseStats);
}
}
if (DEBUG) {
@@ -663,77 +1023,103 @@
@GuardedBy("mLock")
private void checkAndHandleDateChangeLocked() {
- ZonedDateTime previousUTC = mLastStatsReportUTC;
- mLastStatsReportUTC = ZonedDateTime.now(ZoneOffset.UTC);
- if (mLastStatsReportUTC.getDayOfYear() == previousUTC.getDayOfYear()
- && mLastStatsReportUTC.getYear() == previousUTC.getYear()) {
+ ZonedDateTime currentDate = mTimeSource.now().atZone(ZoneOffset.UTC)
+ .truncatedTo(STATS_TEMPORAL_UNIT);
+ if (currentDate.equals(mLatestStatsReportDate)) {
return;
}
- for (PackageResourceUsage usage : mUsageByUserPackage.values()) {
- if (usage.oldEnabledState > 0) {
- // Forgive the daily disabled package on date change.
- try {
- IPackageManager packageManager = ActivityThread.getPackageManager();
- if (packageManager.getApplicationEnabledSetting(usage.packageName,
- usage.userId)
- != COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
- continue;
- }
- packageManager.setApplicationEnabledSetting(usage.packageName,
- usage.oldEnabledState,
- /* flags= */ 0, usage.userId, mContext.getPackageName());
- } catch (RemoteException e) {
- Slogf.e(TAG, "Failed to reset enabled setting for disabled package '%s', user "
- + "%d", usage.packageName, usage.userId);
- }
- }
- /* TODO(b/170741935): Stash the old usage into SQLite DB storage. */
- usage.resetStats();
+ /* After the first database read or on the first stats sync from the daemon, whichever
+ * happens first, the cached stats would either be empty or initialized from the database.
+ * In either case, don't write to database.
+ */
+ if (mLatestStatsReportDate != null && !mIsWrittenToDatabase) {
+ writeStatsLocked();
}
+ for (int i = 0; i < mUsageByUserPackage.size(); ++i) {
+ mUsageByUserPackage.valueAt(i).resetStats();
+ }
+ mLatestStatsReportDate = currentDate;
if (DEBUG) {
Slogf.d(TAG, "Handled date change successfully");
}
}
- private PackageResourceUsage cacheAndFetchUsageLocked(@UserIdInt int userId, String packageName,
- android.automotive.watchdog.IoOveruseStats internalStats) {
- String key = getUserPackageUniqueId(userId, packageName);
- PackageResourceUsage usage = mUsageByUserPackage.getOrDefault(key,
- new PackageResourceUsage(userId, packageName));
- usage.updateLocked(internalStats);
+ @GuardedBy("mLock")
+ private PackageResourceUsage cacheAndFetchUsageLocked(int uid, String genericPackageName,
+ android.automotive.watchdog.IoOveruseStats internalStats,
+ android.automotive.watchdog.PerStateBytes forgivenWriteBytes) {
+ int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
+ String key = getUserPackageUniqueId(userId, genericPackageName);
+ int defaultKillableState = getDefaultKillableStateLocked(genericPackageName);
+ PackageResourceUsage usage = mUsageByUserPackage.get(key);
+ if (usage == null) {
+ usage = new PackageResourceUsage(userId, genericPackageName, defaultKillableState);
+ }
+ usage.update(uid, internalStats, forgivenWriteBytes, defaultKillableState);
mUsageByUserPackage.put(key, usage);
return usage;
}
@GuardedBy("mLock")
- private boolean isRecurringOveruseLocked(PackageResourceUsage ioUsage) {
- /*
- * TODO(b/185287136): Look up I/O overuse history and determine whether or not the package
- * has recurring I/O overuse behavior.
- */
- return false;
+ private boolean isRecurringOveruseLocked(PackageResourceUsage usage) {
+ // TODO(b/195425666): Look up I/O overuse history and determine whether or not the package
+ // has recurring I/O overuse behavior.
+ return usage.ioUsage.getInternalIoOveruseStats().totalOveruses
+ > mRecurringOveruseThreshold;
}
- private IoOveruseStats getIoOveruseStats(int userId, String packageName,
+ private IoOveruseStats getIoOveruseStatsForPeriod(int userId, String genericPackageName,
+ @CarWatchdogManager.StatsPeriod int maxStatsPeriod) {
+ synchronized (mLock) {
+ String key = getUserPackageUniqueId(userId, genericPackageName);
+ PackageResourceUsage usage = mUsageByUserPackage.get(key);
+ if (usage == null) {
+ return null;
+ }
+ return getIoOveruseStatsLocked(usage, /* minimumBytesWritten= */ 0, maxStatsPeriod);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private IoOveruseStats getIoOveruseStatsLocked(PackageResourceUsage usage,
long minimumBytesWritten, @CarWatchdogManager.StatsPeriod int maxStatsPeriod) {
- String key = getUserPackageUniqueId(userId, packageName);
- PackageResourceUsage usage = mUsageByUserPackage.get(key);
- if (usage == null) {
+ if (!usage.ioUsage.hasUsage()) {
+ /* Return I/O overuse stats only when the package has usage for the current day.
+ * Without the current day usage, the returned stats will contain zero remaining
+ * bytes, which is incorrect.
+ */
return null;
}
- IoOveruseStats stats = usage.getIoOveruseStats();
- long totalBytesWritten = stats != null ? stats.getTotalBytesWritten() : 0;
- /*
- * TODO(b/185431129): When maxStatsPeriod > current day, populate the historical stats
- * from the local database. Also handle the case where the package doesn't have current
- * day stats but has historical stats.
- */
+ IoOveruseStats currentStats = usage.getIoOveruseStats();
+ long totalBytesWritten = currentStats.getTotalBytesWritten();
+ int numDays = toNumDays(maxStatsPeriod);
+ IoOveruseStats historyStats = null;
+ if (numDays > 0) {
+ historyStats = mWatchdogStorage.getHistoricalIoOveruseStats(
+ usage.userId, usage.genericPackageName, numDays - 1);
+ totalBytesWritten += historyStats != null ? historyStats.getTotalBytesWritten() : 0;
+ }
if (totalBytesWritten < minimumBytesWritten) {
return null;
}
- return stats;
+ if (historyStats == null) {
+ return currentStats;
+ }
+ IoOveruseStats.Builder statsBuilder = new IoOveruseStats.Builder(
+ historyStats.getStartTime(),
+ historyStats.getDurationInSeconds() + currentStats.getDurationInSeconds());
+ statsBuilder.setTotalTimesKilled(
+ historyStats.getTotalTimesKilled() + currentStats.getTotalTimesKilled());
+ statsBuilder.setTotalOveruses(
+ historyStats.getTotalOveruses() + currentStats.getTotalOveruses());
+ statsBuilder.setTotalBytesWritten(
+ historyStats.getTotalBytesWritten() + currentStats.getTotalBytesWritten());
+ statsBuilder.setKillableOnOveruse(currentStats.isKillableOnOveruse());
+ statsBuilder.setRemainingWriteBytes(currentStats.getRemainingWriteBytes());
+ return statsBuilder.build();
}
+ @GuardedBy("mLock")
private void addResourceOveruseListenerLocked(
@CarWatchdogManager.ResourceOveruseFlag int resourceOveruseFlag,
@NonNull IResourceOveruseListener listener,
@@ -750,8 +1136,8 @@
listenerInfos = new ArrayList<>();
listenerInfosByUid.put(callingUid, listenerInfos);
}
- for (ResourceOveruseListenerInfo listenerInfo : listenerInfos) {
- if (listenerInfo.listener.asBinder() == binder) {
+ for (int i = 0; i < listenerInfos.size(); ++i) {
+ if (listenerInfos.get(i).listener.asBinder() == binder) {
throw new IllegalStateException(
"Cannot add " + listenerType + " as it is already added");
}
@@ -772,6 +1158,7 @@
}
}
+ @GuardedBy("mLock")
private void removeResourceOveruseListenerLocked(@NonNull IResourceOveruseListener listener,
SparseArray<ArrayList<ResourceOveruseListenerInfo>> listenerInfosByUid) {
int callingUid = Binder.getCallingUid();
@@ -784,9 +1171,9 @@
}
IBinder binder = listener.asBinder();
ResourceOveruseListenerInfo cachedListenerInfo = null;
- for (ResourceOveruseListenerInfo listenerInfo : listenerInfos) {
- if (listenerInfo.listener.asBinder() == binder) {
- cachedListenerInfo = listenerInfo;
+ for (int i = 0; i < listenerInfos.size(); ++i) {
+ if (listenerInfos.get(i).listener.asBinder() == binder) {
+ cachedListenerInfo = listenerInfos.get(i);
break;
}
}
@@ -844,6 +1231,7 @@
boolean doClearPendingRequest = isPendingRequest;
try {
mCarWatchdogDaemonHelper.updateResourceOveruseConfigurations(configs);
+ mMainHandler.post(this::fetchAndSyncResourceOveruseConfigurations);
} catch (RemoteException e) {
if (e instanceof TransactionTooLargeException) {
throw e;
@@ -861,7 +1249,6 @@
}
}
}
- /* TODO(b/185287136): Fetch safe-to-kill list from daemon and update mSafeToKillPackages. */
if (DEBUG) {
Slogf.d(TAG, "Set the resource overuse configuration successfully");
}
@@ -887,20 +1274,218 @@
}
}
- private static String getUserPackageUniqueId(int userId, String packageName) {
- return String.valueOf(userId) + ":" + packageName;
+ private int[] getAliveUserIds() {
+ UserManager userManager = UserManager.get(mContext);
+ List<UserHandle> aliveUsers = userManager.getUserHandles(/* excludeDying= */ true);
+ int userSize = aliveUsers.size();
+ int[] userIds = new int[userSize];
+ for (int i = 0; i < userSize; ++i) {
+ userIds[i] = aliveUsers.get(i).getIdentifier();
+ }
+ return userIds;
+ }
+
+ @GuardedBy("mLock")
+ private void performOveruseHandlingLocked() {
+ if (mCurrentUxState == UX_STATE_NO_DISTRACTION) {
+ return;
+ }
+ if (!mUserNotifiablePackages.isEmpty()) {
+ notifyUserOnOveruseLocked();
+ }
+ if (mActionableUserPackages.isEmpty() || mCurrentUxState != UX_STATE_NO_INTERACTION) {
+ return;
+ }
+ IPackageManager packageManager = ActivityThread.getPackageManager();
+ ArraySet<String> killedUserPackageKeys = new ArraySet<>();
+ for (int i = 0; i < mActionableUserPackages.size(); ++i) {
+ PackageResourceUsage usage =
+ mUsageByUserPackage.get(mActionableUserPackages.valueAt(i));
+ if (usage == null) {
+ continue;
+ }
+ // Between detecting and handling the overuse, either the package killable state or
+ // the resource overuse configuration was updated. So, verify the killable state
+ // before proceeding.
+ int killableState = usage.getKillableState();
+ if (killableState != KILLABLE_STATE_YES) {
+ continue;
+ }
+ killedUserPackageKeys.add(usage.getUniqueId());
+ PackageResourceOveruseAction overuseAction = new PackageResourceOveruseAction();
+ overuseAction.packageIdentifier = new PackageIdentifier();
+ overuseAction.packageIdentifier.name = usage.genericPackageName;
+ overuseAction.packageIdentifier.uid = usage.getUid();
+ overuseAction.resourceTypes = new int[]{ ResourceType.IO };
+ overuseAction.resourceOveruseActionType = NOT_KILLED;
+ List<String> packages = Collections.singletonList(usage.genericPackageName);
+ if (usage.isSharedPackage()) {
+ packages = mPackageInfoHandler.getPackagesForUid(
+ overuseAction.packageIdentifier.uid, usage.genericPackageName);
+ }
+ for (int pkgIdx = 0; pkgIdx < packages.size(); pkgIdx++) {
+ String packageName = packages.get(pkgIdx);
+ try {
+ int currentEnabledState = packageManager.getApplicationEnabledSetting(
+ packageName, usage.userId);
+ if (currentEnabledState == COMPONENT_ENABLED_STATE_DISABLED
+ || currentEnabledState == COMPONENT_ENABLED_STATE_DISABLED_USER
+ || currentEnabledState
+ == COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
+ continue;
+ }
+ packageManager.setApplicationEnabledSetting(packageName,
+ COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, /* flags= */ 0,
+ usage.userId, mContext.getPackageName());
+ Slogf.i(TAG,
+ "Disabled user %d's package '%s' until used due to disk I/O overuse",
+ usage.userId, packageName);
+ // TODO(b/200599130): When background apps are killed immediately regardless
+ // of the UX state, update the action type as KILLED only for immediately
+ // killed apps and KILLED_RECURRING_OVERUSE only for apps killed on
+ // recurring overuse.
+ overuseAction.resourceOveruseActionType = KILLED_RECURRING_OVERUSE;
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "Failed to disable application for user %d, package '%s'",
+ usage.userId, packageName);
+ }
+ }
+ if (overuseAction.resourceOveruseActionType != NOT_KILLED) {
+ usage.ioUsage.killed();
+ mOveruseActionsByUserPackage.add(overuseAction);
+ }
+ }
+ pushIoOveruseKillMetrics(killedUserPackageKeys);
+ if (!mOveruseActionsByUserPackage.isEmpty()) {
+ mMainHandler.post(this::notifyActionsTakenOnOveruse);
+ }
+ mActionableUserPackages.clear();
+ }
+
+ @GuardedBy("mLock")
+ private void notifyUserOnOveruseLocked() {
+ // TODO(b/197770456): Notify the current user about the resource overusing packages, that
+ // belong only to the current user, from the list {@link mUserNotifiablePackages}.
+ // 1. When {@code mCurrentUxState == UX_STATE_NO_INTERACTION}, the notifications should be
+ // posted only on the notification center.
+ // 2. When there are multiple resource overusing packages, post the heads-up notification
+ // only for one package and the remaining notifications should be posted on
+ // the notification center.
+ // 3. When this method is called multiple times while
+ // {@code mCurrentUxState == UX_STATE_USER_NOTIFICATION}, only one heads-up notification
+ // should be posted and the rest should be posted on the notification center.
+ // 4. If a package's killable state is KILLABLE_STATE_NO or KILLABLE_STATE_NEVER,
+ // skip posting the notification.
+ mUserNotifiablePackages.clear();
+ }
+
+ private void pushIoOveruseMetrics(ArraySet<String> userPackageKeys) {
+ SparseArray<AtomsProto.CarWatchdogIoOveruseStats> statsByUid = new SparseArray<>();
+ synchronized (mLock) {
+ for (int i = 0; i < userPackageKeys.size(); ++i) {
+ String key = userPackageKeys.valueAt(i);
+ PackageResourceUsage usage = mUsageByUserPackage.get(key);
+ if (usage == null) {
+ Slogf.w(TAG, "Missing usage stats for user package key %s", key);
+ continue;
+ }
+ statsByUid.put(usage.getUid(), constructCarWatchdogIoOveruseStatsLocked(usage));
+ }
+ }
+ for (int i = 0; i < statsByUid.size(); ++i) {
+ CarStatsLog.write(CAR_WATCHDOG_IO_OVERUSE_STATS_REPORTED, statsByUid.keyAt(i),
+ statsByUid.valueAt(i).toByteArray());
+ }
+ }
+
+ private void pushIoOveruseKillMetrics(ArraySet<String> userPackageKeys) {
+ int systemState;
+ SparseArray<AtomsProto.CarWatchdogIoOveruseStats> statsByUid = new SparseArray<>();
+ synchronized (mLock) {
+ systemState = inferSystemStateLocked();
+ for (int i = 0; i < userPackageKeys.size(); ++i) {
+ String key = userPackageKeys.valueAt(i);
+ PackageResourceUsage usage = mUsageByUserPackage.get(key);
+ if (usage == null) {
+ Slogf.w(TAG, "Missing usage stats for user package key %s", key);
+ continue;
+ }
+ statsByUid.put(usage.getUid(), constructCarWatchdogIoOveruseStatsLocked(usage));
+ }
+ }
+ for (int i = 0; i < statsByUid.size(); ++i) {
+ // TODO(b/200598815): After watchdog can classify foreground vs background apps,
+ // report the correct uid state.
+ CarStatsLog.write(CAR_WATCHDOG_KILL_STATS_REPORTED, statsByUid.keyAt(i),
+ CAR_WATCHDOG_KILL_STATS_REPORTED__UID_STATE__UNKNOWN_UID_STATE,
+ systemState,
+ CAR_WATCHDOG_KILL_STATS_REPORTED__KILL_REASON__KILLED_ON_IO_OVERUSE,
+ /* arg5= */ null, statsByUid.valueAt(i).toByteArray());
+ }
+ }
+
+ @GuardedBy("mLock")
+ private int inferSystemStateLocked() {
+ if (mCurrentGarageMode == GarageMode.GARAGE_MODE_ON) {
+ return CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__GARAGE_MODE;
+ }
+ return mCurrentUxState == UX_STATE_NO_INTERACTION
+ ? CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE
+ : CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_INTERACTION_MODE;
+ }
+
+ @GuardedBy("mLock")
+ private AtomsProto.CarWatchdogIoOveruseStats constructCarWatchdogIoOveruseStatsLocked(
+ PackageResourceUsage usage) {
+ @ComponentType int componentType = mPackageInfoHandler.getComponentType(
+ usage.getUid(), usage.genericPackageName);
+ android.automotive.watchdog.PerStateBytes threshold =
+ mOveruseConfigurationCache.fetchThreshold(usage.genericPackageName, componentType);
+ android.automotive.watchdog.PerStateBytes writtenBytes =
+ usage.ioUsage.getInternalIoOveruseStats().writtenBytes;
+ return constructCarWatchdogIoOveruseStats(
+ AtomsProto.CarWatchdogIoOveruseStats.Period.DAILY,
+ constructCarWatchdogPerStateBytes(threshold.foregroundBytes,
+ threshold.backgroundBytes, threshold.garageModeBytes),
+ constructCarWatchdogPerStateBytes(writtenBytes.foregroundBytes,
+ writtenBytes.backgroundBytes, writtenBytes.garageModeBytes));
+ }
+
+ /** Notify daemon about the actions take on resource overuse */
+ private void notifyActionsTakenOnOveruse() {
+ List<PackageResourceOveruseAction> actions;
+ synchronized (mLock) {
+ if (mOveruseActionsByUserPackage.isEmpty()) {
+ return;
+ }
+ actions = new ArrayList<>(mOveruseActionsByUserPackage);
+ mOveruseActionsByUserPackage.clear();
+ }
+ try {
+ mCarWatchdogDaemonHelper.actionTakenOnResourceOveruse(actions);
+ } catch (RemoteException | RuntimeException e) {
+ Slogf.w(TAG, e, "Failed to notify car watchdog daemon of actions taken on resource "
+ + "overuse");
+ }
+ if (DEBUG) {
+ Slogf.d(TAG, "Notified car watchdog daemon of actions taken on resource overuse");
+ }
+ }
+
+ private static String getUserPackageUniqueId(int userId, String genericPackageName) {
+ return String.valueOf(userId) + ":" + genericPackageName;
}
@VisibleForTesting
static IoOveruseStats.Builder toIoOveruseStatsBuilder(
- android.automotive.watchdog.IoOveruseStats internalStats) {
- IoOveruseStats.Builder statsBuilder = new IoOveruseStats.Builder(
- internalStats.startTime, internalStats.durationInSeconds);
- statsBuilder.setRemainingWriteBytes(
- toPerStateBytes(internalStats.remainingWriteBytes));
- statsBuilder.setTotalBytesWritten(totalPerStateBytes(internalStats.writtenBytes));
- statsBuilder.setTotalOveruses(internalStats.totalOveruses);
- return statsBuilder;
+ android.automotive.watchdog.IoOveruseStats internalStats,
+ int totalTimesKilled, boolean isKillableOnOveruses) {
+ return new IoOveruseStats.Builder(internalStats.startTime, internalStats.durationInSeconds)
+ .setTotalOveruses(internalStats.totalOveruses)
+ .setTotalTimesKilled(totalTimesKilled)
+ .setTotalBytesWritten(totalPerStateBytes(internalStats.writtenBytes))
+ .setKillableOnOveruse(isKillableOnOveruses)
+ .setRemainingWriteBytes(toPerStateBytes(internalStats.remainingWriteBytes));
}
private static PerStateBytes toPerStateBytes(
@@ -958,6 +1543,8 @@
metadata.appCategoryType = ApplicationCategoryType.MEDIA;
break;
default:
+ Slogf.i(TAG, "Invalid application category type: %s skipping package: %s",
+ entry.getValue(), metadata.packageName);
continue;
}
internalConfig.packageMetadata.add(metadata);
@@ -982,7 +1569,8 @@
config.getPackageSpecificThresholds());
internalConfig.categorySpecificThresholds = toPerStateIoOveruseThresholds(
config.getAppCategorySpecificThresholds());
- for (PerStateIoOveruseThreshold threshold : internalConfig.categorySpecificThresholds) {
+ for (int i = 0; i < internalConfig.categorySpecificThresholds.size(); ++i) {
+ PerStateIoOveruseThreshold threshold = internalConfig.categorySpecificThresholds.get(i);
switch(threshold.name) {
case ResourceOveruseConfiguration.APPLICATION_CATEGORY_TYPE_MAPS:
threshold.name = INTERNAL_APPLICATION_CATEGORY_TYPE_MAPS;
@@ -1020,7 +1608,7 @@
Map<String, PerStateBytes> thresholds) {
List<PerStateIoOveruseThreshold> internalThresholds = new ArrayList<>();
for (Map.Entry<String, PerStateBytes> entry : thresholds.entrySet()) {
- if (!entry.getKey().isEmpty()) {
+ if (!thresholds.isEmpty()) {
internalThresholds.add(toPerStateIoOveruseThreshold(entry.getKey(),
entry.getValue()));
}
@@ -1043,15 +1631,15 @@
toInternalIoOveruseAlertThresholds(List<IoOveruseAlertThreshold> thresholds) {
List<android.automotive.watchdog.internal.IoOveruseAlertThreshold> internalThresholds =
new ArrayList<>();
- for (IoOveruseAlertThreshold threshold : thresholds) {
- if (threshold.getDurationInSeconds() == 0
- || threshold.getWrittenBytesPerSecond() == 0) {
+ for (int i = 0; i < thresholds.size(); ++i) {
+ if (thresholds.get(i).getDurationInSeconds() == 0
+ || thresholds.get(i).getWrittenBytesPerSecond() == 0) {
continue;
}
android.automotive.watchdog.internal.IoOveruseAlertThreshold internalThreshold =
new android.automotive.watchdog.internal.IoOveruseAlertThreshold();
- internalThreshold.durationInSeconds = threshold.getDurationInSeconds();
- internalThreshold.writtenBytesPerSecond = threshold.getWrittenBytesPerSecond();
+ internalThreshold.durationInSeconds = thresholds.get(i).getDurationInSeconds();
+ internalThreshold.writtenBytesPerSecond = thresholds.get(i).getWrittenBytesPerSecond();
internalThresholds.add(internalThreshold);
}
return internalThresholds;
@@ -1060,10 +1648,10 @@
private static ResourceOveruseConfiguration toResourceOveruseConfiguration(
android.automotive.watchdog.internal.ResourceOveruseConfiguration internalConfig,
@CarWatchdogManager.ResourceOveruseFlag int resourceOveruseFlag) {
- Map<String, String> packagesToAppCategoryTypes = new ArrayMap<>();
- for (PackageMetadata metadata : internalConfig.packageMetadata) {
+ ArrayMap<String, String> packagesToAppCategoryTypes = new ArrayMap<>();
+ for (int i = 0; i < internalConfig.packageMetadata.size(); ++i) {
String categoryTypeStr;
- switch (metadata.appCategoryType) {
+ switch (internalConfig.packageMetadata.get(i).appCategoryType) {
case ApplicationCategoryType.MAPS:
categoryTypeStr = ResourceOveruseConfiguration.APPLICATION_CATEGORY_TYPE_MAPS;
break;
@@ -1073,7 +1661,8 @@
default:
continue;
}
- packagesToAppCategoryTypes.put(metadata.packageName, categoryTypeStr);
+ packagesToAppCategoryTypes.put(
+ internalConfig.packageMetadata.get(i).packageName, categoryTypeStr);
}
ResourceOveruseConfiguration.Builder configBuilder =
new ResourceOveruseConfiguration.Builder(
@@ -1097,9 +1686,9 @@
android.automotive.watchdog.internal.IoOveruseConfiguration internalConfig) {
PerStateBytes componentLevelThresholds =
toPerStateBytes(internalConfig.componentLevelThresholds.perStateWriteBytes);
- Map<String, PerStateBytes> packageSpecificThresholds =
+ ArrayMap<String, PerStateBytes> packageSpecificThresholds =
toPerStateBytesMap(internalConfig.packageSpecificThresholds);
- Map<String, PerStateBytes> appCategorySpecificThresholds =
+ ArrayMap<String, PerStateBytes> appCategorySpecificThresholds =
toPerStateBytesMap(internalConfig.categorySpecificThresholds);
replaceKey(appCategorySpecificThresholds, INTERNAL_APPLICATION_CATEGORY_TYPE_MAPS,
ResourceOveruseConfiguration.APPLICATION_CATEGORY_TYPE_MAPS);
@@ -1114,11 +1703,12 @@
return configBuilder.build();
}
- private static Map<String, PerStateBytes> toPerStateBytesMap(
+ private static ArrayMap<String, PerStateBytes> toPerStateBytesMap(
List<PerStateIoOveruseThreshold> thresholds) {
- Map<String, PerStateBytes> thresholdsMap = new ArrayMap<>();
- for (PerStateIoOveruseThreshold threshold : thresholds) {
- thresholdsMap.put(threshold.name, toPerStateBytes(threshold.perStateWriteBytes));
+ ArrayMap<String, PerStateBytes> thresholdsMap = new ArrayMap<>();
+ for (int i = 0; i < thresholds.size(); ++i) {
+ thresholdsMap.put(
+ thresholds.get(i).name, toPerStateBytes(thresholds.get(i).perStateWriteBytes));
}
return thresholdsMap;
}
@@ -1126,14 +1716,79 @@
private static List<IoOveruseAlertThreshold> toIoOveruseAlertThresholds(
List<android.automotive.watchdog.internal.IoOveruseAlertThreshold> internalThresholds) {
List<IoOveruseAlertThreshold> thresholds = new ArrayList<>();
- for (android.automotive.watchdog.internal.IoOveruseAlertThreshold internalThreshold
- : internalThresholds) {
- thresholds.add(new IoOveruseAlertThreshold(internalThreshold.durationInSeconds,
- internalThreshold.writtenBytesPerSecond));
+ for (int i = 0; i < internalThresholds.size(); ++i) {
+ thresholds.add(new IoOveruseAlertThreshold(internalThresholds.get(i).durationInSeconds,
+ internalThresholds.get(i).writtenBytesPerSecond));
}
return thresholds;
}
+ private static void checkResourceOveruseConfigs(
+ List<ResourceOveruseConfiguration> configurations,
+ @CarWatchdogManager.ResourceOveruseFlag int resourceOveruseFlag) {
+ ArraySet<Integer> seenComponentTypes = new ArraySet<>();
+ for (int i = 0; i < configurations.size(); ++i) {
+ ResourceOveruseConfiguration config = configurations.get(i);
+ if (seenComponentTypes.contains(config.getComponentType())) {
+ throw new IllegalArgumentException(
+ "Cannot provide duplicate configurations for the same component type");
+ }
+ checkResourceOveruseConfig(config, resourceOveruseFlag);
+ seenComponentTypes.add(config.getComponentType());
+ }
+ }
+
+ private static void checkResourceOveruseConfig(ResourceOveruseConfiguration config,
+ @CarWatchdogManager.ResourceOveruseFlag int resourceOveruseFlag) {
+ int componentType = config.getComponentType();
+ if (toComponentTypeStr(componentType).equals("UNKNOWN")) {
+ throw new IllegalArgumentException(
+ "Invalid component type in the configuration: " + componentType);
+ }
+ if ((resourceOveruseFlag & FLAG_RESOURCE_OVERUSE_IO) != 0
+ && config.getIoOveruseConfiguration() == null) {
+ throw new IllegalArgumentException("Must provide I/O overuse configuration");
+ }
+ checkIoOveruseConfig(config.getIoOveruseConfiguration(), componentType);
+ }
+
+ private static void checkIoOveruseConfig(IoOveruseConfiguration config, int componentType) {
+ if (config.getComponentLevelThresholds().getBackgroundModeBytes() <= 0
+ || config.getComponentLevelThresholds().getForegroundModeBytes() <= 0
+ || config.getComponentLevelThresholds().getGarageModeBytes() <= 0) {
+ throw new IllegalArgumentException(
+ "For component: " + toComponentTypeStr(componentType)
+ + " some thresholds are zero for: "
+ + config.getComponentLevelThresholds().toString());
+ }
+ if (componentType == ComponentType.SYSTEM) {
+ List<IoOveruseAlertThreshold> systemThresholds = config.getSystemWideThresholds();
+ if (systemThresholds.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Empty system-wide alert thresholds provided in "
+ + toComponentTypeStr(componentType)
+ + " config.");
+ }
+ for (int i = 0; i < systemThresholds.size(); i++) {
+ checkIoOveruseAlertThreshold(systemThresholds.get(i));
+ }
+ }
+ }
+
+ private static void checkIoOveruseAlertThreshold(
+ IoOveruseAlertThreshold ioOveruseAlertThreshold) {
+ if (ioOveruseAlertThreshold.getDurationInSeconds() <= 0) {
+ throw new IllegalArgumentException(
+ "System wide threshold duration must be greater than zero for: "
+ + ioOveruseAlertThreshold);
+ }
+ if (ioOveruseAlertThreshold.getWrittenBytesPerSecond() <= 0) {
+ throw new IllegalArgumentException(
+ "System wide threshold written bytes per second must be greater than zero for: "
+ + ioOveruseAlertThreshold);
+ }
+ }
+
private static void replaceKey(Map<String, PerStateBytes> map, String oldKey, String newKey) {
PerStateBytes perStateBytes = map.get(oldKey);
if (perStateBytes != null) {
@@ -1142,25 +1797,77 @@
}
}
- private final class PackageResourceUsage {
- public final String packageName;
- public @UserIdInt final int userId;
- public final PackageIoUsage ioUsage;
- public int oldEnabledState;
+ private static int toNumDays(@CarWatchdogManager.StatsPeriod int maxStatsPeriod) {
+ switch(maxStatsPeriod) {
+ case STATS_PERIOD_CURRENT_DAY:
+ return 0;
+ case STATS_PERIOD_PAST_3_DAYS:
+ return 3;
+ case STATS_PERIOD_PAST_7_DAYS:
+ return 7;
+ case STATS_PERIOD_PAST_15_DAYS:
+ return 15;
+ case STATS_PERIOD_PAST_30_DAYS:
+ return 30;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid max stats period provided: " + maxStatsPeriod);
+ }
+ }
+ @VisibleForTesting
+ static AtomsProto.CarWatchdogIoOveruseStats constructCarWatchdogIoOveruseStats(
+ AtomsProto.CarWatchdogIoOveruseStats.Period period,
+ AtomsProto.CarWatchdogPerStateBytes threshold,
+ AtomsProto.CarWatchdogPerStateBytes writtenBytes) {
+ // TODO(b/184310189): Report uptime once daemon pushes it to CarService.
+ return AtomsProto.CarWatchdogIoOveruseStats.newBuilder()
+ .setPeriod(period)
+ .setThreshold(threshold)
+ .setWrittenBytes(writtenBytes).build();
+ }
+
+ @VisibleForTesting
+ static AtomsProto.CarWatchdogPerStateBytes constructCarWatchdogPerStateBytes(
+ long foregroundBytes, long backgroundBytes, long garageModeBytes) {
+ return AtomsProto.CarWatchdogPerStateBytes.newBuilder()
+ .setForegroundBytes(foregroundBytes)
+ .setBackgroundBytes(backgroundBytes)
+ .setGarageModeBytes(garageModeBytes).build();
+ }
+
+ private final class PackageResourceUsage {
+ public final String genericPackageName;
+ public @UserIdInt final int userId;
+ public final PackageIoUsage ioUsage = new PackageIoUsage();
private @KillableState int mKillableState;
+ private int mUid;
/** Must be called only after acquiring {@link mLock} */
- PackageResourceUsage(@UserIdInt int userId, String packageName) {
- this.packageName = packageName;
+ PackageResourceUsage(@UserIdInt int userId, String genericPackageName,
+ @KillableState int defaultKillableState) {
+ this.genericPackageName = genericPackageName;
this.userId = userId;
- this.ioUsage = new PackageIoUsage();
- this.oldEnabledState = -1;
- this.mKillableState = mDefaultNotKillablePackages.contains(packageName)
- ? KILLABLE_STATE_NO : KILLABLE_STATE_YES;
+ this.mKillableState = defaultKillableState;
}
- public void updateLocked(android.automotive.watchdog.IoOveruseStats internalStats) {
+ public boolean isSharedPackage() {
+ return this.genericPackageName.startsWith(SHARED_PACKAGE_PREFIX);
+ }
+
+ public String getUniqueId() {
+ return getUserPackageUniqueId(userId, genericPackageName);
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ public void update(int uid, android.automotive.watchdog.IoOveruseStats internalStats,
+ android.automotive.watchdog.PerStateBytes forgivenWriteBytes,
+ @KillableState int defaultKillableState) {
+ // Package UID would change if it was re-installed, so keep it up-to-date.
+ mUid = uid;
if (!internalStats.killableOnOveruse) {
/*
* Killable value specified in the internal stats is provided by the native daemon.
@@ -1175,29 +1882,32 @@
* This case happens when a previously unsafe to kill system/vendor package was
* recently marked as safe-to-kill so update the old state to the default value.
*/
- mKillableState = mDefaultNotKillablePackages.contains(packageName)
- ? KILLABLE_STATE_NO : KILLABLE_STATE_YES;
+ mKillableState = defaultKillableState;
}
- ioUsage.update(internalStats);
+ ioUsage.update(internalStats, forgivenWriteBytes);
}
public ResourceOveruseStats.Builder getResourceOveruseStatsBuilder() {
- return new ResourceOveruseStats.Builder(packageName, UserHandle.of(userId));
+ return new ResourceOveruseStats.Builder(genericPackageName, UserHandle.of(userId));
}
+
public IoOveruseStats getIoOveruseStats() {
if (!ioUsage.hasUsage()) {
return null;
}
- return ioUsage.getStatsBuilder().setKillableOnOveruse(
- mKillableState != KILLABLE_STATE_NEVER).build();
+ return ioUsage.getIoOveruseStats(mKillableState != KILLABLE_STATE_NEVER);
}
public @KillableState int getKillableState() {
return mKillableState;
}
- public boolean setKillableState(boolean isKillable) {
+ public void setKillableState(@KillableState int killableState) {
+ mKillableState = killableState;
+ }
+
+ public boolean verifyAndSetKillableState(boolean isKillable) {
if (mKillableState == KILLABLE_STATE_NEVER) {
return false;
}
@@ -1205,20 +1915,19 @@
return true;
}
- public int syncAndFetchKillableStateLocked(int myComponentType) {
+ public int syncAndFetchKillableState(int myComponentType, boolean isSafeToKill,
+ @KillableState int defaultKillableState) {
/*
* The killable state goes out-of-sync:
- * 1. When the on-device safe-to-kill list is recently updated and the user package
+ * 1. When the on-device safe-to-kill list was recently updated and the user package
* didn't have any resource usage so the native daemon didn't update the killable state.
* 2. When a package has no resource usage and is initialized outside of processing the
* latest resource usage stats.
*/
- if (myComponentType != ComponentType.THIRD_PARTY
- && !mSafeToKillPackages.contains(packageName)) {
+ if (myComponentType != ComponentType.THIRD_PARTY && !isSafeToKill) {
mKillableState = KILLABLE_STATE_NEVER;
} else if (mKillableState == KILLABLE_STATE_NEVER) {
- mKillableState = mDefaultNotKillablePackages.contains(packageName)
- ? KILLABLE_STATE_NO : KILLABLE_STATE_YES;
+ mKillableState = defaultKillableState;
}
return mKillableState;
}
@@ -1228,39 +1937,60 @@
}
}
- private static final class PackageIoUsage {
+ /** Defines I/O usage fields for a package. */
+ public static final class PackageIoUsage {
+ private static final android.automotive.watchdog.PerStateBytes DEFAULT_PER_STATE_BYTES =
+ new android.automotive.watchdog.PerStateBytes();
private android.automotive.watchdog.IoOveruseStats mIoOveruseStats;
private android.automotive.watchdog.PerStateBytes mForgivenWriteBytes;
- private long mTotalTimesKilled;
+ private int mTotalTimesKilled;
- PackageIoUsage() {
+ private PackageIoUsage() {
+ mForgivenWriteBytes = DEFAULT_PER_STATE_BYTES;
mTotalTimesKilled = 0;
}
- public boolean hasUsage() {
+ public PackageIoUsage(android.automotive.watchdog.IoOveruseStats ioOveruseStats,
+ android.automotive.watchdog.PerStateBytes forgivenWriteBytes,
+ int totalTimesKilled) {
+ mIoOveruseStats = ioOveruseStats;
+ mForgivenWriteBytes = forgivenWriteBytes;
+ mTotalTimesKilled = totalTimesKilled;
+ }
+
+ public android.automotive.watchdog.IoOveruseStats getInternalIoOveruseStats() {
+ return mIoOveruseStats;
+ }
+
+ public android.automotive.watchdog.PerStateBytes getForgivenWriteBytes() {
+ return mForgivenWriteBytes;
+ }
+
+ public int getTotalTimesKilled() {
+ return mTotalTimesKilled;
+ }
+
+ boolean hasUsage() {
return mIoOveruseStats != null;
}
- public void update(android.automotive.watchdog.IoOveruseStats internalStats) {
+ void overwrite(PackageIoUsage ioUsage) {
+ mIoOveruseStats = ioUsage.mIoOveruseStats;
+ mForgivenWriteBytes = ioUsage.mForgivenWriteBytes;
+ mTotalTimesKilled = ioUsage.mTotalTimesKilled;
+ }
+
+ void update(android.automotive.watchdog.IoOveruseStats internalStats,
+ android.automotive.watchdog.PerStateBytes forgivenWriteBytes) {
mIoOveruseStats = internalStats;
- if (exceedsThreshold()) {
- /*
- * Forgive written bytes on overuse as the package is either forgiven or killed on
- * overuse. When the package is killed, the user may opt to open the corresponding
- * app and the package should be forgiven anyways.
- * NOTE: If this logic is updated, update the daemon side logic as well.
- */
- mForgivenWriteBytes = internalStats.writtenBytes;
- }
+ mForgivenWriteBytes = forgivenWriteBytes;
}
- public IoOveruseStats.Builder getStatsBuilder() {
- IoOveruseStats.Builder statsBuilder = toIoOveruseStatsBuilder(mIoOveruseStats);
- statsBuilder.setTotalTimesKilled(mTotalTimesKilled);
- return statsBuilder;
+ IoOveruseStats getIoOveruseStats(boolean isKillable) {
+ return toIoOveruseStatsBuilder(mIoOveruseStats, mTotalTimesKilled, isKillable).build();
}
- public boolean exceedsThreshold() {
+ boolean exceedsThreshold() {
if (!hasUsage()) {
return false;
}
@@ -1270,9 +2000,13 @@
|| remaining.garageModeBytes == 0;
}
- public void resetStats() {
+ void killed() {
+ ++mTotalTimesKilled;
+ }
+
+ void resetStats() {
mIoOveruseStats = null;
- mForgivenWriteBytes = null;
+ mForgivenWriteBytes = DEFAULT_PER_STATE_BYTES;
mTotalTimesKilled = 0;
}
}
@@ -1310,16 +2044,18 @@
listenerInfosByUid.remove(uid);
}
};
- if (isListenerForSystem) {
- removeListenerInfo.accept(mOveruseSystemListenerInfosByUid);
- } else {
- removeListenerInfo.accept(mOveruseListenerInfosByUid);
+ synchronized (mLock) {
+ if (isListenerForSystem) {
+ removeListenerInfo.accept(mOveruseSystemListenerInfosByUid);
+ } else {
+ removeListenerInfo.accept(mOveruseListenerInfosByUid);
+ }
}
unlinkToDeath();
}
public void notifyListener(@CarWatchdogManager.ResourceOveruseFlag int resourceType,
- int overusingUid, String overusingPackage,
+ int overusingUid, String overusingGenericPackageName,
ResourceOveruseStats resourceOveruseStats) {
if ((flag & resourceType) == 0) {
return;
@@ -1328,9 +2064,9 @@
listener.onOveruse(resourceOveruseStats);
} catch (RemoteException e) {
Slogf.e(TAG, "Failed to notify %s (uid %d, pid: %d) of resource overuse by "
- + "package(uid %d, package '%s'): %s",
+ + "package(uid %d, generic package name '%s'): %s",
(isListenerForSystem ? "system listener" : "listener"), uid, pid,
- overusingUid, overusingPackage, e);
+ overusingUid, overusingGenericPackageName, e);
}
}
diff --git a/service/src/com/android/car/watchdog/WatchdogProcessHandler.java b/service/src/com/android/car/watchdog/WatchdogProcessHandler.java
index cce3d15..e0cd6f3 100644
--- a/service/src/com/android/car/watchdog/WatchdogProcessHandler.java
+++ b/service/src/com/android/car/watchdog/WatchdogProcessHandler.java
@@ -20,8 +20,6 @@
import static android.car.watchdog.CarWatchdogManager.TIMEOUT_MODERATE;
import static android.car.watchdog.CarWatchdogManager.TIMEOUT_NORMAL;
-import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
-
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.automotive.watchdog.internal.ICarWatchdogServiceForSystem;
@@ -124,6 +122,7 @@
} else {
writer.println("none");
}
+ writer.decreaseIndent();
}
}
@@ -218,8 +217,7 @@
/** Posts health check message */
public void postHealthCheckMessage(int sessionId) {
- mMainHandler.sendMessage(obtainMessage(
- WatchdogProcessHandler::doHealthCheck, this, sessionId));
+ mMainHandler.post(() -> doHealthCheck(sessionId));
}
/** Returns the registered and alive client count. */
@@ -240,6 +238,16 @@
}
}
+ /** Enables/disables the watchdog daemon client health check process. */
+ void controlProcessHealthCheck(boolean disable) {
+ try {
+ mCarWatchdogDaemonHelper.controlProcessHealthCheck(disable);
+ } catch (RemoteException e) {
+ Slogf.w(CarWatchdogService.TAG,
+ "Cannot enable/disable the car watchdog daemon health check process: %s", e);
+ }
+ }
+
private void onClientDeath(ICarWatchdogServiceCallback client, int timeout) {
synchronized (mLock) {
removeClientLocked(client.asBinder(), timeout);
@@ -312,8 +320,8 @@
}
}
sendPingToClients(timeout);
- mMainHandler.sendMessageDelayed(obtainMessage(WatchdogProcessHandler::analyzeClientResponse,
- this, timeout), timeoutToDurationMs(timeout));
+ mMainHandler.postDelayed(
+ () -> analyzeClientResponse(timeout), timeoutToDurationMs(timeout));
}
private int getNewSessionId() {
diff --git a/service/src/com/android/car/watchdog/WatchdogStorage.java b/service/src/com/android/car/watchdog/WatchdogStorage.java
new file mode 100644
index 0000000..5b0ebb5
--- /dev/null
+++ b/service/src/com/android/car/watchdog/WatchdogStorage.java
@@ -0,0 +1,731 @@
+/*
+ * Copyright (C) 2021 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.watchdog;
+
+import static com.android.car.watchdog.CarWatchdogService.SYSTEM_INSTANCE;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.automotive.watchdog.PerStateBytes;
+import android.car.watchdog.IoOveruseStats;
+import android.car.watchdog.PackageKillableState.KillableState;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Process;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.IntArray;
+import android.util.Slog;
+
+import com.android.car.CarLog;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
+
+import java.time.Instant;
+import java.time.Period;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Defines the database to store/retrieve system resource stats history from local storage.
+ */
+public final class WatchdogStorage {
+ private static final String TAG = CarLog.tagFor(WatchdogStorage.class);
+ private static final int RETENTION_PERIOD_IN_DAYS = 30;
+ /* Stats are stored on a daily basis. */
+ public static final TemporalUnit STATS_TEMPORAL_UNIT = ChronoUnit.DAYS;
+ /* Number of days to retain the stats in local storage. */
+ public static final Period RETENTION_PERIOD =
+ Period.ofDays(RETENTION_PERIOD_IN_DAYS).normalized();
+ /* Zone offset for all date based table entries. */
+ public static final ZoneOffset ZONE_OFFSET = ZoneOffset.UTC;
+
+ private final WatchdogDbHelper mDbHelper;
+ private final ArrayMap<String, UserPackage> mUserPackagesByKey = new ArrayMap<>();
+ private final ArrayMap<String, UserPackage> mUserPackagesById = new ArrayMap<>();
+ private TimeSourceInterface mTimeSource = SYSTEM_INSTANCE;
+ private final Object mLock = new Object();
+ // Cache of today's I/O overuse stats collected during the previous boot. The data contained in
+ // the cache won't change until the next boot, so it is safe to cache the data in memory.
+ @GuardedBy("mLock")
+ private final List<IoUsageStatsEntry> mTodayIoUsageStatsEntries = new ArrayList<>();
+
+ public WatchdogStorage(Context context) {
+ this(context, /* useDataSystemCarDir= */ true);
+ }
+
+ @VisibleForTesting
+ WatchdogStorage(Context context, boolean useDataSystemCarDir) {
+ mDbHelper = new WatchdogDbHelper(context, useDataSystemCarDir);
+ }
+
+ /** Releases resources. */
+ public void release() {
+ mDbHelper.close();
+ }
+
+ /** Handles database shrink. */
+ public void shrinkDatabase() {
+ try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+ mDbHelper.onShrink(db);
+ }
+ }
+
+ /** Saves the given user package settings entries and returns whether the change succeeded. */
+ public boolean saveUserPackageSettings(List<UserPackageSettingsEntry> entries) {
+ List<ContentValues> rows = new ArrayList<>(entries.size());
+ /* Capture only unique user ids. */
+ ArraySet<Integer> usersWithMissingIds = new ArraySet<>();
+ for (int i = 0; i < entries.size(); ++i) {
+ UserPackageSettingsEntry entry = entries.get(i);
+ if (mUserPackagesByKey.get(UserPackage.getKey(entry.userId, entry.packageName))
+ == null) {
+ usersWithMissingIds.add(entry.userId);
+ }
+ rows.add(UserPackageSettingsTable.getContentValues(entry));
+ }
+ try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+ if (!atomicReplaceEntries(db, UserPackageSettingsTable.TABLE_NAME, rows)) {
+ return false;
+ }
+ populateUserPackages(db, usersWithMissingIds);
+ }
+ return true;
+ }
+
+ /** Returns the user package setting entries. */
+ public List<UserPackageSettingsEntry> getUserPackageSettings() {
+ ArrayMap<String, UserPackageSettingsEntry> entriesById;
+ try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
+ entriesById = UserPackageSettingsTable.querySettings(db);
+ }
+ List<UserPackageSettingsEntry> entries = new ArrayList<>(entriesById.size());
+ for (int i = 0; i < entriesById.size(); ++i) {
+ String rowId = entriesById.keyAt(i);
+ UserPackageSettingsEntry entry = entriesById.valueAt(i);
+ UserPackage userPackage = new UserPackage(rowId, entry.userId, entry.packageName);
+ mUserPackagesByKey.put(userPackage.getKey(), userPackage);
+ mUserPackagesById.put(userPackage.getUniqueId(), userPackage);
+ entries.add(entry);
+ }
+ return entries;
+ }
+
+ /** Saves the given I/O usage stats. Returns true only on success. */
+ public boolean saveIoUsageStats(List<IoUsageStatsEntry> entries) {
+ return saveIoUsageStats(entries, /* shouldCheckRetention= */ true);
+ }
+
+ /** Returns the saved I/O usage stats for the current day. */
+ public List<IoUsageStatsEntry> getTodayIoUsageStats() {
+ synchronized (mLock) {
+ if (!mTodayIoUsageStatsEntries.isEmpty()) {
+ return new ArrayList<>(mTodayIoUsageStatsEntries);
+ }
+ ZonedDateTime statsDate =
+ mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+ long startEpochSeconds = statsDate.toEpochSecond();
+ long endEpochSeconds = mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond();
+ ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> ioUsagesById;
+ try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
+ ioUsagesById = IoUsageStatsTable.queryStats(db, startEpochSeconds, endEpochSeconds);
+ }
+ for (int i = 0; i < ioUsagesById.size(); ++i) {
+ String id = ioUsagesById.keyAt(i);
+ UserPackage userPackage = mUserPackagesById.get(id);
+ if (userPackage == null) {
+ Slogf.i(TAG,
+ "Failed to find user id and package name for unique database id: '%s'",
+ id);
+ continue;
+ }
+ mTodayIoUsageStatsEntries.add(new IoUsageStatsEntry(
+ userPackage.getUserId(), userPackage.getPackageName(),
+ ioUsagesById.valueAt(i)));
+ }
+ return new ArrayList<>(mTodayIoUsageStatsEntries);
+ }
+ }
+
+ /** Deletes user package settings and resource overuse stats. */
+ public void deleteUserPackage(@UserIdInt int userId, String packageName) {
+ UserPackage userPackage = mUserPackagesByKey.get(UserPackage.getKey(userId, packageName));
+ if (userPackage == null) {
+ Slogf.w(TAG, "Failed to find unique database id for user id '%d' and package '%s",
+ userId, packageName);
+ return;
+ }
+ mUserPackagesByKey.remove(userPackage.getKey());
+ mUserPackagesById.remove(userPackage.getUniqueId());
+ try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+ UserPackageSettingsTable.deleteUserPackage(db, userId, packageName);
+ }
+ }
+
+ /**
+ * Returns the aggregated historical I/O overuse stats for the given user package or
+ * {@code null} when stats are not available.
+ */
+ @Nullable
+ public IoOveruseStats getHistoricalIoOveruseStats(
+ @UserIdInt int userId, String packageName, int numDaysAgo) {
+ ZonedDateTime currentDate =
+ mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+ long startEpochSeconds = currentDate.minusDays(numDaysAgo).toEpochSecond();
+ long endEpochSeconds = currentDate.toEpochSecond();
+ try (SQLiteDatabase db = mDbHelper.getReadableDatabase()) {
+ UserPackage userPackage = mUserPackagesByKey.get(
+ UserPackage.getKey(userId, packageName));
+ if (userPackage == null) {
+ /* Packages without historical stats don't have userPackage entry. */
+ return null;
+ }
+ return IoUsageStatsTable.queryHistoricalStats(
+ db, userPackage.getUniqueId(), startEpochSeconds, endEpochSeconds);
+ }
+ }
+
+ /**
+ * Deletes all user package settings and resource stats for all non-alive users.
+ *
+ * @param aliveUserIds Array of alive user ids.
+ */
+ public void syncUsers(int[] aliveUserIds) {
+ IntArray aliveUsers = IntArray.wrap(aliveUserIds);
+ for (int i = mUserPackagesByKey.size() - 1; i >= 0; --i) {
+ UserPackage userPackage = mUserPackagesByKey.valueAt(i);
+ if (aliveUsers.indexOf(userPackage.getUserId()) == -1) {
+ mUserPackagesByKey.removeAt(i);
+ mUserPackagesById.remove(userPackage.getUniqueId());
+ }
+ }
+ try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+ UserPackageSettingsTable.syncUserPackagesWithAliveUsers(db, aliveUsers);
+ }
+ }
+
+ @VisibleForTesting
+ boolean saveIoUsageStats(List<IoUsageStatsEntry> entries, boolean shouldCheckRetention) {
+ ZonedDateTime currentDate =
+ mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+ List<ContentValues> rows = new ArrayList<>(entries.size());
+ for (int i = 0; i < entries.size(); ++i) {
+ IoUsageStatsEntry entry = entries.get(i);
+ UserPackage userPackage = mUserPackagesByKey.get(
+ UserPackage.getKey(entry.userId, entry.packageName));
+ if (userPackage == null) {
+ Slogf.i(TAG, "Failed to find unique database id for user id '%d' and package '%s",
+ entry.userId, entry.packageName);
+ continue;
+ }
+ android.automotive.watchdog.IoOveruseStats ioOveruseStats =
+ entry.ioUsage.getInternalIoOveruseStats();
+ ZonedDateTime statsDate = Instant.ofEpochSecond(ioOveruseStats.startTime)
+ .atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+ if (shouldCheckRetention && STATS_TEMPORAL_UNIT.between(statsDate, currentDate)
+ >= RETENTION_PERIOD.get(STATS_TEMPORAL_UNIT)) {
+ continue;
+ }
+ long statsDateEpochSeconds = statsDate.toEpochSecond();
+ rows.add(IoUsageStatsTable.getContentValues(
+ userPackage.getUniqueId(), entry, statsDateEpochSeconds));
+ }
+ try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+ return atomicReplaceEntries(db, IoUsageStatsTable.TABLE_NAME, rows);
+ }
+ }
+
+ @VisibleForTesting
+ void setTimeSource(TimeSourceInterface timeSource) {
+ mTimeSource = timeSource;
+ mDbHelper.setTimeSource(timeSource);
+ }
+
+ private void populateUserPackages(SQLiteDatabase db, ArraySet<Integer> users) {
+ List<UserPackage> userPackages = UserPackageSettingsTable.queryUserPackages(db, users);
+ for (int i = 0; i < userPackages.size(); ++i) {
+ UserPackage userPackage = userPackages.get(i);
+ mUserPackagesByKey.put(userPackage.getKey(), userPackage);
+ mUserPackagesById.put(userPackage.getUniqueId(), userPackage);
+ }
+ }
+
+ private static boolean atomicReplaceEntries(
+ SQLiteDatabase db, String tableName, List<ContentValues> rows) {
+ if (rows.isEmpty()) {
+ return true;
+ }
+ try {
+ db.beginTransaction();
+ for (int i = 0; i < rows.size(); ++i) {
+ try {
+ if (db.replaceOrThrow(tableName, null, rows.get(i)) == -1) {
+ Slogf.e(TAG, "Failed to insert %s entry [%s]", tableName, rows.get(i));
+ return false;
+ }
+ } catch (SQLException e) {
+ Slog.e(TAG, "Failed to insert " + tableName + " entry [" + rows.get(i) + "]",
+ e);
+ return false;
+ }
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ return true;
+ }
+
+ /** Defines the user package settings entry stored in the UserPackageSettingsTable. */
+ static final class UserPackageSettingsEntry {
+ public final String packageName;
+ public final @UserIdInt int userId;
+ public final @KillableState int killableState;
+
+ UserPackageSettingsEntry(
+ @UserIdInt int userId, String packageName, @KillableState int killableState) {
+ this.userId = userId;
+ this.packageName = packageName;
+ this.killableState = killableState;
+ }
+ }
+
+ /**
+ * Defines the contents and queries for the user package settings table.
+ */
+ static final class UserPackageSettingsTable {
+ public static final String TABLE_NAME = "user_package_settings";
+ public static final String COLUMN_ROWID = "rowid";
+ public static final String COLUMN_PACKAGE_NAME = "package_name";
+ public static final String COLUMN_USER_ID = "user_id";
+ public static final String COLUMN_KILLABLE_STATE = "killable_state";
+
+ public static void createTable(SQLiteDatabase db) {
+ StringBuilder createCommand = new StringBuilder();
+ createCommand.append("CREATE TABLE ").append(TABLE_NAME).append(" (")
+ .append(COLUMN_PACKAGE_NAME).append(" TEXT NOT NULL, ")
+ .append(COLUMN_USER_ID).append(" INTEGER NOT NULL, ")
+ .append(COLUMN_KILLABLE_STATE).append(" INTEGER NOT NULL, ")
+ .append("PRIMARY KEY (").append(COLUMN_PACKAGE_NAME)
+ .append(", ").append(COLUMN_USER_ID).append("))");
+ db.execSQL(createCommand.toString());
+ Slogf.i(TAG, "Successfully created the %s table in the %s database version %d",
+ TABLE_NAME, WatchdogDbHelper.DATABASE_NAME, WatchdogDbHelper.DATABASE_VERSION);
+ }
+
+ public static ContentValues getContentValues(UserPackageSettingsEntry entry) {
+ ContentValues values = new ContentValues();
+ values.put(COLUMN_USER_ID, entry.userId);
+ values.put(COLUMN_PACKAGE_NAME, entry.packageName);
+ values.put(COLUMN_KILLABLE_STATE, entry.killableState);
+ return values;
+ }
+
+ public static ArrayMap<String, UserPackageSettingsEntry> querySettings(SQLiteDatabase db) {
+ StringBuilder queryBuilder = new StringBuilder();
+ queryBuilder.append("SELECT ")
+ .append(COLUMN_ROWID).append(", ")
+ .append(COLUMN_USER_ID).append(", ")
+ .append(COLUMN_PACKAGE_NAME).append(", ")
+ .append(COLUMN_KILLABLE_STATE)
+ .append(" FROM ").append(TABLE_NAME);
+
+ try (Cursor cursor = db.rawQuery(queryBuilder.toString(), new String[]{})) {
+ ArrayMap<String, UserPackageSettingsEntry> entriesById = new ArrayMap<>(
+ cursor.getCount());
+ while (cursor.moveToNext()) {
+ entriesById.put(cursor.getString(0), new UserPackageSettingsEntry(
+ cursor.getInt(1), cursor.getString(2), cursor.getInt(3)));
+ }
+ return entriesById;
+ }
+ }
+
+ public static List<UserPackage> queryUserPackages(
+ SQLiteDatabase db, ArraySet<Integer> users) {
+ StringBuilder queryBuilder = new StringBuilder();
+ queryBuilder.append("SELECT ")
+ .append(COLUMN_ROWID).append(", ")
+ .append(COLUMN_USER_ID).append(", ")
+ .append(COLUMN_PACKAGE_NAME)
+ .append(" FROM ").append(TABLE_NAME);
+ for (int i = 0; i < users.size(); ++i) {
+ if (i == 0) {
+ queryBuilder.append(" WHERE ").append(COLUMN_USER_ID).append(" IN (");
+ } else {
+ queryBuilder.append(", ");
+ }
+ queryBuilder.append(users.valueAt(i));
+ if (i == users.size() - 1) {
+ queryBuilder.append(")");
+ }
+ }
+
+ try (Cursor cursor = db.rawQuery(queryBuilder.toString(), new String[]{})) {
+ List<UserPackage> userPackages = new ArrayList<>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ userPackages.add(new UserPackage(
+ cursor.getString(0), cursor.getInt(1), cursor.getString(2)));
+ }
+ return userPackages;
+ }
+ }
+
+ public static void deleteUserPackage(SQLiteDatabase db, @UserIdInt int userId,
+ String packageName) {
+ String whereClause = COLUMN_USER_ID + "= ? and " + COLUMN_PACKAGE_NAME + "= ?";
+ String[] whereArgs = new String[]{String.valueOf(userId), packageName};
+ int deletedRows = db.delete(TABLE_NAME, whereClause, whereArgs);
+ Slogf.i(TAG, "Deleted %d user package settings db rows for user %d and package %s",
+ deletedRows, userId, packageName);
+ }
+
+ public static void syncUserPackagesWithAliveUsers(SQLiteDatabase db, IntArray aliveUsers) {
+ StringBuilder queryBuilder = new StringBuilder();
+ for (int i = 0; i < aliveUsers.size(); ++i) {
+ if (i == 0) {
+ queryBuilder.append(COLUMN_USER_ID).append(" NOT IN (");
+ } else {
+ queryBuilder.append(", ");
+ }
+ queryBuilder.append(aliveUsers.get(i));
+ if (i == aliveUsers.size() - 1) {
+ queryBuilder.append(")");
+ }
+ }
+ int deletedRows = db.delete(TABLE_NAME, queryBuilder.toString(), new String[]{});
+ Slogf.i(TAG, "Deleted %d user package settings db rows while syncing with alive users",
+ deletedRows);
+ }
+ }
+
+ /** Defines the I/O usage entry stored in the IoUsageStatsTable. */
+ static final class IoUsageStatsEntry {
+ public final @UserIdInt int userId;
+ public final String packageName;
+ public final WatchdogPerfHandler.PackageIoUsage ioUsage;
+
+ IoUsageStatsEntry(@UserIdInt int userId,
+ String packageName, WatchdogPerfHandler.PackageIoUsage ioUsage) {
+ this.userId = userId;
+ this.packageName = packageName;
+ this.ioUsage = ioUsage;
+ }
+ }
+
+ /**
+ * Defines the contents and queries for the I/O usage stats table.
+ */
+ static final class IoUsageStatsTable {
+ public static final String TABLE_NAME = "io_usage_stats";
+ public static final String COLUMN_USER_PACKAGE_ID = "user_package_id";
+ public static final String COLUMN_DATE_EPOCH = "date_epoch";
+ public static final String COLUMN_NUM_OVERUSES = "num_overuses";
+ public static final String COLUMN_NUM_FORGIVEN_OVERUSES = "num_forgiven_overuses";
+ public static final String COLUMN_NUM_TIMES_KILLED = "num_times_killed";
+ public static final String COLUMN_WRITTEN_FOREGROUND_BYTES = "written_foreground_bytes";
+ public static final String COLUMN_WRITTEN_BACKGROUND_BYTES = "written_background_bytes";
+ public static final String COLUMN_WRITTEN_GARAGE_MODE_BYTES = "written_garage_mode_bytes";
+ /* Below columns will be null for historical stats i.e., when the date != current date. */
+ public static final String COLUMN_REMAINING_FOREGROUND_WRITE_BYTES =
+ "remaining_foreground_write_bytes";
+ public static final String COLUMN_REMAINING_BACKGROUND_WRITE_BYTES =
+ "remaining_background_write_bytes";
+ public static final String COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES =
+ "remaining_garage_mode_write_bytes";
+ public static final String COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES =
+ "forgiven_foreground_write_bytes";
+ public static final String COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES =
+ "forgiven_background_write_bytes";
+ public static final String COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES =
+ "forgiven_garage_mode_write_bytes";
+
+ public static void createTable(SQLiteDatabase db) {
+ StringBuilder createCommand = new StringBuilder();
+ createCommand.append("CREATE TABLE ").append(TABLE_NAME).append(" (")
+ .append(COLUMN_USER_PACKAGE_ID).append(" INTEGER NOT NULL, ")
+ .append(COLUMN_DATE_EPOCH).append(" INTEGER NOT NULL, ")
+ .append(COLUMN_NUM_OVERUSES).append(" INTEGER NOT NULL, ")
+ .append(COLUMN_NUM_FORGIVEN_OVERUSES).append(" INTEGER NOT NULL, ")
+ .append(COLUMN_NUM_TIMES_KILLED).append(" INTEGER NOT NULL, ")
+ .append(COLUMN_WRITTEN_FOREGROUND_BYTES).append(" INTEGER, ")
+ .append(COLUMN_WRITTEN_BACKGROUND_BYTES).append(" INTEGER, ")
+ .append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append(" INTEGER, ")
+ .append(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES).append(" INTEGER, ")
+ .append(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES).append(" INTEGER, ")
+ .append(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES).append(" INTEGER, ")
+ .append(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES).append(" INTEGER, ")
+ .append(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES).append(" INTEGER, ")
+ .append(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES).append(" INTEGER, ")
+ .append("PRIMARY KEY (").append(COLUMN_USER_PACKAGE_ID).append(", ")
+ .append(COLUMN_DATE_EPOCH).append("), FOREIGN KEY (")
+ .append(COLUMN_USER_PACKAGE_ID).append(") REFERENCES ")
+ .append(UserPackageSettingsTable.TABLE_NAME).append(" (")
+ .append(UserPackageSettingsTable.COLUMN_ROWID).append(") ON DELETE CASCADE )");
+ db.execSQL(createCommand.toString());
+ Slogf.i(TAG, "Successfully created the %s table in the %s database version %d",
+ TABLE_NAME, WatchdogDbHelper.DATABASE_NAME, WatchdogDbHelper.DATABASE_VERSION);
+ }
+
+ public static ContentValues getContentValues(
+ String userPackageId, IoUsageStatsEntry entry, long statsDateEpochSeconds) {
+ android.automotive.watchdog.IoOveruseStats ioOveruseStats =
+ entry.ioUsage.getInternalIoOveruseStats();
+ ContentValues values = new ContentValues();
+ values.put(COLUMN_USER_PACKAGE_ID, userPackageId);
+ values.put(COLUMN_DATE_EPOCH, statsDateEpochSeconds);
+ values.put(COLUMN_NUM_OVERUSES, ioOveruseStats.totalOveruses);
+ /* TODO(b/195425666): Put total forgiven overuses for the day. */
+ values.put(COLUMN_NUM_FORGIVEN_OVERUSES, 0);
+ values.put(COLUMN_NUM_TIMES_KILLED, entry.ioUsage.getTotalTimesKilled());
+ values.put(
+ COLUMN_WRITTEN_FOREGROUND_BYTES, ioOveruseStats.writtenBytes.foregroundBytes);
+ values.put(
+ COLUMN_WRITTEN_BACKGROUND_BYTES, ioOveruseStats.writtenBytes.backgroundBytes);
+ values.put(
+ COLUMN_WRITTEN_GARAGE_MODE_BYTES, ioOveruseStats.writtenBytes.garageModeBytes);
+ values.put(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES,
+ ioOveruseStats.remainingWriteBytes.foregroundBytes);
+ values.put(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES,
+ ioOveruseStats.remainingWriteBytes.backgroundBytes);
+ values.put(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES,
+ ioOveruseStats.remainingWriteBytes.garageModeBytes);
+ android.automotive.watchdog.PerStateBytes forgivenWriteBytes =
+ entry.ioUsage.getForgivenWriteBytes();
+ values.put(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES, forgivenWriteBytes.foregroundBytes);
+ values.put(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES, forgivenWriteBytes.backgroundBytes);
+ values.put(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES, forgivenWriteBytes.garageModeBytes);
+ return values;
+ }
+
+ public static ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> queryStats(
+ SQLiteDatabase db, long startEpochSeconds, long endEpochSeconds) {
+ StringBuilder queryBuilder = new StringBuilder();
+ queryBuilder.append("SELECT ")
+ .append(COLUMN_USER_PACKAGE_ID).append(", ")
+ .append("MIN(").append(COLUMN_DATE_EPOCH).append("), ")
+ .append("SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
+ .append("SUM(").append(COLUMN_NUM_TIMES_KILLED).append("), ")
+ .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ")
+ .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ")
+ .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ")
+ .append("SUM(").append(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES).append("), ")
+ .append("SUM(").append(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES).append("), ")
+ .append("SUM(").append(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES).append("), ")
+ .append("SUM(").append(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES).append("), ")
+ .append("SUM(").append(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES).append("), ")
+ .append("SUM(").append(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES).append(") ")
+ .append("FROM ").append(TABLE_NAME).append(" WHERE ")
+ .append(COLUMN_DATE_EPOCH).append(">= ? and ")
+ .append(COLUMN_DATE_EPOCH).append("< ? GROUP BY ")
+ .append(COLUMN_USER_PACKAGE_ID);
+ String[] selectionArgs = new String[]{
+ String.valueOf(startEpochSeconds), String.valueOf(endEpochSeconds)};
+
+ ArrayMap<String, WatchdogPerfHandler.PackageIoUsage> ioUsageById = new ArrayMap<>();
+ try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
+ while (cursor.moveToNext()) {
+ android.automotive.watchdog.IoOveruseStats ioOveruseStats =
+ new android.automotive.watchdog.IoOveruseStats();
+ ioOveruseStats.startTime = cursor.getLong(1);
+ ioOveruseStats.durationInSeconds = endEpochSeconds - startEpochSeconds;
+ ioOveruseStats.totalOveruses = cursor.getInt(2);
+ ioOveruseStats.writtenBytes = new PerStateBytes();
+ ioOveruseStats.writtenBytes.foregroundBytes = cursor.getLong(4);
+ ioOveruseStats.writtenBytes.backgroundBytes = cursor.getLong(5);
+ ioOveruseStats.writtenBytes.garageModeBytes = cursor.getLong(6);
+ ioOveruseStats.remainingWriteBytes = new PerStateBytes();
+ ioOveruseStats.remainingWriteBytes.foregroundBytes = cursor.getLong(7);
+ ioOveruseStats.remainingWriteBytes.backgroundBytes = cursor.getLong(8);
+ ioOveruseStats.remainingWriteBytes.garageModeBytes = cursor.getLong(9);
+ PerStateBytes forgivenWriteBytes = new PerStateBytes();
+ forgivenWriteBytes.foregroundBytes = cursor.getLong(10);
+ forgivenWriteBytes.backgroundBytes = cursor.getLong(11);
+ forgivenWriteBytes.garageModeBytes = cursor.getLong(12);
+
+ ioUsageById.put(cursor.getString(0), new WatchdogPerfHandler.PackageIoUsage(
+ ioOveruseStats, forgivenWriteBytes, cursor.getInt(3)));
+ }
+ }
+ return ioUsageById;
+ }
+
+ public static IoOveruseStats queryHistoricalStats(SQLiteDatabase db, String uniqueId,
+ long startEpochSeconds, long endEpochSeconds) {
+ StringBuilder queryBuilder = new StringBuilder();
+ queryBuilder.append("SELECT SUM(").append(COLUMN_NUM_OVERUSES).append("), ")
+ .append("SUM(").append(COLUMN_NUM_TIMES_KILLED).append("), ")
+ .append("SUM(").append(COLUMN_WRITTEN_FOREGROUND_BYTES).append("), ")
+ .append("SUM(").append(COLUMN_WRITTEN_BACKGROUND_BYTES).append("), ")
+ .append("SUM(").append(COLUMN_WRITTEN_GARAGE_MODE_BYTES).append("), ")
+ .append("MIN(").append(COLUMN_DATE_EPOCH).append(") ")
+ .append("FROM ").append(TABLE_NAME).append(" WHERE ")
+ .append(COLUMN_USER_PACKAGE_ID).append("=? and ")
+ .append(COLUMN_DATE_EPOCH).append(" >= ? and ")
+ .append(COLUMN_DATE_EPOCH).append("< ?");
+ String[] selectionArgs = new String[]{uniqueId,
+ String.valueOf(startEpochSeconds), String.valueOf(endEpochSeconds)};
+ long totalOveruses = 0;
+ long totalTimesKilled = 0;
+ long totalBytesWritten = 0;
+ long earliestEpochSecond = endEpochSeconds;
+ try (Cursor cursor = db.rawQuery(queryBuilder.toString(), selectionArgs)) {
+ if (cursor.getCount() == 0) {
+ return null;
+ }
+ while (cursor.moveToNext()) {
+ totalOveruses += cursor.getLong(0);
+ totalTimesKilled += cursor.getLong(1);
+ totalBytesWritten += cursor.getLong(2) + cursor.getLong(3) + cursor.getLong(4);
+ earliestEpochSecond = Math.min(cursor.getLong(5), earliestEpochSecond);
+ }
+ }
+ if (totalBytesWritten == 0) {
+ return null;
+ }
+ long durationInSeconds = endEpochSeconds - earliestEpochSecond;
+ IoOveruseStats.Builder statsBuilder = new IoOveruseStats.Builder(
+ earliestEpochSecond, durationInSeconds);
+ statsBuilder.setTotalOveruses(totalOveruses);
+ statsBuilder.setTotalTimesKilled(totalTimesKilled);
+ statsBuilder.setTotalBytesWritten(totalBytesWritten);
+ return statsBuilder.build();
+ }
+
+ public static void truncateToDate(SQLiteDatabase db, ZonedDateTime latestTruncateDate) {
+ String selection = COLUMN_DATE_EPOCH + " <= ?";
+ String[] selectionArgs = { String.valueOf(latestTruncateDate.toEpochSecond()) };
+
+ int rows = db.delete(TABLE_NAME, selection, selectionArgs);
+ Slogf.i(TAG, "Truncated %d I/O usage stats entries on pid %d", rows, Process.myPid());
+ }
+
+ public static void trimHistoricalStats(SQLiteDatabase db, ZonedDateTime currentDate) {
+ ContentValues values = new ContentValues();
+ values.putNull(COLUMN_REMAINING_FOREGROUND_WRITE_BYTES);
+ values.putNull(COLUMN_REMAINING_BACKGROUND_WRITE_BYTES);
+ values.putNull(COLUMN_REMAINING_GARAGE_MODE_WRITE_BYTES);
+ values.putNull(COLUMN_FORGIVEN_FOREGROUND_WRITE_BYTES);
+ values.putNull(COLUMN_FORGIVEN_BACKGROUND_WRITE_BYTES);
+ values.putNull(COLUMN_FORGIVEN_GARAGE_MODE_WRITE_BYTES);
+
+ String selection = COLUMN_DATE_EPOCH + " < ?";
+ String[] selectionArgs = { String.valueOf(currentDate.toEpochSecond()) };
+
+ int rows = db.update(TABLE_NAME, values, selection, selectionArgs);
+ Slogf.i(TAG, "Trimmed %d I/O usage stats entries on pid %d", rows, Process.myPid());
+ }
+ }
+
+ /**
+ * Defines the Watchdog database and database level operations.
+ */
+ static final class WatchdogDbHelper extends SQLiteOpenHelper {
+ public static final String DATABASE_DIR = "/data/system/car/watchdog/";
+ public static final String DATABASE_NAME = "car_watchdog.db";
+
+ private static final int DATABASE_VERSION = 1;
+
+ private ZonedDateTime mLatestShrinkDate;
+ private TimeSourceInterface mTimeSource = SYSTEM_INSTANCE;
+
+ WatchdogDbHelper(Context context, boolean useDataSystemCarDir) {
+ /* Use device protected storage because CarService may need to access the database
+ * before the user has authenticated.
+ */
+ super(context.createDeviceProtectedStorageContext(),
+ useDataSystemCarDir ? DATABASE_DIR + DATABASE_NAME : DATABASE_NAME,
+ /* name= */ null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ UserPackageSettingsTable.createTable(db);
+ IoUsageStatsTable.createTable(db);
+ }
+
+ public synchronized void close() {
+ super.close();
+
+ mLatestShrinkDate = null;
+ }
+
+ public void onShrink(SQLiteDatabase db) {
+ ZonedDateTime currentDate =
+ mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(ChronoUnit.DAYS);
+ if (currentDate.equals(mLatestShrinkDate)) {
+ return;
+ }
+ IoUsageStatsTable.truncateToDate(db, currentDate.minus(RETENTION_PERIOD));
+ IoUsageStatsTable.trimHistoricalStats(db, currentDate);
+ mLatestShrinkDate = currentDate;
+ Slogf.i(TAG, "Shrunk watchdog database for the date '%s'", mLatestShrinkDate);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
+ /* Still on the 1st version so no upgrade required. */
+ }
+
+ void setTimeSource(TimeSourceInterface timeSource) {
+ mTimeSource = timeSource;
+ }
+ }
+
+ private static final class UserPackage {
+ private final String mUniqueId;
+ private final int mUserId;
+ private final String mPackageName;
+
+ UserPackage(String uniqueId, int userId, String packageName) {
+ mUniqueId = uniqueId;
+ mUserId = userId;
+ mPackageName = packageName;
+ }
+
+ String getKey() {
+ return getKey(mUserId, mPackageName);
+ }
+
+ static String getKey(int userId, String packageName) {
+ return String.format(Locale.ENGLISH, "%d:%s", userId, packageName);
+ }
+
+ public String getUniqueId() {
+ return mUniqueId;
+ }
+
+ public int getUserId() {
+ return mUserId;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ }
+}
diff --git a/service/src/com/android/car/watchdog/proto/Android.bp b/service/src/com/android/car/watchdog/proto/Android.bp
new file mode 100644
index 0000000..6faa912
--- /dev/null
+++ b/service/src/com/android/car/watchdog/proto/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "carwatchdog-protos",
+ proto: {
+ type: "lite",
+ },
+ sdk_version: "module_current",
+ min_sdk_version: "31",
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.car.framework",
+ ],
+ srcs: [
+ "atoms.proto",
+ ],
+}
diff --git a/service/src/com/android/car/watchdog/proto/atoms.proto b/service/src/com/android/car/watchdog/proto/atoms.proto
new file mode 100644
index 0000000..95f6210
--- /dev/null
+++ b/service/src/com/android/car/watchdog/proto/atoms.proto
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+// Partial clone of frameworks/proto_logging/stats/atoms.proto. CarWatchdogService only uses small
+// number of atoms.
+
+syntax = "proto2";
+
+package android.car.watchdog.statsd;
+option java_package = "com.android.car.watchdog";
+option java_outer_classname = "AtomsProto";
+
+/**
+ * Logs the current state of an application/process before it is killed.
+ *
+ * Keep in sync with proto file at frameworks/proto_logging/stats/atoms.proto
+ */
+message CarWatchdogKillStatsReported {
+ // Linux process uid for the package.
+ optional int32 uid = 1;
+
+ // State of the uid when it was killed.
+ enum UidState {
+ UNKNOWN_UID_STATE = 0;
+ BACKGROUND_MODE = 1;
+ FOREGROUND_MODE = 2;
+ }
+ optional UidState uid_state = 2;
+
+ // System state indicating whether the system was in normal mode or garage mode.
+ enum SystemState {
+ UNKNOWN_SYSTEM_STATE = 0;
+ USER_INTERACTION_MODE = 1;
+ USER_NO_INTERACTION_MODE = 2;
+ GARAGE_MODE = 3;
+ }
+ optional SystemState system_state = 3;
+
+ // Reason for killing the application.
+ // Keep in sync with proto file at packages/services/Car/cpp/watchdog/proto
+ enum KillReason {
+ UNKNOWN_KILL_REASON = 0;
+ KILLED_ON_ANR = 1;
+ KILLED_ON_IO_OVERUSE = 2;
+ KILLED_ON_MEMORY_OVERUSE = 3;
+ }
+ optional KillReason kill_reason = 4;
+
+ // Stats of the processes owned by the application when the application was killed.
+ // The process stack traces are not collected when the application was killed due to IO_OVERUSE.
+ optional CarWatchdogProcessStats process_stats = 5;
+
+ // The application's I/O overuse stats logged only when the kill reason is KILLED_ON_IO_OVERUSE.
+ optional CarWatchdogIoOveruseStats io_overuse_stats = 6;
+}
+
+/**
+ * Logs the I/O overuse stats for an application on detecting I/O overuse.
+ *
+ * Keep in sync with proto file at frameworks/proto_logging/stats/atoms.proto
+ */
+message CarWatchdogIoOveruseStatsReported {
+ // Linux process uid for the package.
+ optional int32 uid = 1;
+
+ // The application's I/O overuse stats.
+ optional CarWatchdogIoOveruseStats io_overuse_stats = 2;
+}
+
+/**
+ * Logs I/O overuse stats for a package.
+ *
+ * Keep in sync with proto file at frameworks/proto_logging/stats/atoms.proto
+ */
+message CarWatchdogIoOveruseStats {
+ enum Period {
+ UNKNOWN_PERIOD = 0;
+ DAILY = 1;
+ WEEKLY = 2;
+ }
+
+ // Threshold and usage stats period.
+ optional Period period = 1;
+
+ // Threshold in-terms of write bytes defined for the package.
+ optional CarWatchdogPerStateBytes threshold = 2;
+
+ // Number of write bytes in each state for the specified period.
+ optional CarWatchdogPerStateBytes written_bytes = 3;
+
+ // Application or service uptime during the aforemetioned period.
+ optional uint64 uptime_millis = 4;
+};
+
+/**
+ * Logs bytes attributed to each application and system states.
+ *
+ * Keep in sync with proto file at frameworks/proto_logging/stats/atoms.proto
+ */
+message CarWatchdogPerStateBytes {
+ // Number of bytes attributed to the application foreground.
+ optional int64 foreground_bytes = 1;
+
+ // Number of bytes attributed to the application background.
+ optional int64 background_bytes = 2;
+
+ // Number of bytes attributed to the garage mode.
+ optional int64 garage_mode_bytes = 3;
+}
+
+/**
+ * Logs each CarWatchdogProcessStat in CarWatchdogProcessStats.
+ *
+ * Keep in sync with proto file at frameworks/proto_logging/stats/atoms.proto
+ */
+message CarWatchdogProcessStats {
+ // Records the stats of the processes owned by an application.
+ repeated CarWatchdogProcessStat process_stat = 1;
+}
+
+/**
+ * Logs a process's stats.
+ *
+ * Keep in sync with proto file at frameworks/proto_logging/stats/atoms.proto
+ */
+message CarWatchdogProcessStat {
+ // Command name of the process.
+ optional string process_name = 1;
+
+ // Process uptime.
+ optional uint64 uptime_millis = 2;
+
+ // Number of major page faults caused by the process and its children.
+ optional uint64 major_page_faults = 3;
+
+ // Peak virtual memory size in kb.
+ optional uint64 vm_peak_kb = 4;
+
+ // Virtual memory size in kb.
+ optional uint64 vm_size_kb = 5;
+
+ // Peak resident set size (high water mark) in kb.
+ optional uint64 vm_hwm_kb = 6;
+
+ // Resident set size in kb.
+ optional uint64 vm_rss_kb = 7;
+}
diff --git a/tests/AdasLocationTestApp/Android.bp b/tests/AdasLocationTestApp/Android.bp
new file mode 100644
index 0000000..cc52ec7
--- /dev/null
+++ b/tests/AdasLocationTestApp/Android.bp
@@ -0,0 +1,51 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "AdasLocationTestApp",
+
+ srcs: ["src/**/*.java"],
+
+ resource_dirs: ["res"],
+
+ platform_apis: true,
+
+ optimize: {
+ enabled: false,
+ },
+
+ enforce_uses_libs: false,
+ dex_preopt: {
+ enabled: false,
+ },
+
+ required: ["allowed_privapp_com.google.android.car.adaslocation"],
+
+ privileged: true,
+
+ certificate: "platform",
+
+ static_libs: [
+ "com.google.android.material_material",
+ "androidx.appcompat_appcompat",
+ ],
+
+ libs: ["android.car"],
+}
\ No newline at end of file
diff --git a/tests/AdasLocationTestApp/AndroidManifest.xml b/tests/AdasLocationTestApp/AndroidManifest.xml
new file mode 100644
index 0000000..6dcd183
--- /dev/null
+++ b/tests/AdasLocationTestApp/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.car.adaslocation"
+ android:sharedUserId="android.uid.system">
+
+ <!-- The app needs to access device location to verify ADAS and main location switch work as
+ expected. -->
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+
+ <application android:label="AdasLocationTestApp">
+ <activity android:name=".AdasLocationActivity"
+ android:theme="@style/Theme.AppCompat"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/tests/AdasLocationTestApp/res/layout/main_activity.xml b/tests/AdasLocationTestApp/res/layout/main_activity.xml
new file mode 100644
index 0000000..0173cd6
--- /dev/null
+++ b/tests/AdasLocationTestApp/res/layout/main_activity.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="fill_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/main_location_enabled"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="100dp"
+ android:textStyle="bold"
+ android:text="main location enabled: "/>
+ <TextView
+ android:id="@+id/main_location_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@id/main_location_enabled"
+ android:layout_marginTop="100dp"
+ android:textStyle="bold"/>
+ <TextView
+ android:id="@+id/adas_location_enabled"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@id/main_location_enabled"
+ android:textStyle="bold"
+ android:text="adas location enabled: "/>
+ <TextView
+ android:id="@+id/adas_location_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/main_location_status"
+ android:layout_toRightOf="@id/adas_location_enabled"
+ android:textStyle="bold"/>
+ <TextView
+ android:id="@+id/current_location"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/current_location"
+ android:layout_alignParentLeft="true"
+ android:layout_marginLeft="350dp"
+ android:layout_marginTop= "200dp"
+ android:textStyle="bold"/>
+ <TextView
+ android:id="@+id/current_location_result"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/current_location"
+ android:layout_alignParentLeft="true"
+ android:layout_marginLeft="350dp"
+ android:textStyle="bold"/>
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/observe_fab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/current_location_result"
+ android:layout_alignParentLeft="true"
+ app:useCompatPadding="true"
+ android:layout_marginLeft="350dp"/>
+ <TextView
+ android:id="@+id/last_location"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/last_location"
+ android:layout_alignParentRight="true"
+ android:layout_marginRight="350dp"
+ android:layout_marginTop= "200dp"
+ android:textStyle="bold"/>
+ <TextView
+ android:id="@+id/last_location_result"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/last_location"
+ android:layout_alignParentRight="true"
+ android:layout_marginRight="350dp"
+ android:textStyle="bold"/>
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/query_fab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/last_location_result"
+ android:layout_alignParentRight="true"
+ app:useCompatPadding="true"
+ android:layout_marginRight="350dp"/>
+</RelativeLayout>
diff --git a/tests/AdasLocationTestApp/res/values/strings.xml b/tests/AdasLocationTestApp/res/values/strings.xml
new file mode 100644
index 0000000..3b97648
--- /dev/null
+++ b/tests/AdasLocationTestApp/res/values/strings.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="last_location" translatable="false">Last Location</string>
+ <string name="current_location" translatable="false">Current Location</string>
+ <string name="no_last_location" translatable="false">no last location</string>
+ <string name="waiting_for_location" translatable="false">waiting for location</string>
+</resources>
\ No newline at end of file
diff --git a/tests/AdasLocationTestApp/src/com/google/android/car/adaslocation/AdasLocationActivity.java b/tests/AdasLocationTestApp/src/com/google/android/car/adaslocation/AdasLocationActivity.java
new file mode 100644
index 0000000..bcb29f1
--- /dev/null
+++ b/tests/AdasLocationTestApp/src/com/google/android/car/adaslocation/AdasLocationActivity.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2021 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.google.android.car.adaslocation;
+
+import android.Manifest;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public final class AdasLocationActivity extends AppCompatActivity {
+ private static final int KS_PERMISSIONS_REQUEST = 1;
+
+ private static final String[] REQUIRED_PERMISSIONS = new String[]{
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ };
+
+ private boolean mIsRegister;
+ private LocationManager mLocationManager;
+ private FloatingActionButton mObserveFab;
+ private TextView mObserveLocationResult;
+ private LocationListener mLocationListener;
+ private FloatingActionButton mQueryFab;
+ private TextView mLastLocationResult;
+ private TextView mMainLocationEnabled;
+ private TextView mAdasLocationEnabled;
+ private BroadcastReceiver mReceiver;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main_activity);
+
+ mLocationManager = getApplicationContext().getSystemService(LocationManager.class);
+ mObserveFab = findViewById(R.id.observe_fab);
+ mObserveLocationResult = findViewById(R.id.current_location_result);
+ mLocationListener = new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ mObserveLocationResult.setText(locationToFormattedString(location));
+ }
+
+ @Override
+ public void onProviderEnabled(String provider) {
+ }
+
+ @Override
+ public void onProviderDisabled(String provider) {
+ }
+ };
+ mObserveFab.setOnClickListener(
+ v -> {
+ if (!mIsRegister) {
+ startListening();
+ } else {
+ stopListening();
+ }
+ }
+ );
+ mQueryFab = findViewById(R.id.query_fab);
+ mLastLocationResult = findViewById(R.id.last_location_result);
+ mQueryFab.setOnClickListener(
+ v -> {
+ Location location = mLocationManager
+ .getLastKnownLocation(LocationManager.GPS_PROVIDER);
+ if (location != null) {
+ mLastLocationResult.setText(locationToFormattedString(location));
+ } else {
+ mLastLocationResult.setText(R.string.no_last_location);
+ }
+ }
+ );
+
+ mReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (LocationManager.MODE_CHANGED_ACTION == intent.getAction()) {
+ mMainLocationEnabled.setText(Boolean.toString(mLocationManager
+ .isLocationEnabled()));
+ return;
+ }
+ if (LocationManager.ACTION_ADAS_GNSS_ENABLED_CHANGED
+ == intent.getAction()) {
+ mAdasLocationEnabled
+ .setText(Boolean.toString(mLocationManager
+ .isAdasGnssLocationEnabled()));
+ return;
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ initPermissions();
+
+ mMainLocationEnabled = findViewById(R.id.main_location_status);
+ mMainLocationEnabled.setText(Boolean.toString(mLocationManager.isLocationEnabled()));
+ mAdasLocationEnabled = findViewById(R.id.adas_location_status);
+ mAdasLocationEnabled.setText(Boolean.toString(mLocationManager
+ .isAdasGnssLocationEnabled()));
+
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(LocationManager.MODE_CHANGED_ACTION);
+ intentFilter.addAction(LocationManager.ACTION_ADAS_GNSS_ENABLED_CHANGED);
+ registerReceiver(mReceiver, intentFilter);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ Set<String> missingPermissions = checkExistingPermissions();
+ if (!missingPermissions.isEmpty()) {
+ return;
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (mIsRegister) {
+ stopListening();
+ }
+ mLastLocationResult.setText("");
+ }
+
+ private static String locationToFormattedString(Location location) {
+ return String.format("Location: lat=%10.6f, lon=%10.6f ",
+ location.getLatitude(),
+ location.getLongitude());
+ }
+
+ private void initPermissions() {
+ Set<String> missingPermissions = checkExistingPermissions();
+ if (!missingPermissions.isEmpty()) {
+ requestPermissions(missingPermissions);
+ }
+ }
+
+ private Set<String> checkExistingPermissions() {
+ return Arrays.stream(REQUIRED_PERMISSIONS).filter(permission -> ActivityCompat
+ .checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED)
+ .collect(Collectors.toSet());
+ }
+
+ private void requestPermissions(Set<String> permissions) {
+ requestPermissions(permissions.toArray(new String[permissions.size()]),
+ KS_PERMISSIONS_REQUEST);
+ }
+
+ private void startListening() {
+ mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
+ 0, 0, mLocationListener);
+ mObserveLocationResult.setText(R.string.waiting_for_location);
+ mIsRegister = true;
+ }
+
+ private void stopListening() {
+ mLocationManager.removeUpdates(mLocationListener);
+ mObserveLocationResult.setText("");
+ mIsRegister = false;
+ }
+}
diff --git a/tests/BugReportApp/res/values-af/strings.xml b/tests/BugReportApp/res/values-af/strings.xml
index 9891c46..0e53cef 100644
--- a/tests/BugReportApp/res/values-af/strings.xml
+++ b/tests/BugReportApp/res/values-af/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Praat en beskryf die kwessie"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Oudioboodskap vir foutverslag om %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Opname is klaar"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"\'n Foutverslag word reeds saamgestel"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Foutverslag is aan die gang"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"\'n Foutverslag is saamgestel"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Voeg oudio by"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Voeg oudio by en Laai op"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Laai op"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Laai op na GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Gee asseblief toestemmings"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Foutverslag word reeds saamgestel"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Foutverslaggewing het begin"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Foutverslag is aan die gang"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Foutverslag is begin"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Foutverslag het misluk"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Kon nie skerm opneem nie"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Stortingstand het misluk"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate het misluk"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Foutverslag is aan die gang"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Foutverslag is saamgestel"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Foutverslag is gaan haal"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Foutverslag se statuskanaal"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-am/strings.xml b/tests/BugReportApp/res/values-am/strings.xml
index 9013eac..83f7059 100644
--- a/tests/BugReportApp/res/values-am/strings.xml
+++ b/tests/BugReportApp/res/values-am/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"ችግሩን ይናገሩ እና ይግለጹ"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s ላይ የሳንካ ሪፖርት የኦዲዮ መልእከት"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"ቀረጻ አልቋል"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"አንድ የሳንካ ሪፖርት አስቀድሞ በመሰብሰብ ላይ ነው"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"የሳንካ ሪፖርት በሂደት ላይ ነው"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"አንድ የሳንካ ሪፖርት ተሰብስቧል"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ኦዲዮ አክል"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ኦዲዮን አክል እና ስቀል"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"ስቀል"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"ወደ GCS ስቀል"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"እባክዎ ፈቃዶችን ይስጡ"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"የሳንካ ሪፖርት አስቀድሞ በመሰብሰብ ላይ ነው"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"ሳንካን ሪፖርት ማድረግ ስራ ጀምሯል"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"የሳንካ ሪፖርት በሂደት ላይ ነው"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"የሳንካ ሪፖርት ተጀምሯል"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"የሳንካ ሪፖርት አልተሳካም"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"ማያ ገጽን ማንሳት አልተሳካም"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"የስህተት ምዝግብ አልተሳካም"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"የስህተት ምዝግብ አልተሳካም"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"የሳንካ ሪፖርት በሂደት ላይ ነው"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"የሳንካ ሪፖርት ተሰብስቧል"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"የሳንካ ሪፖርት ተሰብስቧል"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"የሳንካ ሪፖርት ሁኔታ ሰርጥ"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ar/strings.xml b/tests/BugReportApp/res/values-ar/strings.xml
index 5ffd7e2..8a5f6af 100644
--- a/tests/BugReportApp/res/values-ar/strings.xml
+++ b/tests/BugReportApp/res/values-ar/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"التحدث ووصف المشكلة"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"رسالة صوتية لتقرير الخطأ في %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"انتهى التسجيل."</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"جارٍ جمع تقرير أخطاء."</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"تجميع تقرير الخطأ قيد التقدّم."</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"تم جمع تقرير أخطاء."</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"إضافة رسالة صوتية"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"إضافة رسالة صوتية والتحميل"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"تحميل"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"تحميل إلى GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"يُرجى منح الأذونات."</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"جارٍ جمع تقرير الأخطاء."</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"بدأ إجراء تقرير الخطأ."</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"تجميع تقرير الخطأ قيد التقدّم."</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"بدأ تقرير الخطأ."</string>
<string name="toast_status_failed" msgid="6365384202315043395">"تعذّر إعداد تقرير الأخطاء."</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"تعذّر الحصول على لقطة شاشة."</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"تعذّر نسخ الحالة."</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"تعذّر بدء خدمة Dumpstate."</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"تقرير الأخطاء قيد التقدّم."</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"تم جمع تقرير الأخطاء."</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"تم تجميع تقرير الخطأ."</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"قناة حالة تقرير الأخطاء"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-as/strings.xml b/tests/BugReportApp/res/values-as/strings.xml
index 2b6c382..41609af 100644
--- a/tests/BugReportApp/res/values-as/strings.xml
+++ b/tests/BugReportApp/res/values-as/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"কওক আৰু সমস্যাটোৰ বিষয়ে বৰ্ণনা কৰক"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%sত বাগ ৰিপ’ৰ্টৰ বাবে অডিঅ’ বাৰ্তা"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"ৰেকৰ্ড কৰা সম্পূৰ্ণ হ’ল"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"এটা বাগ ৰিপ’ৰ্ট ইতিমধ্যে সংগ্ৰহ কৰি থকা হৈছে"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"বাগ ৰিপ’ৰ্ট প্ৰগতিত আছে"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"এটা বাগ ৰিপ’ৰ্ট সংগ্ৰহ কৰা হৈছে"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"অডিঅ’ যোগ কৰক"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"অডিঅ’ যোগ কৰক আৰু আপল’ড কৰক"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"আপল’ড কৰক"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCSত আপল’ড কৰক"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"অনুগ্ৰহ কৰি অনুমতি প্ৰদান কৰক"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"বাগ ৰিপ’ৰ্ট ইতিমধ্যে সংগ্ৰহ কৰি থকা হৈছে"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"বাগ ৰিপ’ৰ্ট কৰাটো আৰম্ভ হৈছে"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"বাগ ৰিপ’ৰ্ট প্ৰগতিত আছে"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"বাগ ৰিপ’ৰ্ট আৰম্ভ কৰা হৈছে"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"বাগ ৰিপ’ৰ্ট বিফল হ’ল"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"স্ক্রীন কেপচাৰ কৰাত বিফল হ’ল"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ডাম্প ষ্টেট বিফল হ’ল"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"ডাম্পষ্টেট বিফল হ’ল"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"বাগ ৰিপ’ৰ্ট প্ৰগতিত আছে"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"বাগ ৰিপ’র্ট সংগ্ৰহ কৰা হৈছে"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"বাগ ৰিপ’ৰ্ট সংগ্ৰহ কৰা হৈছে"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"বাগ ৰিপ’ৰ্টৰ স্থিতিৰ চেনেল"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-az/strings.xml b/tests/BugReportApp/res/values-az/strings.xml
index 8fea2ff..fc5129d 100644
--- a/tests/BugReportApp/res/values-az/strings.xml
+++ b/tests/BugReportApp/res/values-az/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Danışın & Məsələni təsvir edin"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s radələrində baq raportu üçün Audio mesaj"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Yazma bitdi"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Baq hesabatı hazırda əldə edilir"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Baq hesabatı davam edir"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Baq hesabatı əldə edilib"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Audio əlavə edin"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Audio əlavə edin və yükləyin"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Yükləyin"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS\'a yükləyin"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"İcazələri təmin edin"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Baq hesabatı hazırda əldə edilir"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Baq raportlaması başladı"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Baq hesabatı davam edir"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Baq hesabatı başladılıb"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Baq hesabatı alınmadı"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Ekran çəkilişi alınmadı"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dampinq alınmadı"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dampinq alınmadı"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Baq hesabatı davam edir"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Baq hesabatı əldə edilib"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Baq hesabatı əldə edilib"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Baq hesabatı status kanalı"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-b+sr+Latn/strings.xml b/tests/BugReportApp/res/values-b+sr+Latn/strings.xml
index 243ec44..8340b2a 100644
--- a/tests/BugReportApp/res/values-b+sr+Latn/strings.xml
+++ b/tests/BugReportApp/res/values-b+sr+Latn/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Izgovorite i objasnite problem"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Audio poruka za izveštaj o grešci u %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Snimanje je završeno"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Izveštaj o grešci se već prikuplja"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"U toku je pravljenje izveštaja o grešci"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Prikupljen je izveštaj o grešci"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Dodaj audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Dodaj audio i otpremi"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Otpremi"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Otpremi u GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Dajte dozvole"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Izveštaj o grešci se već prikuplja"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Pokrenut je izveštaj o grešci"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"U toku je pravljenje izveštaja o grešci"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Izveštaj o grešci je pokrenut"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Pravljenje izveštaja o grešci nije uspelo"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Snimanje ekrana nije uspelo"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Snimanje stanja nije uspelo"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Snimanje stanja nije uspelo"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"U toku je pravljenje izveštaja o grešci"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Izveštaj o grešci je prikupljen"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Izveštaj o grešci je prikupljen"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kanal statusa izveštaja o grešci"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-be/strings.xml b/tests/BugReportApp/res/values-be/strings.xml
index 41b83d9..14df116 100644
--- a/tests/BugReportApp/res/values-be/strings.xml
+++ b/tests/BugReportApp/res/values-be/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Апісанне праблемы"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Галасавое паведамленне для справаздачы пра памылкі ў %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Запіс скончаны"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Справаздача пра памылкі ўжо складаецца"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Справаздача пра памылку апрацоўваецца"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Справаздача пра памылкі складзена"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Дадаць аўдыя"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Дадаць аўдыя і запампаваць"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Запампаваць"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Запампаваць у GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Дайце дазволы"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Справаздача пра памылкі ўжо складаецца"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Запушчана справаздача пра памылкі"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Справаздача пра памылку апрацоўваецца"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Пачалося стварэнне справаздачы пра памылку"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Не ўдалося стварыць справаздачу пра памылкі"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Не ўдалося зрабіць здымак экрана"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Не ўдалося стварыць дамп стану"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Збой сэрвісу Dumpstate"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Справаздача пра памылкі апрацоўваецца"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Справаздача пра памылкі складзена"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Справаздача пра памылку загружана"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Канал стану справаздачы пра памылкі"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-bg/strings.xml b/tests/BugReportApp/res/values-bg/strings.xml
index 32c9ab9..3d8c68e 100644
--- a/tests/BugReportApp/res/values-bg/strings.xml
+++ b/tests/BugReportApp/res/values-bg/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Използвайте гласа си, за да опишете проблема"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Аудиосъобщение за сигнала за програмна грешка в %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Записването завърши"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Създаването на сигнал за програмна грешка вече е в ход"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Създава се сигнал за програмна грешка"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Създаден бе сигнал за програмна грешка"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Добавяне на аудио"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Добавяне на аудио и качване"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Качване"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Качване в GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Моля, предоставете разрешения"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Създаването на сигнал за програмна грешка вече е в ход"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Подаването на сигнал за програмна грешка започна"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Създава се сигнал за програмна грешка"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Сигналът за програмна грешка стартира"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Създаването на сигнал за програмна грешка не бе успешно"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Заснемането на екрана не бе успешно"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Създаването на моментна снимка на състоянието не бе успешно"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Създаването на моментна снимка на състоянието не бе успешно"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Създава се сигнал за програмна грешка"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Сигналът за програмна грешка е създаден"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Сигналът за програмна грешка е събран"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Канал за състоянието на сигнала за програмна грешка"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-bn/strings.xml b/tests/BugReportApp/res/values-bn/strings.xml
index ef19f4d..07200af 100644
--- a/tests/BugReportApp/res/values-bn/strings.xml
+++ b/tests/BugReportApp/res/values-bn/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"কথা বলুন এবং সমস্যার বিষয়ে বিবরণ দিন"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"সমস্যার বিষয়ে রিপোর্ট করার জন্য %s-এ অডিও মেসেজ করুন"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"রেকর্ড করা হয়ে গেছে"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"ইতিমধ্যে একটি সমস্যার বিষয়ে রিপোর্ট সংগ্রহ করা হচ্ছে"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"সমস্যা সংক্রান্ত রিপোর্ট তৈরি করা হচ্ছে"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"সমস্যার বিষয়ে রিপোর্ট সংগ্রহ করা হয়েছে"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"অডিও মেসেজ যোগ করুন"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"অডিও যোগ করুন এবং আপলোড করুন"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"আপলোড করুন"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS-এ আপলোড করুন"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"অনুমতির অনুমোদন দিন"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"সমস্যার বিষয়ে রিপোর্ট ইতিমধ্যে সংগ্রহ করা হচ্ছে"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"সমস্যার বিষয়ে রিপোর্ট করা শুরু করুন"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"সমস্যা সংক্রান্ত রিপোর্ট তৈরি করা হচ্ছে"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"সমস্যা সংক্রান্ত রিপোর্ট তৈরি করার প্রক্রিয়া শুরু করা হয়েছে"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"সমস্যার বিষয়ে রিপোর্ট করা যায়নি"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"স্ক্রিন ক্যাপচার করা যায়নি"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ডাম্পের স্ট্যাটাস লোড করা যায়নি"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate লোড করা যায়নি"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"সমস্যার বিষয়ে রিপোর্ট প্রগ্রেসে রয়েছে"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"সমস্যার বিষয়ে রিপোর্ট সংগ্রহ করা হয়"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"সমস্যা সংক্রান্ত রিপোর্ট সংগ্রহ করা হয়েছে"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"সমস্যার বিষয়ে রিপোর্টের স্ট্যাটাসের চ্যানেল"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-bs/strings.xml b/tests/BugReportApp/res/values-bs/strings.xml
index 0fde893..7963f60 100644
--- a/tests/BugReportApp/res/values-bs/strings.xml
+++ b/tests/BugReportApp/res/values-bs/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Govorite i opišite problem"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Zvučna poruka za izvještaj o grešci u %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Snimanje je završeno"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Izvještaj o greškama se već prikuplja"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Kreiranje izvještaja o grešci je u toku"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Prikupljen je izvještaj o greškama"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Dodaj zvučnu poruku"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Dodaj zvučnu poruku i otpremi"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Otpremi"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Otpremi na GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Dajte odobrenja"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Izvještaj o greškama se već prikuplja"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Započeto je izvještavanje o grešci"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Kreiranje izvještaja o grešci je u toku"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Izvještaj o grešci je pokrenut"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Kreiranje izvještaja o greškama nije uspjelo"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Snimanje ekrana nije uspjelo"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Odbacivanje stanja nije uspjelo"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Prikupljanje informacija o stanju nije uspjelo"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Kreiranje izvještaja o greškama je u toku"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Izvještaj o greškama je prikupljen"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Izvještaj o grešci je prikupljen"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Status kanala izvještaja o greškama"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ca/strings.xml b/tests/BugReportApp/res/values-ca/strings.xml
index dd4d1bc..1035d6f 100644
--- a/tests/BugReportApp/res/values-ca/strings.xml
+++ b/tests/BugReportApp/res/values-ca/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Enuncia i descriu el problema"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Missatge d\'àudio de l\'informe d\'errors a les %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Ha finalitzat la gravació"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Ja s\'està recollint un informe d\'errors"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"L\'informe d\'errors està en curs"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"S\'ha recollit un informe d\'errors"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Afegeix àudio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Afegeix àudio i penja"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Penja"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Penja a GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Concedeix permisos"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Ja s\'està recollint l\'informe d\'errors"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"S\'ha iniciat l\'informe d\'errors"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"L\'informe d\'errors està en curs"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"L\'informe d\'errors ha començat"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Ha fallat l\'informe d\'errors"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"No s\'ha pogut capturar la pantalla"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Ha fallat l\'estat d\'abocament"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"L\'estat d\'abocament ha fallat"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"L\'informe d\'errors està en curs"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"S\'ha recollit l\'informe d\'errors"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"L\'informe d\'errors s\'ha recollit"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Canal de l\'estat de l\'informe d\'errors"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-cs/strings.xml b/tests/BugReportApp/res/values-cs/strings.xml
index f4ebf47..13437aa 100644
--- a/tests/BugReportApp/res/values-cs/strings.xml
+++ b/tests/BugReportApp/res/values-cs/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Popište problém nahlas"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Hlasová zpráva pro hlášení o chybě v %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Nahrávání dokončeno"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Zaznamenávání zprávy o chybě již probíhá"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Probíhá vytváření zprávy o chybě"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Zpráva o chybě byla zaznamenána"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Přidat zvuk"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Přidat zvuk a nahrát"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Nahrát"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Nahrát do GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Udělte prosím oprávnění"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Zaznamenávání zprávy o chybě již probíhá"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Nahlašování chyby bylo zahájeno"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Probíhá vytváření zprávy o chybě"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Vytváření zprávy o chybě"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Vytvoření zprávy o chybě se nezdařilo"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Snímek obrazovky se nezdařil"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Vypsání stavu se nezdařilo"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Vypsání stavu se nezdařilo"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Probíhá vytváření zprávy o chybě"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Zpráva o chybě je zaznamenána"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Zpráva o chybě byla zaznamenána"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kanál stavu zprávy o chybě"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-da/strings.xml b/tests/BugReportApp/res/values-da/strings.xml
index b9459fa..ca716e3 100644
--- a/tests/BugReportApp/res/values-da/strings.xml
+++ b/tests/BugReportApp/res/values-da/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Tal og beskriv problemet"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Lydbesked for fejlrapport kl. %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Registreringen er afsluttet"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Der indsamles allerede en fejlrapport"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Fejlrapporten er under udformning"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Der er blevet indsamlet en fejlrapport"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Tilføj lyd"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Tilføj lyd og upload"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Upload"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Upload til GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Giv tilladelser"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Der indsamles allerede fejlrapport"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Fejlrapportering er i gang"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Fejlrapporten er under udformning"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Fejlrapporten er påbegyndt"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Fejlrapporten blev ikke oprettet"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Screenshot mislykkedes"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dump-tilstand mislykkedes"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Indsamlingstilstand mislykkedes"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Fejlrapporten er under udformning"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Fejlrapporten er indsamlet"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Fejlrapporten er blevet indsamlet"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Statuskanal for fejlrapporten"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-de/strings.xml b/tests/BugReportApp/res/values-de/strings.xml
index eeca3d6..cfc0bbe 100644
--- a/tests/BugReportApp/res/values-de/strings.xml
+++ b/tests/BugReportApp/res/values-de/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Erkläre das Problem"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Audionachricht für Fehlerbericht bei %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Aufnahme abgeschlossen"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Ein Fehlerbericht wird bereits erstellt"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Der Fehlerbericht ist in Bearbeitung"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Ein Fehlerbericht wurde erstellt"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Audionachricht hinzufügen"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Audionachricht hinzufügen & hochladen"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Hochladen"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Auf GSC hochladen"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Erteile Berechtigungen"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Fehlerbericht wird bereits erstellt"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Fehlerbericht wird gestartet"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Der Fehlerbericht ist in Bearbeitung"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Fehlerbericht wird erstellt"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Fehlerbericht fehlgeschlagen"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Fehler bei der Aufnahme des Screenshots"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Fehler beim Erstellen des Dump-Status"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Fehler beim Erstellen des Dumpstate"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Der Fehlerbericht ist in Bearbeitung"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Fehlerbericht wird erstellt"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Der Fehlerbericht wurde erstellt"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kanal für den Fehlerbericht-Status"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-el/strings.xml b/tests/BugReportApp/res/values-el/strings.xml
index 8f2d5a6..e3bb0ec 100644
--- a/tests/BugReportApp/res/values-el/strings.xml
+++ b/tests/BugReportApp/res/values-el/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Εκφωνήστε το πρόβλημα που αντιμετωπίζετε."</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Ηχητικό μήνυμα για αναφορά σφάλματος στις %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Η εγγραφή ολοκληρώθηκε."</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Γίνεται ήδη λήψη μιας αναφοράς σφάλματος."</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Η αναφορά σφάλματος είναι σε εξέλιξη"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Έγινε λήψη μιας αναφοράς σφάλματος."</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Προσθήκη ήχου"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Προσθήκη ήχου και μεταφόρτωση"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Μεταφόρτωση"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Μεταφόρτωση στο GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Παραχωρήστε άδειες"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Γίνεται ήδη λήψη αναφοράς σφάλματος."</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Η αναφορά σφαλμάτων ξεκίνησε"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Η αναφορά σφάλματος είναι σε εξέλιξη"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Η αναφορά σφάλματος ξεκίνησε"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Η αναφορά σφάλματος απέτυχε."</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Η καταγραφή οθόνης απέτυχε."</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Η κατάσταση απόρριψης απέτυχε."</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Η κατάσταση αποτύπωσης απέτυχε"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Η αναφορά σφάλματος είναι σε εξέλιξη."</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Γίνεται λήψη αναφοράς σφάλματος."</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Η αναφορά σφάλματος συλλέχθηκε"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Κανάλι κατάστασης αναφοράς σφάλματος"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-en-rAU/strings.xml b/tests/BugReportApp/res/values-en-rAU/strings.xml
index f6b112f..02e32cd 100644
--- a/tests/BugReportApp/res/values-en-rAU/strings.xml
+++ b/tests/BugReportApp/res/values-en-rAU/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Speak and describe the issue"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Audio message for bug report at %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Recording finished"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"A bug report is already being collected"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Bug report is in progress"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"A bug report has been collected"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Add audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Add audio and upload"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Upload"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Upload to GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Please grant permissions"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Bug report already being collected"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Bug reporting is started"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Bug report is in progress"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Bug report started"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Bug report failed"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Screen capture failed"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dump state failed"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate failed"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Bug report is in progress"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Bug report is collected"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Bug report has been collected"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Bug report status channel"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-en-rCA/strings.xml b/tests/BugReportApp/res/values-en-rCA/strings.xml
index f6b112f..02e32cd 100644
--- a/tests/BugReportApp/res/values-en-rCA/strings.xml
+++ b/tests/BugReportApp/res/values-en-rCA/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Speak and describe the issue"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Audio message for bug report at %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Recording finished"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"A bug report is already being collected"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Bug report is in progress"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"A bug report has been collected"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Add audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Add audio and upload"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Upload"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Upload to GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Please grant permissions"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Bug report already being collected"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Bug reporting is started"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Bug report is in progress"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Bug report started"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Bug report failed"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Screen capture failed"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dump state failed"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate failed"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Bug report is in progress"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Bug report is collected"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Bug report has been collected"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Bug report status channel"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-en-rGB/strings.xml b/tests/BugReportApp/res/values-en-rGB/strings.xml
index f6b112f..02e32cd 100644
--- a/tests/BugReportApp/res/values-en-rGB/strings.xml
+++ b/tests/BugReportApp/res/values-en-rGB/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Speak and describe the issue"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Audio message for bug report at %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Recording finished"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"A bug report is already being collected"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Bug report is in progress"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"A bug report has been collected"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Add audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Add audio and upload"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Upload"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Upload to GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Please grant permissions"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Bug report already being collected"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Bug reporting is started"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Bug report is in progress"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Bug report started"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Bug report failed"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Screen capture failed"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dump state failed"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate failed"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Bug report is in progress"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Bug report is collected"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Bug report has been collected"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Bug report status channel"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-en-rIN/strings.xml b/tests/BugReportApp/res/values-en-rIN/strings.xml
index f6b112f..02e32cd 100644
--- a/tests/BugReportApp/res/values-en-rIN/strings.xml
+++ b/tests/BugReportApp/res/values-en-rIN/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Speak and describe the issue"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Audio message for bug report at %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Recording finished"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"A bug report is already being collected"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Bug report is in progress"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"A bug report has been collected"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Add audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Add audio and upload"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Upload"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Upload to GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Please grant permissions"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Bug report already being collected"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Bug reporting is started"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Bug report is in progress"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Bug report started"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Bug report failed"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Screen capture failed"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dump state failed"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate failed"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Bug report is in progress"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Bug report is collected"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Bug report has been collected"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Bug report status channel"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-en-rXC/strings.xml b/tests/BugReportApp/res/values-en-rXC/strings.xml
index b3b3aa9..8bae50a 100644
--- a/tests/BugReportApp/res/values-en-rXC/strings.xml
+++ b/tests/BugReportApp/res/values-en-rXC/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Speak & Describe The Issue"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Audio message for bug report at %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Recording finished"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"A bug report is already being collected"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Bug report is in progress"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"A bug report has been collected"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Add Audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Add Audio & Upload"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Upload"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Upload to GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Please grant permissions"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Bug report already being collected"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Bug reporting is started"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Bug report is in progress"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Bug report started"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Bug report failed"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Screen capture failed"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dump state failed"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate failed"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Bug report is in progress"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Bug report is collected"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Bug report has been collected"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Bug report status channel"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-es-rUS/strings.xml b/tests/BugReportApp/res/values-es-rUS/strings.xml
index 3216579..105bf53 100644
--- a/tests/BugReportApp/res/values-es-rUS/strings.xml
+++ b/tests/BugReportApp/res/values-es-rUS/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Habla y describe el problema"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Mensaje de audio sobre el informe de errores de la(s) %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Finalizó la grabación"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Ya se está recopilando un informe de errores"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Generando informe de errores"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Se recopiló un informe de errores"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Agregar audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Agregar audio y subir"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Subir"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Subir a GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Otorga los permisos correspondientes"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Ya se está recopilando el informe de errores"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Se inició el informe de errores"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Generando informe de errores"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Se inició el informe de errores"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"No se pudo generar el informe de errores"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"No se pudo tomar la captura de pantalla"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Se produjo un error con el estado de volcado"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Se produjo un error con el estado de volcado"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"El informe de errores está en progreso"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Se recopiló el informe de errores"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"El informe de errores está listo"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Canal de estado del informe de errores"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-es/strings.xml b/tests/BugReportApp/res/values-es/strings.xml
index 9f54a27..74361e1 100644
--- a/tests/BugReportApp/res/values-es/strings.xml
+++ b/tests/BugReportApp/res/values-es/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Habla y describe el problema"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Mensaje de audio para el informe de errores de %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Grabación terminada"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Ya se está recopilando un informe de errores"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"El informe de errores está en curso"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Se ha recopilado un informe de errores"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Añadir audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Añadir audio y subir"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Subir"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Subir a GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Concede permisos"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Ya se está recopilando el informe de errores"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Se ha iniciado el informe de errores"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"El informe de errores está en curso"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Se ha iniciado el informe de errores"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"No se ha podido crear el informe de errores"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"No se ha podido hacer la captura de pantalla"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"No se ha podido obtener el estado de volcado"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"No se ha podido obtener el estado de volcado"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"El informe de errores está en curso"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Se ha recopilado el informe de errores"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Se ha recogido el informe de errores"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Canal de estado de informes de errores"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-et/strings.xml b/tests/BugReportApp/res/values-et/strings.xml
index abba855..99aa45d 100644
--- a/tests/BugReportApp/res/values-et/strings.xml
+++ b/tests/BugReportApp/res/values-et/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Kõnelege ja kirjeldage probleemi"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Helisõnum veaaruande kohta: %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Salvestamine lõpetati"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Veaaruannet juba jäädvustatakse"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Veaaruande jäädvustamine on pooleli"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Jäädvustati veaaruanne"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Lisa heli"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Lisa heli ja laadi üles"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Laadi üles"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Laadi üles teenusesse GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Andke load"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Veaaruannet juba jäädvustatakse"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Veaaruande loomist alustati"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Veaaruande jäädvustamine on pooleli"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Veaaruande jäädvustamist on alustatud"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Veaaruande jäädvustamine ebaõnnestus"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Ekraanikuva jäädvustamine ebaõnnestus"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Mälutõmmise olek: ebaõnnestus"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Mälutõmmise jäädvustamine ebaõnnestus"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Veaaruande jäädvustamine on pooleli"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Veaaruanne on jäädvustatud"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Veaaruanne on jäädvustatud"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Veaaruande oleku kanal"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-eu/strings.xml b/tests/BugReportApp/res/values-eu/strings.xml
index fa395a9..284f4e5 100644
--- a/tests/BugReportApp/res/values-eu/strings.xml
+++ b/tests/BugReportApp/res/values-eu/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Azaldu arazoa ozen"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Akatsen txostenerako audio-mezua (%s)"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Amaitu da grabatzen"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Dagoeneko bildu da akatsen txosten bat"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Akatsen txostena sortzen"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Bildu da akatsen txosten bat"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Gehitu audioa"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Gehitu eta kargatu audioa"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Kargatu"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Kargatu Google-ren hodeiko biltegira"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Eman baimenak"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Dagoeneko bildu da akatsen txostena"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Hasi da akatsen txostena sortzen"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Akatsen txostena sortzen"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Akatsen txostena sortzen hasi da"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Ezin izan da sortu akatsen txostena"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Ezin izan da egin pantaila-argazkia"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Ezin izan da irauli egoera"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate-k huts egin du"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Akatsen txostena sortzen"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Bildu da akatsen txostena"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Sortu da akatsen txostena"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Akatsen txostenaren egoeraren kanala"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-fa/strings.xml b/tests/BugReportApp/res/values-fa/strings.xml
index 86b451a..1b9513b 100644
--- a/tests/BugReportApp/res/values-fa/strings.xml
+++ b/tests/BugReportApp/res/values-fa/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"بیان و توضیح مشکل"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"پیام صوتی برای گزارش اشکال در %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"ضبط تمام شد"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"گزارش اشکالی از قبل جمعآوری شده است"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"گزارش اشکال درحال انجام است"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"گزارش اشکالی جمعآوری شد"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"افزودن صدا"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"افزودن صدا و بارگذاری"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"بارگذاری"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"بارگذاری در GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"لطفاً اجازه اعطا کنید"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"گزارش اشکال از قبل جمعآوری شده است"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"گزارش اشکال شروع شد"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"گزارش اشکال درحال انجام است"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"روند گزارش اشکال شروع شد"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"گزارش اشکال انجام نشد"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"ضبط صفحه انجام نشد"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"وضعیت کپی نشد"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"گزارش سیستم و فایلهای وضعیت کنونی جمعآوری نشد"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"گزارش اشکال درحال انجام است"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"گزارش اشکال جمعآوری شده است"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"گزارش اشکال جمعآوری شده است"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"کانال وضعیت گزارش اشکال"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-fi/strings.xml b/tests/BugReportApp/res/values-fi/strings.xml
index f1f4571..be203a8 100644
--- a/tests/BugReportApp/res/values-fi/strings.xml
+++ b/tests/BugReportApp/res/values-fi/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Kerro ongelmasta tarkemmin puhumalla"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Ääniviesti virheraportin kohdassa %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Tallennus lopetettu"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Virheraporttia kerätään jo"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Virheraporttia luodaan"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Virheraportti on kerätty"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Lisää ääniviesti"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Lisää ääniviesti ja lataa"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Lataa"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Lataa GCS:ään"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Myönnä käyttöluvat"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Virheraporttia kerätään jo"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Virheraportointi aloitettu"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Virheraporttia luodaan"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Virheraportointi aloitettu"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Virheraportti ei onnistunut"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Kuvakaappaus ei onnistunut"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Vedoksen tilavirhe"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate-virhe"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Virheraporttia luodaan"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Virheraportti kerätty"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Virheraportti on kerätty"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Virheraportin tilakanava"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-fr-rCA/strings.xml b/tests/BugReportApp/res/values-fr-rCA/strings.xml
index 21a12d0..ab99d37 100644
--- a/tests/BugReportApp/res/values-fr-rCA/strings.xml
+++ b/tests/BugReportApp/res/values-fr-rCA/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Parler et décrire le problème"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Message audio pour le rapport de bogue à %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Enregistrement terminé"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Un rapport de bogue est déjà en cours de récupération"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Le rapport de bogue est en cours de création"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Un rapport de bogue a été récupéré"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Ajouter un message audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Ajouter un message audio et téléverser"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Téléverser"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Téléverser sur GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Veuillez accorder les autorisations"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Rapport de bogue déjà en cours de récupération"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"La création d\'un rapport de bogue est commencée"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Le rapport de bogue est en cours de création"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Le rapport de bogue a commencé"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Échec du rapport de bogue"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Échec de la capture d\'écran"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Échec de l\'état de la capture"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Échec de l\'état de la capture"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Le rapport de bogue est en cours de création"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Récupération du rapport de bogue en cours…"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Le rapport de bogue a été collecté"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Chaîne d\'état du rapport de bogue"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-fr/strings.xml b/tests/BugReportApp/res/values-fr/strings.xml
index ff67a34..77453fb 100644
--- a/tests/BugReportApp/res/values-fr/strings.xml
+++ b/tests/BugReportApp/res/values-fr/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Parler et décrire le problème"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Message audio pour le rapport de bug à %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Enregistrement terminé"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Un rapport de bug est déjà en cours de création"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Le rapport de bug est en cours de création"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Un rapport de bug a été créé"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Ajouter un message audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Ajouter un message audio et l\'importer"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Importer"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Importer sur GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Veuillez accorder les autorisations"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Un rapport de bug est déjà en cours de création"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"La création d\'un rapport de bug a commencé"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Le rapport de bug est en cours de création"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"La création du rapport de bug a commencé"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Échec du rapport de bug"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Échec de la capture d\'écran"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Échec de l\'état de la copie"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Échec de l\'état de la copie"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Le rapport de bug est en cours de création"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Le rapport de bug a été créé"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Le rapport de bug a été récupéré"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Chaîne d\'état du rapport de bug"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-gl/strings.xml b/tests/BugReportApp/res/values-gl/strings.xml
index adaa47a..a7e4a96 100644
--- a/tests/BugReportApp/res/values-gl/strings.xml
+++ b/tests/BugReportApp/res/values-gl/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Fala e describe o problema"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Mensaxe de audio para o informe de erro (%s)"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"A gravación rematou"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Xa se está recompilando un informe de erros"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Recompilando informe de erros"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Recompilouse un informe de erros"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Engadir audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Engadir audio e cargar"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Cargar"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Cargar en GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Concede permisos"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Recompilando xa un informe de erros"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Iniciouse o informe de erro"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Recompilando informe de erros"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Iniciouse o informe de erros"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Produciuse un erro no informe de erros"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Produciuse un erro ao capturar a pantalla"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Produciuse un erro no estado do baleirado"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Produciuse un erro no estado do baleirado"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"O informe de erros está en curso"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Recompilouse o informe de erros"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Recompilouse o informe de erros"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Canle de estado do informe de erros"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-gu/strings.xml b/tests/BugReportApp/res/values-gu/strings.xml
index 124fec9..b213bd6 100644
--- a/tests/BugReportApp/res/values-gu/strings.xml
+++ b/tests/BugReportApp/res/values-gu/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"સમસ્યા જણાવો અને વર્ણન કરો"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s વાગ્યે ખામીની જાણકારી માટેનો ઑડિયો સંદેશ"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"રેકૉર્ડ કરવાનું સમાપ્ત થયું"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"ખામીની જાણકારી પહેલેથી એકત્રિત કરવામાં આવી રહી છે"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"બગ રિપોર્ટની પ્રક્રિયા ચાલુ છે"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"ખામીની જાણકારી એકત્રિત કરવામાં આવી છે"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ઑડિયો ઉમેરો"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ઑડિયો ઉમેરો અને અપલોડ કરો"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"અપલોડ કરો"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS પર અપલોડ કરો"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"કૃપા કરીને પરવાનાગીઓ આપો"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"ખામીની જાણકારી પહેલેથી એકત્રિત કરેલી છે"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"ખામીની જાણકારી આપવાની સુવિધા શરૂ કરી છે"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"બગ રિપોર્ટની પ્રક્રિયા ચાલુ છે"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"બગ રિપોર્ટની પ્રક્રિયા શરૂ થઈ છે"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"ખામીની જાણકારી નિષ્ફળ થઈ"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"સ્ક્રીન કૅપ્ચર કરવું નિષ્ફળ થયું"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ડમ્પ સ્ટેટ નિષ્ફળ થયું"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"ડમ્પસ્ટેટ નિષ્ફળ થયું"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"ખામીની જાણકારીની પ્રક્રિયા ચાલુ છે"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"ખામીની જાણકારી એકત્રિત કરેલી છે"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"બગ રિપોર્ટ એકત્રિત કરવામાં આવ્યો છે"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"ખામીની જાણકારી સ્ટેટસ ચૅનલ"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-hi/strings.xml b/tests/BugReportApp/res/values-hi/strings.xml
index ff12509..66421ae 100644
--- a/tests/BugReportApp/res/values-hi/strings.xml
+++ b/tests/BugReportApp/res/values-hi/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"बोलें और समस्या बताएं"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"गड़बड़ी की रिपोर्ट का ऑडियो मैसेज इस %s पर उपलब्ध है"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"रिकॉर्डिंग पूरी हुई"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"गड़बड़ी की रिपोर्ट इकट्ठा की जा रही है"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"गड़बड़ी की रिपोर्ट बनाई जा रही है"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"गड़बड़ी की रिपोर्ट इकट्ठा की गई"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ऑडियो मैसेज जोड़ें"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ऑडियो मैसेज जोड़ें और अपलोड करें"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"अपलोड करें"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS (जीसीएस) पर अपलोड करें"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"कृपया अनुमति दें"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"गड़बड़ी की रिपोर्ट इकट्ठा की जा रही है"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"गड़बड़ी की रिपोर्ट मिलने की सुविधा उपलब्ध है"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"गड़बड़ी की रिपोर्ट बनाई जा रही है"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"गड़बड़ी की रिपोर्ट बनाने की प्रक्रिया शुरू की गई"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"गड़बड़ी की रिपोर्ट नहीं बन पाई"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"स्क्रीन कैप्चर नहीं की जा सकी"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"डंप की स्थिति लोड नहीं की जा सकी"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate लोड नहीं की जा सकी"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"गड़बड़ी की रिपोर्ट बनाई जा रही है"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"गड़बड़ी की रिपोर्ट इकट्ठा की गई"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"गड़बड़ी की रिपोर्ट को इकट्ठा किया गया"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"गड़बड़ी की रिपोर्ट की स्थिति का चैनल"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-hr/strings.xml b/tests/BugReportApp/res/values-hr/strings.xml
index 86945ea..c599807 100644
--- a/tests/BugReportApp/res/values-hr/strings.xml
+++ b/tests/BugReportApp/res/values-hr/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Govorite i opišite poteškoću"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Audioporuka za izvješće o programskoj pogrešci u %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Snimanje je završeno"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Izvješće o programskoj pogrešci već se izrađuje"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"U tijeku je izrada izvješća o programskim pogreškama"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Izrađeno je izvješće o programskoj pogrešci"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Dodaj zvuk"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Dodaj zvuk i prenesi"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Prijenos"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Prenesi na GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Odobrite dopuštenja"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Izvješće o programskoj pogrešci već se izrađuje"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Započelo je izvješćivanje o programskoj pogrešci"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"U tijeku je izrada izvješća o programskim pogreškama"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Započela je izrada izvješća o programskim pogreškama"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Izrada izvješća o programskoj pogrešci nije uspjela"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Snimanje zaslona nije uspjelo"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Snimanje stanja nije uspjelo"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Snimanje stanja nije uspjelo"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"U tijeku je izrada izvješća o programskoj pogrešci"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Izvješće o programskoj pogrešci je izrađeno"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Izrađeno je izvješće o programskim pogreškama"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kanal statusa izvješća o programskoj pogrešci"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-hu/strings.xml b/tests/BugReportApp/res/values-hu/strings.xml
index 01379ca..6a48b3a 100644
--- a/tests/BugReportApp/res/values-hu/strings.xml
+++ b/tests/BugReportApp/res/values-hu/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Beszéljen, és írja le a problémát"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Hangüzenet hozzáadása a hibajelentéshez (%s)"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Felvétel befejezve"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Már folyamatban van a hibajelentés begyűjtése"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"A hibajelentés folyamatban van"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"A rendszerbe hibajelentés érkezett"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Hang hozzáadása"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Hang hozzáadása és feltöltés"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Feltöltés"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Feltöltés ide: GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Adja meg az engedélyeket"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Már folyamatban van a hibajelentés begyűjtése"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"A hibajelentés elkezdődött"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"A hibajelentés folyamatban van"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"A hibajelentés elkezdődött"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Sikertelen hibajelentés"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Nem sikerült a képernyő rögzítése"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Kiírás állapota sikertelen"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Kiírás állapota sikertelen"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"A hibajelentés folyamatban van"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Hibajelentés begyűjtve"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"A hibajelentés begyűjtése sikeresen megtörtént"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Hibajelentés állapotcsatornája"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-hy/strings.xml b/tests/BugReportApp/res/values-hy/strings.xml
index fae2e3b..e39e460 100644
--- a/tests/BugReportApp/res/values-hy/strings.xml
+++ b/tests/BugReportApp/res/values-hy/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Բարձրաձայն նկարագրեք սխալը"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Վրիպակների մասին հաշվետվության վերաբերյալ ձայնային հաղորդագրություն, ժամը՝ %s։"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Գրանցումն ավարտվեց"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Վրիպակների մասին հաշվետվությունն արդեն բեռնվում է"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Վրիպակի մասին զեկույցը բեռնվում է"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Սխալի մասին զեկույցը բեռնվել է"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Ավելացնել աուդիո"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Ավելացնել աուդիո և վերբեռնել"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Վերբեռնել"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Վերբեռնել GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Տրամադրեք թույլտվություններ"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Վրիպակների մասին հաշվետվությունն արդեն բեռնվել է"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Վրիպակների մասին հաղորդումը սկսված է"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Վրիպակի մասին զեկույցը բեռնվում է"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Վրիպակի մասին զեկույցը ստեղծվում է"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Վրիպակի զեկույցի հետ կապված սխալ առաջացավ"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Չհաջողվեց ստեղծել սքրինշոթ"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Գրանցման կարգավիճակի սխալ"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate ծառայության սխալ"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Վրիպակի մասին զեկույցը բեռնվում է"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Վրիպակի մասին զեկույցը բեռնվել է"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Վրիպակի մասին զեկույցը բեռնվել է"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Վրիպակի մասին զեկույցի կարգավիճակի ալիք"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-in/strings.xml b/tests/BugReportApp/res/values-in/strings.xml
index 2d76a43..230a1a9 100644
--- a/tests/BugReportApp/res/values-in/strings.xml
+++ b/tests/BugReportApp/res/values-in/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Ungkapkan & Jelaskan Masalah"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Pesan audio untuk laporan bug pada %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Perekaman selesai"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Satu laporan bug siap dikumpulkan"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Laporan bug sedang berlangsung"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Satu laporan bug telah dikumpulkan"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Tambah Audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Tambah Audio & Upload"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Upload"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Upload ke GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Harap beri izin"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Laporan bug siap dikumpulkan"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Pelaporan bug dimulai"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Laporan bug sedang berlangsung"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Laporan bug telah dimulai"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Laporan bug gagal"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Screenshot gagal"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Status dump gagal"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Status dump gagal"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Laporan bug sedang berlangsung"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Laporan bug dikumpulkan"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Laporan bug telah diambil"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Saluran status laporan bug"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-is/strings.xml b/tests/BugReportApp/res/values-is/strings.xml
index 2641eee..3352789 100644
--- a/tests/BugReportApp/res/values-is/strings.xml
+++ b/tests/BugReportApp/res/values-is/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Talaðu og lýstu vandamálinu"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Hljóðskilaboð villutilkynningar kl. %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Skráningu lokið"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Þegar er verið að skrá villutilkynningu"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Verið er að útbúa villutilkynningu"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Villutilkynning var skráð"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Bæta við hljóði"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Bæta við hljóði og hlaða inn"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Hlaða inn"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Hlaða inn á GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Veittu heimildir"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Villutilkynning hefur þegar verið skráð"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Byrjað að tilkynna villu"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Verið er að útbúa villutilkynningu"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Villutilkynning er hafin"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Ekki tókst að útbúa villutilkynningu"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Skjámyndataka mistókst"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Ekki tókst sækja stöðu"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate mistókst"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Verið er að útbúa villutilkynningu"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Villutilkynning skráð"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Búið er að sækja villutilkynningu"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Stöðurás villutilkynninga"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-it/strings.xml b/tests/BugReportApp/res/values-it/strings.xml
index fa508f9..8412e3b 100644
--- a/tests/BugReportApp/res/values-it/strings.xml
+++ b/tests/BugReportApp/res/values-it/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Parla e descrivi il problema"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Messaggio audio per segnalazione di bug alle %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Registrazione terminata"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"È già in corso la raccolta di una segnalazione di bug"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Segnalazione di bug in corso…"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"È stata raccolta una segnalazione di bug"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Aggiungi audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Aggiungi audio e carica"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Carica"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Carica su GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Concedi le autorizzazioni"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Raccolta della segnalazione di bug già in corso"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Segnalazione di bug avviata"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Segnalazione di bug in corso…"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Segnalazione di bug iniziata"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Segnalazione di bug non riuscita"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Acquisizione schermata non riuscita"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dump dello stato non riuscito"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Errore del servizio Dumpstate"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Segnalazione di bug in corso"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Segnalazione di bug raccolta"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Segnalazione di bug raccolta"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Canale stato segnalazione di bug"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-iw/strings.xml b/tests/BugReportApp/res/values-iw/strings.xml
index bb94591..2324ba1 100644
--- a/tests/BugReportApp/res/values-iw/strings.xml
+++ b/tests/BugReportApp/res/values-iw/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"דיבור ותיאור של הבעיה"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"הודעת האודיו בנושא הדוח על באג ב-%s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"ההקלטה הסתיימה"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"כבר מתבצע איסוף של דוח על באג"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"הדוח על הבאג נמצא בתהליך"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"נאסף דוח על באג"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"הוספת אודיו"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"הוספת אודיו והעלאה"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"העלאה"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"העלאה אל GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"יש להעניק הרשאות"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"כבר מתבצע איסוף של דוח על באג"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"תהליך הדיווח על באג החל"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"הדוח על הבאג נמצא בתהליך"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"יצירת הדוח על הבאג החלה"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"נכשל דוח על באג"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"צילום מסך נכשל"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"נכשל מצב Dump"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"מצב Dump נכשל"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"דוח על באג נמצא בתהליך"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"מתבצע איסוף של דוח על באג"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"הדוח על הבאג נאסף"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"ערוץ סטטוס של דוח על באג"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ja/strings.xml b/tests/BugReportApp/res/values-ja/strings.xml
index afc40a5..0d4ef1b 100644
--- a/tests/BugReportApp/res/values-ja/strings.xml
+++ b/tests/BugReportApp/res/values-ja/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"問題の説明"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"バグレポート(%s)の音声メッセージ"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"録画を終了しました"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"バグレポートの収集はすでに開始しています"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"バグレポートを収集しています"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"バグレポートを収集しました"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"音声を追加"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"音声を追加してアップロード"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"アップロード"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS にアップロード"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"権限を付与してください"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"バグレポートの収集はすでに開始しています"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"バグレポートを開始しました"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"バグレポートを収集しています"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"バグレポートの収集を開始しました"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"バグレポートを収集できませんでした"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"画面をキャプチャできませんでした"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ステータスをダンプできませんでした"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate を実行できませんでした"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"バグレポートを収集しています"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"バグレポートを収集しました"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"バグレポートを収集しました"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"バグレポート ステータス チャンネル"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ka/strings.xml b/tests/BugReportApp/res/values-ka/strings.xml
index cdc732e..6263be1 100644
--- a/tests/BugReportApp/res/values-ka/strings.xml
+++ b/tests/BugReportApp/res/values-ka/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"ისაუბრეთ და აღწერეთ პრობლემა"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"სისტემის ხარვეზის ანგარიშის აუდიო შეტყობინება %s-ზე"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"ჩაწერა დასრულდა"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"სისტემის ხარვეზის ანგარიში უკვე მზადდება"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"მიმდინარეობს სისტემის ხარვეზის ანგარიშის მომზადება"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"სისტემის ხარვეზის ანგარიში მომზადდა"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"აუდიოს დამატება"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"აუდიოს დამატება და ატვირთვა"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"ატვირთვა"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS-ზე ატვირთვა"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"გთხოვთ მისცეთ უფლებები"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"სისტემის ხარვეზის ანგარიში უკვე მომზადდა"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"სისტემის ხარვეზის ანგარიშის გაგზავნა დაიწყო"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"მიმდინარეობს სისტემის ხარვეზის ანგარიშის მომზადება"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"სისტემის ხარვეზის ანგარიშის მომზადება დაიწყო"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"სისტემის ხარვეზის ანგარიში ვერ მომზადდა"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"ეკრანის აღბეჭდვა ვერ მოხერხდა"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ჩაწერის მდგომარეობა შეფერხდა"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate შეფერხდა"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"მიმდინარეობს სისტემის ხარვეზის ანგარიშის მომზადება"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"სისტემის ხარვეზის ანგარიში მომზადებულია"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"სისტემის ხარვეზის ანგარიში მიღებულია"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"სისტემის ხარვეზის ანგარიშის სტატუსის არხი"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-kk/strings.xml b/tests/BugReportApp/res/values-kk/strings.xml
index f78cf0f..01c4209 100644
--- a/tests/BugReportApp/res/values-kk/strings.xml
+++ b/tests/BugReportApp/res/values-kk/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Қатені ауызша сипаттап беріңіз"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Қате туралы есептің аудиохабары (%s)"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Жазу аяқталды."</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Қате туралы есеп әлдеқашан алынып жатыр"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Қате туралы есеп алынуда"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Қате туралы есеп алынды"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Аудиохабар енгізу"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Аудиохабар енгізу және жүктеп салу"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Жүктеп салу"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS серверіне жүктеп салу"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Рұқсаттар беріңіз."</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Қате туралы есеп әлдеқашан алынып жатыр."</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Қате туралы есеп жүктелуде"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Қате туралы есеп алынуда"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Қате туралы есепті алу басталды"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Қате туралы есеп алынбады."</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Скриншот түсірілмеді."</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Дамп күйі алынбады."</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate қызметінің қатесі"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Қате туралы есеп алынуда"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Қате туралы есеп алынды"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Қате туралы есеп алынды"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Қате туралы есеп арнасы"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-km/strings.xml b/tests/BugReportApp/res/values-km/strings.xml
index a7b95b8..d664b58 100644
--- a/tests/BugReportApp/res/values-km/strings.xml
+++ b/tests/BugReportApp/res/values-km/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"និយាយ និងរៀបរាប់អំពីបញ្ហា"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"សារជាសំឡេងសម្រាប់របាយការណ៍អំពីបញ្ហានៅម៉ោង %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"បានបញ្ចប់ការថតហើយ"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"កំពុងប្រមូលរបាយការណ៍អំពីបញ្ហាស្រាប់ហើយ"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"ការរាយការណ៍អំពីបញ្ហាកំពុងដំណើរការ"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"បានប្រមូលរបាយការណ៍អំពីបញ្ហាហើយ"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"បញ្ចូលសំឡេង"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"បញ្ចូលសំឡេង និងបង្ហោះ"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"បង្ហោះ"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"បង្ហោះទៅ GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"សូមផ្ដល់ការអនុញ្ញាត"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"កំពុងប្រមូលរបាយការណ៍អំពីបញ្ហាស្រាប់ហើយ"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"ការរាយការណ៍អំពីបញ្ហាត្រូវបានចាប់ផ្ដើម"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"ការរាយការណ៍អំពីបញ្ហាកំពុងដំណើរការ"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"បានចាប់ផ្ដើមការរាយការណ៍អំពីបញ្ហា"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"មិនអាចប្រមូលរបាយការណ៍អំពីបញ្ហាបានទេ"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"មិនអាចថតអេក្រង់បានទេ"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"មិនអាចប្រមូលស្ថិតិបានទេ"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate ដំណើរការមិនបានសម្រេចទេ"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"កំពុងប្រមូលរបាយការណ៍អំពីបញ្ហា"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"របាយការណ៍អំពីបញ្ហាត្រូវបានប្រមូល"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"បានប្រមូលរបាយការណ៍អំពីបញ្ហា"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"បណ្ដាញស្ថានភាពនៃរបាយការណ៍អំពីបញ្ហា"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-kn/strings.xml b/tests/BugReportApp/res/values-kn/strings.xml
index 8149343..77e563c 100644
--- a/tests/BugReportApp/res/values-kn/strings.xml
+++ b/tests/BugReportApp/res/values-kn/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"ಸಮಸ್ಯೆಯ ಕುರಿತು ಮಾತನಾಡಿ ಮತ್ತು ವಿವರಿಸಿ"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s ನಲ್ಲಿ ಬಗ್ ವರದಿ ಮಾಡುವಿಕೆಗಾಗಿ ಆಡಿಯೊ ಸಂದೇಶ"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"ರೆಕಾರ್ಡಿಂಗ್ ಪೂರ್ಣಗೊಂಡಿದೆ"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"ಬಗ್ ವರದಿಯನ್ನು ಈಗಾಗಲೇ ಸಂಗ್ರಹಿಸಲಾಗಿದೆ"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"ಬಗ್ ವರದಿಮಾಡುವಿಕೆ ಪ್ರಕ್ರಿಯೆಯಲ್ಲಿದೆ"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"ಬಗ್ ವರದಿಯನ್ನು ಸಂಗ್ರಹಿಸಲಾಗಿದೆ"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ಆಡಿಯೊವನ್ನು ಸೇರಿಸಿ"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ಆಡಿಯೊವನ್ನು ಸೇರಿಸಿ ಮತ್ತು ಅಪ್ಲೋಡ್ ಮಾಡಿ"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"ಅಪ್ಲೋಡ್ ಮಾಡಿ"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS ಗೆ ಅಪ್ಲೋಡ್ ಮಾಡಿ"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"ಅನುಮತಿಗಳನ್ನು ನೀಡಿ"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"ಬಗ್ ವರದಿಯನ್ನು ಈಗಾಗಲೇ ಸಂಗ್ರಹಿಸಲಾಗಿದೆ"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"ಬಗ್ ವರದಿ ಮಾಡುವಿಕೆಯನ್ನು ಪ್ರಾರಂಭಿಸಲಾಗಿದೆ"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"ಬಗ್ ವರದಿಮಾಡುವಿಕೆ ಪ್ರಕ್ರಿಯೆಯಲ್ಲಿದೆ"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"ಬಗ್ ವರದಿ ಮಾಡುವಿಕೆಯನ್ನು ಪ್ರಾರಂಭಿಸಲಾಗಿದೆ"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"ಬಗ್ ವರದಿಮಾಡುವಿಕೆ ವಿಫಲವಾಗಿದೆ"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"ಸ್ಕ್ರೀನ್ ಕ್ಯಾಪ್ಚರ್ ವಿಫಲವಾಗಿದೆ"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ಡಂಪ್ ಸ್ಥಿತಿ ವಿಫಲವಾಗಿದೆ"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"ಡಂಪ್ಸ್ಟೇಟ್ ವಿಫಲವಾಗಿದೆ"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"ಬಗ್ ವರದಿಮಾಡುವಿಕೆ ಪ್ರಕ್ರಿಯೆಯಲ್ಲಿದೆ"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"ಬಗ್ ವರದಿಯನ್ನು ಸಂಗ್ರಹಿಸಲಾಗಿದೆ"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"ಬಗ್ ವರದಿಯನ್ನು ಸಂಗ್ರಹಿಸಲಾಗಿದೆ"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"ಬಗ್ ವರದಿಮಾಡುವಿಕೆ ಸ್ಥಿತಿ ಚಾನಲ್"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ko/strings.xml b/tests/BugReportApp/res/values-ko/strings.xml
index 6162aae..ae1d65f 100644
--- a/tests/BugReportApp/res/values-ko/strings.xml
+++ b/tests/BugReportApp/res/values-ko/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"문제 말하기 및 설명하기"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"버그 신고용 음성 메시지가 %s에 등록되었습니다."</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"녹음 완료"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"이미 버그 신고를 수집하는 중"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"버그 신고 진행 중"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"버그 신고가 수집됨"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"오디오 추가"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"오디오 추가 및 업로드"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"업로드"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS로 업로드"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"권한을 부여해 주세요."</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"이미 버그 신고를 수집하는 중입니다."</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"버그 신고를 시작했습니다."</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"버그 신고 진행 중"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"버그 신고 시작됨"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"버그 신고 실패"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"화면 캡처에 실패했습니다."</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"dumpstate에 실패했습니다."</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate 실패"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"버그 신고가 진행 중입니다."</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"버그 신고가 수집됨"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"버그 신고 접수됨"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"버그 신고 상태 채널"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ky/strings.xml b/tests/BugReportApp/res/values-ky/strings.xml
index de9b68e..4bb26fb 100644
--- a/tests/BugReportApp/res/values-ky/strings.xml
+++ b/tests/BugReportApp/res/values-ky/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Маселени сүрөттөп бериңиз"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Саат %s мүчүлүштүк тууралуу кабардын аудио билдирүүсү"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Жаздырылып бүттү"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Мүчүлүштүк тууралуу кабар алынган"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Мүчүлүштүк тууралуу кабар даярдалууда"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Мүчүлүштүк тууралуу кабар алынды"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Аудио билдирүү кошуу"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Аудио билдирүү кошуп, жүктөп берүү"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Жүктөп берүү"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS\'ке жүктөп берүү"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Уруксаттарды бериңиз"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Мүчүлүштүк тууралуу кабар алынган"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Мүчүлүштүк тууралуу кабар берилип баштады"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Мүчүлүштүк тууралуу кабар даярдалууда"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Мүчүлүштүк тууралуу кабар даярдалып баштады"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Мүчүлүштүк тууралуу кабар берилген жок"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Экран тартылып алынган жок"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Абал тууралуу маалымат алынган жок"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Абал тууралуу маалымат алынган жок"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Мүчүлүштүк тууралуу кабар даярдалууда"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Мүчүлүштүк тууралуу кабар алынды"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Мүчүлүштүк тууралуу кабар даярдалды"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Мүчүлүштүк тууралуу кабарлоо абалынын каналы"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-lo/strings.xml b/tests/BugReportApp/res/values-lo/strings.xml
index 57d0d82..9161712 100644
--- a/tests/BugReportApp/res/values-lo/strings.xml
+++ b/tests/BugReportApp/res/values-lo/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"ເວົ້າ ແລະ ອະທິບາຍບັນຫາ"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"ຂໍ້ຄວາມສຽງສຳລັບລາຍງານບັນຫາເມື່ອ %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"ສຳເລັດການບັນທຶກແລ້ວ"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"ກຳລັງເກັບກຳລາຍງານບັນຫາ"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"ລາຍງານບັນຫາພວມດຳເນີນຢູ່"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"ເກັບກຳລາຍງານບັນຫາແລ້ວ"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ເພີ່ມສຽງ"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ເພີ່ມສຽງ ແລະ ອັບໂຫຼດ"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"ອັບໂຫລດ"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"ອັບໂຫຼດໃສ່ GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"ກະລຸນາໃຫ້ການອະນຸຍາດ"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"ເກັບກຳລາຍງານບັນຫາແລ້ວ"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"ເລີ່ມການລາຍງານບັນຫາແລ້ວ"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"ລາຍງານບັນຫາພວມດຳເນີນຢູ່"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"ເລີ່ມລາຍງານຂໍ້ຜິດພາດແລ້ວ"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"ລາຍງານບັນຫາບໍ່ສຳເລັດ"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"ບັນທຶກພາບໜ້າຈໍບໍ່ສຳເລັດ"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ດຳສະເຕດບໍ່ສຳເລັດ"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"ດຳສະເຕດບໍ່ສຳເລັດ"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"ລາຍງານບັນຫາພວມດຳເນີນຢູ່"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"ເກັບກຳລາຍງານບັນຫາແລ້ວ"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"ເກັບກຳລາຍງານຂໍ້ຜິດພາດແລ້ວ"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"ຊ່ອງທາງຂອງສະຖານະລາຍງານບັນຫາ"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-lt/strings.xml b/tests/BugReportApp/res/values-lt/strings.xml
index 6b5dd33..c5651bb 100644
--- a/tests/BugReportApp/res/values-lt/strings.xml
+++ b/tests/BugReportApp/res/values-lt/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Kalbėkite ir apibūdinkite problemą"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Garso pranešimas apie riktą: %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Įrašymas baigtas"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Pranešimas apie riktą jau gaunamas"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Pranešimas apie riktą vykdomas"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Gautas pranešimas apie riktą"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Pridėti garso įrašą"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Pridėti garso įrašą ir įkelti"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Įkelti"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Įkelti į GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Suteikite leidimus"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Pranešimas apie riktą jau gaunamas"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Pranešimo apie riktą procesas pradėtas"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Pranešimas apie riktą vykdomas"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Pradėtas kurti pranešimas apie riktą"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Nepavyko gauti pranešimo apie riktą"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Nepavyko užfiksuoti ekrano"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Nepavyko pateikti atminties išklotinės"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Nepavyko pateikti atminties išklotinės"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Pranešimas apie riktą vykdomas"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Pranešimas apie riktą gautas"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Pranešimas apie riktą gautas"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Pranešimo apie riktą kanalas"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-lv/strings.xml b/tests/BugReportApp/res/values-lv/strings.xml
index fe21bd7..3352dc1 100644
--- a/tests/BugReportApp/res/values-lv/strings.xml
+++ b/tests/BugReportApp/res/values-lv/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Runājiet un raksturojiet problēmu"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Audio ziņojums kļūdas pārskatam (%s)"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Ierakstīšana pabeigta"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Kļūdas pārskats jau tiek iegūts"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Notiek kļūdas pārskata izveide…"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Kļūdas pārskats ir iegūts"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Pievienot audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Pievienot audio un augšupielādēt"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Augšupielādēt"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Augšupielādēt GCS krātuvē"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Lūdzu, piešķiriet atļaujas"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Kļūdas pārskats jau tiek iegūts."</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Sākta kļūdas pārskata izveide."</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Notiek kļūdas pārskata izveide…"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Ir sākta kļūdas pārskata izveide."</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Neizdevās izveidot kļūdas pārskatu."</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Neizdevās veikt ekrānuzņēmumu."</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Neizdevās iegūt sistēmas datus un statusu."</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Pakalpojuma Dumpstate kļūme."</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Kļūdas pārskats tiek veidots."</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Kļūdas pārskats ir iegūts"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Kļūdas pārskats ir izveidots."</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kļūdas pārskata statusa kanāls"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-mk/strings.xml b/tests/BugReportApp/res/values-mk/strings.xml
index 9681a1b..49a5a1d 100644
--- a/tests/BugReportApp/res/values-mk/strings.xml
+++ b/tests/BugReportApp/res/values-mk/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Зборувајте и објаснете го проблемот"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Аудиопорака за извештај за грешка на %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Снимањето заврши"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Веќе се собира извештај за грешка"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Во тек е извештај за грешка"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Собран е извештај за грешка"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Додај аудио"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Додај аудио и прикачи"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Прикачи"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Прикачи на GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Дајте дозвола"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Веќе се собира извештај за грешка"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Пријавувањето грешка започна"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Во тек е извештај за грешка"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Извештајот за грешка е започнат"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Извештајот за грешка не успеа"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Снимањето на екранот не успеа"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Состојбата на меморијата не успеа"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Состојбата на меморијата не успеа"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Во тек е извештај за грешка"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Извештајот за грешка е собран"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Го прибравме извештајот за грешка"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Канал за статус на извештајот за грешка"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ml/strings.xml b/tests/BugReportApp/res/values-ml/strings.xml
index e00f964..3fdc500 100644
--- a/tests/BugReportApp/res/values-ml/strings.xml
+++ b/tests/BugReportApp/res/values-ml/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"പ്രശ്നം പറഞ്ഞ് വിവരിക്കുക"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s-നുള്ള ബഗ് റിപ്പോർട്ടിന്റെ ഓഡിയോ സന്ദേശം"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"റെക്കോർഡ് ചെയ്യൽ പൂർത്തിയായി"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"ഒരു ബഗ് റിപ്പോർട്ട് മുമ്പേ ശേഖരിക്കുന്നുണ്ട്"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"ബഗ് റിപ്പോർട്ട് പുരോഗതിയിലാണ്"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"ഒരു ബഗ് റിപ്പോർട്ട് ശേഖരിച്ചിട്ടുണ്ട്"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ഓഡിയോ ചേർക്കുക"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ഓഡിയോ ചേർത്ത് അപ്ലോഡ് ചെയ്യുക"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"അപ്ലോഡ് ചെയ്യുക"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS-ലേക്ക് അപ്ലോഡ് ചെയ്യുക"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"അനുമതികൾ നൽകുക"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"ബഗ് റിപ്പോർട്ട് മുമ്പേ ശേഖരിക്കുന്നുണ്ട്"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"ബഗ് റിപ്പോർട്ട് ചെയ്യൽ ആരംഭിച്ചു"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"ബഗ് റിപ്പോർട്ട് പുരോഗതിയിലാണ്"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"ബഗ് റിപ്പോർട്ട് ആരംഭിച്ചു"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"ബഗ് റിപ്പോർട്ട് ചെയ്യാനായില്ല"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"സ്ക്രീനിന്റെ ചിത്രമെടുക്കാനായില്ല"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ഡംപ് സ്റ്റേറ്റ് ചെയ്യാനായില്ല"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"ഡംപ്സ്റ്റേറ്റ് റൺ ചെയ്യാനായില്ല"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"ബഗ് റിപ്പോർട്ട് പുരോഗതിയിലാണ്"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"ബഗ് റിപ്പോർട്ട് ശേഖരിച്ചു"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"ബഗ് റിപ്പോർട്ട് ശേഖരിച്ചു"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"ബഗ് റിപ്പോർട്ട് സ്റ്റാറ്റസ് ചാനൽ"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-mn/strings.xml b/tests/BugReportApp/res/values-mn/strings.xml
index cd503a7..620097c 100644
--- a/tests/BugReportApp/res/values-mn/strings.xml
+++ b/tests/BugReportApp/res/values-mn/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Асуудлыг ярих болон тайлбарлах"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s-н алдааны мэдээний аудио мессеж"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Дуу хурааж дууслаа"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Алдааны мэдээг аль хэдийн цуглуулж байна"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Алдааны мэдээг цуглуулж байна"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Алдааны мэдээг цуглууллаа"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Аудио нэмэх"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Аудио нэмэх & Байршуулах"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Байршуулах"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS-д байршуулах"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Зөвшөөрөл олгоно уу"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Алдааны мэдээг аль хэдийн цуглуулж байна"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Алдааны мэдээг эхлүүлсэн"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Алдааны мэдээг цуглуулж байна"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Алдааны мэдээг эхлүүлсэн"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Алдааны мэдээг цуглуулж чадсангүй"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Дэлгэцийн зургийг авч чадсангүй"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dump state амжилтгүй боллоо"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate амжилтгүй боллоо"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Алдааны мэдээг цуглуулж байна"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Алдааны мэдээг цуглуулсан"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Алдааны мэдээг цуглууллаа"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Алдааны мэдээний төлөвийн суваг"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-mr/strings.xml b/tests/BugReportApp/res/values-mr/strings.xml
index c69f650..20dda8d 100644
--- a/tests/BugReportApp/res/values-mr/strings.xml
+++ b/tests/BugReportApp/res/values-mr/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"बोलून समस्येचे वर्णन करा"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"बग रिपोर्टचा ऑडिओ मेसेज %s वाजता आला"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"रेकॉर्ड करणे पूर्ण झाले आहे"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"बग रिपोर्ट आधीच गोळा केला गेला आहे"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"बग रिपोर्ट तयार करणे प्रगतिपथावर आहे"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"बग रिपोर्ट गोळा केला गेला आहे"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ऑडिओ जोडा"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ऑडिओ जोडा आणि अपलोड करा"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"अपलोड करा"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS वर अपलोड करा"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"कृपया परवानगी द्या"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"बग रिपोर्ट आधीपासून गोळा केला जात आहे"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"बग रिपोर्टिंग सुरू केले आहे"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"बग रिपोर्ट तयार करणे प्रगतिपथावर आहे"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"बग रिपोर्ट तयार करणे सुरू झाले आहे"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"बग रिपोर्ट तयार करता आला नाही"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"स्क्रीन कॅप्चर करता आली नाही"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"डंप स्थितीवर बदलता आले नाही"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"डंपस्टेट अयशस्वी झाली"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"बग रिपोर्ट तयार करणे प्रगतिपथावर आहे"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"बग रिपोर्ट गोळा केला"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"बग रिपोर्ट गोळा केला गेला आहे"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"बग रिपोर्ट स्थिती चॅनल"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ms/strings.xml b/tests/BugReportApp/res/values-ms/strings.xml
index 5e77e6b..c435ab8 100644
--- a/tests/BugReportApp/res/values-ms/strings.xml
+++ b/tests/BugReportApp/res/values-ms/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Sebut & Terangkan Isu Tersebut"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Mesej audio untuk pelaporan pepijat pada %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Rakaman selesai"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Laporan pepijat sudah dikumpulkan"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Laporan pepijat sedang berjalan"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Laporan pepijat telah dikumpulkan"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Tambah Audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Tambah Audio & Muat Naik"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Muat naik"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Muat naik ke GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Sila beri kebenaran"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Laporan pepijat sudah dikumpulkan"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Pelaporan pepijat dimulakan"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Laporan pepijat sedang berjalan"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Laporan pepijat bermula"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Laporan pepijat gagal"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Tangkapan skrin gagal"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Keadaan longgokan gagal"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Keadaan longgokan gagal"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Laporan pepijat sedang berjalan"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Laporan pepijat dikumpulkan"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Laporan pepijat telah dikumpulkan"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Saluran status laporan pepijat"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-my/strings.xml b/tests/BugReportApp/res/values-my/strings.xml
index 0d7b693..36eaf80 100644
--- a/tests/BugReportApp/res/values-my/strings.xml
+++ b/tests/BugReportApp/res/values-my/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"ပြဿနာကို စကားပြော၍ ဖော်ပြပါ"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s ၌ ပြုလုပ်ခဲ့သည့် ချွတ်ယွင်းမှု အစီရင်ခံချက်အတွက် အသံမက်ဆေ့ဂျ်"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"အသံသွင်းပြီးသွားပါပြီ"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"ချွတ်ယွင်းမှု အစီရင်ခံစာကို လက်ခံရယူနေပါပြီ"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"ချွတ်ယွင်းမှု တိုင်ကြားချက်ကို ဆောင်ရွက်နေဆဲဖြစ်သည်"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"ချွတ်ယွင်းမှု အစီရင်ခံစာကို လက်ခံရယူပြီးပါပြီ"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"အသံထည့်ရန်"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"အသံထည့်ပြီး အပ်လုဒ်လုပ်ရန်"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"အပ်လုဒ်လုပ်ရန်"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS သို့ အပ်လုဒ်လုပ်ရန်"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"ခွင့်ပြုချက်များ ပေးပါ"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"ချွတ်ယွင်းမှု အစီရင်ခံစာကို လက်ခံရယူနေပြီး ဖြစ်သည်"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"ချွတ်ယွင်းမှု အစီရင်ခံချက်ကို စတင်လိုက်ပါပြီ"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"ချွတ်ယွင်းမှု တိုင်ကြားချက်ကို ဆောင်ရွက်နေဆဲဖြစ်သည်"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"ချွတ်ယွင်းမှု တိုင်ကြားချက်ကို စတင်ထားသည်"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"ချွတ်ယွင်းမှု အစီရင်ခံ၍မရပါ"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"မျက်နှာပြင် ပုံဖမ်း၍ မရပါ"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dump state မအောင်မြင်ပါ"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate မအောင်မြင်ပါ"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"ချွတ်ယွင်းမှု အစီရင်ခံစာကို ဆောင်ရွက်နေဆဲဖြစ်သည်"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"ချွတ်ယွင်းမှု အစီရင်ခံစာကို လက်ခံရယူပြီးပါပြီ"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"ချွတ်ယွင်းမှု တိုင်ကြားချက်ကို စုစည်းပြီးပြီ"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"ချွတ်ယွင်းမှု အစီရင်ခံချက် အခြေအနေ ချန်နယ်"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-nb/strings.xml b/tests/BugReportApp/res/values-nb/strings.xml
index 4aa225a..27e450f 100644
--- a/tests/BugReportApp/res/values-nb/strings.xml
+++ b/tests/BugReportApp/res/values-nb/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Les opp og beskriv problemet"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Lydmelding for feilrapporten klokken %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Opptaket er fullført"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"En feilrapport blir allerede samlet inn"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"En feilrapport pågår"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"En feilrapport er samlet inn"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Legg til lyd"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Legg til lyd og last opp"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Last opp"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Last opp til GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Gi tillatelser"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"En feilrapport blir allerede samlet inn"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Rapporteringen av feil er startet"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"En feilrapport pågår"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"En feilrapport er startet"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Feilrapporten mislyktes"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Skjermdumpen mislyktes"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dumptilstand mislyktes"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumptilstand mislyktes"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"En feilrapport pågår"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"En feilrapport samles inn"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"En feilrapport er samlet inn"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Statuskanal for feilrapport"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ne/strings.xml b/tests/BugReportApp/res/values-ne/strings.xml
index 5114772..dcda57e 100644
--- a/tests/BugReportApp/res/values-ne/strings.xml
+++ b/tests/BugReportApp/res/values-ne/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"बोलेर समस्याको वर्णन गर्नुहोस्"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s बजे बग रिपोर्टसम्बन्धी अडियो सन्देश"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"रेकर्ड गर्ने कार्य पूरा भयो"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"एउटा बग रिपोर्ट पहिलेदेखि नै सङ्कलन भइरहेको छ"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"त्रुटिसम्बन्धी रिपोर्ट सङ्कलन गर्ने कार्य जारी छ"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"बग रिपोर्ट सङ्कलन गरिएको छ"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"अडियो थप्नुहोस्"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"अडियो थप्नुहोस् तथा अपलोड गर्नुहोस्"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"अपलोड गर्नुहोस्"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS मा अपलोड गर्नुहोस्"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"कृपया अनुमति दिनुहोस्"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"बग रिपोर्ट पहिलेदेखि नै सङ्कलन भइरहेको छ"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"बगबारे रिपोर्ट गर्ने कार्य सुरु भयो"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"त्रुटिसम्बन्धी रिपोर्ट सङ्कलन गर्ने कार्य जारी छ"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"त्रुटिसम्बन्धी रिपोर्ट सङ्कलन गर्न थालिएको छ"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"बग रिपोर्ट सङ्कलन गर्न सकिएन"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"स्क्रिनको फोटो खिच्न सकिएन"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"डम्प स्टेटले काम गर्न सकेन"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate ले काम गर्न सकेन"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"बग रिपोर्ट सङ्कलन गर्ने कार्य जारी छ"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"बग रिपोर्ट सङ्कलन गरियो"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"त्रुटिसम्बन्धी रिपोर्ट सङ्कलन गरिएको छ"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"बग रिपोर्टको स्थिति देखाउने च्यानल"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-nl/strings.xml b/tests/BugReportApp/res/values-nl/strings.xml
index b893a19..2ee0f31 100644
--- a/tests/BugReportApp/res/values-nl/strings.xml
+++ b/tests/BugReportApp/res/values-nl/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Spreek en beschrijf het probleem"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Audiobericht voor bugrapportage om %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Opname gestopt"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Er wordt al een bugrapport verzameld"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Bugrapport wordt uitgevoerd"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Er is een bugrapport verzameld"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Audio toevoegen"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Audio toevoegen en uploaden"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Uploaden"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Uploaden naar GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Verleen rechten"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Bugrapport wordt al verzameld"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Bugrapportage is gestart"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Bugrapport wordt uitgevoerd"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Bugrapport is gestart"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Bugrapport is mislukt"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Screenshot maken is mislukt"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Dumpstatus mislukt"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstatus mislukt"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Bugrapport wordt uitgevoerd"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Bugrapport wordt verzameld"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Bugrapport is verzameld"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kanaal voor bugrapportstatus"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-or/strings.xml b/tests/BugReportApp/res/values-or/strings.xml
index 678830d..79ce668 100644
--- a/tests/BugReportApp/res/values-or/strings.xml
+++ b/tests/BugReportApp/res/values-or/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"ସମସ୍ୟା କୁହନ୍ତୁ ଏବଂ ବ୍ୟାଖ୍ୟା କରନ୍ତୁ"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%sରେ ବଗ୍ ରିପୋର୍ଟ ପାଇଁ ଅଡିଓ ମେସେଜ୍"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"ରେକର୍ଡିଂ ସମାପ୍ତ ହୋଇଛି"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"ଏକ ବଗ୍ ରିପୋର୍ଟ ପୂର୍ବରୁ ସଂଗ୍ରହ କରାଯାଇଛି"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"ବଗ ରିପୋର୍ଟ କରିବା ପ୍ରକ୍ରିୟା ଚାଲିଛି"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"ଏକ ବଗ୍ ରିପୋର୍ଟ ସଂଗ୍ରହ କରାଯାଇଛି"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ଅଡିଓ ଯୋଗ କରନ୍ତୁ"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ଅଡିଓ ଯୋଗ କରନ୍ତୁ ଓ ଅପ୍ଲୋଡ୍ କରନ୍ତୁ"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"ଅପ୍ଲୋଡ୍ କରନ୍ତୁ"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCSରେ ଅପ୍ଲୋଡ୍ କରନ୍ତୁ"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"ଦୟାକରି ଅନୁମତି ଅନୁମୋଦନ କରନ୍ତୁ"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"ବଗ୍ ରିପୋର୍ଟ ପୂର୍ବରୁ ସଂଗୃହିତ ହୋଇଛି"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"ବଗ୍ ରିପୋର୍ଟିଂ ଆରମ୍ଭ ହୋଇଛି"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"ବଗ ରିପୋର୍ଟ କରିବା ପ୍ରକ୍ରିୟା ଚାଲିଛି"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"ବଗ ରିପୋର୍ଟ କରିବା ପ୍ରକ୍ରିୟା ଆରମ୍ଭ କରାଯାଇଛି"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"ବଗ୍ ରିପୋର୍ଟ ବିଫଳ ହୋଇଛି"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"ସ୍କ୍ରିନ୍ କ୍ୟାପ୍ଚର୍ ବିଫଳ ହୋଇଛି"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ଡମ୍ପ ଷ୍ଟେଟ୍ ବିଫଳ ହୋଇଛି"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"ଡମ୍ପଷ୍ଟେଟ ବିଫଳ ହୋଇଛି"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"ବଗ୍ ରିପୋର୍ଟ ଜାରି ଅଛି"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"ବଗ୍ ରିପୋର୍ଟ ସଂଗ୍ରହ କରାଯାଇଛି"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"ବଗ ରିପୋର୍ଟ ସଂଗ୍ରହ କରାଯାଇଛି"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"ବଗ୍ ରିପୋର୍ଟ ସ୍ଥିତି ଚ୍ୟାନେଲ୍"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-pa/strings.xml b/tests/BugReportApp/res/values-pa/strings.xml
index 3d34363..ea06fcd 100644
--- a/tests/BugReportApp/res/values-pa/strings.xml
+++ b/tests/BugReportApp/res/values-pa/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"ਬੋਲ ਕੇ ਸਮੱਸਿਆ ਦਾ ਵਰਣਨ ਕਰੋ"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s ’ਤੇ ਬੱਗ ਰਿਪੋਰਟ ਲਈ ਆਡੀਓ ਸੁਨੇਹਾ"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"ਰਿਕਾਰਡਿੰਗ ਪੂਰੀ ਹੋ ਗਈ"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"ਬੱਗ ਰਿਪੋਰਟ ਪਹਿਲਾਂ ਹੀ ਇਕੱਤਰ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"ਬੱਗ ਰਿਪੋਰਟ ਪ੍ਰਕਿਰਿਆ-ਅਧੀਨ ਹੈ"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"ਬੱਗ ਰਿਪੋਰਟ ਇਕੱਤਰ ਕੀਤੀ ਜਾ ਚੁੱਕੀ ਹੈ"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ਆਡੀਓ ਸ਼ਾਮਲ ਕਰੋ"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ਆਡੀਓ ਸ਼ਾਮਲ ਕਰਕੇ ਅੱਪਲੋਡ ਕਰੋ"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"ਅੱਪਲੋਡ ਕਰੋ"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS \'ਤੇ ਅੱਪਲੋਡ ਕਰੋ"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"ਕਿਰਪਾ ਕਰਕੇ ਇਜਾਜ਼ਤਾਂ ਦਿਓ"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"ਬੱਗ ਰਿਪੋਰਟ ਪਹਿਲਾਂ ਹੀ ਇਕੱਤਰ ਕੀਤੀ ਗਈ ਹੈ"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"ਬੱਗ ਦੀ ਰਿਪੋਰਟਿੰਗ ਸ਼ੁਰੂ ਹੋ ਗਈ ਹੈ"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"ਬੱਗ ਰਿਪੋਰਟ ਪ੍ਰਕਿਰਿਆ-ਅਧੀਨ ਹੈ"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"ਬੱਗ ਰਿਪੋਰਟ ਸ਼ੁਰੂ ਕੀਤੀ ਗਈ"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"ਬੱਗ ਰਿਪੋਰਟ ਇਕੱਤਰ ਕਰਨਾ ਅਸਫਲ ਰਿਹਾ"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"ਸਕ੍ਰੀਨ ਨੂੰ ਕੈਪਚਰ ਕਰਨਾ ਅਸਫਲ ਰਿਹਾ"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ਡੰਪ ਸਥਿਤੀ ਅਸਫਲ ਰਹੀ"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate ਅਸਫਲ ਰਿਹਾ"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"ਬੱਗ ਰਿਪੋਰਟ ਇਕੱਤਰ ਕਰਨਾ ਜਾਰੀ ਹੈ"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"ਬੱਗ ਰਿਪੋਰਟ ਇਕੱਤਰ ਕੀਤੀ ਜਾਂਦੀ ਹੈ"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"ਬੱਗ ਰਿਪੋਰਟ ਇਕੱਤਰ ਕੀਤੀ ਗਈ"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"ਬੱਗ ਰਿਪੋਰਟ ਦੀ ਸਥਿਤੀ ਸੰਬੰਧੀ ਚੈਨਲ"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-pl/strings.xml b/tests/BugReportApp/res/values-pl/strings.xml
index 052665a..c4cc073 100644
--- a/tests/BugReportApp/res/values-pl/strings.xml
+++ b/tests/BugReportApp/res/values-pl/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Opowiedz nam o tym problemie"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Wiadomość głosowa dotycząca zgłoszenia błędu z %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Ukończono rejestrowanie"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Dane do raportu o błędzie są już zbierane"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Trwa zgłaszanie błędu"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Zebrano dane do raportu o błędzie"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Dodaj wiadomość głosową"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Dodaj wiadomość głosową i prześlij"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Prześlij"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Prześlij do GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Przyznaj uprawnienia"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Dane do raportu o błędzie są już zbierane"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Rozpoczęło się zgłaszanie błędu"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Trwa zgłaszanie błędu"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Rozpoczęto zgłaszanie błędu"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Nie udało się zgłosić błędu"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Nie udało się przechwycić zawartości ekranu"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Nie udało się utworzyć zrzutu stanu"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Nie udało się utworzyć zrzutu stanu"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Trwa zgłaszanie błędu"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Dane do raportu o błędzie zostały zebrane"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Zgłoszono błąd"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kanał stanu zgłaszania błędu"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-pt-rPT/strings.xml b/tests/BugReportApp/res/values-pt-rPT/strings.xml
index a9dfbae..7d8d6c7 100644
--- a/tests/BugReportApp/res/values-pt-rPT/strings.xml
+++ b/tests/BugReportApp/res/values-pt-rPT/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Fale e descreva o problema"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Mensagem de áudio do relatório de erro à(s) %s."</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Gravação concluída."</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Já está a ser recolhido um relatório de erro"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"O relatório de erro está em curso"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Foi recolhido um relatório de erro"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Adicionar áudio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Adicionar áudio e carregar"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Carregar"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Carregar para o GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Conceda as autorizações."</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Já está a ser recolhido um relatório de erro."</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"A criação do relatório de erro foi iniciada."</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"O relatório de erro está em curso"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"O relatório de erro teve início"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Falha no relatório de erro."</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Falha ao efetuar a captura de ecrã."</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Falha do estado de captura."</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Falha do estado de captura"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"O relatório de erro está em curso."</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"O relatório de erro foi recolhido"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"O relatório de erro foi recolhido"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Canal do estado do relatório de erro"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-pt/strings.xml b/tests/BugReportApp/res/values-pt/strings.xml
index 0d872a7..1b0b7fc 100644
--- a/tests/BugReportApp/res/values-pt/strings.xml
+++ b/tests/BugReportApp/res/values-pt/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Fale e descreva o problema"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Mensagem de áudio para o relatório do bug às %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Gravação concluída"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Um relatório do bug já está sendo coletado"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"O relatório do bug está em progresso"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Um relatório do bug foi coletado"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Adicionar áudio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Adicionar áudio e fazer upload"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Fazer upload"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Fazer upload para o GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Conceda permissões"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"O relatório do bug já sendo coletado"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"A geração do relatório do bug foi iniciada"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"O relatório do bug está em progresso"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"O relatório do bug foi iniciado"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Falha no relatório do bug"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Falha ao capturar a tela"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Falha no estado de despejo"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Falha no dumpstate"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"O relatório do bug está em progresso"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"O relatório do bug foi coletado"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"O relatório do bug foi coletado"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Canal de status do relatório do bug"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ro/strings.xml b/tests/BugReportApp/res/values-ro/strings.xml
index 435869b..490a056 100644
--- a/tests/BugReportApp/res/values-ro/strings.xml
+++ b/tests/BugReportApp/res/values-ro/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Vorbiți și descrieți problema"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Mesaj audio pentru raportul de eroare la %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Înregistrarea s-a încheiat"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Se colectează deja un raport de eroare"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Raportul de eroare este în curs"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"S-a colectat un raport de eroare"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Adăugați conținut audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Adăugați conținut audio și încărcați"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Încărcați"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Încărcați în GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Acordați permisiuni"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Se colectează deja raportul de eroare"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"A fost inițiată raportarea erorilor"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Raportul de eroare este în curs"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Raportul de eroare a fost inițiat"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Raportul de eroare nu s-a realizat"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Captura de ecran nu a reușit"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Nu s-a preluat starea fișierului dump"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Nu s-a preluat starea fișierului dump"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Raportul de eroare este în curs"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Se colectează raportul de eroare"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Raportul de eroare a fost colectat"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Canalul de stare a raportului de eroare"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ru/strings.xml b/tests/BugReportApp/res/values-ru/strings.xml
index 479c80e..89f4e95 100644
--- a/tests/BugReportApp/res/values-ru/strings.xml
+++ b/tests/BugReportApp/res/values-ru/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Опишите ошибку вслух"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Аудио для отчета об ошибке (%s)"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Запись завершена."</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Отчет об ошибке уже загружается"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Отчет об ошибке загружается…"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Отчет об ошибке загружен."</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Добавить аудио"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Добавить и загрузить аудио"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Загрузить"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Загрузить в GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Предоставьте разрешения."</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Отчет об ошибке уже загружается."</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Выполняется загрузка отчета об ошибке."</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Отчет об ошибке загружается…"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Создается отчет об ошибке…"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Произошла ошибка."</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Не удалось сделать скриншот."</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Ошибка дампа состояния."</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Ошибка сервиса Dumpstate."</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Отчет об ошибке загружается…"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Отчет об ошибке загружен"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Отчет об ошибке загружен"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Канал статуса отчета об ошибке"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-si/strings.xml b/tests/BugReportApp/res/values-si/strings.xml
index 994f997..3ac291a 100644
--- a/tests/BugReportApp/res/values-si/strings.xml
+++ b/tests/BugReportApp/res/values-si/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"ගැටලුව ගැන කථා කර විස්තර කරන්න"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s හිදී දෝෂ වාර්තාව සඳහා ශ්රව්ය පණිවිඩය"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"පටිගත කිරීම අවසන් විය"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"දෝෂ වාර්තාවක් දැනටමත් එක් කෙරෙමින් පවතියි"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"දෝෂ වාර්තාව ප්රගතියේ පවතියි"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"දෝෂ වාර්තාවක් එකතු කර ඇත"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ශ්රව්ය එක් කරන්න"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ශ්රව්ය එක් කර උඩුගත කරන්න"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"උඩුගත කරන්න"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS වෙත උඩුගත කරන්න"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"අවසර දෙන්න"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"දෝෂ වාර්තාව දැනටමත් එක් කෙරෙමින් පවතියි"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"දෝෂ වාර්තා කිරීම ඇරඹිණි"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"දෝෂ වාර්තාව ප්රගතියේ පවතියි"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"දෝෂ වර්තාව ආරම්භ විය"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"දෝෂ වාර්තාව අසාර්ථකයි"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"තිර ග්රහණය අසාර්ථකයි"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"නික්ෂේප තත්ත්වය අසාර්ථකයි"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"ඩම්ප්ස්ටේට් අසාර්ථක විය"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"දෝෂ වාර්තාව ප්රගතියේ පවතියි"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"දෝෂ වාර්තාව එක් කෙරේ"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"දෝෂ වාර්තාව ලබා ගෙන ඇත"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"දෝෂ වාර්තා තත්ත්ව නාලිකාව"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-sk/strings.xml b/tests/BugReportApp/res/values-sk/strings.xml
index a13406c..1af046d 100644
--- a/tests/BugReportApp/res/values-sk/strings.xml
+++ b/tests/BugReportApp/res/values-sk/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Hovorte a opíšte problém"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Zvuková správa pre hlásenie chyby o %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Nahrávanie bolo dokončené"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Hlásenie chyby sa už získava"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Nahlasuje sa chyba"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Bolo prijaté hlásenie chyby"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Pridať zvuk"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Pridať zvuk a nahrať"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Nahrať"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Nahrať do GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Udeľte povolenia"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Hlásenie chyby už bolo získané"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Hlásenie chyby bolo spustené"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Nahlasuje sa chyba"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Bolo spustené hlásenie chyby"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Hlásenie chyby zlyhalo"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Nepodarilo sa vytvoriť snímku obrazovky"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Nepodarilo sa obnoviť stav zo zálohy"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Nepodarilo sa obnoviť stav zo zálohy"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Prebieha hlásenie chyby"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Prebieha získavanie hlásenia chyby"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Bola nahlásená chyba"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kanál stavu hlásenia chyby"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-sl/strings.xml b/tests/BugReportApp/res/values-sl/strings.xml
index 6c8368f..84db60b 100644
--- a/tests/BugReportApp/res/values-sl/strings.xml
+++ b/tests/BugReportApp/res/values-sl/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Govorite in opišite težavo"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Zvočno sporočilo za poročilo o napakah pri %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Snemanje je dokončano"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Poročilo o napakah se že zbira"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Poročilo o napakah se ustvarja."</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Poročilo o napakah je zbrano"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Dodaj zvok"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Dodaj zvok in naloži"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Naloži"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Naloži v GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Odobrite dovoljenja"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Poročilo o napakah se že zbira"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Poročanje o napakah se je začelo"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Poročilo o napakah se ustvarja."</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Poročilo o napakah je začelo nastajati."</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Ustvarjanje poročila o napakah ni uspelo"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Zajemanje zaslona ni uspelo"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Izvoz stanja ni uspel"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Storitev Dumpstate ni bila uspešna."</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Poročilo o napakah se ustvarja"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Poročilo o napakah je zbrano"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Poročilo o napakah je zbrano."</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kanala stanja poročila o napakah"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-sq/strings.xml b/tests/BugReportApp/res/values-sq/strings.xml
index 4f96081..ee33f90 100644
--- a/tests/BugReportApp/res/values-sq/strings.xml
+++ b/tests/BugReportApp/res/values-sq/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Fol dhe përshkruaj problemin"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Mesazh me audio për defektin në kod në %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Regjistrimi përfundoi"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Një raport i defekteve në kod po merret tashmë"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Raporti i defekteve në kod është në vazhdim"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Është marrë një raport i defekteve në kod"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Shto audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Shto audio dhe ngarko"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Ngarko"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Ngarko në GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Jep lejet"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Raporti i defekteve në kod është marrë tashmë"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Filloi raportimi për defektin në kod"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Raporti i defekteve në kod është në vazhdim"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Raporti i defekteve në kod filloi"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Raporti i defekteve në kod dështoi"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Regjistrimi i ekranit dështoi"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Gjendja e mbledhjes dështoi"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Gjendja e mbledhjes dështoi"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Raporti i defekteve në kod është në vazhdim"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Raporti i defekteve në kod u mor"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Raporti i defekteve në kod është mbledhur"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kanali i statusit të raportit të defekteve në kod"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-sr/strings.xml b/tests/BugReportApp/res/values-sr/strings.xml
index b77f27c..7c49841 100644
--- a/tests/BugReportApp/res/values-sr/strings.xml
+++ b/tests/BugReportApp/res/values-sr/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Изговорите и објасните проблем"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Аудио порука за извештај о грешци у %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Снимање је завршено"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Извештај о грешци се већ прикупља"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"У току је прављење извештаја о грешци"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Прикупљен је извештај о грешци"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Додај аудио"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Додај аудио и отпреми"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Отпреми"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Отпреми у GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Дајте дозволе"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Извештај о грешци се већ прикупља"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Покренут је извештај о грешци"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"У току је прављење извештаја о грешци"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Извештај о грешци је покренут"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Прављење извештаја о грешци није успело"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Снимање екрана није успело"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Снимање стања није успело"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Снимање стања није успело"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"У току је прављење извештаја о грешци"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Извештај о грешци је прикупљен"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Извештај о грешци је прикупљен"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Канал статуса извештаја о грешци"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-sv/strings.xml b/tests/BugReportApp/res/values-sv/strings.xml
index a5b3fda..9d1914d 100644
--- a/tests/BugReportApp/res/values-sv/strings.xml
+++ b/tests/BugReportApp/res/values-sv/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Beskriv problemet genom att prata"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Ljudmeddelande till felrapporten klockan %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Inspelningen är slutförd"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"En felrapport samlas redan in"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Felrapportering pågår"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"En felrapport har samlats in"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Lägg till ljud"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Lägg till ljud och ladda upp"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Ladda upp"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Ladda upp till GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Bevilja behörigheter"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Felrapporten samlas redan in"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Felrapportering startas"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Felrapportering pågår"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Felrapporten har påbörjats"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Felrapporten misslyckades"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Skärmbilden misslyckades"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Statusdumpen misslyckades"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Statusdumpen misslyckades"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Felrapporten har påbörjats"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Felrapporten samlas in"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Felrapporten har samlats in"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kanal för felrapportstatus"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-sw/strings.xml b/tests/BugReportApp/res/values-sw/strings.xml
index 881ca52..96cb870 100644
--- a/tests/BugReportApp/res/values-sw/strings.xml
+++ b/tests/BugReportApp/res/values-sw/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Zungumza na Ueleze Tatizo"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Ujumbe wa sauti wa ripoti ya hitilafu wa %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Imemaliza kurekodi"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Ripoti ya hitilafu tayari inakusanywa"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Inaendelea kuripoti hitilafu"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Ripoti ya hitilafu imekusanywa"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Weka Ujumbe wa Sauti"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Weka Ujumbe wa Sauti na Upakie"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Pakia"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Pakia kwenye GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Tafadhali toa ruhusa"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Ripoti ya hitilafu tayari inakusanywa"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Imeanza kuripoti hitilafu"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Inaendelea kuripoti hitilafu"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Imeanza kuripoti hitilafu"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Imeshindwa kuripoti hitilafu"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Imeshindwa kupiga picha ya skrini"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Imeshindwa kuhamishia data kwingine"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Huduma ya Dumpstate haikukamilika"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Inaendelea kuripoti hitilafu"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Ripoti ya hitilafu imekusanywa"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Imekusanya ripoti ya hitilafu"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kituo cha hali ya ripoti ya hitilafu"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ta/strings.xml b/tests/BugReportApp/res/values-ta/strings.xml
index bf0e227..c1da3e6 100644
--- a/tests/BugReportApp/res/values-ta/strings.xml
+++ b/tests/BugReportApp/res/values-ta/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"பேசுங்கள் & சிக்கலை விளக்குங்கள்"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"பிழை அறிக்கைக்கான ஆடியோ மெசெஜ் %sக்கு"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"ரெக்கார்டிங் முடிந்தது"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"பிழை அறிக்கை ஏற்கனவே சேகரிக்கப்பட்டுவிட்டது"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"பிழை அறிக்கை செயலிலுள்ளது"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"பிழை அறிக்கை சேகரிக்கப்பட்டது"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ஆடியோவைச் சேர்"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ஆடியோவைச் சேர்த்து பதிவேற்றவும்"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"பதிவேற்று"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCSஸுக்குப் பதிவேற்று"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"அனுமதிகளை வழங்குங்கள்"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"பிழை அறிக்கை ஏற்கனவே சேகரிக்கப்பட்டுவிட்டது"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"பிழை அறிக்கை தொடங்கப்பட்டது"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"பிழை அறிக்கை செயலிலுள்ளது"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"பிழை அறிக்கை தொடங்கப்பட்டது"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"பிழை அறிக்கை தோல்வி"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"திரையைப் படமெடுக்க முடியவில்லை"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"டம்ப் நிலை தோல்வி"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"டம்ப்ஸ்டேட் சேவை தோல்வியடைந்தது"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"பிழை அறிக்கை செயல்பாட்டிலுள்ளது"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"பிழை அறிக்கை சேகரிக்கப்பட்டது"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"பிழை அறிக்கை சேகரிக்கப்பட்டது"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"பிழை அறிக்கை நிலை தொடர்பான சேனல்"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-te/strings.xml b/tests/BugReportApp/res/values-te/strings.xml
index 1a8d6c5..f3710cd 100644
--- a/tests/BugReportApp/res/values-te/strings.xml
+++ b/tests/BugReportApp/res/values-te/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"మాట్లాడండి & సమస్యను వివరించండి"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"బగ్ రిపోర్ట్ కోసం %s వద్ద ఆడియో మెసేజ్"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"రికార్డ్ చేయడం పూర్తయింది"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"బగ్ రిపోర్ట్ ఇప్పటికే సేకరించబడుతోంది"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"బగ్ రిపోర్ట్ ప్రోగ్రెస్లో ఉంది"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"బగ్ రిపోర్ట్ సేకరించబడింది"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"ఆడియోను జోడించు"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"ఆడియోను జోడించు & అప్లోడ్ చేయి"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"అప్లోడ్ చేయండి"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCSకు అప్లోడ్ చేయండి"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"దయచేసి అనుమతులను మంజూరు చేయండి"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"బగ్ రిపోర్ట్ ఇప్పటికే సేకరించబడుతోంది"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"బగ్ను నివేదించడం మొదలైంది"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"బగ్ రిపోర్ట్ ప్రోగ్రెస్లో ఉంది"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"బగ్ రిపోర్ట్ ప్రారంభించబడింది"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"బగ్ రిపోర్ట్ విఫలమైంది"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"స్క్రీన్ క్యాప్చర్ విఫలమైంది"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"డంప్ స్థితి విఫలమైంది"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"డంప్స్టేట్ విఫలమైంది"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"బగ్ రిపోర్ట్ ప్రోగ్రెస్లో ఉంది"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"బగ్ రిపోర్ట్ సేకరించబడింది"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"బగ్ రిపోర్ట్ సేకరించబడింది"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"బగ్ రిపోర్ట్ స్థితి ఛానెల్"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-th/strings.xml b/tests/BugReportApp/res/values-th/strings.xml
index 74a83aa..59d5499 100644
--- a/tests/BugReportApp/res/values-th/strings.xml
+++ b/tests/BugReportApp/res/values-th/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"พูดอธิบายปัญหา"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"ข้อความเสียงสำหรับรายงานข้อบกพร่องเมื่อ %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"บันทึกเสียงเสร็จแล้ว"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"กำลังรวบรวมรายงานข้อบกพร่อง"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"อยู่ระหว่างรายงานข้อบกพร่อง"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"รวบรวมรายงานข้อบกพร่องแล้ว"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"เพิ่มเสียง"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"เพิ่มเสียงและอัปโหลด"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"อัปโหลด"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"อัปโหลดไปยัง GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"โปรดให้สิทธิ์"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"กำลังรวบรวมรายงานข้อบกพร่อง"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"เริ่มการรายงานข้อบกพร่องแล้ว"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"อยู่ระหว่างรายงานข้อบกพร่อง"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"เริ่มรายงานข้อบกพร่องแล้ว"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"รายงานข้อบกพร่องไม่สำเร็จ"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"จับภาพหน้าจอไม่สำเร็จ"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ดัมพ์สเตทไม่สำเร็จ"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"ดัมพ์สเตทไม่สำเร็จ"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"อยู่ระหว่างรายงานข้อบกพร่อง"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"รวบรวมรายงานข้อบกพร่องแล้ว"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"รวบรวมรายงานข้อบกพร่องแล้ว"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"ช่องทางของสถานะรายงานข้อบกพร่อง"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-tl/strings.xml b/tests/BugReportApp/res/values-tl/strings.xml
index f1f391e..c4fa6a7 100644
--- a/tests/BugReportApp/res/values-tl/strings.xml
+++ b/tests/BugReportApp/res/values-tl/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Magsalita at Ilarawan Ang Isyu"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Audio message para sa ulat ng bug noong %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Tapos na ang pag-record"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"May kinukuha nang ulat ng bug"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Isinasagawa ang ulat ng bug"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"May nakuhang ulat ng bug"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Magdagdag ng Audio"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Magdagdag ng Audio at I-upload"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"I-upload"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"I-upload sa GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Pakibigay ang mga pahintulot"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Kinukuha na ang ulat ng bug"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Sinimulan ang pag-uulat ng bug"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Isinasagawa ang ulat ng bug"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Sinimulan ang ulat ng bug"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Hindi naisagawa ang ulat ng bug"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Hindi na-capture ang screen"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Hindi naisagawa ang dump state"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Hindi naisagawa ang dumpstate"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Isinasagawa ang ulat ng bug"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Nakuha ang ulat ng bug"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Nakolekta na ang ulat ng bug"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Channel ng status ng ulat ng bug"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-tr/strings.xml b/tests/BugReportApp/res/values-tr/strings.xml
index d0f5e35..2770b53 100644
--- a/tests/BugReportApp/res/values-tr/strings.xml
+++ b/tests/BugReportApp/res/values-tr/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Konuşun ve Sorunu Açıklayın"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s hata raporu için sesli mesaj"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Kayıt tamamlandı"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Şu anda zaten bir hata raporu alınıyor"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Hata raporu devam ediyor"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Hata raporu alındı"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Ses Ekle"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Ses Ekle ve Yükle"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Yükle"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS\'ye yükle"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Lütfen izinleri verin"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Hata raporu zaten alınmış"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Hata raporlama işlemi başladı"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Hata raporu devam ediyor"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Hata raporu başlatıldı"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Hata raporu başarısız"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Ekran görüntüsü alınamadı"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Döküm durumu başarısız"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Döküm durumu başarısız"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Hata raporu devam ediyor"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Hata raporu alındı"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Hata raporu toplandı"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Hata raporu durum kanalı"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-uk/strings.xml b/tests/BugReportApp/res/values-uk/strings.xml
index 9a350e0..d0e20d5 100644
--- a/tests/BugReportApp/res/values-uk/strings.xml
+++ b/tests/BugReportApp/res/values-uk/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Говоріть і опишіть проблему"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Аудіо для звіту про помилку о %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Запис завершено"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Звіт про помилку вже завантажується"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Звіт про помилку формується"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Звіт про помилку отримано"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Додати аудіоповідомлення"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Додати аудіоповідомлення й завантажити"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Завантажити"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Завантажити в GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Надайте дозволи"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Звіт про помилку вже завантажується"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Повідомлення про помилку розпочато"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Звіт про помилку формується"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Розпочато формування звіту про помилку"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Не вдалося надіслати звіт про помилку"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Не вдалося зробити знімок екрана"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Помилка запису стану"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Помилка Dumpstate"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Звіт про помилку створюється"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Звіт про помилку отримано"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Звіт про помилку отримано"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Канал стану звіту про помилку"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-ur/strings.xml b/tests/BugReportApp/res/values-ur/strings.xml
index a70586a..890f677 100644
--- a/tests/BugReportApp/res/values-ur/strings.xml
+++ b/tests/BugReportApp/res/values-ur/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"بولیں اور مسئلہ بیان کریں"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s پر بگ رپورٹ کے لیے آڈیو پیغام"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"رکارڈنگ مکمل ہو گئی"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"بگ رپورٹ پہلے سے ہی جمع کی جارہی ہے"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"بگ رپورٹ پیشرفت میں ہے"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"بگ رپورٹ جمع کر لی گئی ہے"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"آڈیو شامل کریں"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"آڈیو شامل اور اپ لوڈ کریں"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"اپ لوڈ کریں"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS پر اپ لوڈ کریں"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"براہ کرم اجازتوں کی منظوری دیں"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"بگ رپورٹ پہلے سے ہی جمع کی جا رہی ہے"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"بگ رپورٹنگ شروع کر دی گئی ہے"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"بگ رپورٹ پیشرفت میں ہے"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"بگ رپورٹ شروع ہو گئی ہے"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"بگ رپورٹ ناکام ہو گئی"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"اسکرین کیپچر ناکام ہو گیا"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"ڈمپ اسٹیٹ ناکام ہو گیا"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"ڈمپ اسٹیٹ ناکام ہو گیا"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"بگ رپورٹ پیشرفت میں ہے"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"بگ رپورٹ جمع کی گئی ہے"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"بگ رپورٹ جمع کر لی گئی ہے"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"بگ رپورٹ کی صورتحال کا چینل"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-uz/strings.xml b/tests/BugReportApp/res/values-uz/strings.xml
index 7ae806b..4ff4d43 100644
--- a/tests/BugReportApp/res/values-uz/strings.xml
+++ b/tests/BugReportApp/res/values-uz/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Muammoni ovoz yordamida tasvirlang"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Xatolik hisoboti uchun audio xabar (%s)"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Yozib olish tugadi"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Xatoliklar hisoboti yuklanmoqda"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Xatoliklar hisoboti yuklanmoqda"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Xatoliklar hisoboti jamlangan"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Audio kiritish"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Audio kiritish va yuklash"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Yuklash"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"GCS serverlariga yuklash"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Ruxsat bering"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Xatoliklar hisoboti yuklanmoqda"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Xato haqida hisobot berish boshlandi"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Xatoliklar hisoboti yuklanmoqda"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Xatoliklar hisoboti yaratilmoqda"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Xatoliklar hisoboti yuborilmadi"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Skrinshot olinmadi"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Damp holatida xatolik"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate xizmatida xatolik"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Xatoliklar hisoboti yuklanmoqda"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Xatoliklar hisoboti yuklandi"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Xatoliklar hisoboti jamlandi"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Xatoliklar hisoboti holati kanali"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-vi/strings.xml b/tests/BugReportApp/res/values-vi/strings.xml
index 7454927..61599dd 100644
--- a/tests/BugReportApp/res/values-vi/strings.xml
+++ b/tests/BugReportApp/res/values-vi/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Hãy nói để mô tả vấn đề"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Thông báo bằng âm thanh cho báo cáo lỗi lúc %s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Đã ghi xong"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Đang thu thập báo cáo lỗi"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Đang báo cáo lỗi"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Đã thu thập báo cáo lỗi"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Thêm âm thanh"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Thêm âm thanh và tải lên"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Tải lên"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Tải lên GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Vui lòng cấp quyền"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Đang thu thập báo cáo lỗi"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Đã bắt đầu báo cáo lỗi"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Đang báo cáo lỗi"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Đã bắt đầu báo cáo lỗi"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Không báo cáo được lỗi"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Không chụp được màn hình"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Không thu thập được nhật ký hệ thống"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Không thực hiện được dịch vụ dumpstate"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Đang báo cáo lỗi"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Đã thu thập báo cáo lỗi"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Đã thu thập báo cáo lỗi"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Kênh trạng thái báo cáo lỗi"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-zh-rCN/strings.xml b/tests/BugReportApp/res/values-zh-rCN/strings.xml
index 4460403..3734ce8 100644
--- a/tests/BugReportApp/res/values-zh-rCN/strings.xml
+++ b/tests/BugReportApp/res/values-zh-rCN/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"读出并说明问题"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"%s的错误报告的语音消息"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"录制完成"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"已在收集错误报告"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"正在收集错误报告"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"已收集错误报告"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"添加语音消息"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"添加语音消息并上传"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"上传"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"上传到 GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"请授予权限"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"已在收集错误报告"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"错误报告流程已启动"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"正在收集错误报告"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"已开始收集错误报告"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"无法收集错误报告"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"无法截取屏幕截图"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"无法转储状态"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"转储状态失败"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"正在收集错误报告"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"已收集错误报告"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"已收集错误报告"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"错误报告状态渠道"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-zh-rHK/strings.xml b/tests/BugReportApp/res/values-zh-rHK/strings.xml
index dc0d168..e2d15b2 100644
--- a/tests/BugReportApp/res/values-zh-rHK/strings.xml
+++ b/tests/BugReportApp/res/values-zh-rHK/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"朗讀和說明問題"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"錯誤報告語音訊息時間為:%s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"已完成記錄"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"已收集錯誤報告"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"正在產生錯誤報告"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"已收集到錯誤報告"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"新增語音訊息"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"新增語音訊息並上載"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"上載"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"上載至 GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"請授予權限"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"已收集錯誤報告"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"已開始錯誤報告程序"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"正在產生錯誤報告"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"已開始錯誤報告程序"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"無法產生錯誤報告"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"無法擷取螢幕"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"無法傾印狀態"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"無法傾印狀態"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"正在產生錯誤報告"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"已收集錯誤報告"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"已收集錯誤報告"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"錯誤報告狀態頻道"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-zh-rTW/strings.xml b/tests/BugReportApp/res/values-zh-rTW/strings.xml
index 75fc620..e6adf18 100644
--- a/tests/BugReportApp/res/values-zh-rTW/strings.xml
+++ b/tests/BugReportApp/res/values-zh-rTW/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"朗讀並說明問題"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"錯誤報告的語音訊息 (時間:%s)"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"錄製結束"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"正在收集錯誤報告"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"正在處理錯誤報告"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"錯誤報告收集完畢"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"新增語音訊息"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"新增語音訊息並上傳"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"上傳"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"上傳到 GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"請授予權限"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"正在收集錯誤報告"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"已啟動錯誤回報程序"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"正在處理錯誤報告"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"錯誤報告作業已啟動"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"錯誤報告發生問題"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"無法擷取螢幕畫面"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"傾印狀態發生問題"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Dumpstate 失敗"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"正在收集錯誤報告"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"錯誤報告收集完畢"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"錯誤報告收集完成"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"錯誤報告狀態頻道"</string>
</resources>
diff --git a/tests/BugReportApp/res/values-zu/strings.xml b/tests/BugReportApp/res/values-zu/strings.xml
index 061f105..06a7f78 100644
--- a/tests/BugReportApp/res/values-zu/strings.xml
+++ b/tests/BugReportApp/res/values-zu/strings.xml
@@ -31,7 +31,7 @@
<string name="bugreport_dialog_title" msgid="3315160684205929910">"Khuluma uphinde uchaze inkinga"</string>
<string name="bugreport_dialog_add_audio_to_existing" msgid="4958460267276935700">"Umlayezo womsindo wokubika kwesiphazamiso ku-%s"</string>
<string name="bugreport_dialog_recording_finished" msgid="3982335902169398758">"Ukurekhoda kuqediwe"</string>
- <string name="bugreport_dialog_in_progress_title" msgid="3782308141532622394">"Umbiko wesiphazamisi usuvele ulandiwe"</string>
+ <string name="bugreport_dialog_in_progress_title" msgid="1663500052146177338">"Umbiko wesiphazamisi uyaqhubeka"</string>
<string name="bugreport_dialog_in_progress_title_finished" msgid="1610236990020413471">"Kuqoqwe umbiko wesiphazamisi"</string>
<string name="bugreport_add_audio_button_text" msgid="8606400151705699144">"Engeza umsindo"</string>
<string name="bugreport_add_audio_upload_button_text" msgid="3830917832551764694">"Engeza umsindo nokulayisha"</string>
@@ -39,12 +39,12 @@
<string name="bugreport_upload_button_text" msgid="4136749466634820848">"Layisha"</string>
<string name="bugreport_upload_gcs_button_text" msgid="5844929656507607424">"Layisha ku-GCS"</string>
<string name="toast_permissions_denied" msgid="7054832711916992770">"Sicela unikeze izimvume"</string>
- <string name="toast_bug_report_in_progress" msgid="8319601113129121579">"Umbiko wesiphazamisi usuvele uqoqiwe"</string>
- <string name="toast_bug_report_started" msgid="7154589593986557754">"Ukubikwa kwesiphazamiso kuqalile"</string>
+ <string name="toast_bug_report_in_progress" msgid="5218530088025955746">"Umbiko wesiphazamisi uyaqhubeka"</string>
+ <string name="toast_bug_report_started" msgid="891404618481185195">"Umbiko wesiphazamisi uqalile"</string>
<string name="toast_status_failed" msgid="6365384202315043395">"Umbiko wesiphazamisi uhlulekile"</string>
<string name="toast_status_screencap_failed" msgid="2187083897594745149">"Ukuthathwa kwesikrini kuhlulekile"</string>
- <string name="toast_status_dump_state_failed" msgid="8072469535227541761">"Isimo sokulahla sihlulekile"</string>
+ <string name="toast_status_dump_state_failed" msgid="3496460783060512078">"Isimo sokulahla sihlulekile"</string>
<string name="notification_bugreport_in_progress" msgid="8486454116357963238">"Umbiko wesiphazamisi uyaqhubeka"</string>
- <string name="notification_bugreport_finished_title" msgid="3970195939909624320">"Umbiko wesiphazamisi uyaqoqwa"</string>
+ <string name="notification_bugreport_finished_title" msgid="1188447311929693472">"Umbiko wesiphazamisi uqoqiwe"</string>
<string name="notification_bugreport_channel_name" msgid="776902295433824255">"Isiteshi sesimo sombiko wesiphazamisi"</string>
</resources>
diff --git a/tests/BugReportApp/res/values/strings.xml b/tests/BugReportApp/res/values/strings.xml
index a2fc716..34ec183 100644
--- a/tests/BugReportApp/res/values/strings.xml
+++ b/tests/BugReportApp/res/values/strings.xml
@@ -34,7 +34,7 @@
<!-- %s is the timestamp of a bugreport. -->
<string name="bugreport_dialog_add_audio_to_existing">Audio message for bug report at %s</string>
<string name="bugreport_dialog_recording_finished">Recording finished</string>
- <string name="bugreport_dialog_in_progress_title">A bug report is already being collected</string>
+ <string name="bugreport_dialog_in_progress_title">Bug report is in progress</string>
<string name="bugreport_dialog_in_progress_title_finished">A bug report has been collected</string>
<!-- A button to add audio message to the bugreport. It will show Save button on the dialog. -->
<string name="bugreport_add_audio_button_text">Add Audio</string>
@@ -45,14 +45,14 @@
<string name="bugreport_upload_gcs_button_text">Upload to GCS</string>
<string name="toast_permissions_denied">Please grant permissions</string>
- <string name="toast_bug_report_in_progress">Bug report already being collected</string>
- <string name="toast_bug_report_started">Bug reporting is started</string>
+ <string name="toast_bug_report_in_progress">Bug report is in progress</string>
+ <string name="toast_bug_report_started">Bug report started</string>
<string name="toast_status_failed">Bug report failed</string>
<string name="toast_status_screencap_failed">Screen capture failed</string>
- <string name="toast_status_dump_state_failed">Dump state failed</string>
+ <string name="toast_status_dump_state_failed">Dumpstate failed</string>
<!-- Notification strings -->
<string name="notification_bugreport_in_progress">Bug report is in progress</string>
- <string name="notification_bugreport_finished_title">Bug report is collected</string>
+ <string name="notification_bugreport_finished_title">Bug report has been collected</string>
<string name="notification_bugreport_channel_name">Bug report status channel</string>
</resources>
diff --git a/tests/CarEvsCameraPreviewApp/Android.bp b/tests/CarEvsCameraPreviewApp/Android.bp
index 99f0505..fea6aaf 100644
--- a/tests/CarEvsCameraPreviewApp/Android.bp
+++ b/tests/CarEvsCameraPreviewApp/Android.bp
@@ -26,8 +26,8 @@
resource_dirs: ["res"],
- // This app uses system APIs.
- sdk_version: "system_current",
+ // registerReceiverForAllUsers() is a hidden api.
+ platform_apis: true,
certificate: "platform",
diff --git a/tests/CarEvsCameraPreviewApp/AndroidManifest.xml b/tests/CarEvsCameraPreviewApp/AndroidManifest.xml
index 6e4c254..201c90b 100644
--- a/tests/CarEvsCameraPreviewApp/AndroidManifest.xml
+++ b/tests/CarEvsCameraPreviewApp/AndroidManifest.xml
@@ -24,6 +24,8 @@
<uses-permission android:name="android.car.permission.MONITOR_CAR_EVS_STATUS" />
<uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
+ <!-- for registerReceiverForAllUsers() -->
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
<application android:label="@string/app_name"
android:icon="@drawable/rearview"
@@ -38,6 +40,8 @@
android:showForAllUsers="true"
android:theme="@style/Theme.Transparent"
android:turnScreenOn="true">
+ <meta-data android:name="distractionOptimized"
+ android:value="true"/>
</activity>
<activity android:name=".CarEvsCameraActivity"
@@ -54,6 +58,8 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
+ <meta-data android:name="distractionOptimized"
+ android:value="true"/>
</activity>
</application>
diff --git a/tests/CarEvsCameraPreviewApp/OWNERS b/tests/CarEvsCameraPreviewApp/OWNERS
new file mode 100644
index 0000000..9b47ecd
--- /dev/null
+++ b/tests/CarEvsCameraPreviewApp/OWNERS
@@ -0,0 +1,4 @@
+# Project owners
+ankitarora@google.com
+changyeon@google.com
+ycheo@google.com
diff --git a/tests/CarEvsCameraPreviewApp/res/values/config.xml b/tests/CarEvsCameraPreviewApp/res/values/config.xml
new file mode 100644
index 0000000..85eb0b1
--- /dev/null
+++ b/tests/CarEvsCameraPreviewApp/res/values/config.xml
@@ -0,0 +1,23 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources>
+ <!-- Shade of the background behind the camera window. 1.0 for fully opaque, 0.0 for fully
+ transparent. -->
+ <item name="config_cameraBackgroundScrim" format="float" type="dimen">0.7</item>
+
+ <!-- In-plane rotation angle of the rearview camera device in degree -->
+ <integer name="config_evsRearviewCameraInPlaneRotationAngle">0</integer>
+</resources>
diff --git a/tests/CarEvsCameraPreviewApp/res/values/overlayable.xml b/tests/CarEvsCameraPreviewApp/res/values/overlayable.xml
new file mode 100644
index 0000000..45f48d9
--- /dev/null
+++ b/tests/CarEvsCameraPreviewApp/res/values/overlayable.xml
@@ -0,0 +1,34 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!-- Copyright (C) 2021 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.-->
+<!--
+THIS FILE WAS AUTO GENERATED, DO NOT EDIT MANUALLY.
+REGENERATE USING packages/apps/Car/tests/tools/rro/generate-overlayable.py
+-->
+<resources>
+ <overlayable name="CarEvsCameraPreviewApp">
+ <policy type="system|product|signature">
+ <item type="color" name="button_background"/>
+ <item type="color" name="button_text"/>
+ <item type="dimen" name="camera_preview_height"/>
+ <item type="dimen" name="camera_preview_width"/>
+ <item type="dimen" name="close_button_text_size"/>
+ <item type="dimen" name="config_cameraBackgroundScrim"/>
+ <item type="id" name="close_button"/>
+ <item type="id" name="evs_preview_container"/>
+ <item type="layout" name="evs_preview_activity"/>
+ <item type="string" name="app_name"/>
+ <item type="string" name="close_button_text"/>
+ <item type="style" name="Theme.Transparent"/>
+ <item type="integer" name="config_evsRearviewCameraInPlaneRotationAngle"/>
+ </policy>
+ </overlayable>
+</resources>
diff --git a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java
index 737d918..cd6c840 100644
--- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java
+++ b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/CarEvsCameraPreviewActivity.java
@@ -24,7 +24,10 @@
import android.car.CarNotConnectedException;
import android.car.evs.CarEvsBufferDescriptor;
import android.car.evs.CarEvsManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.graphics.PixelFormat;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
@@ -34,8 +37,8 @@
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
import android.view.WindowManager;
-import android.widget.Button;
import android.widget.LinearLayout;
import java.util.ArrayList;
@@ -55,6 +58,7 @@
/** GL backed surface view to render the camera preview */
private CarEvsCameraGLSurfaceView mEvsView;
+ private ViewGroup mRootView;
private LinearLayout mPreviewContainer;
/** Display manager to monitor the display's state */
@@ -145,11 +149,32 @@
}
};
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
+ finish();
+ } else {
+ Log.e(TAG, "Unexpected intent " + intent);
+ }
+ }
+ };
+
+ // To close the PreviewActiivty when Home button is clicked.
+ private void registerBroadcastReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ // Need to register the receiver for all users, because we want to receive the Intent after
+ // the user is changed.
+ registerReceiverForAllUsers(mBroadcastReceiver, filter, null, null);
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
+ registerBroadcastReceiver();
parseExtra(getIntent());
setShowWhenLocked(true);
@@ -164,8 +189,9 @@
Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mCarServiceLifecycleListener);
mEvsView = new CarEvsCameraGLSurfaceView(getApplication(), this);
- mPreviewContainer = (LinearLayout) LayoutInflater.from(this).inflate(
+ mRootView = (ViewGroup) LayoutInflater.from(this).inflate(
R.layout.evs_preview_activity, /* root= */ null);
+ mPreviewContainer = mRootView.findViewById(R.id.evs_preview_container);
LinearLayout.LayoutParams viewParam = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT,
@@ -173,31 +199,31 @@
);
mEvsView.setLayoutParams(viewParam);
mPreviewContainer.addView(mEvsView, 0);
- Button closeButton = mPreviewContainer.findViewById(R.id.close_button);
- closeButton.setOnClickListener((v) -> finish());
+ View closeButton = mRootView.findViewById(R.id.close_button);
+ if (closeButton != null) {
+ closeButton.setOnClickListener(v -> finish());
+ }
int width = WindowManager.LayoutParams.MATCH_PARENT;
int height = WindowManager.LayoutParams.MATCH_PARENT;
- int x = 0;
- int y = 0;
if (mUseSystemWindow) {
width = getResources().getDimensionPixelOffset(R.dimen.camera_preview_width);
height = getResources().getDimensionPixelOffset(R.dimen.camera_preview_height);
- x = (getResources().getDisplayMetrics().widthPixels - width) / 2;
- y = (getResources().getDisplayMetrics().heightPixels - height) / 2;
}
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
- width, height, x, y,
+ width, height,
2020 /* WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY */,
- WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT);
- params.gravity = Gravity.LEFT | Gravity.TOP;
+ params.gravity = Gravity.CENTER;
+ params.dimAmount = getResources().getFloat(R.dimen.config_cameraBackgroundScrim);
+
if (mUseSystemWindow) {
WindowManager wm = getSystemService(WindowManager.class);
- wm.addView(mPreviewContainer, params);
+ wm.addView(mRootView, params);
} else {
- setContentView(mPreviewContainer, params);
+ setContentView(mRootView, params);
}
}
@@ -252,8 +278,10 @@
mDisplayManager.unregisterDisplayListener(mDisplayListener);
if (mUseSystemWindow) {
WindowManager wm = getSystemService(WindowManager.class);
- wm.removeView(mPreviewContainer);
+ wm.removeView(mRootView);
}
+
+ unregisterReceiver(mBroadcastReceiver);
}
private void handleVideoStreamLocked() {
diff --git a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/GLES20CarEvsCameraPreviewRenderer.java b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/GLES20CarEvsCameraPreviewRenderer.java
index 6d8b1f4..f04d15b 100644
--- a/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/GLES20CarEvsCameraPreviewRenderer.java
+++ b/tests/CarEvsCameraPreviewApp/src/com/google/android/car/evs/GLES20CarEvsCameraPreviewRenderer.java
@@ -54,10 +54,10 @@
1.0f, -1.0f, 0.0f };
private static final float[] sVertCarTexData = {
- 0.0f, 0.0f,
- 1.0f, 0.0f,
- 0.0f, 1.0f,
- 1.0f, 1.0f };
+ -0.5f, -0.5f,
+ 0.5f, -0.5f,
+ -0.5f, 0.5f,
+ 0.5f, 0.5f };
private static final float[] sIdentityMatrix = {
1.0f, 0.0f, 0.0f, 0.0f,
@@ -105,9 +105,27 @@
mVertCarPos = ByteBuffer.allocateDirect(sVertCarPosData.length * FLOAT_SIZE_BYTES)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
mVertCarPos.put(sVertCarPosData).position(0);
+
+ // Rotates the matrix in counter-clockwise
+ int angleInDegree = mContext.getResources().getInteger(
+ R.integer.config_evsRearviewCameraInPlaneRotationAngle);
+ double angleInRadian = Math.toRadians(angleInDegree);
+ float[] rotated = {0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f};
+ float sin = (float)Math.sin(angleInRadian);
+ float cos = (float)Math.cos(angleInRadian);
+
+ rotated[0] += cos * sVertCarTexData[0] - sin * sVertCarTexData[1];
+ rotated[1] += sin * sVertCarTexData[0] + cos * sVertCarTexData[1];
+ rotated[2] += cos * sVertCarTexData[2] - sin * sVertCarTexData[3];
+ rotated[3] += sin * sVertCarTexData[2] + cos * sVertCarTexData[3];
+ rotated[4] += cos * sVertCarTexData[4] - sin * sVertCarTexData[5];
+ rotated[5] += sin * sVertCarTexData[4] + cos * sVertCarTexData[5];
+ rotated[6] += cos * sVertCarTexData[6] - sin * sVertCarTexData[7];
+ rotated[7] += sin * sVertCarTexData[6] + cos * sVertCarTexData[7];
+
mVertCarTex = ByteBuffer.allocateDirect(sVertCarTexData.length * FLOAT_SIZE_BYTES)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
- mVertCarTex.put(sVertCarTexData).position(0);
+ mVertCarTex.put(rotated).position(0);
}
public void clearBuffer() {
diff --git a/tests/CarLibTests/src/android/car/CarTelemetryManagerTest.java b/tests/CarLibTests/src/android/car/CarTelemetryManagerTest.java
deleted file mode 100644
index 906662d..0000000
--- a/tests/CarLibTests/src/android/car/CarTelemetryManagerTest.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright (C) 2021 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 android.car;
-
-import static android.car.telemetry.CarTelemetryManager.ERROR_NONE;
-import static android.car.telemetry.CarTelemetryManager.ERROR_SAME_MANIFEST_EXISTS;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.verify;
-
-import android.app.Application;
-import android.car.telemetry.CarTelemetryManager;
-import android.car.telemetry.ManifestKey;
-import android.car.testapi.CarTelemetryController;
-import android.car.testapi.FakeCar;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.internal.DoNotInstrument;
-
-import java.util.concurrent.Executor;
-
-@RunWith(RobolectricTestRunner.class)
-@DoNotInstrument
-public class CarTelemetryManagerTest {
- @Rule
- public MockitoRule rule = MockitoJUnit.rule();
-
- private static final byte[] ERROR_BYTES = "ERROR".getBytes();
- private static final byte[] MANIFEST_BYTES = "MANIFEST".getBytes();
- private static final byte[] SCRIPT_RESULT_BYTES = "SCRIPT RESULT".getBytes();
- private static final ManifestKey DEFAULT_MANIFEST_KEY =
- new ManifestKey("NAME", 1);
- private static final Executor DIRECT_EXECUTOR = Runnable::run;
-
- private CarTelemetryController mCarTelemetryController;
- private CarTelemetryManager mCarTelemetryManager;
-
- @Mock
- private CarTelemetryManager.CarTelemetryResultsListener mListener;
-
-
-
- @Before
- public void setUp() {
- Application context = ApplicationProvider.getApplicationContext();
- FakeCar fakeCar = FakeCar.createFakeCar(context);
- Car carApi = fakeCar.getCar();
-
- mCarTelemetryManager =
- (CarTelemetryManager) carApi.getCarManager(Car.CAR_TELEMETRY_SERVICE);
- mCarTelemetryController = fakeCar.getCarTelemetryController();
- }
-
- @Test
- public void setListener_shouldSucceed() {
- mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
-
- assertThat(mCarTelemetryController.isListenerSet()).isTrue();
- }
-
- @Test
- public void clearListener_shouldSucceed() {
- mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
- mCarTelemetryManager.clearListener();
-
- assertThat(mCarTelemetryController.isListenerSet()).isFalse();
- }
-
- @Test
- public void addManifest_whenNew_shouldSucceed() {
- int result = mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
-
- assertThat(result).isEqualTo(ERROR_NONE);
- assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(1);
- }
-
- @Test
- public void addManifest_whenDuplicate_shouldIgnore() {
- int firstResult =
- mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
- int secondResult =
- mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
-
- assertThat(firstResult).isEqualTo(ERROR_NONE);
- assertThat(secondResult).isEqualTo(ERROR_SAME_MANIFEST_EXISTS);
- assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(1);
- }
-
- @Test
- public void removeManifest_whenValid_shouldSucceed() {
- mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
-
- boolean result = mCarTelemetryManager.removeManifest(DEFAULT_MANIFEST_KEY);
-
- assertThat(result).isTrue();
- assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(0);
- }
-
- @Test
- public void removeManifest_whenInvalid_shouldIgnore() {
- mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
-
- boolean result = mCarTelemetryManager.removeManifest(new ManifestKey("NAME", 100));
-
- assertThat(result).isFalse();
- assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(1);
- }
-
- @Test
- public void removeAllManifests_shouldSucceed() {
- mCarTelemetryManager.addManifest(DEFAULT_MANIFEST_KEY, MANIFEST_BYTES);
- mCarTelemetryManager.addManifest(new ManifestKey("NAME", 100), MANIFEST_BYTES);
-
- mCarTelemetryManager.removeAllManifests();
-
- assertThat(mCarTelemetryController.getValidManifestsCount()).isEqualTo(0);
- }
-
- @Test
- public void sendFinishedReports_shouldSucceed() {
- mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
- mCarTelemetryController.addDataForKey(DEFAULT_MANIFEST_KEY, SCRIPT_RESULT_BYTES);
-
- mCarTelemetryManager.sendFinishedReports(DEFAULT_MANIFEST_KEY);
-
- verify(mListener).onResult(DEFAULT_MANIFEST_KEY, SCRIPT_RESULT_BYTES);
- }
-
- @Test
- public void sendAllFinishedReports_shouldSucceed() {
- mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
- mCarTelemetryController.addDataForKey(DEFAULT_MANIFEST_KEY, SCRIPT_RESULT_BYTES);
- ManifestKey key2 = new ManifestKey("key name", 1);
- mCarTelemetryController.addDataForKey(key2, SCRIPT_RESULT_BYTES);
-
- mCarTelemetryManager.sendAllFinishedReports();
-
- verify(mListener).onResult(DEFAULT_MANIFEST_KEY, SCRIPT_RESULT_BYTES);
- verify(mListener).onResult(key2, SCRIPT_RESULT_BYTES);
- }
-
- @Test
- public void sendScriptExecutionErrors_shouldSucceed() {
- mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
- mCarTelemetryController.setErrorData(ERROR_BYTES);
-
- mCarTelemetryManager.sendScriptExecutionErrors();
-
- verify(mListener).onError(ERROR_BYTES);
- }
-}
diff --git a/tests/CarSecurityPermissionTest/src/com/android/car/input/CarInputManagerPermisisonTest.java b/tests/CarSecurityPermissionTest/src/com/android/car/input/CarInputManagerPermisisonTest.java
index 01cff52..466333b 100644
--- a/tests/CarSecurityPermissionTest/src/com/android/car/input/CarInputManagerPermisisonTest.java
+++ b/tests/CarSecurityPermissionTest/src/com/android/car/input/CarInputManagerPermisisonTest.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
import static org.testng.Assert.assertThrows;
import static org.testng.Assert.expectThrows;
@@ -35,6 +36,8 @@
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
+import java.util.concurrent.Executor;
+
/**
* This class contains security permission tests for the {@link CarInputManager}'s system APIs.
*/
@@ -62,14 +65,22 @@
}
@Test
- public void testEnableFeaturePermission() {
+ public void testRequestInputEventCapturePermission() {
assertThrows(SecurityException.class, () -> mCarInputManager.requestInputEventCapture(
CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
new int[]{CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION}, 0, mMockedCallback));
}
@Test
- public void testInjectKeyEvent() {
+ public void testRequestInputEventCaptureWithExecutorPermission() {
+ assertThrows(SecurityException.class, () -> mCarInputManager.requestInputEventCapture(
+ CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
+ new int[]{CarInputManager.INPUT_TYPE_ROTARY_NAVIGATION}, 0,
+ mock(Executor.class), mMockedCallback));
+ }
+
+ @Test
+ public void testInjectKeyEventPermission() {
long currentTime = SystemClock.uptimeMillis();
KeyEvent anyKeyEvent = new KeyEvent(/* downTime= */ currentTime,
/* eventTime= */ currentTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HOME,
@@ -82,4 +93,3 @@
"Injecting KeyEvent requires INJECT_EVENTS permission");
}
}
-
diff --git a/tests/CarSecurityPermissionTest/src/com/android/car/telemetry/CarTelemetryManagerPermissionTest.java b/tests/CarSecurityPermissionTest/src/com/android/car/telemetry/CarTelemetryManagerPermissionTest.java
index 509c931..de8b8d1 100644
--- a/tests/CarSecurityPermissionTest/src/com/android/car/telemetry/CarTelemetryManagerPermissionTest.java
+++ b/tests/CarSecurityPermissionTest/src/com/android/car/telemetry/CarTelemetryManagerPermissionTest.java
@@ -22,7 +22,7 @@
import android.car.Car;
import android.car.telemetry.CarTelemetryManager;
-import android.car.telemetry.ManifestKey;
+import android.car.telemetry.MetricsConfigKey;
import android.content.Context;
import androidx.annotation.NonNull;
@@ -45,8 +45,8 @@
public class CarTelemetryManagerPermissionTest {
private final Context mContext =
InstrumentationRegistry.getInstrumentation().getTargetContext();
- private final ManifestKey mManifestKey = new ManifestKey("name", 1);
- private final byte[] mManifestBytes = "manifest".getBytes();
+ private final MetricsConfigKey mMetricsConfigKey = new MetricsConfigKey("name", 1);
+ private final byte[] mMetricsConfigBytes = "manifest".getBytes();
private Car mCar;
private CarTelemetryManager mCarTelemetryManager;
@@ -83,25 +83,26 @@
}
@Test
- public void testAddManifest() throws Exception {
+ public void testAddMetricsConfig() throws Exception {
Exception e = expectThrows(SecurityException.class,
- () -> mCarTelemetryManager.addManifest(mManifestKey, mManifestBytes));
+ () -> mCarTelemetryManager.addMetricsConfig(mMetricsConfigKey,
+ mMetricsConfigBytes));
assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
}
@Test
- public void testRemoveManifest() throws Exception {
+ public void testRemoveMetricsConfig() {
Exception e = expectThrows(SecurityException.class,
- () -> mCarTelemetryManager.removeManifest(mManifestKey));
+ () -> mCarTelemetryManager.removeMetricsConfig(mMetricsConfigKey));
assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
}
@Test
- public void testRemoveAllManifests() throws Exception {
+ public void testRemoveAllMetricsConfigs() throws Exception {
Exception e = expectThrows(SecurityException.class,
- () -> mCarTelemetryManager.removeAllManifests());
+ () -> mCarTelemetryManager.removeAllMetricsConfigs());
assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
}
@@ -109,7 +110,7 @@
@Test
public void testSendFinishedReports() throws Exception {
Exception e = expectThrows(SecurityException.class,
- () -> mCarTelemetryManager.sendFinishedReports(mManifestKey));
+ () -> mCarTelemetryManager.sendFinishedReports(mMetricsConfigKey));
assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
}
@@ -122,23 +123,18 @@
assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
}
- @Test
- public void testSendScriptExecutionErrors() throws Exception {
- Exception e = expectThrows(SecurityException.class,
- () -> mCarTelemetryManager.sendScriptExecutionErrors());
-
- assertThat(e.getMessage()).contains(Car.PERMISSION_USE_CAR_TELEMETRY_SERVICE);
- }
-
private class FakeCarTelemetryResultsListener implements
CarTelemetryManager.CarTelemetryResultsListener {
@Override
- public void onResult(@NonNull ManifestKey key, @NonNull byte[] result) {
+ public void onResult(@NonNull MetricsConfigKey key, @NonNull byte[] result) {
}
@Override
- public void onError(@NonNull byte[] error) {
+ public void onError(@NonNull MetricsConfigKey key, @NonNull byte[] error) {
+ }
+
+ @Override
+ public void onAddMetricsConfigStatus(@NonNull MetricsConfigKey key, int statusCode) {
}
}
-
}
diff --git a/tests/EmbeddedKitchenSinkApp/Android.bp b/tests/EmbeddedKitchenSinkApp/Android.bp
index ef70cc9..ac942aa 100644
--- a/tests/EmbeddedKitchenSinkApp/Android.bp
+++ b/tests/EmbeddedKitchenSinkApp/Android.bp
@@ -46,12 +46,14 @@
"androidx.appcompat_appcompat",
"car-admin-ui-lib",
"car-ui-lib",
+ "car-qc-lib",
"android.hidl.base-V1.0-java",
"android.hardware.automotive.vehicle-V2.0-java",
"vehicle-hal-support-lib-for-test",
"com.android.car.keventreader-client",
"guava",
"android.car.cluster.navigation",
+ "cartelemetry-protos",
"car-experimental-api-static-lib",
],
diff --git a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
index 8f9f2d2..9b22570 100644
--- a/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
+++ b/tests/EmbeddedKitchenSinkApp/AndroidManifest.xml
@@ -26,6 +26,7 @@
<!-- use for CarServiceTest -->
<uses-permission android:name="android.car.permission.CAR_DRIVING_STATE"/>
<uses-permission android:name="android.car.permission.CAR_ENERGY"/>
+ <uses-permission android:name="android.car.permission.CAR_MONITOR_CLUSTER_NAVIGATION_STATE" />
<!-- use for AndroidCarApiTest -->
<uses-permission android:name="android.car.permission.CAR_INFO"/>
<!-- use for AndroidCarApiTest -->
@@ -46,11 +47,15 @@
<uses-permission android:name="android.car.permission.CAR_VENDOR_EXTENSION"/>
<!-- use for CarServiceTest -->
<uses-permission android:name="android.car.permission.CAR_UX_RESTRICTIONS_CONFIGURATION"/>
+ <!-- use for AndroidCarApiTest -->
+ <uses-permission android:name="android.car.permission.CONTROL_CAR_APP_LAUNCH"/>
<uses-permission android:name="android.car.permission.CONTROL_CAR_CLIMATE"/>
<uses-permission android:name="android.car.permission.READ_CAR_STEERING"/>
<uses-permission android:name="android.car.permission.STORAGE_MONITORING"/>
<uses-permission android:name="android.car.permission.CAR_DYNAMICS_STATE"/>
<uses-permission android:name="android.car.permission.CONTROL_APP_BLOCKING"/>
+ <!-- use for CarServiceTest -->
+ <uses-permission android:name="android.car.permission.USE_CAR_TELEMETRY_SERVICE"/>
<!-- Allow querying and writing to any property -->
<uses-permission android:name="android.car.permission.CAR_ENERGY_PORTS" />
<uses-permission android:name="android.car.permission.PERMISSION_CONTROL_ENERGY_PORTS" />
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml b/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml
new file mode 100644
index 0000000..cfad246
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/car_telemetry_test.xml
@@ -0,0 +1,137 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/gear_change_config"/>
+ <Button
+ android:id="@+id/send_on_gear_change_config"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/add_metrics_config"/>
+ <Button
+ android:id="@+id/remove_on_gear_change_config"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/remove_metrics_config"/>
+ <Button
+ android:id="@+id/get_on_gear_change_report"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/get_report"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/process_memory_config"/>
+ <Button
+ android:id="@+id/send_on_process_memory_config"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/add_metrics_config"/>
+ <Button
+ android:id="@+id/remove_on_process_memory_config"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/remove_metrics_config"/>
+ <Button
+ android:id="@+id/get_on_process_memory_report"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/get_report"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/app_start_memory_state_captured_config"/>
+ <Button
+ android:id="@+id/send_on_app_start_memory_state_captured_config"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/add_metrics_config"/>
+ <Button
+ android:id="@+id/remove_on_app_start_memory_state_captured_config"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/remove_metrics_config"/>
+ <Button
+ android:id="@+id/get_on_app_start_memory_state_captured_report"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/get_report"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/activity_foreground_state_changed_config"/>
+ <Button
+ android:id="@+id/send_on_activity_foreground_state_changed_config"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/add_metrics_config"/>
+ <Button
+ android:id="@+id/remove_on_activity_foreground_state_changed_config"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/remove_metrics_config"/>
+ <Button
+ android:id="@+id/get_on_activity_foreground_state_changed_report"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/get_report"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <Button
+ android:id="@+id/show_mem_info_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/show_mem_info"/>
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/output_textview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="20dp"/>
+</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
index a29296c..82c2c30 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/instrument_cluster.xml
@@ -48,5 +48,40 @@
android:padding="20dp"
android:text="@string/cluster_stop"
android:id="@+id/cluster_stop_button"/>
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:padding="20dp">
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:layout_margin="5dp"
+ android:text="@string/cluster_activity_state"/>
+ <RadioGroup
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <RadioButton
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:id="@+id/cluster_activity_state_default"
+ android:text="@string/cluster_activity_state_default"/>
+ <RadioButton
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:id="@+id/cluster_activity_state_enabled"
+ android:text="@string/cluster_activity_state_enabled"/>
+ <RadioButton
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="5dp"
+ android:id="@+id/cluster_activity_state_disabled"
+ android:text="@string/cluster_activity_state_disabled"/>
+ </RadioGroup>
+ </LinearLayout>
</LinearLayout>
</LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml b/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
index f1d7d81..7aaa3e0 100644
--- a/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/notification_fragment.xml
@@ -44,6 +44,58 @@
android:background="#334666"
android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:orientation="horizontal">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="10dp"
+ android:layout_marginEnd="10dp"
+ android:layout_gravity="center_vertical"
+ android:text="Number of messages:"/>
+
+ <NumberPicker
+ android:id="@+id/number_messages"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="#1da9ff"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:textSize="30sp"/>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="10dp"
+ android:layout_marginEnd="10dp"
+ android:layout_gravity="center_vertical"
+ android:text="Number of people:"/>
+
+ <NumberPicker
+ android:id="@+id/number_people"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="#1da9ff"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:textSize="30sp"/>
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/customizable_message_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_marginBottom="50dp"
+ android:layout_marginStart="10dp"
+ android:layout_marginEnd="10dp"
+ android:background="#1da9ff"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:text="Customizable message notification builder"
+ android:textSize="30sp"/>
+
<Button
android:id="@+id/category_call_button"
android:layout_width="wrap_content"
@@ -75,6 +127,16 @@
android:textSize="30sp"/>
<Button
+ android:id="@+id/category_long_message_same_person_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="10dp"
+ android:background="#ffa9a8"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:text="Long message from same person"
+ android:textSize="30sp"/>
+
+ <Button
android:id="@+id/category_message_mute_action_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/tests/EmbeddedKitchenSinkApp/res/layout/qc_viewer.xml b/tests/EmbeddedKitchenSinkApp/res/layout/qc_viewer.xml
new file mode 100644
index 0000000..d48284b
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/res/layout/qc_viewer.xml
@@ -0,0 +1,43 @@
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_horizontal">
+
+ <EditText
+ android:id="@+id/qc_uri_input"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="text"
+ android:singleLine="true"/>
+
+ <Button
+ android:id="@+id/submit_uri_btn"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Update QCView"/>
+
+ <com.android.car.qc.view.QCView
+ android:id="@+id/qc_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:focusable="false"/>
+
+ </LinearLayout>
diff --git a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
index 0613b45..c4144e0 100644
--- a/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
+++ b/tests/EmbeddedKitchenSinkApp/res/values/strings.xml
@@ -167,6 +167,10 @@
<string name="cluster_start_activity" translatable="false">Start Nav Activity</string>
<string name="cluster_start_activity_failed" translatable="false">Failed to start activity in cluster</string>
<string name="cluster_not_started" translatable="false">Missing navigation focus</string>
+ <string name="cluster_activity_state" translatable="false">Cluster activity state</string>
+ <string name="cluster_activity_state_default" translatable="false">Default</string>
+ <string name="cluster_activity_state_enabled" translatable="false">On</string>
+ <string name="cluster_activity_state_disabled" translatable="false">Off</string>
<!-- input test -->
<string name="volume_up" translatable="false">Volume +</string>
@@ -377,4 +381,14 @@
<!-- Fullscreen Activity Test -->
<string name="nav_to_full_screen" translatable="false">Navigate to Full Screen</string>
<string name="cancel" translatable="false">Cancel</string>
+
+ <!-- CarTelemetryService Test -->
+ <string name="gear_change_config" translate="false">on_gear_change:</string>
+ <string name="process_memory_config" translate="false">process_memory:</string>
+ <string name="app_start_memory_state_captured_config" translate="false">app_start_memory_state_captured:</string>
+ <string name="activity_foreground_state_changed_config" translate="false">activity_foreground_state_changed:</string>
+ <string name="add_metrics_config" translatable="false">Add MetricsConfig</string>
+ <string name="remove_metrics_config" translatable="false">Remove MetricsConfig</string>
+ <string name="get_report" translatable="false">Get Report</string>
+ <string name="show_mem_info" translate="false">Show Memory Info</string>
</resources>
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
index beeac2a..fd8819a 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/KitchenSinkActivity.java
@@ -23,6 +23,7 @@
import android.car.hardware.hvac.CarHvacManager;
import android.car.hardware.power.CarPowerManager;
import android.car.hardware.property.CarPropertyManager;
+import android.car.telemetry.CarTelemetryManager;
import android.car.watchdog.CarWatchdogManager;
import android.content.Context;
import android.content.Intent;
@@ -66,12 +67,14 @@
import com.google.android.car.kitchensink.power.PowerTestFragment;
import com.google.android.car.kitchensink.projection.ProjectionFragment;
import com.google.android.car.kitchensink.property.PropertyTestFragment;
+import com.google.android.car.kitchensink.qc.QCViewerFragment;
import com.google.android.car.kitchensink.rotary.RotaryFragment;
import com.google.android.car.kitchensink.sensor.SensorsTestFragment;
import com.google.android.car.kitchensink.storagelifetime.StorageLifetimeFragment;
import com.google.android.car.kitchensink.storagevolumes.StorageVolumesFragment;
import com.google.android.car.kitchensink.systembars.SystemBarsFragment;
import com.google.android.car.kitchensink.systemfeatures.SystemFeaturesFragment;
+import com.google.android.car.kitchensink.telemetry.CarTelemetryTestFragment;
import com.google.android.car.kitchensink.touch.TouchTestFragment;
import com.google.android.car.kitchensink.users.ProfileUserFragment;
import com.google.android.car.kitchensink.users.UserFragment;
@@ -199,12 +202,14 @@
new FragmentMenuEntry("profile_user", ProfileUserFragment.class),
new FragmentMenuEntry("projection", ProjectionFragment.class),
new FragmentMenuEntry("property test", PropertyTestFragment.class),
+ new FragmentMenuEntry("qc viewer", QCViewerFragment.class),
new FragmentMenuEntry("rotary", RotaryFragment.class),
new FragmentMenuEntry("sensors", SensorsTestFragment.class),
new FragmentMenuEntry("storage lifetime", StorageLifetimeFragment.class),
new FragmentMenuEntry("storage volumes", StorageVolumesFragment.class),
new FragmentMenuEntry("system bars", SystemBarsFragment.class),
new FragmentMenuEntry("system features", SystemFeaturesFragment.class),
+ new FragmentMenuEntry("telemetry", CarTelemetryTestFragment.class),
new FragmentMenuEntry("touch test", TouchTestFragment.class),
new FragmentMenuEntry("users", UserFragment.class),
new FragmentMenuEntry("user restrictions", UserRestrictionsFragment.class),
@@ -223,6 +228,7 @@
private CarSensorManager mSensorManager;
private CarAppFocusManager mCarAppFocusManager;
private CarProjectionManager mCarProjectionManager;
+ private CarTelemetryManager mCarTelemetryManager;
private CarWatchdogManager mCarWatchdogManager;
private Object mPropertyManagerReady = new Object();
@@ -250,6 +256,10 @@
return mCarProjectionManager;
}
+ public CarTelemetryManager getCarTelemetryManager() {
+ return mCarTelemetryManager;
+ }
+
public CarWatchdogManager getCarWatchdogManager() {
return mCarWatchdogManager;
}
@@ -411,6 +421,8 @@
(CarAppFocusManager) car.getCarManager(Car.APP_FOCUS_SERVICE);
mCarProjectionManager =
(CarProjectionManager) car.getCarManager(Car.PROJECTION_SERVICE);
+ mCarTelemetryManager =
+ (CarTelemetryManager) car.getCarManager(Car.CAR_TELEMETRY_SERVICE);
mCarWatchdogManager =
(CarWatchdogManager) car.getCarManager(Car.CAR_WATCHDOG_SERVICE);
mPropertyManagerReady.notifyAll();
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
index ab4e2d9..5599465 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/cluster/InstrumentClusterFragment.java
@@ -34,6 +34,7 @@
import android.car.cluster.navigation.NavigationState.Step;
import android.car.cluster.navigation.NavigationState.Timestamp;
import android.car.navigation.CarNavigationStatusManager;
+import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
@@ -41,6 +42,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
+import android.widget.RadioButton;
import android.widget.Toast;
import androidx.annotation.IdRes;
@@ -124,6 +126,7 @@
NavigationStateProto[] navigationStateArray = new NavigationStateProto[1];
navigationStateArray[0] = NavigationStateProto.newBuilder()
+ .setServiceStatus(NavigationStateProto.ServiceStatus.NORMAL)
.addSteps(Step.newBuilder()
.setManeuver(Maneuver.newBuilder()
.setType(Maneuver.Type.DEPART)
@@ -205,6 +208,13 @@
view.findViewById(R.id.cluster_start_button).setOnClickListener(v -> initCluster());
view.findViewById(R.id.cluster_stop_button).setOnClickListener(v -> stopCluster());
+ view.findViewById(R.id.cluster_activity_state_default).setOnClickListener(v ->
+ changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_DEFAULT));
+ view.findViewById(R.id.cluster_activity_state_enabled).setOnClickListener(v ->
+ changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_ENABLED));
+ view.findViewById(R.id.cluster_activity_state_disabled).setOnClickListener(v ->
+ changeClusterActivityState(PackageManager.COMPONENT_ENABLED_STATE_DISABLED));
+ updateInitialClusterActivityState(view);
mTurnByTurnButton = view.findViewById(R.id.cluster_turn_left_button);
mTurnByTurnButton.setOnClickListener(v -> toggleSendTurn());
@@ -212,6 +222,36 @@
return view;
}
+ private void updateInitialClusterActivityState(View view) {
+ PackageManager pm = getContext().getPackageManager();
+ ComponentName clusterActivity =
+ new ComponentName(getContext(), FakeClusterNavigationActivity.class);
+ int currentComponentState = pm.getComponentEnabledSetting(clusterActivity);
+ RadioButton button = view.findViewById(
+ convertClusterActivityStateToViewId(currentComponentState));
+ button.setChecked(true);
+ }
+
+ private int convertClusterActivityStateToViewId(int componentState) {
+ switch (componentState) {
+ case PackageManager.COMPONENT_ENABLED_STATE_DEFAULT:
+ return R.id.cluster_activity_state_default;
+ case PackageManager.COMPONENT_ENABLED_STATE_ENABLED:
+ return R.id.cluster_activity_state_enabled;
+ case PackageManager.COMPONENT_ENABLED_STATE_DISABLED:
+ return R.id.cluster_activity_state_disabled;
+ }
+ throw new IllegalStateException("Unknown component state: " + componentState);
+ }
+
+ private void changeClusterActivityState(int newComponentState) {
+ PackageManager pm = getContext().getPackageManager();
+ ComponentName clusterActivity =
+ new ComponentName(getContext(), FakeClusterNavigationActivity.class);
+ pm.setComponentEnabledSetting(clusterActivity, newComponentState,
+ PackageManager.DONT_KILL_APP);
+ }
+
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
initCarApi();
@@ -271,6 +311,7 @@
mTimer.cancel();
mTimer = null;
}
+ sendTurn(NavigationStateProto.newBuilder().build());
mTurnByTurnButton.setText(R.string.cluster_start_guidance);
}
@@ -278,13 +319,11 @@
* Sends one update of the navigation state through the {@link CarNavigationStatusManager}
*/
private void sendTurn(@NonNull NavigationStateProto state) {
- try {
+ if (hasFocus()) {
Bundle bundle = new Bundle();
bundle.putByteArray("navstate2", state.toByteArray());
- mCarNavigationStatusManager.sendEvent(1, bundle);
+ mCarNavigationStatusManager.sendNavigationStateChange(bundle);
Log.i(TAG, "Sending nav state: " + state);
- } catch (CarNotConnectedException e) {
- Log.e(TAG, "Failed to send turn information.", e);
}
}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
index 3d6d76d..f7378af 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/notification/NotificationFragment.java
@@ -14,6 +14,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.NumberPicker;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Action;
@@ -26,7 +27,9 @@
import com.google.android.car.kitchensink.KitchenSinkActivity;
import com.google.android.car.kitchensink.R;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
/**
* Test fragment that can send all sorts of notifications.
@@ -92,6 +95,7 @@
initOngoingButton(view);
initMessagingStyleButtonForDiffPerson(view);
initMessagingStyleButtonForSamePerson(view);
+ initMessagingStyleButtonForLongMessageSamePerson(view);
initMessagingStyleButtonWithMuteAction(view);
initTestMessagesButton(view);
initProgressButton(view);
@@ -100,6 +104,7 @@
initCallButton(view);
initCustomGroupSummaryButton(view);
initGroupWithoutSummaryButton(view);
+ initCustomizableMessageButton(view);
return view;
}
@@ -244,6 +249,83 @@
});
}
+ private void initCustomizableMessageButton(View view) {
+ NumberPicker messagesPicker = view.findViewById(R.id.number_messages);
+ messagesPicker.setMinValue(1);
+ messagesPicker.setMaxValue(25);
+ messagesPicker.setWrapSelectorWheel(true);
+ NumberPicker peoplePicker = view.findViewById(R.id.number_people);
+ peoplePicker.setMinValue(1);
+ peoplePicker.setMaxValue(25);
+ peoplePicker.setWrapSelectorWheel(true);
+
+ view.findViewById(R.id.customizable_message_button).setOnClickListener(v -> {
+ int id = mCurrentNotificationId++;
+
+ int numPeople = peoplePicker.getValue();
+ int numMessages = messagesPicker.getValue();
+
+ PendingIntent replyIntent = createServiceIntent(id, "reply");
+ PendingIntent markAsReadIntent = createServiceIntent(id, "read");
+
+ List<Person> personList = new ArrayList<>();
+
+ for (int i = 1; i <= numPeople; i++) {
+ personList.add(new Person.Builder()
+ .setName("Person " + i)
+ .setIcon(IconCompat.createWithResource(v.getContext(),
+ i % 2 == 1 ? R.drawable.avatar1 : R.drawable.avatar2))
+ .build());
+ }
+
+ MessagingStyle messagingStyle =
+ new MessagingStyle(personList.get(0))
+ .setConversationTitle("Customizable Group chat");
+ if (personList.size() > 1) {
+ messagingStyle.setGroupConversation(true);
+ }
+
+ int messageNumber = 1;
+ for (int i = 0; i < numMessages; i++) {
+ int personNum = i % numPeople;
+ if (personNum == numPeople - 1) {
+ messageNumber++;
+ }
+ Person person = personList.get(personNum);
+ String messageText = person.getName() + "'s " + messageNumber + " message";
+ messagingStyle.addMessage(
+ new MessagingStyle.Message(
+ messageText,
+ System.currentTimeMillis(),
+ person));
+ }
+
+ NotificationCompat.Builder notification = new NotificationCompat
+ .Builder(mContext, IMPORTANCE_HIGH_ID)
+ .setContentTitle("Customizable Group chat (Title)")
+ .setContentText("Customizable Group chat (Text)")
+ .setShowWhen(true)
+ .setCategory(Notification.CATEGORY_MESSAGE)
+ .setSmallIcon(R.drawable.car_ic_mode)
+ .setStyle(messagingStyle)
+ .setAutoCancel(true)
+ .setColor(mContext.getColor(android.R.color.holo_green_light))
+ .addAction(
+ new Action.Builder(R.drawable.ic_check_box, "read", markAsReadIntent)
+ .setSemanticAction(Action.SEMANTIC_ACTION_MARK_AS_READ)
+ .setShowsUserInterface(false)
+ .build())
+ .addAction(
+ new Action.Builder(R.drawable.ic_check_box, "reply", replyIntent)
+ .setSemanticAction(Action.SEMANTIC_ACTION_REPLY)
+ .setShowsUserInterface(false)
+ .addRemoteInput(new RemoteInput.Builder("input").build())
+ .build());
+
+ mManager.notify(id, notification.build());
+ });
+ }
+
private void initMessagingStyleButtonForDiffPerson(View view) {
view.findViewById(R.id.category_message_diff_person_button).setOnClickListener(v -> {
int id = mCurrentNotificationId++;
@@ -354,6 +436,54 @@
});
}
+ private void initMessagingStyleButtonForLongMessageSamePerson(View view) {
+ view.findViewById(R.id.category_long_message_same_person_button).setOnClickListener(v -> {
+ int id = mCurrentNotificationId++;
+
+ PendingIntent replyIntent = createServiceIntent(id, "reply");
+ PendingIntent markAsReadIntent = createServiceIntent(id, "read");
+
+
+
+ Person person = new Person.Builder().setName("John Doe").build();
+ MessagingStyle messagingStyle =
+ new MessagingStyle(person).setConversationTitle("Hello!");
+ NotificationCompat.Builder builder = new NotificationCompat
+ .Builder(mContext, IMPORTANCE_HIGH_ID)
+ .setContentTitle("Message from someone")
+ .setContentText("hi")
+ .setShowWhen(true)
+ .setCategory(Notification.CATEGORY_MESSAGE)
+ .setSmallIcon(R.drawable.car_ic_mode)
+ .setAutoCancel(true)
+ .setColor(mContext.getColor(android.R.color.holo_green_light))
+ .addAction(
+ new Action.Builder(R.drawable.ic_check_box, "read", markAsReadIntent)
+ .setSemanticAction(Action.SEMANTIC_ACTION_MARK_AS_READ)
+ .setShowsUserInterface(false)
+ .build())
+ .addAction(
+ new Action.Builder(R.drawable.ic_check_box, "reply", replyIntent)
+ .setSemanticAction(Action.SEMANTIC_ACTION_REPLY)
+ .setShowsUserInterface(false)
+ .addRemoteInput(new RemoteInput.Builder("input").build())
+ .build());
+
+ String messageText = "";
+ for (int i = 0; i < 100; i++) {
+ messageText += " test";
+ }
+
+ NotificationCompat.Builder updateNotification =
+ builder.setStyle(messagingStyle.addMessage(
+ new MessagingStyle.Message(
+ id + messageText,
+ System.currentTimeMillis(),
+ person)));
+ mManager.notify(12345, updateNotification.build());
+ });
+ }
+
private void initMessagingStyleButtonWithMuteAction(View view) {
view.findViewById(R.id.category_message_mute_action_button).setOnClickListener(v -> {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/qc/QCViewerFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/qc/QCViewerFragment.java
new file mode 100644
index 0000000..20e96aa
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/qc/QCViewerFragment.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2021 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.google.android.car.kitchensink.qc;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.android.car.qc.controller.RemoteQCController;
+import com.android.car.qc.view.QCView;
+
+import com.google.android.car.kitchensink.R;
+
+public final class QCViewerFragment extends Fragment {
+ private static final String KEY_CURRENT_URI_STRING = "CURRENT_URI_STRING";
+
+ private RemoteQCController mController;
+ private String mUriString;
+ private EditText mInput;
+ private Button mButton;
+ private QCView mQCView;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mUriString = savedInstanceState.getString(KEY_CURRENT_URI_STRING);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.qc_viewer, container, false);
+ mInput = v.findViewById(R.id.qc_uri_input);
+ mButton = v.findViewById(R.id.submit_uri_btn);
+ mQCView = v.findViewById(R.id.qc_view);
+
+ mButton.setOnClickListener(v1 -> {
+ mUriString = mInput.getText().toString();
+ Uri uri = Uri.parse(mUriString);
+ updateQCView(uri);
+ });
+
+ if (mUriString != null) {
+ Uri uri = Uri.parse(mUriString);
+ updateQCView(uri);
+ }
+
+ return v;
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString(KEY_CURRENT_URI_STRING, mUriString);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (mController != null) {
+ mController.listen(true);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (mController != null) {
+ mController.listen(false);
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mUriString = null;
+ if (mController != null) {
+ mController.destroy();
+ mController = null;
+ }
+ }
+
+ private void updateQCView(Uri uri) {
+ if (uri == null) {
+ return;
+ }
+ if (mController != null) {
+ // destroy old controller
+ mController.destroy();
+ }
+
+ mController = new RemoteQCController(getContext(), uri);
+ mController.addObserver(mQCView);
+ mController.listen(true);
+ }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java
new file mode 100644
index 0000000..64f7aa8
--- /dev/null
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/telemetry/CarTelemetryTestFragment.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2021 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.google.android.car.kitchensink.telemetry;
+
+import static com.android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.ACTIVITY_FOREGROUND_STATE_CHANGED;
+import static com.android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.APP_START_MEMORY_STATE_CAPTURED;
+import static com.android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.PROCESS_MEMORY_STATE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.car.telemetry.CarTelemetryManager;
+import android.car.telemetry.MetricsConfigKey;
+import android.hardware.automotive.vehicle.V2_0.VehicleProperty;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.fragment.app.Fragment;
+
+import com.android.car.telemetry.TelemetryProto;
+
+import com.google.android.car.kitchensink.KitchenSinkActivity;
+import com.google.android.car.kitchensink.R;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class CarTelemetryTestFragment extends Fragment {
+
+ /** Hello World test from vehicle property publisher by injecting gear change. */
+ private static final String LUA_SCRIPT_ON_GEAR_CHANGE =
+ "function onGearChange(published_data, state)\n"
+ + " result = {data = \"Hello World!\"}\n"
+ + " on_script_finished(result)\n"
+ + "end\n";
+ private static final TelemetryProto.Publisher VEHICLE_PROPERTY_PUBLISHER =
+ TelemetryProto.Publisher.newBuilder()
+ .setVehicleProperty(
+ TelemetryProto.VehiclePropertyPublisher.newBuilder()
+ .setVehiclePropertyId(VehicleProperty.GEAR_SELECTION)
+ .setReadRate(0f)
+ .build()
+ ).build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_ON_GEAR_CHANGE_V1 =
+ TelemetryProto.MetricsConfig.newBuilder()
+ .setName("my_metrics_config")
+ .setVersion(1)
+ .setScript(LUA_SCRIPT_ON_GEAR_CHANGE)
+ .addSubscribers(
+ TelemetryProto.Subscriber.newBuilder()
+ .setHandler("onGearChange")
+ .setPublisher(VEHICLE_PROPERTY_PUBLISHER)
+ .setPriority(100)) // low priority
+ .build();
+ private static final MetricsConfigKey ON_GEAR_CHANGE_KEY_V1 = new MetricsConfigKey(
+ METRICS_CONFIG_ON_GEAR_CHANGE_V1.getName(),
+ METRICS_CONFIG_ON_GEAR_CHANGE_V1.getVersion());
+
+ /** Memory metrics test. */
+ private static final String LUA_SCRIPT_ON_PROCESS_MEMORY_STATE = new StringBuilder()
+ .append("function calculateAverage(tbl)\n")
+ .append(" sum = 0\n")
+ .append(" size = 0\n")
+ .append(" for _, value in ipairs(tbl) do\n")
+ .append(" sum = sum + value\n")
+ .append(" size = size + 1\n")
+ .append(" end\n")
+ .append(" return sum/size\n")
+ .append("end\n")
+ .append("function onProcessMemory(published_data, state)\n")
+ .append(" result = {}\n")
+ .append(" result.page_fault_avg = calculateAverage(published_data.page_fault)\n")
+ .append(" result.major_page_fault_avg = calculateAverage("
+ + "published_data.page_major_fault)\n")
+ .append(" result.oom_adj_score_avg = calculateAverage("
+ + "published_data.oom_adj_score)\n")
+ .append(" result.rss_in_bytes_avg = calculateAverage("
+ + "published_data.rss_in_bytes)\n")
+ .append(" result.swap_in_bytes_avg = calculateAverage("
+ + "published_data.swap_in_bytes)\n")
+ .append(" result.cache_in_bytes_avg = calculateAverage("
+ + "published_data.cache_in_bytes)\n")
+ .append(" on_script_finished(result)\n")
+ .append("end\n")
+ .toString();
+
+ private static final TelemetryProto.Publisher PROCESS_MEMORY_PUBLISHER =
+ TelemetryProto.Publisher.newBuilder()
+ .setStats(
+ TelemetryProto.StatsPublisher.newBuilder()
+ .setSystemMetric(PROCESS_MEMORY_STATE)
+ ).build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_PROCESS_MEMORY_V1 =
+ TelemetryProto.MetricsConfig.newBuilder()
+ .setName("process_memory_metrics_config")
+ .setVersion(1)
+ .setScript(LUA_SCRIPT_ON_PROCESS_MEMORY_STATE)
+ .addSubscribers(
+ TelemetryProto.Subscriber.newBuilder()
+ .setHandler("onProcessMemory")
+ .setPublisher(PROCESS_MEMORY_PUBLISHER)
+ .setPriority(0)) // high priority
+ .build();
+ private static final MetricsConfigKey PROCESS_MEMORY_KEY_V1 = new MetricsConfigKey(
+ METRICS_CONFIG_PROCESS_MEMORY_V1.getName(),
+ METRICS_CONFIG_PROCESS_MEMORY_V1.getVersion());
+
+ /** AppStartMemoryStateCaptured metric test. */
+ private static final String LUA_SCRIPT_ON_APP_START_MEMORY_STATE_CAPTURED = new StringBuilder()
+ .append("function calculateAverage(tbl)\n")
+ .append(" sum = 0\n")
+ .append(" size = 0\n")
+ .append(" for _, value in ipairs(tbl) do\n")
+ .append(" sum = sum + value\n")
+ .append(" size = size + 1\n")
+ .append(" end\n")
+ .append(" return sum/size\n")
+ .append("end\n")
+ .append("function onAppStartMemoryStateCaptured(published_data, state)\n")
+ .append(" result = {}\n")
+ .append(" result.uid = published_data.uid\n")
+ .append(" result.page_fault_avg = calculateAverage(published_data.page_fault)\n")
+ .append(" result.major_page_fault_avg = calculateAverage("
+ + "published_data.page_major_fault)\n")
+ .append(" result.rss_in_bytes_avg = calculateAverage("
+ + "published_data.rss_in_bytes)\n")
+ .append(" result.swap_in_bytes_avg = calculateAverage("
+ + "published_data.swap_in_bytes)\n")
+ .append(" result.cache_in_bytes_avg = calculateAverage("
+ + "published_data.cache_in_bytes)\n")
+ .append(" on_script_finished(result)\n")
+ .append("end\n")
+ .toString();
+
+ private static final TelemetryProto.Publisher APP_START_MEMORY_STATE_CAPTURED_PUBLISHER =
+ TelemetryProto.Publisher.newBuilder()
+ .setStats(
+ TelemetryProto.StatsPublisher.newBuilder()
+ .setSystemMetric(APP_START_MEMORY_STATE_CAPTURED)
+ ).build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_APP_START_MEMORY_V1 =
+ TelemetryProto.MetricsConfig.newBuilder()
+ .setName("app_start_memory_metrics_config")
+ .setVersion(1)
+ .setScript(LUA_SCRIPT_ON_APP_START_MEMORY_STATE_CAPTURED)
+ .addSubscribers(
+ TelemetryProto.Subscriber.newBuilder()
+ .setHandler("onAppStartMemoryStateCaptured")
+ .setPublisher(APP_START_MEMORY_STATE_CAPTURED_PUBLISHER)
+ .setPriority(0)) // high priority
+ .build();
+ private static final MetricsConfigKey APP_START_MEMORY_STATE_CAPTURED_KEY_V1 =
+ new MetricsConfigKey(
+ METRICS_CONFIG_APP_START_MEMORY_V1.getName(),
+ METRICS_CONFIG_APP_START_MEMORY_V1.getVersion());
+
+ /** ActivityForegroundStateChanged metric test. */
+ private static final String LUA_SCRIPT_ON_ACTIVITY_FOREGROUND_STATE_CHANGED =
+ new StringBuilder()
+ .append("function onActivityForegroundStateChanged(published_data, state)\n")
+ .append(" result = {}\n")
+ .append(" result.uid = published_data.uid\n")
+ .append(" result.pkg_name = published_data.pkg_name\n")
+ .append(" result.class_name = published_data.class_name\n")
+ .append(" result.state = published_data.state\n")
+ .append(" on_script_finished(result)\n")
+ .append("end\n")
+ .toString();
+
+ private static final TelemetryProto.Publisher ACTIVITY_FOREGROUND_STATE_CHANGED_PUBLISHER =
+ TelemetryProto.Publisher.newBuilder()
+ .setStats(
+ TelemetryProto.StatsPublisher.newBuilder()
+ .setSystemMetric(ACTIVITY_FOREGROUND_STATE_CHANGED)
+ ).build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_ACTIVITY_FOREGROUND_STATE_V1 =
+ TelemetryProto.MetricsConfig.newBuilder()
+ .setName("activity_foreground_state_changed_config")
+ .setVersion(1)
+ .setScript(LUA_SCRIPT_ON_ACTIVITY_FOREGROUND_STATE_CHANGED)
+ .addSubscribers(
+ TelemetryProto.Subscriber.newBuilder()
+ .setHandler("onActivityForegroundStateChanged")
+ .setPublisher(ACTIVITY_FOREGROUND_STATE_CHANGED_PUBLISHER)
+ .setPriority(0)) // high priority
+ .build();
+ private static final MetricsConfigKey ACTIVITY_FOREGROUND_STATE_CHANGED_KEY_V1 =
+ new MetricsConfigKey(
+ METRICS_CONFIG_ACTIVITY_FOREGROUND_STATE_V1.getName(),
+ METRICS_CONFIG_ACTIVITY_FOREGROUND_STATE_V1.getVersion());
+
+ private final Executor mExecutor = Executors.newSingleThreadExecutor();
+
+ private CarTelemetryManager mCarTelemetryManager;
+ private CarTelemetryResultsListenerImpl mListener;
+ private KitchenSinkActivity mActivity;
+ private TextView mOutputTextView;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ mActivity = (KitchenSinkActivity) getActivity();
+ mCarTelemetryManager = mActivity.getCarTelemetryManager();
+ mListener = new CarTelemetryResultsListenerImpl();
+ mCarTelemetryManager.setListener(mExecutor, mListener);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ @NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.car_telemetry_test, container, false);
+ mOutputTextView = view.findViewById(R.id.output_textview);
+ view.findViewById(R.id.send_on_gear_change_config)
+ .setOnClickListener(this::onSendGearChangeConfigBtnClick);
+ view.findViewById(R.id.remove_on_gear_change_config)
+ .setOnClickListener(this::onRemoveGearChangeConfigBtnClick);
+ view.findViewById(R.id.get_on_gear_change_report)
+ .setOnClickListener(this::onGetGearChangeReportBtnClick);
+ view.findViewById(R.id.send_on_process_memory_config)
+ .setOnClickListener(this::onSendProcessMemoryConfigBtnClick);
+ view.findViewById(R.id.remove_on_process_memory_config)
+ .setOnClickListener(this::onRemoveProcessMemoryConfigBtnClick);
+ view.findViewById(R.id.get_on_process_memory_report)
+ .setOnClickListener(this::onGetProcessMemoryReportBtnClick);
+ view.findViewById(R.id.send_on_app_start_memory_state_captured_config)
+ .setOnClickListener(this::onSendAppStartMemoryStateCapturedConfigBtnClick);
+ view.findViewById(R.id.remove_on_app_start_memory_state_captured_config)
+ .setOnClickListener(this::onRemoveAppStartMemoryStateCapturedConfigBtnClick);
+ view.findViewById(R.id.get_on_app_start_memory_state_captured_report)
+ .setOnClickListener(this::onGetAppStartMemoryStateCapturedReportBtnClick);
+ view.findViewById(R.id.send_on_activity_foreground_state_changed_config)
+ .setOnClickListener(this::onSendActivityForegroundStateChangedConfigBtnClick);
+ view.findViewById(R.id.remove_on_activity_foreground_state_changed_config)
+ .setOnClickListener(this::onRemoveActivityForegroundStateChangedConfigBtnClick);
+ view.findViewById(R.id.get_on_activity_foreground_state_changed_report)
+ .setOnClickListener(this::onGetActivityForegroundStateChangedReportBtnClick);
+ view.findViewById(R.id.show_mem_info_btn).setOnClickListener(this::onShowMemInfoBtnClick);
+ return view;
+ }
+
+ private void showOutput(String s) {
+ mActivity.runOnUiThread(() -> {
+ String now = LocalDateTime.now()
+ .format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS"));
+ mOutputTextView.setText(
+ now + " : " + s + "\n" + mOutputTextView.getText());
+ });
+ }
+
+ private void onSendGearChangeConfigBtnClick(View view) {
+ showOutput("Sending MetricsConfig that listen for gear change...");
+ mCarTelemetryManager.addMetricsConfig(ON_GEAR_CHANGE_KEY_V1,
+ METRICS_CONFIG_ON_GEAR_CHANGE_V1.toByteArray());
+ }
+
+ private void onRemoveGearChangeConfigBtnClick(View view) {
+ showOutput("Removing MetricsConfig that listens for gear change...");
+ mCarTelemetryManager.removeMetricsConfig(ON_GEAR_CHANGE_KEY_V1);
+ }
+
+ private void onGetGearChangeReportBtnClick(View view) {
+ showOutput("Fetching report for on_gear_change... If nothing shows up within 5 seconds, "
+ + "there is no result yet");
+ mCarTelemetryManager.sendFinishedReports(ON_GEAR_CHANGE_KEY_V1);
+ }
+
+ private void onSendProcessMemoryConfigBtnClick(View view) {
+ showOutput("Sending MetricsConfig that listens for PROCESS_MEMORY_STATE...");
+ mCarTelemetryManager.addMetricsConfig(PROCESS_MEMORY_KEY_V1,
+ METRICS_CONFIG_PROCESS_MEMORY_V1.toByteArray());
+ }
+
+ private void onRemoveProcessMemoryConfigBtnClick(View view) {
+ showOutput("Removing MetricsConfig that listens for PROCESS_MEMORY_STATE...");
+ mCarTelemetryManager.removeMetricsConfig(PROCESS_MEMORY_KEY_V1);
+ }
+
+ private void onGetProcessMemoryReportBtnClick(View view) {
+ showOutput("Fetching report for PROCESS_MEMORY_STATE... If nothing shows up within 5 "
+ + "seconds, there is no result yet");
+ mCarTelemetryManager.sendFinishedReports(PROCESS_MEMORY_KEY_V1);
+ }
+
+ private void onSendAppStartMemoryStateCapturedConfigBtnClick(View view) {
+ showOutput("Sending MetricsConfig that listens for APP_START_MEMORY_STATE_CAPTURED...");
+ mCarTelemetryManager.addMetricsConfig(APP_START_MEMORY_STATE_CAPTURED_KEY_V1,
+ METRICS_CONFIG_APP_START_MEMORY_V1.toByteArray());
+ }
+
+ private void onRemoveAppStartMemoryStateCapturedConfigBtnClick(View view) {
+ showOutput("Removing MetricsConfig that listens for APP_START_MEMORY_STATE_CAPTURED...");
+ mCarTelemetryManager.removeMetricsConfig(APP_START_MEMORY_STATE_CAPTURED_KEY_V1);
+ }
+
+ private void onGetAppStartMemoryStateCapturedReportBtnClick(View view) {
+ showOutput("Fetching report for APP_START_MEMORY_STATE_CAPTURED... "
+ + "If nothing shows up within 5 seconds, there is no result yet");
+ mCarTelemetryManager.sendFinishedReports(APP_START_MEMORY_STATE_CAPTURED_KEY_V1);
+ }
+
+ private void onSendActivityForegroundStateChangedConfigBtnClick(View view) {
+ showOutput("Sending MetricsConfig that listens for ACTIVITY_FOREGROUND_STATE_CHANGED...");
+ mCarTelemetryManager.addMetricsConfig(ACTIVITY_FOREGROUND_STATE_CHANGED_KEY_V1,
+ METRICS_CONFIG_ACTIVITY_FOREGROUND_STATE_V1.toByteArray());
+ }
+
+ private void onRemoveActivityForegroundStateChangedConfigBtnClick(View view) {
+ showOutput("Removing MetricsConfig that listens for ACTIVITY_FOREGROUND_STATE_CHANGED...");
+ mCarTelemetryManager.removeMetricsConfig(ACTIVITY_FOREGROUND_STATE_CHANGED_KEY_V1);
+ }
+
+ private void onGetActivityForegroundStateChangedReportBtnClick(View view) {
+ showOutput("Fetching report for ACTIVITY_FOREGROUND_STATE_CHANGED... "
+ + "If nothing shows up within 5 seconds, there is no result yet");
+ mCarTelemetryManager.sendFinishedReports(ACTIVITY_FOREGROUND_STATE_CHANGED_KEY_V1);
+ }
+
+ /** Gets a MemoryInfo object for the device's current memory status. */
+ private ActivityManager.MemoryInfo getAvailableMemory() {
+ ActivityManager activityManager = getActivity().getSystemService(ActivityManager.class);
+ ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
+ activityManager.getMemoryInfo(memoryInfo);
+ return memoryInfo;
+ }
+
+ private void onShowMemInfoBtnClick(View view) {
+ // Use android's "alloc-stress" system tool to create an artificial memory pressure.
+ ActivityManager.MemoryInfo info = getAvailableMemory();
+ showOutput("MemoryInfo availMem=" + (info.availMem / 1024 / 1024) + "/"
+ + (info.totalMem / 1024 / 1024) + "mb, isLowMem=" + info.lowMemory
+ + ", threshold=" + (info.threshold / 1024 / 1024) + "mb");
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ }
+
+ /**
+ * Implementation of the {@link CarTelemetryManager.CarTelemetryResultsListener}. They update
+ * the view to show the outputs from the APIs of {@link CarTelemetryManager}.
+ * The callbacks are executed in {@link mExecutor}.
+ */
+ private final class CarTelemetryResultsListenerImpl
+ implements CarTelemetryManager.CarTelemetryResultsListener {
+
+ @Override
+ public void onResult(@NonNull MetricsConfigKey key, @NonNull byte[] result) {
+ PersistableBundle bundle;
+ try (ByteArrayInputStream bis = new ByteArrayInputStream(result)) {
+ bundle = PersistableBundle.readFromStream(bis);
+ } catch (IOException e) {
+ bundle = null;
+ }
+ showOutput("Result for " + key.getName() + ": " + bundle.toString());
+ }
+
+ @Override
+ public void onError(@NonNull MetricsConfigKey key, @NonNull byte[] error) {
+ try {
+ TelemetryProto.TelemetryError telemetryError =
+ TelemetryProto.TelemetryError.parseFrom(error);
+ showOutput("Error for " + key.getName() + ": " + telemetryError);
+ } catch (InvalidProtocolBufferException e) {
+ showOutput("Unable to parse error result for MetricsConfig " + key.getName()
+ + ": " + e.getMessage());
+ }
+ }
+
+ @Override
+ public void onAddMetricsConfigStatus(@NonNull MetricsConfigKey key, int statusCode) {
+ showOutput("Add MetricsConfig status for " + key.getName() + ": "
+ + statusCodeToString(statusCode));
+ }
+
+ private String statusCodeToString(int statusCode) {
+ switch (statusCode) {
+ case CarTelemetryManager.ERROR_METRICS_CONFIG_NONE:
+ return "SUCCESS";
+ case CarTelemetryManager.ERROR_METRICS_CONFIG_ALREADY_EXISTS:
+ return "ERROR ALREADY_EXISTS";
+ case CarTelemetryManager.ERROR_METRICS_CONFIG_VERSION_TOO_OLD:
+ return "ERROR VERSION_TOO_OLD";
+ case CarTelemetryManager.ERROR_METRICS_CONFIG_PARSE_FAILED:
+ return "ERROR PARSE_FAILED";
+ case CarTelemetryManager.ERROR_METRICS_CONFIG_SIGNATURE_VERIFICATION_FAILED:
+ return "ERROR SIGNATURE_VERIFICATION_FAILED";
+ default:
+ return "ERROR UNKNOWN";
+ }
+ }
+ }
+}
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeAdapter.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeAdapter.java
index 4493806..f8d19d6 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeAdapter.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeAdapter.java
@@ -66,25 +66,26 @@
}
if (mVolumeList[position] != null) {
vh.id.setText(mVolumeList[position].id);
- vh.maxVolume.setText(String.valueOf(mVolumeList[position].maxGain));
vh.currentVolume.setText(String.valueOf(mVolumeList[position].currentGain));
int color = mVolumeList[position].hasAudioFocus ? Color.GREEN : Color.GRAY;
vh.requestButton.setBackgroundColor(color);
if (position == 0) {
+ vh.maxVolume.setText("Max");
vh.upButton.setVisibility(View.INVISIBLE);
vh.downButton.setVisibility(View.INVISIBLE);
vh.requestButton.setVisibility(View.INVISIBLE);
vh.muteButton.setVisibility(View.INVISIBLE);
} else {
+ vh.maxVolume.setText(String.valueOf(mVolumeList[position].maxGain));
vh.upButton.setVisibility(View.VISIBLE);
vh.downButton.setVisibility(View.VISIBLE);
vh.requestButton.setVisibility(View.VISIBLE);
vh.muteButton.setVisibility(mGroupMuteEnabled ? View.VISIBLE : View.INVISIBLE);
vh.upButton.setOnClickListener((view) -> {
- mFragment.adjustVolumeByOne(mVolumeList[position].groupId, true);
+ mFragment.adjustVolumeUp(mVolumeList[position].groupId);
});
vh.downButton.setOnClickListener((view) -> {
- mFragment.adjustVolumeByOne(mVolumeList[position].groupId, false);
+ mFragment.adjustVolumeDown(mVolumeList[position].groupId);
});
vh.muteButton.setChecked(mVolumeList[position].isMuted);
vh.muteButton.setOnClickListener((view) -> {
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeFragment.java
index 675f495..2419511 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/CarAudioZoneVolumeFragment.java
@@ -52,7 +52,10 @@
private static final int MSG_REQUEST_FOCUS = 1;
private static final int MSG_FOCUS_CHANGED = 2;
private static final int MSG_STOP_RINGTONE = 3;
+ private static final int MSG_ADJUST_VOLUME = 4;
private static final long RINGTONE_STOP_TIME_MS = 3_000;
+ private static final int ADJUST_VOLUME_UP = 0;
+ private static final int ADJUST_VOLUME_DOWN = 1;
private final int mZoneId;
private final Object mLock = new Object();
@@ -68,10 +71,19 @@
@GuardedBy("mLock")
private Ringtone mRingtone;
- public void sendVolumeChangedMessage(int groupId, int flags) {
+ void sendVolumeChangedMessage(int groupId, int flags) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_VOLUME_CHANGED, groupId, flags));
}
+ void adjustVolumeUp(int groupId) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_ADJUST_VOLUME, groupId, ADJUST_VOLUME_UP));
+ }
+
+ void adjustVolumeDown(int groupId) {
+ mHandler.sendMessage(mHandler
+ .obtainMessage(MSG_ADJUST_VOLUME, groupId, ADJUST_VOLUME_DOWN));
+ }
+
private class VolumeHandler extends Handler {
private AudioFocusListener mFocusListener;
@@ -105,10 +117,12 @@
mVolumeInfos[mGroupIdIndexMap.get(focusGroupId)].hasAudioFocus = true;
mCarAudioZoneVolumeAdapter.refreshVolumes(mVolumeInfos);
break;
- default :
- Log.wtf(TAG,"VolumeHandler handleMessage called with unknown message"
+ case MSG_ADJUST_VOLUME:
+ adjustVolumeByOne(msg.arg1, msg.arg2 == ADJUST_VOLUME_UP);
+ break;
+ default:
+ Log.wtf(TAG, "VolumeHandler handleMessage called with unknown message"
+ msg.what);
-
}
}
}
@@ -144,7 +158,6 @@
CarAudioZoneVolumeInfo titlesInfo = new CarAudioZoneVolumeInfo();
titlesInfo.id = "Group id";
titlesInfo.currentGain = "Current";
- titlesInfo.maxGain = "Max";
mVolumeInfos[0] = titlesInfo;
int i = 1;
@@ -155,13 +168,14 @@
volumeInfo.id = String.valueOf(groupId);
int current = mCarAudioManager.getGroupVolume(mZoneId, groupId);
int max = mCarAudioManager.getGroupMaxVolume(mZoneId, groupId);
+ int min = mCarAudioManager.getGroupMinVolume(mZoneId, groupId);
volumeInfo.currentGain = String.valueOf(current);
- volumeInfo.maxGain = String.valueOf(max);
+ volumeInfo.maxGain = max;
+ volumeInfo.minGain = min;
volumeInfo.isMuted = mCarAudioManager.isVolumeGroupMuted(mZoneId, groupId);
mVolumeInfos[i] = volumeInfo;
- if (DEBUG)
- {
+ if (DEBUG) {
Log.d(TAG, groupId + " max: " + volumeInfo.maxGain + " current: "
+ volumeInfo.currentGain + " is muted " + volumeInfo.isMuted);
}
@@ -170,18 +184,38 @@
mCarAudioZoneVolumeAdapter.refreshVolumes(mVolumeInfos);
}
- public void adjustVolumeByOne(int groupId, boolean up) {
+ private void adjustVolumeByOne(int groupId, boolean up) {
if (mCarAudioManager == null) {
Log.e(TAG, "CarAudioManager is null");
return;
}
int current = mCarAudioManager.getGroupVolume(mZoneId, groupId);
- int volume = current + (up ? 1 : -1);
- mCarAudioManager.setGroupVolume(mZoneId, groupId, volume, AudioManager.FLAG_SHOW_UI);
- if (DEBUG) {
- Log.d(TAG, "Set group " + groupId + " volume " + volume + " in audio zone "
- + mZoneId);
+ CarAudioZoneVolumeInfo info = getVolumeInfo(groupId);
+ int volume = up ? current + 1 : current - 1;
+ if (volume > info.maxGain) {
+ if (DEBUG) {
+ Log.d(TAG, "Reached " + groupId + " max volume "
+ + " limit " + volume);
+ }
+ return;
}
+ if (volume < info.minGain) {
+ if (DEBUG) {
+ Log.d(TAG, "Reached " + groupId + " min volume "
+ + " limit " + volume);
+ }
+ return;
+ }
+ mCarAudioManager.setGroupVolume(mZoneId, groupId, volume, /* flags= */ 0);
+ if (DEBUG) {
+ Log.d(TAG, "Set group " + groupId + " volume "
+ + mCarAudioManager.getGroupVolume(mZoneId, groupId)
+ + " in audio zone " + mZoneId);
+ }
+ }
+
+ private CarAudioZoneVolumeInfo getVolumeInfo(int groupId) {
+ return mVolumeInfos[mGroupIdIndexMap.get(groupId)];
}
public void toggleMute(int groupId) {
@@ -197,7 +231,7 @@
}
}
- public void requestFocus(int groupId) {
+ void requestFocus(int groupId) {
// Automatic volume change only works for primary audio zone.
if (mZoneId == CarAudioManager.PRIMARY_AUDIO_ZONE) {
mHandler.sendMessage(mHandler
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
index 2a2c38e..821d362 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/volume/VolumeTestFragment.java
@@ -62,7 +62,8 @@
public static final class CarAudioZoneVolumeInfo {
public int groupId;
public String id;
- public String maxGain;
+ public int maxGain;
+ public int minGain;
public String currentGain;
public boolean hasAudioFocus;
public boolean isMuted;
diff --git a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java
index 33bc54a..36a300e 100644
--- a/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java
+++ b/tests/EmbeddedKitchenSinkApp/src/com/google/android/car/kitchensink/watchdog/CarWatchdogTestFragment.java
@@ -73,6 +73,7 @@
private static final long TEN_MEGABYTES = 1024 * 1024 * 10;
private static final int DISK_DELAY_MS = 3000;
private static final String TAG = "CarWatchdogTestFragment";
+ private static final double WARN_THRESHOLD_PERCENT = 0.8;
private static final double EXCEED_WARN_THRESHOLD_PERCENT = 0.9;
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
@@ -133,13 +134,23 @@
return;
}
+ /*
+ * CarService notifies applications on exceeding 80% of the
+ * threshold. The app maybe notified before completing the
+ * following write. Ergo, the minimum expected written bytes
+ * should be the warn threshold rather than the actual amount
+ * of bytes written by the app.
+ */
+ long bytesToWarnThreshold = (long) Math.ceil(
+ (remainingBytes + TEN_MEGABYTES)
+ * WARN_THRESHOLD_PERCENT);
+
+ listener.setExpectedMinWrittenBytes(bytesToWarnThreshold);
+
long bytesToExceedWarnThreshold =
(long) Math.ceil(remainingBytes
* EXCEED_WARN_THRESHOLD_PERCENT);
- listener.setExpectedMinWrittenBytes(
- TEN_MEGABYTES + bytesToExceedWarnThreshold);
-
if (!writeToDisk(bytesToExceedWarnThreshold)) {
mCarWatchdogManager.removeResourceOveruseListener(listener);
return;
diff --git a/tests/NetworkPreferenceApp/Android.bp b/tests/NetworkPreferenceApp/Android.bp
index 22c5f85..e378450 100644
--- a/tests/NetworkPreferenceApp/Android.bp
+++ b/tests/NetworkPreferenceApp/Android.bp
@@ -43,7 +43,6 @@
static_libs: [
"vehicle-hal-support-lib",
- "car-experimental-api-static-lib",
"androidx.legacy_legacy-support-v4",
],
diff --git a/tests/NetworkPreferenceApp/res/layout/manager.xml b/tests/NetworkPreferenceApp/res/layout/manager.xml
index 0f826ee..a36bc2e 100644
--- a/tests/NetworkPreferenceApp/res/layout/manager.xml
+++ b/tests/NetworkPreferenceApp/res/layout/manager.xml
@@ -24,7 +24,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
- android:weightSum="2">
+ android:weightSum="3">
<LinearLayout
style="@style/SectionContainer"
android:layout_width="match_parent"
@@ -47,34 +47,29 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:weightSum="2">
- <LinearLayout
- android:layout_width="match_parent"
+ <TextView
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:weightSum="2">
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/label_apply_latest_policy_on_boot"/>
- <Switch
- android:id="@+id/reapplyPANSOnBootSwitch"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
- </LinearLayout>
- <LinearLayout
- android:layout_width="match_parent"
+ android:text="@string/label_apply_latest_policy_on_boot"/>
+ <Switch
+ android:id="@+id/reapplyPANSOnBootSwitch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ </LinearLayout>
+ <LinearLayout
+ style="@style/SectionContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:weightSum="2">
+ <TextView
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:weightSum="2">
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/label_apply_wifi_policy_on_boot"/>
- <Switch
- android:id="@+id/reapplyWifiSuggestionsOnBootSwitch"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
- </LinearLayout>
+ android:text="@string/label_apply_wifi_policy_on_boot"/>
+ <Switch
+ android:id="@+id/reapplyWifiSuggestionsOnBootSwitch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
diff --git a/tests/NetworkPreferenceApp/src/com/google/android/car/networking/preferenceupdater/components/CarDriverDistractionManagerAdapter.java b/tests/NetworkPreferenceApp/src/com/google/android/car/networking/preferenceupdater/components/CarDriverDistractionManagerAdapter.java
index eb4d527..b2a096f 100644
--- a/tests/NetworkPreferenceApp/src/com/google/android/car/networking/preferenceupdater/components/CarDriverDistractionManagerAdapter.java
+++ b/tests/NetworkPreferenceApp/src/com/google/android/car/networking/preferenceupdater/components/CarDriverDistractionManagerAdapter.java
@@ -16,9 +16,7 @@
package com.google.android.car.networking.preferenceupdater.components;
import android.car.Car;
-import android.car.experimental.CarDriverDistractionManager;
-import android.car.experimental.DriverDistractionChangeEvent;
-import android.car.experimental.ExperimentalCar;
+import android.car.drivingstate.CarUxRestrictionsManager;
import android.content.Context;
/**
@@ -26,21 +24,15 @@
* information about about driving state to the caller.
*/
public final class CarDriverDistractionManagerAdapter {
- private final CarDriverDistractionManager mCarDriverDistractionManager;
+ private final CarUxRestrictionsManager mCarUxRestrictionsManager;
private final Car mCar;
public CarDriverDistractionManagerAdapter(Context ctx) {
// Connect to car service
mCar = Car.createCar(ctx);
- if (mCar.isFeatureEnabled(
- ExperimentalCar.DRIVER_DISTRACTION_EXPERIMENTAL_FEATURE_SERVICE)) {
- mCarDriverDistractionManager = (CarDriverDistractionManager) mCar.getCarManager(
- ExperimentalCar.DRIVER_DISTRACTION_EXPERIMENTAL_FEATURE_SERVICE);
- } else {
- mCarDriverDistractionManager = null;
- }
+ mCarUxRestrictionsManager = (CarUxRestrictionsManager) mCar.getCarManager(
+ Car.CAR_UX_RESTRICTION_SERVICE);
}
-
/** Method that has to be called during destroy. */
public void destroy() {
mCar.disconnect();
@@ -50,24 +42,8 @@
* Returns true/false boolean based on the whether driver can be distracted right now or not
*/
public boolean allowedToBeDistracted() {
- if (mCarDriverDistractionManager == null) {
- // This means we could not bind to CarDriverDistractionManager. Return true.
- return true;
- }
- DriverDistractionChangeEvent event = mCarDriverDistractionManager.getLastDistractionEvent();
- /**
- * event.getAwarenessPercentage returns the current driver awareness value, a float number
- * between 0.0 -> 1.0, which represents a percentage of the required awareness from driver.
- * - 0.0 indicates that the driver has no situational awareness of the surrounding
- * environment, which means driver is allawed to be distracted.
- * - 1.0 indicates that the driver has the target amount of situational awareness
- * necessary for the current driving environment, meaning driver can not be distracted at
- * this moment.
- */
- if (event.getAwarenessPercentage() > 0) {
- // To simplify logic, we consider everything above 0% as dangerous and return false.
- return false;
- }
- return true;
+ return mCarUxRestrictionsManager == null
+ || !mCarUxRestrictionsManager.getCurrentCarUxRestrictions()
+ .isRequiresDistractionOptimization();
}
}
diff --git a/tests/ThemePlayground/AndroidManifest.xml b/tests/ThemePlayground/AndroidManifest.xml
index cebff5e..3e18934 100644
--- a/tests/ThemePlayground/AndroidManifest.xml
+++ b/tests/ThemePlayground/AndroidManifest.xml
@@ -48,6 +48,12 @@
android:resizeableActivity="true"
android:allowEmbedded="true">
</activity>
+ <activity android:name=".ColorPalette"
+ android:label="@string/palette_elements"
+ android:windowSoftInputMode="stateUnchanged"
+ android:resizeableActivity="true"
+ android:allowEmbedded="true">
+ </activity>
<activity android:name=".ProgressBarSamples"
android:label="@string/progress_bar_elements"
android:windowSoftInputMode="stateUnchanged"
@@ -68,6 +74,7 @@
</activity>
<activity android:name=".CarUiRecyclerViewSamples"
android:label="@string/car_ui_recycler_view"
+ android:theme="@style/Theme.CarUi"
android:parentActivityName="com.android.car.themeplayground.TextSamples">
</activity>
<activity android:name=".DefaultThemeSamples"
diff --git a/tests/ThemePlayground/res/layout/color_palette.xml b/tests/ThemePlayground/res/layout/color_palette.xml
new file mode 100644
index 0000000..e335b30
--- /dev/null
+++ b/tests/ThemePlayground/res/layout/color_palette.xml
@@ -0,0 +1,436 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <ScrollView
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginBottom="8dp"
+ android:layout_marginEnd="16dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_0">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_0"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_10">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_10"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_50">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_50"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_100">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_100"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_200">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_200"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_300">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_300"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_400">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_400"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_500">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_500"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_600">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_600"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_700">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_700"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_800">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_800"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_900">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_900"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent1_1000">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent1_1000"/>
+ </FrameLayout>
+
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_0">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_0"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_10">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_10"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_50">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_50"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_100">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_100"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_200">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_200"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_300">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_300"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_400">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_400"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_500">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_500"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_600">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_600"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_700">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_700"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_800">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_800"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_900">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_900"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent2_1000">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent2_1000"/>
+ </FrameLayout>
+
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_0">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_0"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_10">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_10"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_50">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_50"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_100">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_100"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_200">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_200"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_300">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_300"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_400">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_400"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_500">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_500"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_600">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_600"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_700">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_700"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_800">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_800"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_900">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_900"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:background="@drawable/system_accent3_1000">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="*android:color/system_accent3_1000"/>
+ </FrameLayout>
+
+ </LinearLayout>
+ </ScrollView>
+ <include layout="@layout/menu_button"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/tests/ThemePlayground/res/menu/menu_main.xml b/tests/ThemePlayground/res/menu/menu_main.xml
index 8e4fd8c..647b02d 100644
--- a/tests/ThemePlayground/res/menu/menu_main.xml
+++ b/tests/ThemePlayground/res/menu/menu_main.xml
@@ -30,6 +30,10 @@
android:orderInCategory="100"
android:title="@string/panel_elements"/>
<item
+ android:id="@+id/palette_elements"
+ android:orderInCategory="100"
+ android:title="@string/palette_elements"/>
+ <item
android:id="@+id/progress_bar_elements"
android:orderInCategory="100"
android:title="@string/progress_bar_elements"/>
diff --git a/tests/ThemePlayground/res/values/drawables.xml b/tests/ThemePlayground/res/values/drawables.xml
new file mode 100644
index 0000000..aeb9c88
--- /dev/null
+++ b/tests/ThemePlayground/res/values/drawables.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <drawable name="system_accent1_0">@*android:color/system_accent1_0</drawable>
+ <drawable name="system_accent1_10">@*android:color/system_accent1_10</drawable>
+ <drawable name="system_accent1_50">@*android:color/system_accent1_50</drawable>
+ <drawable name="system_accent1_100">@*android:color/system_accent1_100</drawable>
+ <drawable name="system_accent1_200">@*android:color/system_accent1_200</drawable>
+ <drawable name="system_accent1_300">@*android:color/system_accent1_300</drawable>
+ <drawable name="system_accent1_400">@*android:color/system_accent1_400</drawable>
+ <drawable name="system_accent1_500">@*android:color/system_accent1_500</drawable>
+ <drawable name="system_accent1_600">@*android:color/system_accent1_600</drawable>
+ <drawable name="system_accent1_700">@*android:color/system_accent1_700</drawable>
+ <drawable name="system_accent1_800">@*android:color/system_accent1_800</drawable>
+ <drawable name="system_accent1_900">@*android:color/system_accent1_900</drawable>
+ <drawable name="system_accent1_1000">@*android:color/system_accent1_1000</drawable>
+
+ <drawable name="system_accent2_0">@*android:color/system_accent2_0</drawable>
+ <drawable name="system_accent2_10">@*android:color/system_accent2_10</drawable>
+ <drawable name="system_accent2_50">@*android:color/system_accent2_50</drawable>
+ <drawable name="system_accent2_100">@*android:color/system_accent2_100</drawable>
+ <drawable name="system_accent2_200">@*android:color/system_accent2_200</drawable>
+ <drawable name="system_accent2_300">@*android:color/system_accent2_300</drawable>
+ <drawable name="system_accent2_400">@*android:color/system_accent2_400</drawable>
+ <drawable name="system_accent2_500">@*android:color/system_accent2_500</drawable>
+ <drawable name="system_accent2_600">@*android:color/system_accent2_600</drawable>
+ <drawable name="system_accent2_700">@*android:color/system_accent2_700</drawable>
+ <drawable name="system_accent2_800">@*android:color/system_accent2_800</drawable>
+ <drawable name="system_accent2_900">@*android:color/system_accent2_900</drawable>
+ <drawable name="system_accent2_1000">@*android:color/system_accent2_1000</drawable>
+
+ <drawable name="system_accent3_0">@*android:color/system_accent3_0</drawable>
+ <drawable name="system_accent3_10">@*android:color/system_accent3_10</drawable>
+ <drawable name="system_accent3_50">@*android:color/system_accent3_50</drawable>
+ <drawable name="system_accent3_100">@*android:color/system_accent3_100</drawable>
+ <drawable name="system_accent3_200">@*android:color/system_accent3_200</drawable>
+ <drawable name="system_accent3_300">@*android:color/system_accent3_300</drawable>
+ <drawable name="system_accent3_400">@*android:color/system_accent3_400</drawable>
+ <drawable name="system_accent3_500">@*android:color/system_accent3_500</drawable>
+ <drawable name="system_accent3_600">@*android:color/system_accent3_600</drawable>
+ <drawable name="system_accent3_700">@*android:color/system_accent3_700</drawable>
+ <drawable name="system_accent3_800">@*android:color/system_accent3_800</drawable>
+ <drawable name="system_accent3_900">@*android:color/system_accent3_900</drawable>
+ <drawable name="system_accent3_1000">@*android:color/system_accent3_1000</drawable>
+
+</resources>
diff --git a/tests/ThemePlayground/res/values/strings.xml b/tests/ThemePlayground/res/values/strings.xml
index c355777..bd4b79a 100644
--- a/tests/ThemePlayground/res/values/strings.xml
+++ b/tests/ThemePlayground/res/values/strings.xml
@@ -20,6 +20,7 @@
<string name="button_elements" translatable="false">Button Elements</string>
<string name="progress_bar_elements" translatable="false">Progress Bar Elements</string>
<string name="panel_elements" translatable="false">Color Panels</string>
+ <string name="palette_elements" translatable="false">Color Palette</string>
<string name="dialog_elements" translatable="false">Dialogs</string>
<string name="toggle_theme" translatable="false">Change configuration(Day/Night)</string>
<string name="apply" translatable="false">Apply</string>
diff --git a/tests/ThemePlayground/src/com/android/car/themeplayground/AbstractSampleActivity.java b/tests/ThemePlayground/src/com/android/car/themeplayground/AbstractSampleActivity.java
index c197925..0f7cdfa 100644
--- a/tests/ThemePlayground/src/com/android/car/themeplayground/AbstractSampleActivity.java
+++ b/tests/ThemePlayground/src/com/android/car/themeplayground/AbstractSampleActivity.java
@@ -58,6 +58,8 @@
return startSampleActivity(ProgressBarSamples.class);
case R.id.panel_elements:
return startSampleActivity(ColorSamples.class);
+ case R.id.palette_elements:
+ return startSampleActivity(ColorPalette.class);
case R.id.dialog_elements:
return startSampleActivity(DialogSamples.class);
case R.id.toggle_theme:
diff --git a/tests/ThemePlayground/src/com/android/car/themeplayground/ColorPalette.java b/tests/ThemePlayground/src/com/android/car/themeplayground/ColorPalette.java
new file mode 100644
index 0000000..5f9f9ac
--- /dev/null
+++ b/tests/ThemePlayground/src/com/android/car/themeplayground/ColorPalette.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2021 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.themeplayground;
+
+import android.os.Bundle;
+
+/**
+ * Activity that renders a bunch of color values from the theme.
+ */
+public class ColorPalette extends AbstractSampleActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Utils.onActivityCreateSetTheme(this);
+ setContentView(R.layout.color_palette);
+ }
+}
diff --git a/tests/UserSwitchMonitorApp/Android.bp b/tests/UserSwitchMonitorApp/Android.bp
index 9aa39a9..b5f29eb 100644
--- a/tests/UserSwitchMonitorApp/Android.bp
+++ b/tests/UserSwitchMonitorApp/Android.bp
@@ -27,3 +27,18 @@
sdk_version: "system_current",
}
+
+// "Cloned" app used to make sure events are received by apps with shared uid
+android_app {
+ name: "UserSwitchMonitorApp2",
+
+ manifest: "AndroidManifest2.xml",
+
+ libs: [
+ "android.car-system-stubs",
+ ],
+
+ srcs: ["src/**/*.java"],
+
+ sdk_version: "system_current",
+}
diff --git a/tests/UserSwitchMonitorApp/AndroidManifest.xml b/tests/UserSwitchMonitorApp/AndroidManifest.xml
index e78d690..fec167a 100644
--- a/tests/UserSwitchMonitorApp/AndroidManifest.xml
+++ b/tests/UserSwitchMonitorApp/AndroidManifest.xml
@@ -16,15 +16,17 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.google.android.car.userswitchmonitor">
+ package="com.google.android.car.userswitchmonitor"
+ android:sharedUserId="com.google.android.car.userswitchmonitor">
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
- <application android:label="User Switch Monitor">
- <service android:name=".UserSwitchMonitorService"/>
- <receiver android:name=".BootCompletedReceiver" android:exported="true">
+ <application android:icon="@drawable/ic_launcher" android:label="User Switch Monitor">
+ <service android:name="com.google.android.car.userswitchmonitor.UserSwitchMonitorService"/>
+ <receiver android:name="com.google.android.car.userswitchmonitor.BootCompletedReceiver"
+ android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
diff --git a/tests/UserSwitchMonitorApp/AndroidManifest2.xml b/tests/UserSwitchMonitorApp/AndroidManifest2.xml
new file mode 100644
index 0000000..bb38752
--- /dev/null
+++ b/tests/UserSwitchMonitorApp/AndroidManifest2.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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
+ -->
+
+<!-- "Cloned" app used to make sure events are received by apps with shared uid -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.car.userswitchmonitor2"
+ android:sharedUserId="com.google.android.car.userswitchmonitor">
+
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+
+ <application android:icon="@drawable/ic_launcher" android:label="User Switch Monitor">
+ <service android:name="com.google.android.car.userswitchmonitor.UserSwitchMonitorService"/>
+ <receiver android:name="com.google.android.car.userswitchmonitor.BootCompletedReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED"/>
+ </intent-filter>
+ </receiver>
+ </application>
+</manifest>
diff --git a/tests/UserSwitchMonitorApp/src/com/google/android/car/userswitchmonitor/UserSwitchMonitorService.java b/tests/UserSwitchMonitorApp/src/com/google/android/car/userswitchmonitor/UserSwitchMonitorService.java
index f76b0d6..73c5cc4 100644
--- a/tests/UserSwitchMonitorApp/src/com/google/android/car/userswitchmonitor/UserSwitchMonitorService.java
+++ b/tests/UserSwitchMonitorApp/src/com/google/android/car/userswitchmonitor/UserSwitchMonitorService.java
@@ -41,6 +41,10 @@
static final String TAG = "UserSwitchMonitor";
+ private static final String CMD_HELP = "help";
+ private static final String CMD_REGISTER = "register";
+ private static final String CMD_UNREGISTER = "unregister";
+
private final Object mLock = new Object();
private final int mUserId = android.os.Process.myUserHandle().getIdentifier();
@@ -64,11 +68,16 @@
mContext = getApplicationContext();
mCar = Car.createCar(mContext);
mCarUserManager = (CarUserManager) mCar.getCarManager(Car.CAR_USER_SERVICE);
- mCarUserManager.addListener((r)-> r.run(), mListener);
+ registerListener();
mNotificationManager = mContext.getSystemService(NotificationManager.class);
}
+ private void registerListener() {
+ Log.d(TAG, "registerListener(): " + mListener);
+ mCarUserManager.addListener((r)-> r.run(), mListener);
+ }
+
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand(" + mUserId + "): " + intent);
@@ -79,11 +88,13 @@
NotificationManager.IMPORTANCE_MIN);
mNotificationManager.createNotificationChannel(channel);
+ // Cannot use R.drawable because package name is different on app2
+ int iconResId = mContext.getApplicationInfo().icon;
startForeground(startId,
new Notification.Builder(mContext, channelId)
.setContentText(name)
.setContentTitle(name)
- .setSmallIcon(R.drawable.ic_launcher)
+ .setSmallIcon(iconResId)
.build());
return super.onStartCommand(intent, flags, startId);
@@ -93,19 +104,29 @@
public void onDestroy() {
Log.d(TAG, "onDestroy(" + mUserId + ")");
- if (mCarUserManager != null) {
- mCarUserManager.removeListener(mListener);
- } else {
- Log.w(TAG, "Cannot remove listener because manager is null");
- }
+ unregisterListener();
if (mCar != null && mCar.isConnected()) {
mCar.disconnect();
}
super.onDestroy();
}
+ private void unregisterListener() {
+ Log.d(TAG, "unregisterListener(): " + mListener);
+ if (mCarUserManager != null) {
+ mCarUserManager.removeListener(mListener);
+ } else {
+ Log.w(TAG, "Cannot remove listener because manager is null");
+ }
+ }
+
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (args != null && args.length > 0) {
+ executeCommand(pw, args);
+ return;
+ }
+
pw.printf("User id: %d\n", mUserId);
synchronized (mLock) {
if (mEvents.isEmpty()) {
@@ -127,4 +148,48 @@
return null;
}
+ private void executeCommand(PrintWriter pw, String[] args) {
+ String cmd = args[0];
+ switch (cmd) {
+ case CMD_HELP:
+ cmdHelp(pw);
+ break;
+ case CMD_REGISTER:
+ cmdRegister(pw);
+ break;
+ case CMD_UNREGISTER:
+ cmdUnregister(pw);
+ break;
+ default:
+ pw.printf("invalid command: %s\n\n", cmd);
+ cmdHelp(pw);
+ }
+ }
+
+ private void cmdHelp(PrintWriter pw) {
+ pw.printf("Options:\n");
+ pw.printf(" help: show this help\n");
+ pw.printf(" register: register the service to receive events\n");
+ pw.printf(" unregister: unregister the service from receiving events\n");
+ }
+
+
+ private void cmdRegister(PrintWriter pw) {
+ pw.printf("registering listener %s\n", mListener);
+ runCmd(pw, () -> registerListener());
+ }
+
+ private void cmdUnregister(PrintWriter pw) {
+ pw.printf("unregistering listener %s\n", mListener);
+ runCmd(pw, () -> unregisterListener());
+ }
+
+ private void runCmd(PrintWriter pw, Runnable r) {
+ try {
+ r.run();
+ } catch (Exception e) {
+ Log.e(TAG, "error running command", e);
+ pw.printf("failed: %s\n", e);
+ }
+ }
}
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarActivityManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarActivityManagerTest.java
new file mode 100644
index 0000000..f1d90be
--- /dev/null
+++ b/tests/android_car_api_test/src/android/car/apitest/CarActivityManagerTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2021 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 android.car.apitest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.car.Car;
+import android.car.app.CarActivityManager;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.Display;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@MediumTest
+public class CarActivityManagerTest extends CarApiTestBase {
+ private static final String TAG = CarActivityManagerTest.class.getSimpleName();
+
+ // Comes from android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER
+ private static final int FEATURE_DEFAULT_TASK_CONTAINER = 1;
+
+ // Comes from android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED
+ private static final int FEATURE_UNDEFINED = -1;
+
+ private CarActivityManager mCarActivityManager;
+
+ private final ComponentName mTestActivity = new ComponentName("test.pkg", "test.activity");
+
+ @Before
+ public void setUp() throws Exception {
+ mCarActivityManager = (CarActivityManager) getCar().getCarManager(Car.CAR_ACTIVITY_SERVICE);
+ assertThat(mCarActivityManager).isNotNull();
+ }
+
+ @Test
+ public void testSetPersistentActivity() {
+ // Set
+ int retSet = mCarActivityManager.setPersistentActivity(
+ mTestActivity, Display.DEFAULT_DISPLAY, FEATURE_DEFAULT_TASK_CONTAINER);
+ assertThat(retSet).isEqualTo(CarActivityManager.RESULT_SUCCESS);
+
+ // Remove
+ int retRemove = mCarActivityManager.setPersistentActivity(
+ mTestActivity, Display.DEFAULT_DISPLAY, FEATURE_UNDEFINED);
+ assertThat(retRemove).isEqualTo(CarActivityManager.RESULT_SUCCESS);
+ }
+
+ @Test
+ public void testSetPersistentActivity_throwsExceptionForInvalidDisplayId() {
+ int invalidDisplayId = 999999990;
+
+ assertThrows(IllegalArgumentException.class,
+ () -> mCarActivityManager.setPersistentActivity(
+ mTestActivity, invalidDisplayId, FEATURE_DEFAULT_TASK_CONTAINER));
+ }
+
+ @Test
+ public void testSetPersistentActivity_throwsExceptionForInvalidFeatureId() {
+ int unknownFeatureId = 999999990;
+
+ assertThrows(IllegalArgumentException.class,
+ () -> mCarActivityManager.setPersistentActivity(
+ mTestActivity, Display.DEFAULT_DISPLAY, unknownFeatureId));
+ }
+
+ @Test
+ public void testSetPersistentActivity_throwsExceptionForUnknownActivity() {
+ // Tries to remove the Activity without registering it.
+ assertThrows(ActivityNotFoundException.class,
+ () -> mCarActivityManager.setPersistentActivity(
+ mTestActivity, Display.DEFAULT_DISPLAY, FEATURE_UNDEFINED));
+ }
+}
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java
index af81c61..276e829 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarAppFocusManagerTest.java
@@ -36,6 +36,8 @@
import org.junit.Before;
import org.junit.Test;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
@@ -246,6 +248,24 @@
APP_FOCUS_TYPE_NAVIGATION, false)).isTrue();
}
+ @Test
+ public void testGetAppTypeOwner() throws Exception {
+ CarAppFocusManager manager = createManager(getContext(), mEventThread);
+
+ assertThat(manager.getAppTypeOwner(APP_FOCUS_TYPE_NAVIGATION)).isNull();
+
+ FocusOwnershipCallback owner = new FocusOwnershipCallback();
+ assertThat(manager.requestAppFocus(APP_FOCUS_TYPE_NAVIGATION, owner))
+ .isEqualTo(APP_FOCUS_REQUEST_SUCCEEDED);
+
+ assertThat(manager.getAppTypeOwner(APP_FOCUS_TYPE_NAVIGATION))
+ .containsExactly("android.car.apitest", "com.google.android.car.kitchensink");
+
+ manager.abandonAppFocus(owner, APP_FOCUS_TYPE_NAVIGATION);
+
+ assertThat(manager.getAppTypeOwner(APP_FOCUS_TYPE_NAVIGATION)).isNull();
+ }
+
private CarAppFocusManager createManager() throws InterruptedException {
return createManager(getContext(), mEventThread);
}
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarBugreportManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarBugreportManagerTest.java
index 6a7ccdb..21aef7b 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarBugreportManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarBugreportManagerTest.java
@@ -26,6 +26,7 @@
import android.car.CarBugreportManager;
import android.car.CarBugreportManager.CarBugreportManagerCallback;
import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.FlakyTest;
import android.test.suitebuilder.annotation.LargeTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -71,7 +72,7 @@
}
@Test
- public void test_requestBugreport_failsWhenNoPermission() throws Exception {
+ public void test_requestBugreport_failsWhenNoPermission() {
dropPermissions();
SecurityException expected =
@@ -83,6 +84,7 @@
}
@Test
+ @FlakyTest(bugId = 197652182)
public void test_requestBugreport_works() throws Exception {
getPermissions();
diff --git a/tests/android_car_api_test/src/android/car/apitest/CarUserManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/CarUserManagerTest.java
index 62f2b0a..2662eaf 100644
--- a/tests/android_car_api_test/src/android/car/apitest/CarUserManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/CarUserManagerTest.java
@@ -18,15 +18,22 @@
import static android.car.test.util.UserTestingHelper.clearUserLockCredentials;
import static android.car.test.util.UserTestingHelper.setMaxSupportedUsers;
import static android.car.test.util.UserTestingHelper.setUserLockCredentials;
+import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_STARTING;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING;
import static android.car.user.CarUserManager.USER_LIFECYCLE_EVENT_TYPE_UNLOCKED;
import static com.google.common.truth.Truth.assertWithMessage;
+import android.annotation.UserIdInt;
import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.car.Car;
import android.car.testapi.BlockingUserLifecycleListener;
+import android.car.user.CarUserManager;
import android.car.user.CarUserManager.UserLifecycleEvent;
import android.content.pm.UserInfo;
+import android.os.Process;
+import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
@@ -34,12 +41,15 @@
import org.junit.BeforeClass;
import org.junit.Test;
+import java.util.List;
+
public final class CarUserManagerTest extends CarMultiUserTestBase {
private static final String TAG = CarUserManagerTest.class.getSimpleName();
private static final int PIN = 2345;
+ private static final int START_TIMEOUT_MS = 20_000;
private static final int SWITCH_TIMEOUT_MS = 70_000;
private static final int sMaxNumberUsersBefore = UserManager.getMaxSupportedUsers();
@@ -92,6 +102,75 @@
assertUserInfo(newGuest, loadedGuest);
}
+ @Test
+ public void testLifecycleMultipleListeners() throws Exception {
+ int newUserId = createUser("Test").id;
+ Car car2 = Car.createCar(getContext().getApplicationContext());
+ CarUserManager mgr2 = (CarUserManager) car2.getCarManager(Car.CAR_USER_SERVICE);
+ CarUserManager mgr1 = mCarUserManager;
+ Log.d(TAG, "myUid=" + Process.myUid() + ",mgr1=" + mgr1 + ", mgr2=" + mgr2);
+ assertWithMessage("mgrs").that(mgr1).isNotSameInstanceAs(mgr2);
+
+ BlockingUserLifecycleListener listener1 = BlockingUserLifecycleListener
+ .forSpecificEvents()
+ .forUser(newUserId)
+ .setTimeout(START_TIMEOUT_MS)
+ .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING)
+ .build();
+ BlockingUserLifecycleListener listener2 = BlockingUserLifecycleListener
+ .forSpecificEvents()
+ .forUser(newUserId)
+ .setTimeout(START_TIMEOUT_MS)
+ .addExpectedEvent(USER_LIFECYCLE_EVENT_TYPE_STARTING)
+ .build();
+
+ Log.d(TAG, "registering listener1: " + listener1);
+ mgr1.addListener(Runnable::run, listener1);
+ Log.v(TAG, "ok");
+ try {
+ Log.d(TAG, "registering listener2: " + listener2);
+ mgr2.addListener(Runnable::run, listener2);
+ Log.v(TAG, "ok");
+ try {
+ IActivityManager am = ActivityManager.getService();
+ Log.d(TAG, "Starting user " + newUserId);
+ am.startUserInBackground(newUserId);
+ Log.v(TAG, "ok");
+
+ Log.d(TAG, "Waiting for events");
+ List<UserLifecycleEvent> events1 = listener1.waitForEvents();
+ Log.d(TAG, "events1: " + events1);
+ List<UserLifecycleEvent> events2 = listener2.waitForEvents();
+ Log.d(TAG, "events2: " + events2);
+ assertStartUserEvent(events1, newUserId);
+ assertStartUserEvent(events2, newUserId);
+ } finally {
+ Log.d(TAG, "unregistering listener2: " + listener2);
+ mgr2.removeListener(listener2);
+ Log.v(TAG, "ok");
+ }
+ } finally {
+ Log.d(TAG, "unregistering listener1: " + listener1);
+ mgr1.removeListener(listener1);
+ Log.v(TAG, "ok");
+ }
+ }
+
+ private void assertStartUserEvent(List<UserLifecycleEvent> events, @UserIdInt int userId) {
+ assertWithMessage("events").that(events).hasSize(1);
+
+ UserLifecycleEvent event = events.get(0);
+ assertWithMessage("type").that(event.getEventType())
+ .isEqualTo(USER_LIFECYCLE_EVENT_TYPE_STARTING);
+ assertWithMessage("user id on %s", event).that(event.getUserId()).isEqualTo(userId);
+ assertWithMessage("user handle on %s", event).that(event.getUserHandle().getIdentifier())
+ .isEqualTo(userId);
+ assertWithMessage("previous user id on %s", event).that(event.getPreviousUserId())
+ .isEqualTo(UserHandle.USER_NULL);
+ assertWithMessage("previous user handle on %s", event).that(event.getPreviousUserHandle())
+ .isNull();
+ }
+
/**
* Tests resume behavior when current user is ephemeral guest, a new guest user should be
* created and switched to.
diff --git a/tests/android_car_api_test/src/android/car/apitest/media/CarAudioManagerTest.java b/tests/android_car_api_test/src/android/car/apitest/media/CarAudioManagerTest.java
index 9fb2781..cab66f9 100644
--- a/tests/android_car_api_test/src/android/car/apitest/media/CarAudioManagerTest.java
+++ b/tests/android_car_api_test/src/android/car/apitest/media/CarAudioManagerTest.java
@@ -16,14 +16,18 @@
package android.car.apitest.media;
+import static android.car.Car.AUDIO_SERVICE;
+import static android.car.media.CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING;
+import static android.car.media.CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING;
+import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static android.media.AudioAttributes.USAGE_MEDIA;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assume.assumeTrue;
-import android.car.Car;
import android.car.apitest.CarApiTestBase;
import android.car.media.CarAudioManager;
-import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
import android.os.Process;
@@ -38,11 +42,13 @@
@RunWith(AndroidJUnit4.class)
public class CarAudioManagerTest extends CarApiTestBase {
+ private static final int TEST_FLAGS = 0;
+
private CarAudioManager mCarAudioManager;
@Before
public void setUp() throws Exception {
- mCarAudioManager = (CarAudioManager) getCar().getCarManager(Car.AUDIO_SERVICE);
+ mCarAudioManager = (CarAudioManager) getCar().getCarManager(AUDIO_SERVICE);
assertThat(mCarAudioManager).isNotNull();
}
@@ -52,21 +58,21 @@
List<Integer> zoneIds = mCarAudioManager.getAudioZoneIds();
assertThat(zoneIds).isNotEmpty();
- assertThat(zoneIds).contains(CarAudioManager.PRIMARY_AUDIO_ZONE);
+ assertThat(zoneIds).contains(PRIMARY_AUDIO_ZONE);
}
@Test
public void test_isAudioFeatureEnabled() throws Exception {
// nothing to assert. Just call the API.
- mCarAudioManager.isAudioFeatureEnabled(CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING);
- mCarAudioManager.isAudioFeatureEnabled(CarAudioManager.AUDIO_FEATURE_VOLUME_GROUP_MUTING);
+ mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING);
+ mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_MUTING);
}
@Test
public void test_getVolumeGroupCount() throws Exception {
int primaryZoneCount = mCarAudioManager.getVolumeGroupCount();
assertThat(
- mCarAudioManager.getVolumeGroupCount(CarAudioManager.PRIMARY_AUDIO_ZONE)).isEqualTo(
+ mCarAudioManager.getVolumeGroupCount(PRIMARY_AUDIO_ZONE)).isEqualTo(
primaryZoneCount);
}
@@ -74,14 +80,14 @@
public void test_getGroupVolume() throws Exception {
int groudId = 0;
int volume = mCarAudioManager.getGroupVolume(groudId);
- assertThat(mCarAudioManager.getGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE,
- groudId)).isEqualTo(volume);
+ assertThat(mCarAudioManager.getGroupVolume(PRIMARY_AUDIO_ZONE, groudId))
+ .isEqualTo(volume);
int maxVolume = mCarAudioManager.getGroupMaxVolume(groudId);
- assertThat(mCarAudioManager.getGroupMaxVolume(CarAudioManager.PRIMARY_AUDIO_ZONE,
- groudId)).isEqualTo(maxVolume);
+ assertThat(mCarAudioManager.getGroupMaxVolume(PRIMARY_AUDIO_ZONE, groudId))
+ .isEqualTo(maxVolume);
int minVolume = mCarAudioManager.getGroupMinVolume(groudId);
- assertThat(mCarAudioManager.getGroupMinVolume(CarAudioManager.PRIMARY_AUDIO_ZONE,
- groudId)).isEqualTo(minVolume);
+ assertThat(mCarAudioManager.getGroupMinVolume(PRIMARY_AUDIO_ZONE, groudId))
+ .isEqualTo(minVolume);
assertThat(volume).isAtLeast(minVolume);
assertThat(volume).isAtMost(maxVolume);
}
@@ -90,8 +96,8 @@
public void test_setGroupVolume() throws Exception {
int groudId = 0;
int volume = mCarAudioManager.getGroupVolume(groudId);
- mCarAudioManager.setGroupVolume(groudId, volume, 0);
- mCarAudioManager.setGroupVolume(CarAudioManager.PRIMARY_AUDIO_ZONE, groudId, volume, 0);
+ mCarAudioManager.setGroupVolume(groudId, volume, TEST_FLAGS);
+ mCarAudioManager.setGroupVolume(PRIMARY_AUDIO_ZONE, groudId, volume, TEST_FLAGS);
assertThat(mCarAudioManager.getGroupVolume(groudId)).isEqualTo(volume);
}
@@ -99,8 +105,7 @@
public void test_getInputDevicesForZoneId() throws Exception {
assumeDynamicRoutingIsEnabled();
- List<AudioDeviceInfo> info = mCarAudioManager.getInputDevicesForZoneId(
- CarAudioManager.PRIMARY_AUDIO_ZONE);
+ List<AudioDeviceInfo> info = mCarAudioManager.getInputDevicesForZoneId(PRIMARY_AUDIO_ZONE);
assertThat(info).isNotEmpty();
}
@@ -109,15 +114,15 @@
assumeDynamicRoutingIsEnabled();
AudioDeviceInfo device = mCarAudioManager.getOutputDeviceForUsage(
- CarAudioManager.PRIMARY_AUDIO_ZONE, AudioAttributes.USAGE_MEDIA);
+ PRIMARY_AUDIO_ZONE, USAGE_MEDIA);
assertThat(device).isNotNull();
}
@Test
public void test_getVolumeGroupIdForUsage() throws Exception {
- int groupId = mCarAudioManager.getVolumeGroupIdForUsage(AudioAttributes.USAGE_MEDIA);
- assertThat(mCarAudioManager.getVolumeGroupIdForUsage(CarAudioManager.PRIMARY_AUDIO_ZONE,
- AudioAttributes.USAGE_MEDIA)).isEqualTo(groupId);
+ int groupId = mCarAudioManager.getVolumeGroupIdForUsage(USAGE_MEDIA);
+ assertThat(mCarAudioManager.getVolumeGroupIdForUsage(PRIMARY_AUDIO_ZONE, USAGE_MEDIA))
+ .isEqualTo(groupId);
int primaryZoneCount = mCarAudioManager.getVolumeGroupCount();
assertThat(groupId).isLessThan(primaryZoneCount);
}
@@ -126,8 +131,99 @@
public void test_getZoneIdForUid() throws Exception {
assumeDynamicRoutingIsEnabled();
- assertThat(mCarAudioManager.getZoneIdForUid(Process.myUid())).isEqualTo(
- CarAudioManager.PRIMARY_AUDIO_ZONE);
+ assertThat(mCarAudioManager.getZoneIdForUid(Process.myUid())).isEqualTo(PRIMARY_AUDIO_ZONE);
+ }
+
+ @Test
+ public void setVolumeGroupMute_toMute_mutesVolumeGroup() throws Exception {
+ assumeVolumeGroupMutingIsEnabled();
+ int groupId = 0;
+ boolean muteState = mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId);
+
+ try {
+ mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, true, TEST_FLAGS);
+ assertThat(mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId))
+ .isEqualTo(true);
+ } finally {
+ mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, muteState, TEST_FLAGS);
+ }
+ }
+
+ @Test
+ public void setVolumeGroupMute_toUnMute_unMutesVolumeGroup() throws Exception {
+ assumeVolumeGroupMutingIsEnabled();
+ int groupId = 0;
+ boolean muteState = mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId);
+
+ try {
+ mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, false, TEST_FLAGS);
+ assertThat(mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId))
+ .isEqualTo(false);
+ } finally {
+ mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, muteState, TEST_FLAGS);
+ }
+ }
+
+ @Test
+ public void setGroupVolume_whileMuted_unMutesVolumeGroup() throws Exception {
+ assumeVolumeGroupMutingIsEnabled();
+ int groupId = 0;
+ boolean muteState = mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId);
+ int volume = mCarAudioManager.getGroupVolume(PRIMARY_AUDIO_ZONE, groupId);
+ int minVolume = mCarAudioManager.getGroupMinVolume(PRIMARY_AUDIO_ZONE, groupId);
+
+ try {
+ mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, true, TEST_FLAGS);
+
+ mCarAudioManager.setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, minVolume, TEST_FLAGS);
+ assertThat(mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId))
+ .isEqualTo(false);
+ } finally {
+ mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, muteState, TEST_FLAGS);
+ mCarAudioManager.setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, volume, TEST_FLAGS);
+ }
+ }
+
+ @Test
+ public void getGroupVolume_whileMuted_returnsMinVolume() throws Exception {
+ assumeVolumeGroupMutingIsEnabled();
+ int groupId = 0;
+ boolean muteState = mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId);
+ int minVolume = mCarAudioManager.getGroupMinVolume(PRIMARY_AUDIO_ZONE, groupId);
+
+ try {
+ mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, true, TEST_FLAGS);
+
+ assertThat(mCarAudioManager.getGroupVolume(PRIMARY_AUDIO_ZONE, groupId))
+ .isEqualTo(minVolume);
+ } finally {
+ mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, muteState, TEST_FLAGS);
+ }
+ }
+
+ @Test
+ public void getGroupVolume_whileUnMuted_returnLastSetValue() throws Exception {
+ assumeVolumeGroupMutingIsEnabled();
+ int groupId = 0;
+ boolean muteState = mCarAudioManager.isVolumeGroupMuted(PRIMARY_AUDIO_ZONE, groupId);
+ int volume = mCarAudioManager.getGroupVolume(PRIMARY_AUDIO_ZONE, groupId);
+ int minVolume = mCarAudioManager.getGroupMinVolume(PRIMARY_AUDIO_ZONE, groupId);
+ int maxVolume = mCarAudioManager.getGroupMaxVolume(PRIMARY_AUDIO_ZONE, groupId);
+ int testVolume = (minVolume + maxVolume) / 2;
+
+ try {
+ mCarAudioManager.setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, testVolume, TEST_FLAGS);
+
+ mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, true, TEST_FLAGS);
+
+ mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, false, TEST_FLAGS);
+
+ assertThat(mCarAudioManager.getGroupVolume(PRIMARY_AUDIO_ZONE, groupId))
+ .isEqualTo(testVolume);
+ } finally {
+ mCarAudioManager.setVolumeGroupMute(PRIMARY_AUDIO_ZONE, groupId, muteState, TEST_FLAGS);
+ mCarAudioManager.setGroupVolume(PRIMARY_AUDIO_ZONE, groupId, volume, TEST_FLAGS);
+ }
}
@Test
@@ -136,12 +232,14 @@
// TODO(b/191660867): Better to change this to play something and asert true.
assertThat(
- mCarAudioManager.isPlaybackOnVolumeGroupActive(CarAudioManager.PRIMARY_AUDIO_ZONE,
+ mCarAudioManager.isPlaybackOnVolumeGroupActive(PRIMARY_AUDIO_ZONE,
0)).isFalse();
}
private void assumeDynamicRoutingIsEnabled() {
- assumeTrue(mCarAudioManager.isAudioFeatureEnabled(
- CarAudioManager.AUDIO_FEATURE_DYNAMIC_ROUTING));
+ assumeTrue(mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_DYNAMIC_ROUTING));
+ }
+ private void assumeVolumeGroupMutingIsEnabled() {
+ assumeTrue(mCarAudioManager.isAudioFeatureEnabled(AUDIO_FEATURE_VOLUME_GROUP_MUTING));
}
}
diff --git a/tests/carservice_test/AndroidManifest.xml b/tests/carservice_test/AndroidManifest.xml
index 7bf749e..47dbb26 100644
--- a/tests/carservice_test/AndroidManifest.xml
+++ b/tests/carservice_test/AndroidManifest.xml
@@ -59,6 +59,11 @@
<meta-data android:name="distractionOptimized"
android:value="true"/>
</activity>
+ <activity android:name="androidx.car.app.activity.CarAppActivity"
+ android:label="CarAppActivity">
+ <meta-data android:name="distractionOptimized"
+ android:value="true"/>
+ </activity>
<receiver android:name="com.android.car.CarStorageMonitoringBroadcastReceiver"
android:exported="true"
diff --git a/tests/carservice_test/src/androidx/car/app/activity/CarAppActivity.java b/tests/carservice_test/src/androidx/car/app/activity/CarAppActivity.java
new file mode 100644
index 0000000..b42b7cc
--- /dev/null
+++ b/tests/carservice_test/src/androidx/car/app/activity/CarAppActivity.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2021 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 androidx.car.app.activity;
+
+
+import static com.android.car.pm.ActivityBlockingActivityTest.DoActivity.DIALOG_TITLE;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+/**
+ * An activity to represent a template activity in tests.
+ */
+public class CarAppActivity extends Activity {
+ public static final String ACTION_SHOW_DIALOG = "SHOW_DIALOG";
+ public static final String ACTION_START_SECOND_INSTANCE = "START_SECOND_INSTANCE";
+ public static final String SECOND_INSTANCE_TITLE = "Second Instance";
+ private static final String BUNDLE_KEY_IS_SECOND_INSTANCE = "is_second_instance";
+
+ private final ShowDialogReceiver mShowDialogReceiver = new ShowDialogReceiver();
+ private final StartSecondInstanceReceiver
+ mStartSecondInstanceReceiver = new StartSecondInstanceReceiver();
+
+ private class ShowDialogReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ showDialog();
+ }
+ }
+
+ private class StartSecondInstanceReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ startSecondInstance();
+ }
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent().getBooleanExtra(BUNDLE_KEY_IS_SECOND_INSTANCE, false)) {
+ getActionBar().setTitle(SECOND_INSTANCE_TITLE);
+ }
+ this.registerReceiver(mShowDialogReceiver, new IntentFilter(ACTION_SHOW_DIALOG));
+ this.registerReceiver(mStartSecondInstanceReceiver,
+ new IntentFilter(ACTION_START_SECOND_INSTANCE));
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ this.unregisterReceiver(mShowDialogReceiver);
+ this.unregisterReceiver(mStartSecondInstanceReceiver);
+ }
+
+ private void startSecondInstance() {
+ Intent intent = new Intent(CarAppActivity.this, CarAppActivity.class);
+ intent.putExtra(BUNDLE_KEY_IS_SECOND_INSTANCE, true);
+ startActivity(intent);
+ }
+
+ private void showDialog() {
+ AlertDialog dialog = new AlertDialog.Builder(this)
+ .setTitle(DIALOG_TITLE)
+ .setMessage("Message")
+ .create();
+ dialog.show();
+ }
+}
diff --git a/tests/carservice_test/src/com/android/car/AppFocusTest.java b/tests/carservice_test/src/com/android/car/AppFocusTest.java
index 390acf2..1afb52f 100644
--- a/tests/carservice_test/src/com/android/car/AppFocusTest.java
+++ b/tests/carservice_test/src/com/android/car/AppFocusTest.java
@@ -16,6 +16,9 @@
package com.android.car;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import static com.google.common.truth.Truth.assertThat;
import android.car.Car;
import android.car.CarAppFocusManager;
@@ -46,10 +49,13 @@
manager.requestAppFocus(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, ownershipListener);
listener.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, true);
+ assertThat(manager.getAppTypeOwner(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION))
+ .containsExactly("com.android.car.test", "com.google.android.car.kitchensink");
listener.resetWait();
manager.abandonAppFocus(ownershipListener, CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION);
listener.waitForFocusChangeAndAssert(DEFAULT_WAIT_TIMEOUT_MS,
CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION, false);
+ assertNull(manager.getAppTypeOwner(CarAppFocusManager.APP_FOCUS_TYPE_NAVIGATION));
manager.removeFocusListener(listener);
}
diff --git a/tests/carservice_test/src/com/android/car/CarTelemetryManagerTest.java b/tests/carservice_test/src/com/android/car/CarTelemetryManagerTest.java
new file mode 100644
index 0000000..1a489d7
--- /dev/null
+++ b/tests/carservice_test/src/com/android/car/CarTelemetryManagerTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2017 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;
+
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_ALREADY_EXISTS;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_NONE;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_PARSE_FAILED;
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_VERSION_TOO_OLD;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import android.annotation.NonNull;
+import android.car.Car;
+import android.car.telemetry.CarTelemetryManager;
+import android.car.telemetry.MetricsConfigKey;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import com.android.car.telemetry.CarTelemetryService;
+import com.android.car.telemetry.TelemetryProto;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/** Test the public entry points for the CarTelemetryManager. */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class CarTelemetryManagerTest extends MockedCarTestBase {
+ private static final long TIMEOUT_MS = 5_000L;
+ private static final String TAG = CarTelemetryManagerTest.class.getSimpleName();
+ private static final byte[] INVALID_METRICS_CONFIG = "bad config".getBytes();
+ private static final Executor DIRECT_EXECUTOR = Runnable::run;
+ private static final MetricsConfigKey KEY_V1 = new MetricsConfigKey("my_metrics_config", 1);
+ private static final MetricsConfigKey KEY_V2 = new MetricsConfigKey("my_metrics_config", 2);
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_V1 =
+ TelemetryProto.MetricsConfig.newBuilder()
+ .setName("my_metrics_config").setVersion(1).setScript("no-op").build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_V2 =
+ METRICS_CONFIG_V1.toBuilder().setVersion(2).build();
+
+ private final FakeCarTelemetryResultsListener mListener = new FakeCarTelemetryResultsListener();
+ private final HandlerThread mTelemetryThread =
+ CarServiceUtils.getHandlerThread(CarTelemetryService.class.getSimpleName());
+ private final Handler mHandler = new Handler(mTelemetryThread.getLooper());
+
+ private CarTelemetryManager mCarTelemetryManager;
+ private CountDownLatch mIdleHandlerLatch = new CountDownLatch(1);
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ assumeTrue(getCar().isFeatureEnabled(Car.CAR_TELEMETRY_SERVICE));
+
+ mTelemetryThread.getLooper().getQueue().addIdleHandler(() -> {
+ mIdleHandlerLatch.countDown();
+ return true;
+ });
+
+ Log.i(TAG, "attempting to get CAR_TELEMETRY_SERVICE");
+ mCarTelemetryManager = (CarTelemetryManager) getCar().getCarManager(
+ Car.CAR_TELEMETRY_SERVICE);
+ mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
+ }
+
+ @Test
+ public void testSetClearListener() {
+ mCarTelemetryManager.clearListener();
+ mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener);
+
+ // setListener multiple times should fail
+ assertThrows(IllegalStateException.class,
+ () -> mCarTelemetryManager.setListener(DIRECT_EXECUTOR, mListener));
+ }
+
+ @Test
+ public void testApiInvocationWithoutSettingListener() {
+ mCarTelemetryManager.clearListener();
+
+ assertThrows(IllegalStateException.class,
+ () -> mCarTelemetryManager.addMetricsConfig(
+ KEY_V1, METRICS_CONFIG_V1.toByteArray()));
+ assertThrows(IllegalStateException.class,
+ () -> mCarTelemetryManager.removeMetricsConfig(KEY_V1));
+ assertThrows(IllegalStateException.class,
+ () -> mCarTelemetryManager.removeAllMetricsConfigs());
+ assertThrows(IllegalStateException.class,
+ () -> mCarTelemetryManager.sendFinishedReports(KEY_V1));
+ assertThrows(IllegalStateException.class,
+ () -> mCarTelemetryManager.sendAllFinishedReports());
+ }
+
+ @Test
+ public void testAddMetricsConfig() throws Exception {
+ // invalid config, should fail
+ mCarTelemetryManager.addMetricsConfig(KEY_V1, INVALID_METRICS_CONFIG);
+ waitForHandlerThreadToFinish();
+ assertThat(mListener.getAddConfigStatus(KEY_V1)).isEqualTo(
+ ERROR_METRICS_CONFIG_PARSE_FAILED);
+
+ // new valid config, should succeed
+ mCarTelemetryManager.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+ waitForHandlerThreadToFinish();
+ assertThat(mListener.getAddConfigStatus(KEY_V1)).isEqualTo(ERROR_METRICS_CONFIG_NONE);
+
+ // duplicate config, should fail
+ mCarTelemetryManager.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+ waitForHandlerThreadToFinish();
+ assertThat(mListener.getAddConfigStatus(KEY_V1)).isEqualTo(
+ ERROR_METRICS_CONFIG_ALREADY_EXISTS);
+
+ // newer version of the config should replace older version
+ mCarTelemetryManager.addMetricsConfig(KEY_V2, METRICS_CONFIG_V2.toByteArray());
+ waitForHandlerThreadToFinish();
+ assertThat(mListener.getAddConfigStatus(KEY_V2)).isEqualTo(ERROR_METRICS_CONFIG_NONE);
+
+ // older version of the config should not be accepted
+ mCarTelemetryManager.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+ waitForHandlerThreadToFinish();
+ assertThat(mListener.getAddConfigStatus(KEY_V1)).isEqualTo(
+ ERROR_METRICS_CONFIG_VERSION_TOO_OLD);
+ }
+
+ private void waitForHandlerThreadToFinish() throws Exception {
+ assertWithMessage("handler not idle in %sms", TIMEOUT_MS)
+ .that(mIdleHandlerLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+ mIdleHandlerLatch = new CountDownLatch(1); // reset idle handler condition
+ mHandler.runWithScissors(() -> {
+ }, TIMEOUT_MS);
+ }
+
+
+ private static final class FakeCarTelemetryResultsListener
+ implements CarTelemetryManager.CarTelemetryResultsListener {
+
+ private Map<MetricsConfigKey, Integer> mAddConfigStatusMap = new ArrayMap<>();
+
+ @Override
+ public void onResult(@NonNull MetricsConfigKey key, @NonNull byte[] result) {
+ }
+
+ @Override
+ public void onError(@NonNull MetricsConfigKey key, @NonNull byte[] error) {
+ }
+
+ @Override
+ public void onAddMetricsConfigStatus(@NonNull MetricsConfigKey key, int statusCode) {
+ mAddConfigStatusMap.put(key, statusCode);
+ }
+
+ public int getAddConfigStatus(MetricsConfigKey key) {
+ return mAddConfigStatusMap.getOrDefault(key, -100);
+ }
+ }
+}
diff --git a/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperLegacyTest.java b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperLegacyTest.java
index e006387..49ca55a 100644
--- a/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperLegacyTest.java
+++ b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperLegacyTest.java
@@ -15,6 +15,10 @@
*/
package com.android.car.audio;
+import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+import static android.media.AudioDeviceInfo.TYPE_FM_TUNER;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -23,6 +27,8 @@
import android.annotation.XmlRes;
import android.content.Context;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
import android.util.SparseArray;
import androidx.test.core.app.ApplicationProvider;
@@ -65,9 +71,10 @@
RuntimeException exception = expectThrows(RuntimeException.class,
() -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
- carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings));
+ carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings,
+ getInputDevices()));
- assertThat(exception.getMessage()).contains("Two addresses map to same bus number:");
+ assertThat(exception).hasMessageThat().contains("Two addresses map to same bus number:");
}
@Test
@@ -78,9 +85,79 @@
RuntimeException exception = expectThrows(RuntimeException.class,
() -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
- carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings));
+ carAudioDeviceInfos, mMockAudioControlWrapper,
+ mMockCarAudioSettings, getInputDevices()));
- assertThat(exception.getMessage()).contains("Invalid bus -1 was associated with context");
+ assertThat(exception).hasMessageThat()
+ .contains("Invalid bus -1 was associated with context");
+ }
+
+ @Test
+ public void constructor_throwsIfNullInputDevices() throws Exception {
+ List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+ when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(INVALID_BUS);
+
+ NullPointerException exception = expectThrows(NullPointerException.class,
+ () -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+ carAudioDeviceInfos, mMockAudioControlWrapper,
+ mMockCarAudioSettings, null));
+
+ assertThat(exception).hasMessageThat().contains("Input Devices");
+ }
+
+ @Test
+ public void constructor_throwsIfNullContext() throws Exception {
+ List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+ when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(INVALID_BUS);
+
+ NullPointerException exception = expectThrows(NullPointerException.class,
+ () -> new CarAudioZonesHelperLegacy(null, mCarVolumeGroups,
+ carAudioDeviceInfos, mMockAudioControlWrapper,
+ mMockCarAudioSettings, getInputDevices()));
+
+ assertThat(exception).hasMessageThat().contains("Context");
+ }
+
+ @Test
+ public void constructor_throwsIfNullCarAudioDeviceInfo() throws Exception {
+ when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(INVALID_BUS);
+
+ NullPointerException exception = expectThrows(NullPointerException.class,
+ () -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+ null, mMockAudioControlWrapper,
+ mMockCarAudioSettings, getInputDevices()));
+
+ assertThat(exception).hasMessageThat().contains("Car Audio Device Info");
+ }
+
+ @Test
+ public void constructor_throwsIfNullCarAudioControl() throws Exception {
+ List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+ when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(INVALID_BUS);
+
+ NullPointerException exception = expectThrows(NullPointerException.class,
+ () -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+ carAudioDeviceInfos, null,
+ mMockCarAudioSettings, getInputDevices()));
+
+ assertThat(exception).hasMessageThat().contains("Car Audio Control");
+ }
+
+ @Test
+ public void constructor_throwsIfNullCarAudioSettings() throws Exception {
+ List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+ when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(INVALID_BUS);
+
+ NullPointerException exception = expectThrows(NullPointerException.class,
+ () -> new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+ carAudioDeviceInfos, mMockAudioControlWrapper,
+ null, getInputDevices()));
+
+ assertThat(exception).hasMessageThat().contains("Car Audio Settings");
}
@Test
@@ -89,7 +166,8 @@
when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(1);
CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
- carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings);
+ carAudioDeviceInfos, mMockAudioControlWrapper,
+ mMockCarAudioSettings, getInputDevices());
SparseArray<CarAudioZone> zones = helper.loadAudioZones();
@@ -103,7 +181,8 @@
when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(1);
CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
- carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings);
+ carAudioDeviceInfos, mMockAudioControlWrapper,
+ mMockCarAudioSettings, getInputDevices());
SparseArray<CarAudioZone> zones = helper.loadAudioZones();
CarVolumeGroup[] volumeGroups = zones.get(0).getVolumeGroups();
@@ -111,6 +190,38 @@
}
@Test
+ public void loadAudioZones_primaryZoneHasInputDevice() throws Exception {
+ List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+ when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(1);
+
+ CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+ carAudioDeviceInfos, mMockAudioControlWrapper,
+ mMockCarAudioSettings, getInputDevices());
+
+ SparseArray<CarAudioZone> zones = helper.loadAudioZones();
+ CarAudioZone primaryZone = zones.get(PRIMARY_AUDIO_ZONE);
+ assertThat(primaryZone.getInputAudioDevices()).hasSize(1);
+ }
+
+ @Test
+ public void loadAudioZones_primaryZoneHasMicrophoneInputDevice() throws Exception {
+ List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
+
+ when(mMockAudioControlWrapper.getBusForContext(anyInt())).thenReturn(1);
+
+ CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
+ carAudioDeviceInfos, mMockAudioControlWrapper,
+ mMockCarAudioSettings, getInputDevices());
+
+ SparseArray<CarAudioZone> zones = helper.loadAudioZones();
+ CarAudioZone primaryZone = zones.get(PRIMARY_AUDIO_ZONE);
+ List<AudioDeviceAttributes> audioDeviceInfos =
+ primaryZone.getInputAudioDevices();
+ assertThat(audioDeviceInfos.get(0).getType()).isEqualTo(TYPE_BUILTIN_MIC);
+ }
+
+ @Test
public void loadAudioZones_associatesLegacyContextsWithCorrectBuses() throws Exception {
List<CarAudioDeviceInfo> carAudioDeviceInfos = getValidCarAudioDeviceInfos();
@@ -118,7 +229,8 @@
when(mMockAudioControlWrapper.getBusForContext(CarAudioContext.MUSIC)).thenReturn(1);
CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
- carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings);
+ carAudioDeviceInfos, mMockAudioControlWrapper,
+ mMockCarAudioSettings, getInputDevices());
SparseArray<CarAudioZone> zones = helper.loadAudioZones();
@@ -145,7 +257,8 @@
.thenReturn(1);
CarAudioZonesHelperLegacy helper = new CarAudioZonesHelperLegacy(mContext, mCarVolumeGroups,
- carAudioDeviceInfos, mMockAudioControlWrapper, mMockCarAudioSettings);
+ carAudioDeviceInfos, mMockAudioControlWrapper,
+ mMockCarAudioSettings, getInputDevices());
SparseArray<CarAudioZone> zones = helper.loadAudioZones();
@@ -166,6 +279,18 @@
return Lists.newArrayList(deviceInfo1, deviceInfo2);
}
+ private AudioDeviceInfo[] getInputDevices() {
+ AudioDeviceInfo deviceInfo1 = Mockito.mock(AudioDeviceInfo.class);
+ when(deviceInfo1.getType()).thenReturn(TYPE_BUILTIN_MIC);
+ when(deviceInfo1.getAddress()).thenReturn("mic");
+ when(deviceInfo1.isSink()).thenReturn(false);
+ AudioDeviceInfo deviceInfo2 = Mockito.mock(AudioDeviceInfo.class);
+ when(deviceInfo2.getAddress()).thenReturn("tuner");
+ when(deviceInfo2.getType()).thenReturn(TYPE_FM_TUNER);
+ when(deviceInfo2.isSink()).thenReturn(false);
+ return new AudioDeviceInfo[]{deviceInfo1, deviceInfo2};
+ }
+
private List<CarAudioDeviceInfo> getValidCarAudioDeviceInfos() {
CarAudioDeviceInfo deviceInfo1 = Mockito.mock(CarAudioDeviceInfo.class);
when(deviceInfo1.getAddress()).thenReturn("bus001_media");
diff --git a/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperTest.java b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperTest.java
index 74b00c7..14d3ba8 100644
--- a/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperTest.java
+++ b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesHelperTest.java
@@ -15,6 +15,11 @@
*/
package com.android.car.audio;
+import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+import static android.media.AudioDeviceInfo.TYPE_BUS;
+import static android.media.AudioDeviceInfo.TYPE_FM_TUNER;
+
import static com.android.car.audio.CarAudioService.DEFAULT_AUDIO_CONTEXT;
import static com.google.common.truth.Truth.assertThat;
@@ -23,7 +28,6 @@
import static org.mockito.Mockito.when;
import static org.testng.Assert.expectThrows;
-import android.car.media.CarAudioManager;
import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
@@ -104,14 +108,11 @@
private AudioDeviceInfo[] generateInputDeviceInfos() {
return new AudioDeviceInfo[]{
- generateInputAudioDeviceInfo(PRIMARY_ZONE_MICROPHONE_ADDRESS,
- AudioDeviceInfo.TYPE_BUILTIN_MIC),
- generateInputAudioDeviceInfo(PRIMARY_ZONE_FM_TUNER_ADDRESS,
- AudioDeviceInfo.TYPE_FM_TUNER),
- generateInputAudioDeviceInfo(SECONDARY_ZONE_BACK_MICROPHONE_ADDRESS,
- AudioDeviceInfo.TYPE_BUS),
+ generateInputAudioDeviceInfo(PRIMARY_ZONE_MICROPHONE_ADDRESS, TYPE_BUILTIN_MIC),
+ generateInputAudioDeviceInfo(PRIMARY_ZONE_FM_TUNER_ADDRESS, TYPE_FM_TUNER),
+ generateInputAudioDeviceInfo(SECONDARY_ZONE_BACK_MICROPHONE_ADDRESS, TYPE_BUS),
generateInputAudioDeviceInfo(SECONDARY_ZONE_BUS_1000_INPUT_ADDRESS,
- AudioDeviceInfo.TYPE_BUILTIN_MIC)
+ TYPE_BUILTIN_MIC)
};
}
@@ -169,7 +170,7 @@
List<Integer> zoneIds = getListOfZoneIds(zones);
assertThat(zones.size()).isEqualTo(2);
- assertThat(zones.contains(CarAudioManager.PRIMARY_AUDIO_ZONE)).isTrue();
+ assertThat(zones.contains(PRIMARY_AUDIO_ZONE)).isTrue();
assertThat(zones.contains(SECONDARY_ZONE_ID)).isTrue();
}
@@ -184,7 +185,7 @@
SparseIntArray audioZoneIdToOccupantZoneIdMapping =
cazh.getCarAudioZoneIdToOccupantZoneIdMapping();
- assertThat(audioZoneIdToOccupantZoneIdMapping.get(CarAudioManager.PRIMARY_AUDIO_ZONE))
+ assertThat(audioZoneIdToOccupantZoneIdMapping.get(PRIMARY_AUDIO_ZONE))
.isEqualTo(PRIMARY_OCCUPANT_ID);
assertThat(audioZoneIdToOccupantZoneIdMapping.get(SECONDARY_ZONE_ID, -1))
.isEqualTo(-1);
@@ -305,7 +306,7 @@
SparseArray<CarAudioZone> zones = cazh.loadAudioZones();
assertThat(zones.size()).isEqualTo(2);
- assertThat(zones.contains(CarAudioManager.PRIMARY_AUDIO_ZONE)).isTrue();
+ assertThat(zones.contains(PRIMARY_AUDIO_ZONE)).isTrue();
assertThat(zones.contains(SECONDARY_ZONE_ID)).isTrue();
}
}
@@ -339,6 +340,30 @@
}
@Test
+ public void loadAudioZones_primaryZoneHasInputDevices() throws Exception {
+ CarAudioZonesHelper cazh = new CarAudioZonesHelper(mCarAudioSettings, mInputStream,
+ mCarAudioOutputDeviceInfos, mInputAudioDeviceInfos, false);
+
+ SparseArray<CarAudioZone> zones = cazh.loadAudioZones();
+
+ CarAudioZone primaryZone = zones.get(PRIMARY_AUDIO_ZONE);
+ assertThat(primaryZone.getInputAudioDevices()).hasSize(2);
+ }
+
+ @Test
+ public void loadAudioZones_primaryZoneHasMicrophoneDevice() throws Exception {
+ CarAudioZonesHelper cazh = new CarAudioZonesHelper(mCarAudioSettings, mInputStream,
+ mCarAudioOutputDeviceInfos, mInputAudioDeviceInfos, false);
+
+ SparseArray<CarAudioZone> zones = cazh.loadAudioZones();
+
+ CarAudioZone primaryZone = zones.get(PRIMARY_AUDIO_ZONE);
+ for (AudioDeviceAttributes info : primaryZone.getInputAudioDevices()) {
+ assertThat(info.getType()).isEqualTo(TYPE_BUILTIN_MIC);
+ }
+ }
+
+ @Test
public void loadAudioZones_parsesInputDevices() throws Exception {
try (InputStream inputDevicesStream = mContext.getResources().openRawResource(
R.raw.car_audio_configuration_with_input_devices)) {
diff --git a/tests/carservice_test/src/com/android/car/audio/CarAudioZonesValidatorTest.java b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesValidatorTest.java
index 9c32893..552eedb 100644
--- a/tests/carservice_test/src/com/android/car/audio/CarAudioZonesValidatorTest.java
+++ b/tests/carservice_test/src/com/android/car/audio/CarAudioZonesValidatorTest.java
@@ -15,9 +15,18 @@
*/
package com.android.car.audio;
-import static org.mockito.Mockito.when;
+import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+import static android.media.AudioDeviceInfo.TYPE_BUS;
+import static android.media.AudioDeviceInfo.TYPE_FM_TUNER;
-import android.car.media.CarAudioManager;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.expectThrows;
+
+import android.media.AudioDeviceAttributes;
import android.util.SparseArray;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -28,6 +37,7 @@
import org.junit.runner.RunWith;
import org.mockito.Mockito;
+import java.util.ArrayList;
import java.util.List;
@RunWith(AndroidJUnit4.class)
@@ -37,10 +47,49 @@
@Test
public void validate_thereIsAtLeastOneZone() {
- thrown.expect(RuntimeException.class);
- thrown.expectMessage("At least one zone should be defined");
+ RuntimeException exception = expectThrows(RuntimeException.class,
+ () -> CarAudioZonesValidator.validate(new SparseArray<CarAudioZone>()));
- CarAudioZonesValidator.validate(new SparseArray<CarAudioZone>());
+ assertThat(exception).hasMessageThat().contains("At least one zone should be defined");
+
+ }
+
+ @Test
+ public void validate_failsOnEmptyInputDevices() {
+ CarAudioZone zone = new MockBuilder().withInputDevices(new ArrayList<>()).build();
+ SparseArray<CarAudioZone> zones = new SparseArray<>();
+ zones.put(zone.getId(), zone);
+
+ IllegalArgumentException exception = expectThrows(IllegalArgumentException.class,
+ () -> CarAudioZonesValidator.validate(zones));
+
+ assertThat(exception).hasMessageThat().contains("Primary Zone Input Devices");
+ }
+
+ @Test
+ public void validate_failsOnNullInputDevices() {
+ CarAudioZone zone = new MockBuilder().withInputDevices(null).build();
+ SparseArray<CarAudioZone> zones = new SparseArray<>();
+ zones.put(zone.getId(), zone);
+
+ NullPointerException exception = expectThrows(NullPointerException.class,
+ () -> CarAudioZonesValidator.validate(zones));
+
+ assertThat(exception).hasMessageThat().contains("Primary Zone Input Devices");
+ }
+
+ @Test
+ public void validate_failsOnMissingMicrophoneInputDevices() {
+ CarAudioZone zone = new MockBuilder().withInputDevices(
+ List.of(generateInputAudioDeviceAttributeInfo("tuner", TYPE_FM_TUNER)))
+ .build();
+ SparseArray<CarAudioZone> zones = new SparseArray<>();
+ zones.put(zone.getId(), zone);
+
+ RuntimeException exception = expectThrows(RuntimeException.class,
+ () -> CarAudioZonesValidator.validate(zones));
+
+ assertThat(exception).hasMessageThat().contains("Primary Zone must have");
}
@Test
@@ -52,10 +101,11 @@
.build();
zones.put(zoneOne.getId(), zoneOne);
- thrown.expect(RuntimeException.class);
- thrown.expectMessage("Invalid volume groups configuration for zone " + 1);
+ RuntimeException exception = expectThrows(RuntimeException.class,
+ () -> CarAudioZonesValidator.validate(zones));
- CarAudioZonesValidator.validate(zones);
+ assertThat(exception).hasMessageThat()
+ .contains("Invalid volume groups configuration for zone " + 1);
}
@Test
@@ -77,12 +127,11 @@
zones.put(primaryZone.getId(), primaryZone);
zones.put(secondaryZone.getId(), secondaryZone);
+ RuntimeException exception = expectThrows(RuntimeException.class,
+ () -> CarAudioZonesValidator.validate(zones));
- thrown.expect(RuntimeException.class);
- thrown.expectMessage(
+ assertThat(exception).hasMessageThat().contains(
"Device with address three appears in multiple volume groups or audio zones");
-
- CarAudioZonesValidator.validate(zones);
}
@Test
@@ -93,7 +142,7 @@
}
private SparseArray<CarAudioZone> generateAudioZonesWithPrimary() {
- CarAudioZone zone = new MockBuilder().build();
+ CarAudioZone zone = new MockBuilder().withInputDevices(getValidInputDevices()).build();
SparseArray<CarAudioZone> zones = new SparseArray<>();
zones.put(zone.getId(), zone);
return zones;
@@ -105,21 +154,23 @@
return mockVolumeGroup;
}
- private CarAudioZone getMockPrimaryZone() {
- CarAudioZone zoneMock = Mockito.mock(CarAudioZone.class);
- when(zoneMock.getId()).thenReturn(CarAudioManager.PRIMARY_AUDIO_ZONE);
- return zoneMock;
+ private List<AudioDeviceAttributes> getValidInputDevices() {
+ return List.of(generateInputAudioDeviceAttributeInfo("mic", TYPE_BUILTIN_MIC),
+ generateInputAudioDeviceAttributeInfo("tuner", TYPE_FM_TUNER),
+ generateInputAudioDeviceAttributeInfo("bus", TYPE_BUS));
}
private static class MockBuilder {
private boolean mHasValidVolumeGroups = true;
- private int mZoneId = 0;
+ private int mZoneId = PRIMARY_AUDIO_ZONE;
private CarVolumeGroup[] mVolumeGroups = new CarVolumeGroup[0];
+ private List<AudioDeviceAttributes> mInputDevices = new ArrayList<>();
CarAudioZone build() {
CarAudioZone zoneMock = Mockito.mock(CarAudioZone.class);
when(zoneMock.getId()).thenReturn(mZoneId);
when(zoneMock.validateVolumeGroups()).thenReturn(mHasValidVolumeGroups);
when(zoneMock.getVolumeGroups()).thenReturn(mVolumeGroups);
+ when(zoneMock.getInputAudioDevices()).thenReturn(mInputDevices);
return zoneMock;
}
@@ -137,5 +188,17 @@
mVolumeGroups = volumeGroups;
return this;
}
+
+ MockBuilder withInputDevices(List<AudioDeviceAttributes> inputDevices) {
+ mInputDevices = inputDevices;
+ return this;
+ }
+ }
+
+ private AudioDeviceAttributes generateInputAudioDeviceAttributeInfo(String address, int type) {
+ AudioDeviceAttributes inputMock = mock(AudioDeviceAttributes.class);
+ when(inputMock.getAddress()).thenReturn(address);
+ when(inputMock.getType()).thenReturn(type);
+ return inputMock;
}
}
\ No newline at end of file
diff --git a/tests/carservice_test/src/com/android/car/cluster/ClusterHomeManagerTest.java b/tests/carservice_test/src/com/android/car/cluster/ClusterHomeManagerTest.java
index b56c841..40ed29b 100644
--- a/tests/carservice_test/src/com/android/car/cluster/ClusterHomeManagerTest.java
+++ b/tests/carservice_test/src/com/android/car/cluster/ClusterHomeManagerTest.java
@@ -86,9 +86,12 @@
private ClusterHomeManager mClusterHomeManager;
private final ClusterPropertyHandler mPropertyHandler = new ClusterPropertyHandler();
private final CountDownLatch mPropertySetReady = new CountDownLatch(1);
- private final CountDownLatch mCallbackReceived = new CountDownLatch(1);
+ private final CountDownLatch mClusterStateListenerCalled = new CountDownLatch(1);
+ private final CountDownLatch mClusterNavigationStateListenerCalled = new CountDownLatch(1);
- private ClusterHomeCallbackImpl mClusterHomeCallback = new ClusterHomeCallbackImpl();
+ private ClusterStateListenerImpl mClusterStateListener = new ClusterStateListenerImpl();
+ private ClusterNavigationStateListenerImpl mClusterNavigationStateListener =
+ new ClusterNavigationStateListenerImpl();
private ClusterState mState;
private int mChanges = 0;
private byte[] mNavigationState;
@@ -155,8 +158,11 @@
super.setUp();
mClusterHomeManager = (ClusterHomeManager) getCar().getCarManager(Car.CLUSTER_HOME_SERVICE);
if (!isNoHalPropertyTest() && mClusterHomeManager != null) {
- mClusterHomeManager.registerClusterHomeCallback(
- getContext().getMainExecutor(), mClusterHomeCallback);
+ mClusterHomeManager.registerClusterStateListener(
+ getContext().getMainExecutor(), mClusterStateListener);
+
+ mClusterHomeManager.registerClusterNavigationStateListener(
+ getContext().getMainExecutor(), mClusterNavigationStateListener);
}
}
@@ -173,7 +179,9 @@
@Override
public void tearDown() throws Exception {
if (!isNoHalPropertyTest() && mClusterHomeManager != null) {
- mClusterHomeManager.unregisterClusterHomeCallback(mClusterHomeCallback);
+ mClusterHomeManager.unregisterClusterStateListener(mClusterStateListener);
+ mClusterHomeManager
+ .unregisterClusterNavigationStateListener(mClusterNavigationStateListener);
}
super.tearDown();
}
@@ -181,7 +189,7 @@
@Test
public void testClusterSwitchUi() throws InterruptedException {
getMockedVehicleHal().injectEvent(createSwitchUiEvent(UI_TYPE_2));
- mCallbackReceived.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ mClusterStateListenerCalled.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertThat(mState).isNotNull();
assertThat(mState.uiType).isEqualTo(UI_TYPE_2);
@@ -193,7 +201,7 @@
getMockedVehicleHal().injectEvent(createDisplayStateEvent(
DISPLAY_ON, BOUNDS_LEFT, BOUNDS_TOP, BOUNDS_RIGHT, BOUNDS_BOTTOM,
INSET_LEFT, INSET_TOP, INSET_RIGHT, INSET_BOTTOM));
- mCallbackReceived.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ mClusterStateListenerCalled.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertThat(mState).isNotNull();
assertThat(mState.on).isEqualTo(true);
@@ -246,10 +254,16 @@
assertThrows(IllegalStateException.class,
() -> mClusterHomeManager.requestDisplay(UI_TYPE_1));
assertThrows(IllegalStateException.class,
- () -> mClusterHomeManager.registerClusterHomeCallback(
- getContext().getMainExecutor(), mClusterHomeCallback));
+ () -> mClusterHomeManager.registerClusterStateListener(
+ getContext().getMainExecutor(), mClusterStateListener));
assertThrows(IllegalStateException.class,
- () -> mClusterHomeManager.unregisterClusterHomeCallback(mClusterHomeCallback));
+ () -> mClusterHomeManager.unregisterClusterStateListener(mClusterStateListener));
+ assertThrows(IllegalStateException.class,
+ () -> mClusterHomeManager.registerClusterNavigationStateListener(
+ getContext().getMainExecutor(), mClusterNavigationStateListener));
+ assertThrows(IllegalStateException.class,
+ () -> mClusterHomeManager
+ .unregisterClusterNavigationStateListener(mClusterNavigationStateListener));
}
@Test
@@ -319,15 +333,19 @@
}
}
- private class ClusterHomeCallbackImpl implements ClusterHomeManager.ClusterHomeCallback {
+ private class ClusterStateListenerImpl implements ClusterHomeManager.ClusterStateListener {
public void onClusterStateChanged(ClusterState state, int changes) {
mState = state;
mChanges = changes;
- mCallbackReceived.countDown();
+ mClusterStateListenerCalled.countDown();
}
+ }
+ private class ClusterNavigationStateListenerImpl implements
+ ClusterHomeManager.ClusterNavigationStateListener {
+ @Override
public void onNavigationState(byte[] navigationState) {
mNavigationState = navigationState;
- mCallbackReceived.countDown();
+ mClusterNavigationStateListenerCalled.countDown();
}
}
diff --git a/tests/carservice_test/src/com/android/car/garagemode/ControllerTest.java b/tests/carservice_test/src/com/android/car/garagemode/ControllerTest.java
index ca3598a..64c68fc 100644
--- a/tests/carservice_test/src/com/android/car/garagemode/ControllerTest.java
+++ b/tests/carservice_test/src/com/android/car/garagemode/ControllerTest.java
@@ -24,16 +24,21 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.car.Car;
import android.car.hardware.power.CarPowerManager;
import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Resources;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
@@ -42,6 +47,8 @@
import androidx.test.filters.SmallTest;
import com.android.car.CarLocalServices;
+import com.android.car.R;
+import com.android.car.power.CarPowerManagementService;
import com.android.car.systeminterface.SystemInterface;
import com.android.car.user.CarUserService;
@@ -74,8 +81,10 @@
@Mock private CarPowerManager mCarPowerManagerMock;
@Mock private CarUserService mCarUserServiceMock;
@Mock private SystemInterface mSystemInterfaceMock;
+ @Mock private CarPowerManagementService mCarPowerManagementServiceMock;
private CarUserService mCarUserServiceOriginal;
private SystemInterface mSystemInterfaceOriginal;
+ private CarPowerManagementService mCarPowerManagementServiceOriginal;
@Captor private ArgumentCaptor<Intent> mIntentCaptor;
@Captor private ArgumentCaptor<Integer> mIntegerCaptor;
@@ -106,10 +115,15 @@
mController.setCarPowerManager(mCarPowerManagerMock);
mFuture = new CompletableFuture<>();
mCarUserServiceOriginal = CarLocalServices.getService(CarUserService.class);
+ mCarPowerManagementServiceOriginal = CarLocalServices.getService(
+ CarPowerManagementService.class);
CarLocalServices.removeServiceForTest(CarUserService.class);
CarLocalServices.addService(CarUserService.class, mCarUserServiceMock);
CarLocalServices.removeServiceForTest(SystemInterface.class);
CarLocalServices.addService(SystemInterface.class, mSystemInterfaceMock);
+ CarLocalServices.removeServiceForTest(CarPowerManagementService.class);
+ CarLocalServices.addService(CarPowerManagementService.class,
+ mCarPowerManagementServiceMock);
doReturn(new ArrayList<Integer>()).when(mCarUserServiceMock)
.startAllBackgroundUsersInGarageMode();
doNothing().when(mSystemInterfaceMock)
@@ -122,6 +136,9 @@
CarLocalServices.addService(CarUserService.class, mCarUserServiceOriginal);
CarLocalServices.removeServiceForTest(SystemInterface.class);
CarLocalServices.addService(SystemInterface.class, mSystemInterfaceOriginal);
+ CarLocalServices.removeServiceForTest(CarPowerManagementService.class);
+ CarLocalServices.addService(CarPowerManagementService.class,
+ mCarPowerManagementServiceOriginal);
}
@Test
@@ -219,4 +236,83 @@
// Verify that worker that polls running jobs from JobScheduler is scheduled.
verify(mHandlerMock).postDelayed(any(), eq(JOB_SNAPSHOT_INITIAL_UPDATE_MS));
}
+
+ @Test
+ public void testInitAndRelease() {
+
+ GarageMode garageMode = mock(GarageMode.class);
+ Controller controller = new Controller(mContextMock, mLooperMock, mWakeupPolicy,
+ mHandlerMock, garageMode);
+
+ controller.init();
+ controller.release();
+
+ verify(garageMode).init();
+ verify(garageMode).release();
+ }
+
+ @Test
+ public void testConstructor() {
+ Resources resourcesMock = mock(Resources.class);
+ when(mContextMock.getResources()).thenReturn(resourcesMock);
+ when(resourcesMock.getStringArray(R.array.config_garageModeCadence))
+ .thenReturn(sTemplateWakeupSchedule);
+ Controller controller = new Controller(mContextMock, mLooperMock);
+
+ assertThat(controller).isNotNull();
+ }
+
+ @Test
+ public void testScheduleNextWakeup() {
+ GarageMode garageMode = mock(GarageMode.class);
+
+ // Enter GarageMode only 1 time, no wake up after that
+ WakeupPolicy wakeUpPolicy = new WakeupPolicy(new String[] { "15m,1" });
+
+ Controller controller = new Controller(mContextMock, mLooperMock, wakeUpPolicy,
+ mHandlerMock, garageMode);
+ controller.setCarPowerManager(mCarPowerManagerMock);
+
+ // Imitate entering and leavimg GarageMode
+ controller.initiateGarageMode(/* future= */ null);
+
+ controller.scheduleNextWakeup();
+
+ verify(mCarPowerManagerMock).scheduleNextWakeupTime(900);
+
+ // Imitate entering Garage mode after sleep
+ controller.initiateGarageMode(/* future= */ null);
+
+ // Should be no more calls to scheduleNextWakeupTime
+ controller.scheduleNextWakeup();
+
+ Mockito.verifyNoMoreInteractions(mCarPowerManagerMock);
+ }
+
+ @Test
+ public void testOnStateChanged() {
+ GarageMode garageMode = mock(GarageMode.class);
+
+ Controller controller = Mockito.spy(new Controller(mContextMock, mLooperMock, mWakeupPolicy,
+ mHandlerMock, garageMode));
+
+ controller.onStateChanged(CarPowerStateListener.SHUTDOWN_CANCELLED, null);
+ verify(controller).resetGarageMode();
+
+ clearInvocations(controller);
+ controller.onStateChanged(CarPowerStateListener.SHUTDOWN_ENTER, null);
+ verify(controller).resetGarageMode();
+
+ clearInvocations(controller);
+ controller.onStateChanged(CarPowerStateListener.SUSPEND_ENTER, null);
+ verify(controller).resetGarageMode();
+
+ clearInvocations(controller);
+ controller.onStateChanged(CarPowerStateListener.SUSPEND_EXIT, null);
+ verify(controller).resetGarageMode();
+
+ clearInvocations(controller);
+ controller.onStateChanged(CarPowerStateListener.INVALID , null);
+ verify(controller, never()).resetGarageMode();
+ }
}
diff --git a/tests/carservice_test/src/com/android/car/garagemode/GarageModeTest.java b/tests/carservice_test/src/com/android/car/garagemode/GarageModeTest.java
index 6133baf..6183549 100644
--- a/tests/carservice_test/src/com/android/car/garagemode/GarageModeTest.java
+++ b/tests/carservice_test/src/com/android/car/garagemode/GarageModeTest.java
@@ -22,6 +22,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -36,6 +37,7 @@
import androidx.test.filters.SmallTest;
import com.android.car.CarLocalServices;
+import com.android.car.power.CarPowerManagementService;
import com.android.car.user.CarUserService;
import org.junit.After;
@@ -50,6 +52,7 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -131,6 +134,42 @@
105);
}
+ @Test
+ public void garageModeTestExitImmediately() throws Exception {
+ CarPowerManagementService mockCarPowerManagementService =
+ mock(CarPowerManagementService.class);
+
+ // Mock CPMS to force Garage Mode early exit
+ CarLocalServices.removeServiceForTest(CarPowerManagementService.class);
+ CarLocalServices.addService(CarPowerManagementService.class, mockCarPowerManagementService);
+ when(mockCarPowerManagementService.garageModeShouldExitImmediately()).thenReturn(true);
+
+ // Check exit immediately without future
+ GarageMode garageMode = new GarageMode(mController);
+ garageMode.init();
+ garageMode.enterGarageMode(/* future= */ null);
+ assertThat(garageMode.isGarageModeActive()).isFalse();
+
+ // Create new instance of GarageMode
+ garageMode = new GarageMode(mController);
+ garageMode.init();
+ // Check exit immediately with future
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ garageMode.enterGarageMode(future);
+ assertThat(garageMode.isGarageModeActive()).isFalse();
+ assertThat(future.isDone()).isTrue();
+
+ // Create new instance of GarageMode
+ garageMode = new GarageMode(mController);
+ garageMode.init();
+ // Check exit immediately with completed future
+ garageMode.enterGarageMode(future);
+ assertThat(garageMode.isGarageModeActive()).isFalse();
+ assertThat(future.isDone()).isTrue();
+
+ CarLocalServices.removeServiceForTest(CarPowerManagementService.class);
+ }
+
private void waitForHandlerThreadToFinish(CountDownLatch latch) throws Exception {
assertWithMessage("Latch has timed out.")
.that(latch.await(DEFAULT_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
diff --git a/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java b/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java
index 5cb55c3..af14bbf 100644
--- a/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java
+++ b/tests/carservice_test/src/com/android/car/input/CarInputManagerTest.java
@@ -20,6 +20,7 @@
import static android.hardware.automotive.vehicle.V2_0.CustomInputType.CUSTOM_EVENT_F1;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.spy;
@@ -74,10 +75,16 @@
private final class CaptureCallback implements CarInputManager.CarInputCaptureCallback {
- private static final long EVENT_WAIT_TIME = 500;
+ private static final long EVENT_WAIT_TIME = 5_000;
private final Object mLock = new Object();
+ private final String mName;
+
+ private CaptureCallback(String name) {
+ mName = name;
+ }
+
// Stores passed events. Last one in front
@GuardedBy("mLock")
private final LinkedList<Pair<Integer, List<KeyEvent>>> mKeyEvents = new LinkedList<>();
@@ -149,19 +156,24 @@
}
private void waitForStateChange() throws Exception {
- mStateChangeWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS);
+ assertWithMessage("Failed to acquire semaphore in %s ms", EVENT_WAIT_TIME).that(
+ mStateChangeWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS)).isTrue();
}
private void waitForKeyEvent() throws Exception {
- mKeyEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS);
+ assertWithMessage("Failed to acquire semaphore in %s ms", EVENT_WAIT_TIME).that(
+ mKeyEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS)).isTrue();
}
private void waitForRotaryEvent() throws Exception {
- mRotaryEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS);
+ assertWithMessage("Failed to acquire semaphore in %s ms", EVENT_WAIT_TIME).that(
+ mRotaryEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS)).isTrue();
}
private void waitForCustomInputEvent() throws Exception {
- mCustomInputEventWait.tryAcquire(EVENT_WAIT_TIME, TimeUnit.MILLISECONDS);
+ assertWithMessage("Failed to acquire semaphore in %s ms", EVENT_WAIT_TIME).that(
+ mCustomInputEventWait.tryAcquire(
+ EVENT_WAIT_TIME, TimeUnit.MILLISECONDS)).isTrue();
}
private LinkedList<Pair<Integer, List<KeyEvent>>> getkeyEvents() {
@@ -196,11 +208,16 @@
return r;
}
}
+
+ @Override
+ public String toString() {
+ return "CaptureCallback{mName='" + mName + "'}";
+ }
}
- private final CaptureCallback mCallback0 = new CaptureCallback();
- private final CaptureCallback mCallback1 = new CaptureCallback();
- private final CaptureCallback mCallback2 = new CaptureCallback();
+ private final CaptureCallback mCallback0 = new CaptureCallback("callback0");
+ private final CaptureCallback mCallback1 = new CaptureCallback("callback1");
+ private final CaptureCallback mCallback2 = new CaptureCallback("callback2");
@Override
protected synchronized void configureMockedHal() {
@@ -493,7 +510,7 @@
CarInputManager.INPUT_TYPE_NAVIGATE_KEYS}, 0, mCallback1);
assertThat(r).isEqualTo(CarInputManager.INPUT_CAPTURE_RESPONSE_SUCCEEDED);
- mCallback1.waitForStateChange();
+ mCallback0.waitForStateChange();
assertLastStateChange(CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
new int[]{CarInputManager.INPUT_TYPE_DPAD_KEYS},
mCallback0);
@@ -539,7 +556,6 @@
CarInputManager carInputManager1 = createAnotherCarInputManager();
Log.i(TAG, "requestInputEventCapture callback 0");
-
int r = carInputManager0.requestInputEventCapture(
CarOccupantZoneManager.DISPLAY_TYPE_MAIN,
new int[]{
@@ -675,7 +691,7 @@
injectKeyEvent(true, KeyEvent.KEYCODE_NAVIGATE_NEXT);
// Assert: ensure KeyEvent was delivered
- mCallback1.waitForKeyEvent();
+ mCallback0.waitForKeyEvent();
assertLastKeyEvent(CarOccupantZoneManager.DISPLAY_TYPE_MAIN, true,
KeyEvent.KEYCODE_NAVIGATE_NEXT, mCallback0);
@@ -714,7 +730,7 @@
* Events dispatched to main, so this should guarantee that all event dispatched are completed.
*/
private void waitForDispatchToMain() {
- // Needs to twice as it is dispatched to main inside car service once and it is
+ // Needs to be invoked twice as it is dispatched to main inside car service once and it is
// dispatched to main inside CarInputManager once.
CarServiceUtils.runOnMainSync(() -> {});
CarServiceUtils.runOnMainSync(() -> {});
diff --git a/tests/carservice_test/src/com/android/car/pm/ActivityBlockingActivityTest.java b/tests/carservice_test/src/com/android/car/pm/ActivityBlockingActivityTest.java
index 69632f0..0978d74 100644
--- a/tests/carservice_test/src/com/android/car/pm/ActivityBlockingActivityTest.java
+++ b/tests/carservice_test/src/com/android/car/pm/ActivityBlockingActivityTest.java
@@ -16,13 +16,20 @@
package com.android.car.pm;
+import static androidx.car.app.activity.CarAppActivity.ACTION_SHOW_DIALOG;
+import static androidx.car.app.activity.CarAppActivity.ACTION_START_SECOND_INSTANCE;
+import static androidx.car.app.activity.CarAppActivity.SECOND_INSTANCE_TITLE;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertNotNull;
import android.app.Activity;
import android.app.ActivityOptions;
+import android.app.AlertDialog;
+import android.app.UiAutomation;
import android.car.Car;
+import android.car.content.pm.CarPackageManager;
import android.car.drivingstate.CarDrivingStateEvent;
import android.car.drivingstate.CarDrivingStateManager;
import android.content.ComponentName;
@@ -30,10 +37,12 @@
import android.content.Intent;
import android.os.Bundle;
import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Configurator;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.Until;
import android.view.Display;
+import androidx.car.app.activity.CarAppActivity;
import androidx.test.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
@@ -59,6 +68,7 @@
private static final long ACTIVITY_TIMEOUT_MS = 5000;
private CarDrivingStateManager mCarDrivingStateManager;
+ private CarPackageManager mCarPackageManager;
private UiDevice mDevice;
@@ -70,8 +80,12 @@
Car car = Car.createCar(getContext());
mCarDrivingStateManager = (CarDrivingStateManager)
car.getCarManager(Car.CAR_DRIVING_STATE_SERVICE);
+ mCarPackageManager = (CarPackageManager)
+ car.getCarManager(Car.PACKAGE_SERVICE);
assertNotNull(mCarDrivingStateManager);
+ Configurator.getInstance()
+ .setUiAutomationFlags(UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
setDrivingStateMoving();
@@ -98,6 +112,66 @@
}
@Test
+ public void testBlockingActivity_doActivity_showingDialog_isNotBlocked() throws Exception {
+ Intent intent = new Intent();
+ intent.putExtra(DoActivity.INTENT_EXTRA_SHOW_DIALOG, true);
+ intent.setComponent(toComponentName(getTestContext(), DoActivity.class));
+ startActivity(intent);
+
+ assertThat(mDevice.wait(Until.findObject(By.text(
+ DoActivity.DIALOG_TITLE)),
+ UI_TIMEOUT_MS)).isNotNull();
+ assertBlockingActivityNotFound();
+ }
+
+ @Test
+ public void testBlockingActivity_doTemplateActivity_isNotBlocked() throws Exception {
+ startActivity(toComponentName(getTestContext(), CarAppActivity.class));
+
+ assertThat(mDevice.wait(Until.findObject(By.text(
+ CarAppActivity.class.getSimpleName())),
+ UI_TIMEOUT_MS)).isNotNull();
+ assertBlockingActivityNotFound();
+ }
+
+ @Test
+ public void testBlockingActivity_multipleDoTemplateActivity_notBlocked() throws Exception {
+ startActivity(toComponentName(getTestContext(), CarAppActivity.class));
+ assertThat(mDevice.wait(Until.findObject(By.text(
+ CarAppActivity.class.getSimpleName())),
+ UI_TIMEOUT_MS)).isNotNull();
+ getContext().sendBroadcast(new Intent().setAction(ACTION_START_SECOND_INSTANCE));
+ assertThat(mDevice.wait(Until.findObject(By.text(
+ SECOND_INSTANCE_TITLE)),
+ UI_TIMEOUT_MS)).isNotNull();
+ assertBlockingActivityNotFound();
+ }
+
+ @Test
+ public void testBlockingActivity_doTemplateActivity_showingDialog_isBlocked() throws Exception {
+ startActivity(toComponentName(getTestContext(), CarAppActivity.class));
+ assertThat(mDevice.wait(Until.findObject(By.text(
+ CarAppActivity.class.getSimpleName())),
+ UI_TIMEOUT_MS)).isNotNull();
+ assertBlockingActivityNotFound();
+ assertThat(mCarPackageManager.isActivityDistractionOptimized(
+ getTestContext().getPackageName(),
+ CarAppActivity.class.getName()
+ )).isTrue();
+
+ getContext().sendBroadcast(new Intent().setAction(ACTION_SHOW_DIALOG));
+ assertThat(mDevice.wait(Until.findObject(By.text(DoActivity.DIALOG_TITLE)),
+ UI_TIMEOUT_MS)).isNotNull();
+
+ assertThat(mDevice.wait(Until.findObject(By.res(ACTIVITY_BLOCKING_ACTIVITY_TEXTVIEW_ID)),
+ UI_TIMEOUT_MS)).isNotNull();
+ assertThat(mCarPackageManager.isActivityDistractionOptimized(
+ getTestContext().getPackageName(),
+ CarAppActivity.class.getName()
+ )).isFalse();
+ }
+
+ @Test
public void testBlockingActivity_nonDoActivity_isBlocked() throws Exception {
startNonDoActivity(NonDoActivity.EXTRA_DO_NOTHING);
@@ -153,11 +227,13 @@
private void startActivity(ComponentName name) {
Intent intent = new Intent();
intent.setComponent(name);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+ private void startActivity(Intent intent) {
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(Display.DEFAULT_DISPLAY);
-
getContext().startActivity(intent, options.toBundle());
}
@@ -240,6 +316,20 @@
}
public static class DoActivity extends TempActivity {
+ public static final String INTENT_EXTRA_SHOW_DIALOG = "SHOW_DIALOG";
+ public static final String DIALOG_TITLE = "Title";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (getIntent().getBooleanExtra(INTENT_EXTRA_SHOW_DIALOG, false)) {
+ AlertDialog dialog = new AlertDialog.Builder(DoActivity.this)
+ .setTitle(DIALOG_TITLE)
+ .setMessage("Message")
+ .create();
+ dialog.show();
+ }
+ }
}
/** Activity that closes itself after some timeout to clean up the screen. */
diff --git a/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java b/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java
index aaca795..9a24a7e 100644
--- a/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java
+++ b/tests/carservice_test/src/com/android/car/watchdog/CarWatchdogServiceTest.java
@@ -77,8 +77,8 @@
private final Executor mExecutor =
InstrumentationRegistry.getInstrumentation().getTargetContext().getMainExecutor();
private final UserInfo[] mUserInfos = new UserInfo[] {
- new UserInfoBuilder(10).setName("user 1").build(),
- new UserInfoBuilder(11).setName("user 2").build()
+ new UserInfoBuilder(100).setName("user 1").build(),
+ new UserInfoBuilder(101).setName("user 2").build()
};
@Mock private Context mMockContext;
@@ -87,21 +87,22 @@
@Mock private IBinder mDaemonBinder;
@Mock private IBinder mServiceBinder;
@Mock private ICarWatchdog mCarWatchdogDaemon;
+ @Mock private WatchdogStorage mMockWatchdogStorage;
private CarWatchdogService mCarWatchdogService;
private ICarWatchdogServiceForSystem mWatchdogServiceForSystemImpl;
@Before
public void setUpMocks() throws Exception {
- mCarWatchdogService = new CarWatchdogService(mMockContext);
+ mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage);
mockQueryService(CAR_WATCHDOG_DAEMON_INTERFACE, mDaemonBinder, mCarWatchdogDaemon);
when(mCar.getEventHandler()).thenReturn(mMainHandler);
when(mServiceBinder.queryLocalInterface(anyString())).thenReturn(mCarWatchdogService);
when(mMockContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
mockUmGetAllUsers(mUserManager, mUserInfos);
- mockUmIsUserRunning(mUserManager, 10, true);
- mockUmIsUserRunning(mUserManager, 11, false);
+ mockUmIsUserRunning(mUserManager, 100, true);
+ mockUmIsUserRunning(mUserManager, 101, false);
mCarWatchdogService.init();
mWatchdogServiceForSystemImpl = registerCarWatchdogService();
@@ -223,11 +224,11 @@
}
private void expectRunningUser() {
- doReturn(10).when(() -> UserHandle.getUserId(Binder.getCallingUid()));
+ doReturn(100).when(() -> UserHandle.getUserId(Binder.getCallingUid()));
}
private void expectStoppedUser() {
- doReturn(11).when(() -> UserHandle.getUserId(Binder.getCallingUid()));
+ doReturn(101).when(() -> UserHandle.getUserId(Binder.getCallingUid()));
}
private final class TestClient {
diff --git a/tests/carservice_unit_test/Android.bp b/tests/carservice_unit_test/Android.bp
index 1f22098..57e14d9 100644
--- a/tests/carservice_unit_test/Android.bp
+++ b/tests/carservice_unit_test/Android.bp
@@ -69,8 +69,6 @@
// mockito-target-inline dependency
jni_libs: [
"libdexmakerjvmtiagent",
- "libscriptexecutorjni",
- "libscriptexecutorjniutils-test",
"libstaticjvmtiagent",
],
}
diff --git a/tests/carservice_unit_test/src/android/car/test/mocks/AndroidMockitoHelperTest.java b/tests/carservice_unit_test/src/android/car/test/mocks/AndroidMockitoHelperTest.java
index e1099da..b495220 100644
--- a/tests/carservice_unit_test/src/android/car/test/mocks/AndroidMockitoHelperTest.java
+++ b/tests/carservice_unit_test/src/android/car/test/mocks/AndroidMockitoHelperTest.java
@@ -24,6 +24,7 @@
import static android.car.test.mocks.AndroidMockitoHelper.mockUmCreateUser;
import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetAliveUsers;
import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetSystemUser;
+import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetUserHandles;
import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetUserInfo;
import static android.car.test.mocks.AndroidMockitoHelper.mockUmIsHeadlessSystemUserMode;
import static android.car.test.mocks.AndroidMockitoHelper.mockUmIsUserRunning;
@@ -160,6 +161,16 @@
}
@Test
+ public void testMockUmGetUserHandles() {
+ UserHandle user1 = UserHandle.of(100);
+ UserHandle user2 = UserHandle.of(200);
+
+ mockUmGetUserHandles(mMockedUserManager, true, 100, 200);
+
+ assertThat(mMockedUserManager.getUserHandles(true)).containsExactly(user1, user2).inOrder();
+ }
+
+ @Test
public void testMockBinderGetCallingUserHandle() {
mockBinderGetCallingUserHandle(TEST_USER_ID);
diff --git a/tests/carservice_unit_test/src/android/car/watchdoglib/CarWatchdogDaemonHelperTest.java b/tests/carservice_unit_test/src/android/car/watchdoglib/CarWatchdogDaemonHelperTest.java
index c67d3d8..80cab06 100644
--- a/tests/carservice_unit_test/src/android/car/watchdoglib/CarWatchdogDaemonHelperTest.java
+++ b/tests/carservice_unit_test/src/android/car/watchdoglib/CarWatchdogDaemonHelperTest.java
@@ -38,6 +38,7 @@
import android.automotive.watchdog.internal.PowerCycle;
import android.automotive.watchdog.internal.ResourceOveruseConfiguration;
import android.automotive.watchdog.internal.StateType;
+import android.automotive.watchdog.internal.UserPackageIoUsageStats;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
@@ -193,6 +194,13 @@
verify(mFakeCarWatchdog).actionTakenOnResourceOveruse(eq(actions));
}
+ @Test
+ public void testIndirectCall_controlProcessHealthCheck() throws Exception {
+ mCarWatchdogDaemonHelper.controlProcessHealthCheck(true);
+
+ verify(mFakeCarWatchdog).controlProcessHealthCheck(eq(true));
+ }
+
/*
* Test that the {@link CarWatchdogDaemonHelper} throws {@code IllegalArgumentException} when
* trying to register already-registered service again.
@@ -266,5 +274,10 @@
@Override
public void resetResourceOveruseStats(List<String> packageNames) {}
+
+ @Override
+ public List<UserPackageIoUsageStats> getTodayIoUsageStats() {
+ return new ArrayList<>();
+ }
}
}
diff --git a/tests/carservice_unit_test/src/com/android/car/AbstractICarServiceHelperStub.java b/tests/carservice_unit_test/src/com/android/car/AbstractICarServiceHelperStub.java
index 7304e23..5d387cc 100644
--- a/tests/carservice_unit_test/src/com/android/car/AbstractICarServiceHelperStub.java
+++ b/tests/carservice_unit_test/src/com/android/car/AbstractICarServiceHelperStub.java
@@ -16,6 +16,7 @@
package com.android.car;
import android.annotation.UserIdInt;
+import android.car.app.CarActivityManager;
import android.content.ComponentName;
import android.content.pm.UserInfo;
import android.os.RemoteException;
@@ -73,4 +74,11 @@
+ ", flags=" + flags + ")");
return null;
}
+
+ @Override
+ public int setPersistentActivity(ComponentName activity, int displayId, int featureId) {
+ Log.d(TAG, "setPersistentActivity(activity=" + activity.toShortString()
+ + ", displayId=" + displayId + ", featureId=" + featureId + ")");
+ return CarActivityManager.RESULT_SUCCESS;
+ }
}
diff --git a/tests/carservice_unit_test/src/com/android/car/BluetoothFastPairTest.java b/tests/carservice_unit_test/src/com/android/car/BluetoothFastPairTest.java
index d7a8f3b..ebc9c2e 100644
--- a/tests/carservice_unit_test/src/com/android/car/BluetoothFastPairTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/BluetoothFastPairTest.java
@@ -24,7 +24,9 @@
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothAdapter;
@@ -120,6 +122,7 @@
static final int TEST_PIN_NUMBER = 66051;
static final int TEST_MODEL_ID = 4386;
static final byte[] TEST_MODEL_ID_BYTES = {0x00, 0x11, 0x22};
+ static final int ASYNC_CALL_TIMEOUT_MILLIS = 200;
byte[] mAdvertisementExpectedResults = new byte[]{0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00,
0x11, 0x00};
@Mock
@@ -339,12 +342,27 @@
assertThat(mTestGattServer.validatePairingRequest(encryptedRequest,
testKey.getKeySpec())).isTrue();
//send Wrong Pairing Key
- sendPairingKey(-1);
+ sendPairingKey(-2);
mTestGattServer.processPairingKey(encryptedPairingKey);
verify(mMockBluetoothDevice).setPairingConfirmation(false);
}
@Test
+ public void testNoPairingKey() {
+ FastPairUtils.AccountKey testKey = new FastPairUtils.AccountKey(TEST_SHARED_SECRET);
+ mTestGattServer.setSharedSecretKey(testKey.toBytes());
+ byte[] encryptedRequest = mTestGattServer.encrypt(TEST_PAIRING_REQUEST);
+
+ byte[] encryptedPairingKey = mTestGattServer.encrypt(TEST_PAIRING_KEY);
+
+ assertThat(mTestGattServer.validatePairingRequest(encryptedRequest,
+ testKey.getKeySpec())).isTrue();
+ mTestGattServer.processPairingKey(encryptedPairingKey);
+ verifyNoMoreInteractions(mMockBluetoothDevice);
+ }
+
+
+ @Test
public void testValidPairingKeyAutoAccept() {
FastPairUtils.AccountKey testKey = new FastPairUtils.AccountKey(TEST_SHARED_SECRET);
mTestGattServer.setSharedSecretKey(testKey.toBytes());
@@ -440,10 +458,27 @@
.isEqualTo(TEST_MODEL_ID_BYTES);
}
+ @Test
+ public void testStopAdvertisements() {
+ mTestFastPairProvider.start();
+ when(mMockBluetoothAdapter.isDiscovering()).thenReturn(true);
+ Intent scanMode = new Intent(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
+ scanMode.putExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
+ BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+ mTestFastPairProvider.mDiscoveryModeChanged.onReceive(mMockContext, scanMode);
+
+ when(mMockBluetoothAdapter.isDiscovering()).thenReturn(false);
+ scanMode.putExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
+ BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
+ mTestFastPairProvider.mDiscoveryModeChanged.onReceive(mMockContext, scanMode);
+ verify(mMockLeAdvertiser, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).stopAdvertisingSet(any());
+ }
+
void sendPairingKey(int pairingKey) {
Intent pairingRequest = new Intent(BluetoothDevice.ACTION_PAIRING_REQUEST);
pairingRequest.putExtra(BluetoothDevice.EXTRA_PAIRING_KEY, pairingKey);
pairingRequest.putExtra(BluetoothDevice.EXTRA_DEVICE, mMockBluetoothDevice);
mTestGattServer.mPairingAttemptsReceiver.onReceive(mMockContext, pairingRequest);
+
}
}
diff --git a/tests/carservice_unit_test/src/com/android/car/CarInputRotaryServiceTest.java b/tests/carservice_unit_test/src/com/android/car/CarInputRotaryServiceTest.java
index b832462..acfa606 100644
--- a/tests/carservice_unit_test/src/com/android/car/CarInputRotaryServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/CarInputRotaryServiceTest.java
@@ -16,6 +16,8 @@
package com.android.car;
+import static com.android.car.CarInputService.ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -48,6 +50,7 @@
import com.android.car.hal.InputHalService;
import com.android.car.hal.UserHalService;
import com.android.car.internal.common.CommonConstants.UserLifecycleEventType;
+import com.android.car.pm.CarSafetyAccessibilityService;
import com.android.car.user.CarUserService;
import com.android.internal.app.AssistUtils;
import com.android.internal.util.test.BroadcastInterceptingContext;
@@ -140,12 +143,21 @@
}
@Test
- public void rotaryServiceSettingsUpdated_whenRotaryServiceIsNotEmpty() throws Exception {
+ public void accessibilitySettingsUpdated_whenRotaryServiceIsNotEmpty() throws Exception {
+ final String existingService = "com.android.temp/com.android.car.TempService";
final String rotaryService = "com.android.car.rotary/com.android.car.rotary.RotaryService";
+ final String carSafetyAccessibilityService = mContext.getPackageName()
+ + "/"
+ + CarSafetyAccessibilityService.class.getName();
init(rotaryService);
assertThat(mMockContext.getString(R.string.rotaryService)).isEqualTo(rotaryService);
-
final int userId = 11;
+ Settings.Secure.putStringForUser(
+ mMockContext.getContentResolver(),
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ existingService,
+ userId);
+
// By default RotaryService is not enabled.
String enabledServices = Settings.Secure.getStringForUser(
@@ -167,7 +179,12 @@
mMockContext.getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
userId);
- assertThat(enabledServices).contains(rotaryService);
+ assertThat(enabledServices).isEqualTo(
+ existingService
+ + ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR
+ + carSafetyAccessibilityService
+ + ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR
+ + rotaryService);
enabled = Settings.Secure.getStringForUser(
mMockContext.getContentResolver(),
@@ -177,7 +194,11 @@
}
@Test
- public void rotaryServiceSettingsNotUpdated_whenRotaryServiceIsEmpty() throws Exception {
+ public void accessibilitySettingsUpdated_withoutRotaryService_whenRotaryServiceIsEmpty()
+ throws Exception {
+ final String carSafetyAccessibilityService = mContext.getPackageName()
+ + "/"
+ + CarSafetyAccessibilityService.class.getName();
final String rotaryService = "";
init(rotaryService);
assertThat(mMockContext.getString(R.string.rotaryService)).isEqualTo(rotaryService);
@@ -199,7 +220,55 @@
mMockContext.getContentResolver(),
Settings.Secure.ACCESSIBILITY_ENABLED,
userId);
+ assertThat(enabled).isEqualTo("1");
+ String enabledServices = Settings.Secure.getStringForUser(
+ mMockContext.getContentResolver(),
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ userId);
+ assertThat(enabledServices).isEqualTo(carSafetyAccessibilityService);
+ }
+
+ @Test
+ public void accessibilitySettingsUpdated_accessibilityServicesAlreadyEnabled()
+ throws Exception {
+ final String rotaryService = "com.android.car.rotary/com.android.car.rotary.RotaryService";
+ final String carSafetyAccessibilityService = mContext.getPackageName()
+ + "/"
+ + CarSafetyAccessibilityService.class.getName();
+ init(rotaryService);
+ assertThat(mMockContext.getString(R.string.rotaryService)).isEqualTo(rotaryService);
+ final int userId = 11;
+ Settings.Secure.putStringForUser(
+ mMockContext.getContentResolver(),
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ carSafetyAccessibilityService
+ + ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR
+ + rotaryService,
+ userId);
+
+ String enabled = Settings.Secure.getStringForUser(
+ mMockContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_ENABLED,
+ userId);
assertThat(enabled).isNull();
+
+ // Enable RotaryService by sending user switch event.
+ sendUserLifecycleEvent(CarUserManager.USER_LIFECYCLE_EVENT_TYPE_SWITCHING, userId);
+
+ String enabledServices = Settings.Secure.getStringForUser(
+ mMockContext.getContentResolver(),
+ Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
+ userId);
+ assertThat(enabledServices).isEqualTo(
+ carSafetyAccessibilityService
+ + ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR
+ + rotaryService);
+
+ enabled = Settings.Secure.getStringForUser(
+ mMockContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_ENABLED,
+ userId);
+ assertThat(enabled).isEqualTo("1");
}
@After
diff --git a/tests/carservice_unit_test/src/com/android/car/am/CarActivityServiceUnitTest.java b/tests/carservice_unit_test/src/com/android/car/am/CarActivityServiceUnitTest.java
new file mode 100644
index 0000000..4051686
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/am/CarActivityServiceUnitTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2021 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.am;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.car.Car;
+import android.car.app.CarActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.os.UserHandle;
+
+import com.android.car.internal.ICarServiceHelper;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CarActivityServiceUnitTest {
+
+ // Comes from android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER
+ private static final int FEATURE_DEFAULT_TASK_CONTAINER = 1;
+
+ private CarActivityService mCarActivityService;
+
+ private final ComponentName mTestActivity = new ComponentName("test.pkg", "test.activity");
+
+ @Rule
+ public TestName mTestName = new TestName();
+ @Mock
+ private Context mContext;
+ @Mock
+ private ICarServiceHelper mICarServiceHelper;
+
+ @Before
+ public void setUp() {
+ mCarActivityService = spy(new CarActivityService(mContext));
+
+ int nonCurrentUserId = 9999990;
+ boolean isNonCurrentUserTest = mTestName.getMethodName().contains("NonCurrentUser");
+ int callerId = isNonCurrentUserTest ? nonCurrentUserId : UserHandle.USER_SYSTEM;
+ when(mCarActivityService.getCaller()).thenReturn(callerId);
+ }
+
+ @Test
+ public void setPersistentActivityThrowsException_ifICarServiceHelperIsNotSet() {
+ assertThrows(IllegalStateException.class,
+ () -> mCarActivityService.setPersistentActivity(
+ mTestActivity, DEFAULT_DISPLAY, FEATURE_DEFAULT_TASK_CONTAINER));
+ }
+
+ @Test
+ public void setPersistentActivityThrowsException_withoutPermission() {
+ mCarActivityService.setICarServiceHelper(mICarServiceHelper);
+ when(mContext.checkCallingOrSelfPermission(eq(Car.PERMISSION_CONTROL_CAR_APP_LAUNCH)))
+ .thenReturn(PackageManager.PERMISSION_DENIED);
+
+ assertThrows(SecurityException.class,
+ () -> mCarActivityService.setPersistentActivity(
+ mTestActivity, DEFAULT_DISPLAY, FEATURE_DEFAULT_TASK_CONTAINER));
+ }
+
+ @Test
+ public void setPersistentActivityInvokesICarServiceHelper() throws RemoteException {
+ int displayId = 9;
+
+ mCarActivityService.setICarServiceHelper(mICarServiceHelper);
+
+ int ret = mCarActivityService.setPersistentActivity(
+ mTestActivity, displayId, FEATURE_DEFAULT_TASK_CONTAINER);
+ assertThat(ret).isEqualTo(CarActivityManager.RESULT_SUCCESS);
+
+ ArgumentCaptor<ComponentName> activityCaptor = ArgumentCaptor.forClass(ComponentName.class);
+ ArgumentCaptor<Integer> displayIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ ArgumentCaptor<Integer> featureIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mICarServiceHelper).setPersistentActivity(
+ activityCaptor.capture(), displayIdCaptor.capture(), featureIdCaptor.capture());
+
+ assertThat(activityCaptor.getValue()).isEqualTo(mTestActivity);
+ assertThat(displayIdCaptor.getValue()).isEqualTo(displayId);
+ assertThat(featureIdCaptor.getValue()).isEqualTo(FEATURE_DEFAULT_TASK_CONTAINER);
+ }
+
+ @Test
+ public void setPersistentActivityReturnsErrorForNonCurrentUser() throws RemoteException {
+ mCarActivityService.setICarServiceHelper(mICarServiceHelper);
+
+ int ret = mCarActivityService.setPersistentActivity(
+ mTestActivity, DEFAULT_DISPLAY, FEATURE_DEFAULT_TASK_CONTAINER);
+ assertThat(ret).isEqualTo(CarActivityManager.RESULT_INVALID_USER);
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/am/FixedActivityServiceTest.java b/tests/carservice_unit_test/src/com/android/car/am/FixedActivityServiceTest.java
index b22831d..4a9f0c7 100644
--- a/tests/carservice_unit_test/src/com/android/car/am/FixedActivityServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/am/FixedActivityServiceTest.java
@@ -87,6 +87,8 @@
private CarUserService mCarUserService;
@Mock
private CarPowerManager mCarPowerManager;
+ @Mock
+ private Display mValidDisplay;
private FixedActivityService mFixedActivityService;
@@ -102,6 +104,7 @@
when(mContext.getPackageManager()).thenReturn(mPackageManager);
doReturn(mCarUserService).when(() -> CarLocalServices.getService(CarUserService.class));
doReturn(mCarPowerManager).when(() -> CarLocalServices.createCarPowerManager(mContext));
+ when(mDisplayManager.getDisplay(mValidDisplayId)).thenReturn(mValidDisplay);
mFixedActivityService = new FixedActivityService(mContext, mActivityManager, mUserManager,
mDisplayManager);
}
@@ -252,6 +255,7 @@
assertThat(ret).isTrue();
int anotherValidDisplayId = mValidDisplayId + 1;
+ when(mDisplayManager.getDisplay(anotherValidDisplayId)).thenReturn(mValidDisplay);
ret = mFixedActivityService.startFixedActivityModeForDisplayAndUser(anotherIntent,
options, anotherValidDisplayId, userId);
verify(mContext).startActivityAsUser(eq(anotherIntent), any(Bundle.class),
@@ -272,6 +276,46 @@
}
@Test
+ public void testStartFixedActivityModeForDisplayAndUser_unavailableDisplay() {
+ int userId = 10;
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ ActivityOptions options = new ActivityOptions(new Bundle());
+ int unavailableDisplayId = mValidDisplayId + 1;
+
+ boolean started = mFixedActivityService.startFixedActivityModeForDisplayAndUser(
+ intent, options, unavailableDisplayId, userId);
+ assertThat(started).isFalse();
+ }
+
+ @Test
+ public void testStartFixedActivityModeForDisplayAndUser_displayRemoved()
+ throws Exception {
+ int displayToBeRemoved = mValidDisplayId + 1;
+ when(mDisplayManager.getDisplay(displayToBeRemoved)).thenReturn(
+ mValidDisplay, // for startFixedActivityModeForDisplayAndUser
+ mValidDisplay, // for launchIf
+ null);
+ int userId = 10;
+ ActivityOptions options = new ActivityOptions(new Bundle());
+ Intent intent = expectComponentAvailable("test_package", "com.test.dude", userId);
+ mockAmGetCurrentUser(userId);
+ expectNoActivityStack();
+
+ boolean started = mFixedActivityService.startFixedActivityModeForDisplayAndUser(
+ intent, options, displayToBeRemoved, userId);
+ assertThat(started).isTrue();
+ assertThat(mFixedActivityService.getRunningFixedActivity(displayToBeRemoved)).isNotNull();
+
+ // The display is still valid.
+ mFixedActivityService.launchIfNecessary();
+ assertThat(mFixedActivityService.getRunningFixedActivity(displayToBeRemoved)).isNotNull();
+
+ // The display is removed.
+ mFixedActivityService.launchIfNecessary();
+ assertThat(mFixedActivityService.getRunningFixedActivity(displayToBeRemoved)).isNull();
+ }
+
+ @Test
public void testStartFixedActivityModeForDisplayAndUser_notAllowedUser() {
int currentUserId = 10;
int notAllowedUserId = 11;
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPolicyVolumeCallbackTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPolicyVolumeCallbackTest.java
index fc78159..b070bd8 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPolicyVolumeCallbackTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioPolicyVolumeCallbackTest.java
@@ -153,6 +153,35 @@
}
@Test
+ public void onVolumeAdjustment_withAdjustRaise_whileMuted_setsGroupVolumeToMin() {
+ setGroupVolume(TEST_MAX_VOLUME);
+ setGroupVolumeMute(true);
+
+ CarAudioPolicyVolumeCallback callback =
+ new CarAudioPolicyVolumeCallback(mMockCarAudioService, mMockAudioManager, true);
+
+
+ callback.onVolumeAdjustment(ADJUST_RAISE);
+
+ verify(mMockCarAudioService).setGroupVolume(PRIMARY_AUDIO_ZONE,
+ TEST_VOLUME_GROUP, TEST_MIN_VOLUME, TEST_EXPECTED_FLAGS);
+ }
+
+ @Test
+ public void onVolumeAdjustment_withAdjustLower_whileMuted_setsGroupVolumeToMin() {
+ setGroupVolume(TEST_MAX_VOLUME);
+ setGroupVolumeMute(true);
+
+ CarAudioPolicyVolumeCallback callback =
+ new CarAudioPolicyVolumeCallback(mMockCarAudioService, mMockAudioManager, true);
+
+ callback.onVolumeAdjustment(ADJUST_LOWER);
+
+ verify(mMockCarAudioService).setGroupVolume(PRIMARY_AUDIO_ZONE,
+ TEST_VOLUME_GROUP, TEST_MIN_VOLUME, TEST_EXPECTED_FLAGS);
+ }
+
+ @Test
public void onVolumeAdjustment_withAdjustSame_doesNothing() {
setGroupVolume(TEST_VOLUME);
@@ -207,8 +236,8 @@
@Test
public void onVolumeAdjustment_forGroupMute_withAdjustToggleMute_togglesMutesVolumeGroup() {
- when(mMockCarAudioService.isVolumeGroupMuted(anyInt(), anyInt()))
- .thenReturn(true);
+ setGroupVolumeMute(true);
+
CarAudioPolicyVolumeCallback callback =
new CarAudioPolicyVolumeCallback(mMockCarAudioService, mMockAudioManager, true);
@@ -220,7 +249,8 @@
@Test
public void onVolumeAdjustment_forGroupMute_withAdjustUnMute_UnMutesVolumeGroup() {
- when(mMockCarAudioService.isVolumeGroupMuted(anyInt(), anyInt())).thenReturn(false);
+ setGroupVolumeMute(false);
+
CarAudioPolicyVolumeCallback callback =
new CarAudioPolicyVolumeCallback(mMockCarAudioService, mMockAudioManager, true);
@@ -234,4 +264,9 @@
when(mMockCarAudioService.getGroupVolume(anyInt(), anyInt()))
.thenReturn(groupVolume);
}
+
+ private void setGroupVolumeMute(boolean mute) {
+ when(mMockCarAudioService.isVolumeGroupMuted(anyInt(), anyInt()))
+ .thenReturn(mute);
+ }
}
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioUtilsTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioUtilsTest.java
index e913b53..b3d02be 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarAudioUtilsTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarAudioUtilsTest.java
@@ -16,14 +16,23 @@
package com.android.car.audio;
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+import static android.media.AudioDeviceInfo.TYPE_FM_TUNER;
+
import static com.android.car.audio.CarAudioUtils.hasExpired;
+import static com.android.car.audio.CarAudioUtils.isMicrophoneInputDevice;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import android.media.AudioDeviceInfo;
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mockito;
@RunWith(AndroidJUnit4.class)
public class CarAudioUtilsTest {
@@ -37,4 +46,18 @@
public void hasExpired_forCurrentTimeAfterTimeout_returnsFalse() {
assertThat(hasExpired(0, 300, 200)).isTrue();
}
+
+ @Test
+ public void isMicrophoneInputDevice_forMicrophoneDevice_returnsTrue() {
+ AudioDeviceInfo deviceInfo = Mockito.mock(AudioDeviceInfo.class);
+ when(deviceInfo.getType()).thenReturn(TYPE_BUILTIN_MIC);
+ assertThat(isMicrophoneInputDevice(deviceInfo)).isTrue();
+ }
+
+ @Test
+ public void isMicrophoneInputDevice_forNonMicrophoneDevice_returnsFalse() {
+ AudioDeviceInfo deviceInfo = Mockito.mock(AudioDeviceInfo.class);
+ when(deviceInfo.getType()).thenReturn(TYPE_FM_TUNER);
+ assertThat(isMicrophoneInputDevice(deviceInfo)).isFalse();
+ }
}
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingUtilsTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingUtilsTest.java
index de39471..ece0f00 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingUtilsTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarDuckingUtilsTest.java
@@ -23,8 +23,15 @@
import static android.media.AudioAttributes.USAGE_MEDIA;
import static android.media.AudioAttributes.USAGE_NOTIFICATION;
import static android.media.AudioAttributes.USAGE_SAFETY;
+import static android.media.AudioAttributes.USAGE_VIRTUAL_SOURCE;
import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
+import static com.android.car.audio.CarAudioContext.CALL;
+import static com.android.car.audio.CarAudioContext.EMERGENCY;
+import static com.android.car.audio.CarAudioContext.INVALID;
+import static com.android.car.audio.CarAudioContext.MUSIC;
+import static com.android.car.audio.CarAudioContext.NAVIGATION;
+
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -161,6 +168,16 @@
}
@Test
+ public void getAddressesToDuck_doesNotConsidersInvalidUsage() {
+ CarAudioZone mockZone = generateAudioZoneMock();
+ int[] usages = new int[]{USAGE_VIRTUAL_SOURCE};
+
+ List<String> addresses = CarDuckingUtils.getAddressesToDuck(usages, mockZone);
+
+ assertThat(addresses).isEmpty();
+ }
+
+ @Test
public void getAddressesToDuck_withDuckedAndUnduckedContextsSharingDevice_excludesThatDevice() {
CarAudioZone mockZone = generateAudioZoneMock();
when(mockZone.getAddressForContext(CarAudioContext.SAFETY)).thenReturn(NAVIGATION_ADDRESS);
@@ -220,12 +237,11 @@
private static CarAudioZone generateAudioZoneMock() {
CarAudioZone mockZone = mock(CarAudioZone.class);
- when(mockZone.getAddressForContext(CarAudioContext.MUSIC)).thenReturn(MEDIA_ADDRESS);
- when(mockZone.getAddressForContext(CarAudioContext.EMERGENCY)).thenReturn(
- EMERGENCY_ADDRESS);
- when(mockZone.getAddressForContext(CarAudioContext.CALL)).thenReturn(CALL_ADDRESS);
- when(mockZone.getAddressForContext(CarAudioContext.NAVIGATION)).thenReturn(
- NAVIGATION_ADDRESS);
+ when(mockZone.getAddressForContext(MUSIC)).thenReturn(MEDIA_ADDRESS);
+ when(mockZone.getAddressForContext(EMERGENCY)).thenReturn(EMERGENCY_ADDRESS);
+ when(mockZone.getAddressForContext(CALL)).thenReturn(CALL_ADDRESS);
+ when(mockZone.getAddressForContext(NAVIGATION)).thenReturn(NAVIGATION_ADDRESS);
+ when(mockZone.getAddressForContext(INVALID)).thenThrow(new IllegalArgumentException());
return mockZone;
}
diff --git a/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupUnitTest.java b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupUnitTest.java
index 9e38583..d71a0d5 100644
--- a/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/audio/CarVolumeGroupUnitTest.java
@@ -16,7 +16,15 @@
package com.android.car.audio;
-import static com.google.common.truth.Truth.assertThat;
+import static com.android.car.audio.CarAudioContext.ALARM;
+import static com.android.car.audio.CarAudioContext.CALL;
+import static com.android.car.audio.CarAudioContext.CALL_RING;
+import static com.android.car.audio.CarAudioContext.EMERGENCY;
+import static com.android.car.audio.CarAudioContext.MUSIC;
+import static com.android.car.audio.CarAudioContext.NAVIGATION;
+import static com.android.car.audio.CarAudioContext.NOTIFICATION;
+
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -70,11 +78,12 @@
public void setDeviceInfoForContext_associatesDeviceAddresses() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
- builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(NAVIGATION, mNavigationDeviceInfo);
CarVolumeGroup carVolumeGroup = builder.build();
- assertThat(carVolumeGroup.getAddresses()).containsExactly(MEDIA_DEVICE_ADDRESS,
+ assertWithMessage("%s and %s", MEDIA_DEVICE_ADDRESS, NAVIGATION_DEVICE_ADDRESS)
+ .that(carVolumeGroup.getAddresses()).containsExactly(MEDIA_DEVICE_ADDRESS,
NAVIGATION_DEVICE_ADDRESS);
}
@@ -82,139 +91,149 @@
public void setDeviceInfoForContext_associatesContexts() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
- builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(NAVIGATION, mNavigationDeviceInfo);
CarVolumeGroup carVolumeGroup = builder.build();
- assertThat(carVolumeGroup.getContexts()).asList().containsExactly(CarAudioContext.MUSIC,
- CarAudioContext.NAVIGATION);
+ assertWithMessage("Music[%s] and Navigation[%s] Context", MUSIC, NAVIGATION)
+ .that(carVolumeGroup.getContexts()).asList().containsExactly(MUSIC, NAVIGATION);
}
@Test
public void setDeviceInfoForContext_withDifferentStepSize_throws() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
CarAudioDeviceInfo differentStepValueDevice = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
.setStepValue(mMediaDeviceInfo.getStepValue() + 1).build();
IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
- () -> builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION,
+ () -> builder.setDeviceInfoForContext(NAVIGATION,
differentStepValueDevice));
- assertThat(thrown).hasMessageThat()
+ assertWithMessage("setDeviceInfoForContext failure for different step size")
+ .that(thrown).hasMessageThat()
.contains("Gain controls within one group must have same step value");
}
@Test
public void setDeviceInfoForContext_withSameContext_throws() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
- () -> builder.setDeviceInfoForContext(CarAudioContext.MUSIC,
+ () -> builder.setDeviceInfoForContext(MUSIC,
mNavigationDeviceInfo));
- assertThat(thrown).hasMessageThat()
- .contains("has already been set to");
+ assertWithMessage("setDeviceInfoForSameContext failure for repeated context")
+ .that(thrown).hasMessageThat().contains("has already been set to");
}
@Test
public void setDeviceInfoForContext_withFirstCall_setsMinGain() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
- assertThat(builder.mMinGain).isEqualTo(mMediaDeviceInfo.getMinGain());
+ assertWithMessage("Min Gain from builder")
+ .that(builder.mMinGain).isEqualTo(mMediaDeviceInfo.getMinGain());
}
@Test
public void setDeviceInfoForContext_withFirstCall_setsMaxGain() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
- assertThat(builder.mMaxGain).isEqualTo(mMediaDeviceInfo.getMaxGain());
+ assertWithMessage("Max Gain from builder")
+ .that(builder.mMaxGain).isEqualTo(mMediaDeviceInfo.getMaxGain());
}
@Test
public void setDeviceInfoForContext_withFirstCall_setsDefaultGain() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
- assertThat(builder.mDefaultGain).isEqualTo(mMediaDeviceInfo.getDefaultGain());
+ assertWithMessage("Default Gain from builder")
+ .that(builder.mDefaultGain).isEqualTo(mMediaDeviceInfo.getDefaultGain());
}
@Test
public void setDeviceInfoForContext_SecondCallWithSmallerMinGain_updatesMinGain() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
.setMinGain(mMediaDeviceInfo.getMinGain() - 1).build();
- builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+ builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
- assertThat(builder.mMinGain).isEqualTo(secondInfo.getMinGain());
+ assertWithMessage("Second, smaller min gain from builder")
+ .that(builder.mMinGain).isEqualTo(secondInfo.getMinGain());
}
@Test
public void setDeviceInfoForContext_SecondCallWithLargerMinGain_keepsFirstMinGain() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
.setMinGain(mMediaDeviceInfo.getMinGain() + 1).build();
- builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+ builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
- assertThat(builder.mMinGain).isEqualTo(mMediaDeviceInfo.getMinGain());
+ assertWithMessage("First, smaller min gain from builder")
+ .that(builder.mMinGain).isEqualTo(mMediaDeviceInfo.getMinGain());
}
@Test
public void setDeviceInfoForContext_SecondCallWithLargerMaxGain_updatesMaxGain() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
.setMaxGain(mMediaDeviceInfo.getMaxGain() + 1).build();
- builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+ builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
- assertThat(builder.mMaxGain).isEqualTo(secondInfo.getMaxGain());
+ assertWithMessage("Second, larger max gain from builder")
+ .that(builder.mMaxGain).isEqualTo(secondInfo.getMaxGain());
}
@Test
public void setDeviceInfoForContext_SecondCallWithSmallerMaxGain_keepsFirstMaxGain() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
.setMaxGain(mMediaDeviceInfo.getMaxGain() - 1).build();
- builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+ builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
- assertThat(builder.mMaxGain).isEqualTo(mMediaDeviceInfo.getMaxGain());
+ assertWithMessage("First, larger max gain from builder")
+ .that(builder.mMaxGain).isEqualTo(mMediaDeviceInfo.getMaxGain());
}
@Test
public void setDeviceInfoForContext_SecondCallWithLargerDefaultGain_updatesDefaultGain() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
.setDefaultGain(mMediaDeviceInfo.getDefaultGain() + 1).build();
- builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+ builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
- assertThat(builder.mDefaultGain).isEqualTo(secondInfo.getDefaultGain());
+ assertWithMessage("Second, larger default gain from builder")
+ .that(builder.mDefaultGain).isEqualTo(secondInfo.getDefaultGain());
}
@Test
public void setDeviceInfoForContext_SecondCallWithSmallerDefaultGain_keepsFirstDefaultGain() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
CarAudioDeviceInfo secondInfo = new InfoBuilder(NAVIGATION_DEVICE_ADDRESS)
.setDefaultGain(mMediaDeviceInfo.getDefaultGain() - 1).build();
- builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, secondInfo);
+ builder.setDeviceInfoForContext(NAVIGATION, secondInfo);
- assertThat(builder.mDefaultGain).isEqualTo(mMediaDeviceInfo.getDefaultGain());
+ assertWithMessage("Second, smaller default gain from builder")
+ .that(builder.mDefaultGain).isEqualTo(mMediaDeviceInfo.getDefaultGain());
}
@Test
@@ -223,13 +242,14 @@
Exception e = expectThrows(IllegalArgumentException.class, builder::build);
- assertThat(e).hasMessageThat().isEqualTo(
- "setDeviceInfoForContext has to be called at least once before building");
+ assertWithMessage("Builder build failure").that(e).hasMessageThat()
+ .isEqualTo(
+ "setDeviceInfoForContext has to be called at least once before building");
}
@Test
public void builderBuild_withNoStoredGain_usesDefaultGain() {
- CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+ CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(MUSIC,
mMediaDeviceInfo);
when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
GROUP_ID)).thenReturn(-1);
@@ -237,50 +257,55 @@
CarVolumeGroup carVolumeGroup = builder.build();
- assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
+ assertWithMessage("Current gain index")
+ .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
}
@Test
public void builderBuild_withTooLargeStoredGain_usesDefaultGain() {
- CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+ CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(MUSIC,
mMediaDeviceInfo);
when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
GROUP_ID)).thenReturn(MAX_GAIN_INDEX + 1);
CarVolumeGroup carVolumeGroup = builder.build();
- assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
+ assertWithMessage("Current gain index")
+ .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
}
@Test
public void builderBuild_withTooSmallStoredGain_usesDefaultGain() {
- CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+ CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(MUSIC,
mMediaDeviceInfo);
when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
GROUP_ID)).thenReturn(MIN_GAIN_INDEX - 1);
CarVolumeGroup carVolumeGroup = builder.build();
- assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
+ assertWithMessage("Current gain index")
+ .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(DEFAULT_GAIN_INDEX);
}
@Test
public void builderBuild_withValidStoredGain_usesStoredGain() {
- CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(CarAudioContext.MUSIC,
+ CarVolumeGroup.Builder builder = getBuilder().setDeviceInfoForContext(MUSIC,
mMediaDeviceInfo);
when(mSettingsMock.getStoredVolumeGainIndexForUser(UserHandle.USER_CURRENT, ZONE_ID,
GROUP_ID)).thenReturn(MAX_GAIN_INDEX - 1);
CarVolumeGroup carVolumeGroup = builder.build();
- assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(MAX_GAIN_INDEX - 1);
+ assertWithMessage("Current gain index")
+ .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(MAX_GAIN_INDEX - 1);
}
@Test
public void getAddressForContext_withSupportedContext_returnsAddress() {
CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
- assertThat(carVolumeGroup.getAddressForContext(CarAudioContext.MUSIC))
+ assertWithMessage("Supported context's address")
+ .that(carVolumeGroup.getAddressForContext(MUSIC))
.isEqualTo(mMediaDeviceInfo.getAddress());
}
@@ -288,14 +313,16 @@
public void getAddressForContext_withUnsupportedContext_returnsNull() {
CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
- assertThat(carVolumeGroup.getAddressForContext(CarAudioContext.NAVIGATION)).isNull();
+ assertWithMessage("Unsupported context's address")
+ .that(carVolumeGroup.getAddressForContext(NAVIGATION)).isNull();
}
@Test
public void isMuted_whenDefault_returnsFalse() {
CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
- assertThat(carVolumeGroup.isMuted()).isFalse();
+ assertWithMessage("Default mute state")
+ .that(carVolumeGroup.isMuted()).isFalse();
}
@Test
@@ -304,7 +331,8 @@
carVolumeGroup.setMute(true);
- assertThat(carVolumeGroup.isMuted()).isTrue();
+ assertWithMessage("Set mute state")
+ .that(carVolumeGroup.isMuted()).isTrue();
}
@Test
@@ -313,7 +341,8 @@
carVolumeGroup.setMute(false);
- assertThat(carVolumeGroup.isMuted()).isFalse();
+ assertWithMessage("Set mute state")
+ .that(carVolumeGroup.isMuted()).isFalse();
}
@Test
@@ -352,8 +381,9 @@
List<Integer> contextsList = carVolumeGroup.getContextsForAddress(MEDIA_DEVICE_ADDRESS);
- assertThat(contextsList).containsExactly(CarAudioContext.MUSIC,
- CarAudioContext.CALL, CarAudioContext.CALL_RING);
+ assertWithMessage("Contexts for bounded address %s", MEDIA_DEVICE_ADDRESS)
+ .that(contextsList).containsExactly(MUSIC,
+ CALL, CALL_RING);
}
@Test
@@ -362,7 +392,8 @@
List<Integer> contextsList = carVolumeGroup.getContextsForAddress(OTHER_ADDRESS);
- assertThat(contextsList).isEmpty();
+ assertWithMessage("Contexts for non-bounded address %s", OTHER_ADDRESS)
+ .that(contextsList).isEmpty();
}
@Test
@@ -372,7 +403,8 @@
CarAudioDeviceInfo actualDevice = carVolumeGroup.getCarAudioDeviceInfoForAddress(
MEDIA_DEVICE_ADDRESS);
- assertThat(actualDevice).isEqualTo(mMediaDeviceInfo);
+ assertWithMessage("Device information for bounded address %s", MEDIA_DEVICE_ADDRESS)
+ .that(actualDevice).isEqualTo(mMediaDeviceInfo);
}
@Test
@@ -382,7 +414,8 @@
CarAudioDeviceInfo actualDevice = carVolumeGroup.getCarAudioDeviceInfoForAddress(
OTHER_ADDRESS);
- assertThat(actualDevice).isNull();
+ assertWithMessage("Device information for non-bounded address %s", OTHER_ADDRESS)
+ .that(actualDevice).isNull();
}
@Test
@@ -401,7 +434,8 @@
carVolumeGroup.setCurrentGainIndex(TEST_GAIN_INDEX);
- assertThat(carVolumeGroup.getCurrentGainIndex()).isEqualTo(TEST_GAIN_INDEX);
+ assertWithMessage("Updated current gain index")
+ .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(TEST_GAIN_INDEX);
}
@Test
@@ -410,7 +444,8 @@
IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
() -> carVolumeGroup.setCurrentGainIndex(MIN_GAIN_INDEX - 1));
- assertThat(thrown).hasMessageThat()
+ assertWithMessage("Set out of bound gain index failure")
+ .that(thrown).hasMessageThat()
.contains("Gain out of range (" + MIN_GAIN + ":" + MAX_GAIN + ")");
}
@@ -420,7 +455,8 @@
IllegalArgumentException thrown = expectThrows(IllegalArgumentException.class,
() -> carVolumeGroup.setCurrentGainIndex(MAX_GAIN_INDEX + 1));
- assertThat(thrown).hasMessageThat()
+ assertWithMessage("Set out of bound gain index failure")
+ .that(thrown).hasMessageThat()
.contains("Gain out of range (" + MIN_GAIN + ":" + MAX_GAIN + ")");
}
@@ -456,7 +492,8 @@
carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
- assertThat(carVolumeGroup.isMuted()).isTrue();
+ assertWithMessage("Saved mute state from settings")
+ .that(carVolumeGroup.isMuted()).isTrue();
}
@Test
@@ -465,7 +502,8 @@
carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
- assertThat(carVolumeGroup.isMuted()).isFalse();
+ assertWithMessage("Default mute state")
+ .that(carVolumeGroup.isMuted()).isFalse();
}
@Test
@@ -474,7 +512,8 @@
carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
- assertThat(carVolumeGroup.isMuted()).isFalse();
+ assertWithMessage("Saved mute state from settings")
+ .that(carVolumeGroup.isMuted()).isFalse();
}
@Test
@@ -483,35 +522,70 @@
carVolumeGroup.loadVolumesSettingsForUser(TEST_USER_10);
- assertThat(carVolumeGroup.isMuted()).isFalse();
+ assertWithMessage("Default mute state")
+ .that(carVolumeGroup.isMuted()).isFalse();
}
@Test
public void hasCriticalAudioContexts_withoutCriticalContexts_returnsFalse() {
CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
- assertThat(carVolumeGroup.hasCriticalAudioContexts()).isFalse();
+ assertWithMessage("Group without critical audio context")
+ .that(carVolumeGroup.hasCriticalAudioContexts()).isFalse();
}
@Test
public void hasCriticalAudioContexts_withCriticalContexts_returnsTrue() {
CarVolumeGroup carVolumeGroup = getBuilder()
- .setDeviceInfoForContext(CarAudioContext.EMERGENCY, mMediaDeviceInfo)
+ .setDeviceInfoForContext(EMERGENCY, mMediaDeviceInfo)
.build();
- assertThat(carVolumeGroup.hasCriticalAudioContexts()).isTrue();
+ assertWithMessage("Group with critical audio context")
+ .that(carVolumeGroup.hasCriticalAudioContexts()).isTrue();
+ }
+
+ @Test
+ public void getCurrentGainIndex_whileMuted_returnsMinGain() {
+ CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+ carVolumeGroup.setCurrentGainIndex(TEST_GAIN_INDEX);
+
+ carVolumeGroup.setMute(true);
+
+ assertWithMessage("Muted current gain index")
+ .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(MIN_GAIN_INDEX);
+ }
+
+ @Test
+ public void getCurrentGainIndex_whileUnMuted_returnsLastSetGain() {
+ CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+ carVolumeGroup.setCurrentGainIndex(TEST_GAIN_INDEX);
+
+ carVolumeGroup.setMute(false);
+
+ assertWithMessage("Un-muted current gain index")
+ .that(carVolumeGroup.getCurrentGainIndex()).isEqualTo(TEST_GAIN_INDEX);
+ }
+
+ @Test
+ public void setCurrentGainIndex_whileMuted_unMutesVolumeGroup() {
+ CarVolumeGroup carVolumeGroup = getCarVolumeGroupWithMusicBound();
+ carVolumeGroup.setMute(true);
+ carVolumeGroup.setCurrentGainIndex(TEST_GAIN_INDEX);
+
+ assertWithMessage("Mute state after volume change")
+ .that(carVolumeGroup.isMuted()).isEqualTo(false);
}
private CarVolumeGroup getCarVolumeGroupWithMusicBound() {
return getBuilder()
- .setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo)
+ .setDeviceInfoForContext(MUSIC, mMediaDeviceInfo)
.build();
}
private CarVolumeGroup getCarVolumeGroupWithNavigationBound(CarAudioSettings settings,
boolean useCarVolumeGroupMute) {
return new CarVolumeGroup.Builder(0, 0, settings, useCarVolumeGroupMute)
- .setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo)
+ .setDeviceInfoForContext(NAVIGATION, mNavigationDeviceInfo)
.build();
}
@@ -527,13 +601,13 @@
private CarVolumeGroup testVolumeGroupSetup() {
CarVolumeGroup.Builder builder = getBuilder();
- builder.setDeviceInfoForContext(CarAudioContext.MUSIC, mMediaDeviceInfo);
- builder.setDeviceInfoForContext(CarAudioContext.CALL, mMediaDeviceInfo);
- builder.setDeviceInfoForContext(CarAudioContext.CALL_RING, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(MUSIC, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(CALL, mMediaDeviceInfo);
+ builder.setDeviceInfoForContext(CALL_RING, mMediaDeviceInfo);
- builder.setDeviceInfoForContext(CarAudioContext.NAVIGATION, mNavigationDeviceInfo);
- builder.setDeviceInfoForContext(CarAudioContext.ALARM, mNavigationDeviceInfo);
- builder.setDeviceInfoForContext(CarAudioContext.NOTIFICATION, mNavigationDeviceInfo);
+ builder.setDeviceInfoForContext(NAVIGATION, mNavigationDeviceInfo);
+ builder.setDeviceInfoForContext(ALARM, mNavigationDeviceInfo);
+ builder.setDeviceInfoForContext(NOTIFICATION, mNavigationDeviceInfo);
return builder.build();
}
diff --git a/tests/carservice_unit_test/src/com/android/car/cluster/ClusterHomeServiceUnitTest.java b/tests/carservice_unit_test/src/com/android/car/cluster/ClusterHomeServiceUnitTest.java
index 4afe81e..e5bcd5f 100644
--- a/tests/carservice_unit_test/src/com/android/car/cluster/ClusterHomeServiceUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/cluster/ClusterHomeServiceUnitTest.java
@@ -33,7 +33,8 @@
import android.app.ActivityOptions;
import android.car.cluster.ClusterHomeManager;
import android.car.cluster.ClusterState;
-import android.car.cluster.IClusterHomeCallback;
+import android.car.cluster.IClusterNavigationStateListener;
+import android.car.cluster.IClusterStateListener;
import android.car.cluster.navigation.NavigationState.NavigationStateProto;
import android.car.navigation.CarNavigationInstrumentCluster;
import android.content.ComponentName;
@@ -92,14 +93,18 @@
private int mClusterStateChanges;
private byte[] mNavigationState;
- private IClusterHomeCallback mClusterHomeCallback;
- private class IClusterHomeCallbackImpl extends IClusterHomeCallback.Stub {
+ private IClusterStateListener mClusterStateListener;
+ private IClusterNavigationStateListener mClusterNavigationStateListener;
+
+ private class IClusterStateListenerImpl extends IClusterStateListener.Stub {
@Override
public void onClusterStateChanged(ClusterState state, int changes) {
mClusterState = state;
mClusterStateChanges = changes;
}
+ }
+ private class IClusterNavigationStateListenerImpl extends IClusterNavigationStateListener.Stub {
@Override
public void onNavigationStateChanged(byte[] navigationState) {
mNavigationState = navigationState;
@@ -130,15 +135,21 @@
mClusterHomeService.init();
}
- public void registerClusterHomeCallback() {
- mClusterHomeCallback = new IClusterHomeCallbackImpl();
- mClusterHomeService.registerCallback(mClusterHomeCallback);
+ public void registerClusterHomeCallbacks() {
+ mClusterStateListener = new IClusterStateListenerImpl();
+ mClusterNavigationStateListener = new IClusterNavigationStateListenerImpl();
+ mClusterHomeService.registerClusterStateListener(mClusterStateListener);
+ mClusterHomeService.registerClusterNavigationStateListener(mClusterNavigationStateListener);
}
@After
public void tearDown() throws Exception {
- if (mClusterHomeCallback != null) {
- mClusterHomeService.unregisterCallback(mClusterHomeCallback);
+ if (mClusterStateListener != null) {
+ mClusterHomeService.unregisterClusterStateListener(mClusterStateListener);
+ }
+ if (mClusterNavigationStateListener != null) {
+ mClusterHomeService.unregisterClusterNavigationStateListener(
+ mClusterNavigationStateListener);
}
mClusterHomeService.release();
}
@@ -171,7 +182,7 @@
@Test
public void onSwitchUiSendsDisplayState() {
- registerClusterHomeCallback();
+ registerClusterHomeCallbacks();
mClusterHomeService.onSwitchUi(UI_TYPE_CLUSTER_MAPS);
@@ -183,7 +194,7 @@
@Test
public void displayOnSendsDisplayState() {
- registerClusterHomeCallback();
+ registerClusterHomeCallbacks();
mClusterHomeService.onDisplayState(ClusterHalService.DISPLAY_ON,
/* bounds= */ null, /* insets= */ null);
@@ -196,7 +207,7 @@
@Test
public void displayBoundsSendsDisplayState() {
- registerClusterHomeCallback();
+ registerClusterHomeCallbacks();
Rect newBounds = new Rect(10, 10, CLUSTER_WIDTH - 10, CLUSTER_HEIGHT - 10);
mClusterHomeService.onDisplayState(ClusterHalService.DONT_CARE,
@@ -210,7 +221,7 @@
@Test
public void displayInsetsSendsDisplayState() {
- registerClusterHomeCallback();
+ registerClusterHomeCallbacks();
Insets newInsets = Insets.of(10, 10, 10, 10);
mClusterHomeService.onDisplayState(ClusterHalService.DONT_CARE, /* bounds= */ null,
@@ -224,7 +235,7 @@
@Test
public void onNavigationStateChangedSendsNavigationState() {
- registerClusterHomeCallback();
+ registerClusterHomeCallbacks();
Bundle bundle = new Bundle();
byte[] newNavState = new byte[] {(byte) 1, (byte) 2, (byte) 3};
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java b/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java
index ff50677..fd15435 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/MockedPowerHalService.java
@@ -20,6 +20,8 @@
import android.hardware.automotive.vehicle.V2_0.VehicleApPowerStateReq;
import android.util.Log;
+import com.android.car.CarServiceUtils;
+
import java.util.LinkedList;
public class MockedPowerHalService extends PowerHalService {
@@ -47,7 +49,9 @@
mock(UserHalService.class),
mock(DiagnosticHalService.class),
mock(ClusterHalService.class),
- mock(HalClient.class));
+ mock(TimeHalService.class),
+ mock(HalClient.class),
+ CarServiceUtils.getHandlerThread(VehicleHal.class.getSimpleName()));
return mockedVehicleHal;
}
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/TimeHalServiceTest.java b/tests/carservice_unit_test/src/com/android/car/hal/TimeHalServiceTest.java
new file mode 100644
index 0000000..adc5153
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/hal/TimeHalServiceTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2021 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.hal;
+
+import static android.hardware.automotive.vehicle.V2_0.VehicleProperty.EPOCH_TIME;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.verify;
+
+import android.car.test.util.BroadcastingFakeContext;
+import android.car.test.util.VehicleHalTestingHelper;
+import android.content.Intent;
+import android.hardware.automotive.vehicle.V2_0.VehicleArea;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropConfig;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
+import android.hardware.automotive.vehicle.V2_0.VehiclePropertyStatus;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Collections;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class TimeHalServiceTest {
+
+ private static final VehiclePropConfig ANDROID_TIME_PROP =
+ VehicleHalTestingHelper.newConfig(EPOCH_TIME);
+
+ @Mock private VehicleHal mVehicleHal;
+ private BroadcastingFakeContext mFakeContext;
+
+ private TimeHalService mTimeHalService;
+
+ @Before
+ public void setUp() {
+ mFakeContext = new BroadcastingFakeContext();
+ mTimeHalService = new TimeHalService(mFakeContext, mVehicleHal);
+ }
+
+ @Test
+ public void testInitDoesNothing() {
+ mTimeHalService.takeProperties(Collections.emptyList());
+
+ mTimeHalService.init();
+
+ mFakeContext.verifyReceiverNotRegistered();
+ }
+
+ @Test
+ public void testInitRegistersBroadcastReceiver() {
+ mTimeHalService.takeProperties(Collections.singletonList(ANDROID_TIME_PROP));
+
+ mTimeHalService.init();
+
+ assertThat(mTimeHalService.isAndroidTimeSupported()).isTrue();
+ mFakeContext.verifyReceiverRegistered(Intent.ACTION_TIME_CHANGED);
+ }
+
+ @Test
+ public void testInitSendsAndroidTimeUpdate() {
+ mTimeHalService.takeProperties(Collections.singletonList(ANDROID_TIME_PROP));
+ long sysTimeMillis = System.currentTimeMillis();
+
+ mTimeHalService.init();
+
+ assertThat(mTimeHalService.isAndroidTimeSupported()).isTrue();
+ ArgumentCaptor<VehiclePropValue> captor = ArgumentCaptor.forClass(VehiclePropValue.class);
+ verify(mVehicleHal).set(captor.capture());
+ VehiclePropValue propValue = captor.getValue();
+ assertThat(propValue.prop).isEqualTo(EPOCH_TIME);
+ assertThat(propValue.areaId).isEqualTo(VehicleArea.GLOBAL);
+ assertThat(propValue.status).isEqualTo(VehiclePropertyStatus.AVAILABLE);
+ assertThat(propValue.timestamp).isAtLeast(sysTimeMillis);
+ assertThat(propValue.value.int64Values).hasSize(1);
+ assertThat(propValue.value.int64Values.get(0)).isAtLeast(sysTimeMillis);
+ }
+
+ @Test
+ public void testReleaseUnregistersBroadcastReceiver() {
+ mTimeHalService.takeProperties(Collections.singletonList(ANDROID_TIME_PROP));
+ mTimeHalService.init();
+ clearInvocations(mVehicleHal);
+
+ mTimeHalService.release();
+
+ mFakeContext.verifyReceiverNotRegistered();
+ assertThat(mTimeHalService.isAndroidTimeSupported()).isFalse();
+ }
+
+ @Test
+ public void testSendsAndroidTimeUpdateWhenBroadcast() {
+ mTimeHalService.takeProperties(Collections.singletonList(ANDROID_TIME_PROP));
+ mTimeHalService.init();
+ clearInvocations(mVehicleHal);
+ long sysTimeMillis = System.currentTimeMillis();
+
+ mFakeContext.sendBroadcast(new Intent(Intent.ACTION_TIME_CHANGED));
+
+ ArgumentCaptor<VehiclePropValue> captor = ArgumentCaptor.forClass(VehiclePropValue.class);
+ verify(mVehicleHal).set(captor.capture());
+ VehiclePropValue propValue = captor.getValue();
+ assertThat(propValue.prop).isEqualTo(EPOCH_TIME);
+ assertThat(propValue.areaId).isEqualTo(VehicleArea.GLOBAL);
+ assertThat(propValue.status).isEqualTo(VehiclePropertyStatus.AVAILABLE);
+ assertThat(propValue.timestamp).isAtLeast(sysTimeMillis);
+ assertThat(propValue.value.int64Values).hasSize(1);
+ assertThat(propValue.value.int64Values.get(0)).isAtLeast(sysTimeMillis);
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java b/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java
index 763b680..2a39046 100644
--- a/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/hal/VehicleHalTest.java
@@ -35,6 +35,10 @@
import android.hardware.automotive.vehicle.V2_0.VehiclePropValue;
import android.hardware.automotive.vehicle.V2_0.VehiclePropertyAccess;
import android.hardware.automotive.vehicle.V2_0.VehiclePropertyChangeMode;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.android.car.CarServiceUtils;
import org.junit.Before;
import org.junit.Test;
@@ -67,8 +71,13 @@
@Mock private UserHalService mUserHalService;
@Mock private DiagnosticHalService mDiagnosticHalService;
@Mock private ClusterHalService mClusterHalService;
+ @Mock private TimeHalService mTimeHalService;
@Mock private HalClient mHalClient;
+ private final HandlerThread mHandlerThread = CarServiceUtils.getHandlerThread(
+ VehicleHal.class.getSimpleName());
+ private final Handler mHandler = new Handler(mHandlerThread.getLooper());
+
private VehicleHal mVehicleHal;
/** Hal services configurations */
@@ -78,7 +87,8 @@
public void setUp() throws Exception {
mVehicleHal = new VehicleHal(mPowerHalService,
mPropertyHalService, mInputHalService, mVmsHalService, mUserHalService,
- mDiagnosticHalService, mClusterHalService, mHalClient);
+ mDiagnosticHalService, mClusterHalService, mTimeHalService, mHalClient,
+ mHandlerThread);
mConfigs.clear();
@@ -185,7 +195,8 @@
propValues.add(propValue);
// Act
- mVehicleHal.onPropertyEvent(propValues);
+ mHandler.post(() -> mVehicleHal.onPropertyEvent(propValues));
+ CarServiceUtils.runOnLooperSync(mHandlerThread.getLooper(), () -> {});
// Assert
verify(dispatchList).add(propValue);
@@ -201,7 +212,8 @@
int areaId = VehicleHal.NO_AREA;
// Act
- mVehicleHal.onPropertySetError(errorCode, propId, areaId);
+ mHandler.post(() -> mVehicleHal.onPropertySetError(errorCode, propId, areaId));
+ CarServiceUtils.runOnLooperSync(mHandlerThread.getLooper(), () -> {});
// Assert
verify(mPowerHalService).onPropertySetError(propId, areaId, errorCode);
@@ -215,7 +227,8 @@
int areaId = VehicleHal.NO_AREA;
// Act
- mVehicleHal.onPropertySetError(errorCode, propId, areaId);
+ mHandler.post(() -> mVehicleHal.onPropertySetError(errorCode, propId, areaId));
+ CarServiceUtils.runOnLooperSync(mHandlerThread.getLooper(), () -> {});
// Assert
verify(mPowerHalService).onPropertySetError(propId, areaId, errorCode);
diff --git a/tests/carservice_unit_test/src/com/android/car/pm/CarSafetyAccessibilityServiceTest.java b/tests/carservice_unit_test/src/com/android/car/pm/CarSafetyAccessibilityServiceTest.java
new file mode 100644
index 0000000..7494f5d
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/pm/CarSafetyAccessibilityServiceTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021 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.pm;
+
+
+import static org.mockito.Mockito.verify;
+
+import android.car.AbstractExtendedMockitoCarServiceTestCase;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.car.CarLocalServices;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+@RunWith(JUnit4.class)
+public class CarSafetyAccessibilityServiceTest extends AbstractExtendedMockitoCarServiceTestCase {
+ @Mock
+ private CarPackageManagerService mMockCarPackageManagerService;
+
+ private CarSafetyAccessibilityService mCarSafetyAccessibilityService;
+
+ @Before
+ public void setup() {
+ mockGetCarLocalService(CarPackageManagerService.class, mMockCarPackageManagerService);
+ mCarSafetyAccessibilityService = new CarSafetyAccessibilityService();
+ }
+
+ @Test
+ public void onAccessibilityEvent_carPackageManagerServiceNotified() {
+ mCarSafetyAccessibilityService.onAccessibilityEvent(new AccessibilityEvent());
+
+ verify(mMockCarPackageManagerService).onWindowChangeEvent();
+ }
+
+ @Override
+ protected void onSessionBuilder(CustomMockitoSessionBuilder session) {
+ session.spyStatic(CarLocalServices.class);
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/pm/WindowDumpParserTest.java b/tests/carservice_unit_test/src/com/android/car/pm/WindowDumpParserTest.java
new file mode 100644
index 0000000..b64c6e5
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/pm/WindowDumpParserTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2021 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.pm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class WindowDumpParserTest {
+ private static final String WINDOW_DUMP = "WINDOW MANAGER WINDOWS (dumpsys window windows)\n"
+ + " Window #0 Window{ccae0fb u10 com.app1}:\n"
+ + " mDisplayId=0 rootTaskId=1000006 mSession=Session{ef1bfd2 2683:u10a10088} "
+ + "mClient=android.os.BinderProxy@46bd28a\n"
+ + " mOwnerUid=1010088 showForAllUsers=false package=com.app1 "
+ + "appop=SYSTEM_ALERT_WINDOW\n"
+ + " mAttrs={(0,0)(0x0) gr=TOP RIGHT CENTER sim={adjust=pan} ty=APPLICATION_OVERLAY"
+ + " fmt=TRANSPARENT\n"
+ + " isOnScreen=true\n"
+ + " isVisible=true\n"
+
+ + " Window #1 Window{17aaef4 u0 App 2}:\n"
+ + " mDisplayId=1 rootTaskId=1000006 mSession=Session{629ba4e 2235:u0a10120} "
+ + "mClient=android.os.Binderproxy@3f3ea06\n"
+ + " mOwnerUid=10120 showForAllUsers=true package=com.app2 appop=NONE\n"
+ + " mAttrs={(24,0)(84x419) gr=BOTTOM RIGHT CENTER sim={adjust=pan} "
+ + "ty=DISPLAY_OVERLAY fmt=TRANSLUCENT\n"
+ + " fl=NOT_FOCUSABLE LAYOUT_NO_LIMITS HARDWARE_ACCELERATED\n"
+ + " pfl=NO_MOVE_ANIMATION USE_BLAST INSET_PARENT_FRAME_BY_IME\n"
+ + " bhv=DEFAULT\n"
+ + " fitTypes=STATUS_BARS NAVIGATION_BARS CAPTION_BAR}\n"
+ + " Requested w=1080 h=1920 mLayoutSeq=154\n"
+ + " mBaseLayer=21000 mSubLayer=0 mToken=AppWindowToken{546fe66 token=Token"
+ + "{8b7a6c1 ActivityRecord{2789ba8 u0 com.app2/.MainActivity t5}}}\n"
+ + " mAppToken=AppWindowToken{546fe66 token=Token{8b7a6c1 ActivityRecord{2789ba8 u0"
+ + " com.app2/.MainActivity t5}}}\n"
+
+ + ".BinderProxy@3f3ea06}\n"
+ + " Frames: containing=[0,0][1080,600] parent=[0,0][1080,600] display=[-10000,"
+ + "-10000][10000,10000]\n"
+ + " mFrame=[972,181][1056,600] last=[0,0][0,0]\n"
+ + " surface=[0,0][0,0]\n"
+ + " WindowStateAnimator{8edd4a7 HVAC Passenger Temp}:\n"
+ + " mDrawState=NO_SURFACE mLastHidden=false\n"
+ + " mEnterAnimationPending=false mSystemDecorRect=[0,0][0,0]\n"
+ + " mShownAlpha=0.0 mAlpha=1.0 mLastAlpha=0.0\n"
+ + " mForceSeamlesslyRotate=false seamlesslyRotate: pending=null "
+ + "finishedFrameNumber=0\n"
+ + " isOnScreen=false\n"
+ + " isVisible=false\n"
+
+ + " Window #2 Window{1c5571 u0 HVAC Driver Temp}:\n"
+ + " mDisplayId=1 rootTaskId=1000006 mSession=Session{629ba4e 2235:u0a10120} "
+ + "mClient=android.os.BinderProxy@99ccafb\n"
+ + " mOwnerUid=10120 showForAllUsers=true package=com.app2 appop=NONE\n"
+ + " mAttrs={(24,0)(84x419) gr=BOTTOM LEFT CENTER sim={adjust=pan} "
+ + "ty=DISPLAY_OVERLAY fmt=TRANSLUCENT\n"
+ + " fl=NOT_FOCUSABLE LAYOUT_NO_LIMITS HARDWARE_ACCELERATED\n"
+ + " pfl=NO_MOVE_ANIMATION USE_BLAST INSET_PARENT_FRAME_BY_IME\n"
+ + " bhv=DEFAULT\n"
+ + " fitTypes=STATUS_BARS NAVIGATION_BARS CAPTION_BAR}\n"
+ + " Requested w=84 h=419 mLayoutSeq=143\n"
+ + " mBaseLayer=21000 mSubLayer=0 mToken=ActivityRecord{b44066 u10 com.app2/"
+ + "SecondActivity t1000031}\n"
+ + " mActivityRecord=ActivityRecord{b44066 u10 com.app2/SecondActivity t1000031}\n"
+ + ".BinderProxy@99ccafb}\n"
+ + " mViewVisibility=0x4 mHaveFrame=true mObscured=false\n"
+ + " mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]\n"
+ + " isOnScreen=false\n"
+ + " isVisible=false\n"
+
+ + " Window #3 Window{1c5571 u0 HVAC Driver Temp}:\n"
+ + " mDisplayId=2 rootTaskId=1000006 mSession=Session{629ba4e 2235:u0a10120} "
+ + "mClient=android.os.BinderProxy@99ccafb\n"
+ + " mOwnerUid=10120 showForAllUsers=true package=com.app3 appop=NONE\n"
+ + " mAttrs={(24,0)(84x419) gr=BOTTOM LEFT CENTER sim={adjust=pan} "
+ + "ty=DISPLAY_OVERLAY fmt=TRANSLUCENT\n"
+ + " fl=NOT_FOCUSABLE LAYOUT_NO_LIMITS HARDWARE_ACCELERATED\n"
+ + " pfl=NO_MOVE_ANIMATION USE_BLAST INSET_PARENT_FRAME_BY_IME\n"
+ + " bhv=DEFAULT\n"
+ + " fitTypes=STATUS_BARS NAVIGATION_BARS CAPTION_BAR}\n"
+ + " Requested w=84 h=419 mLayoutSeq=143\n"
+ + " mBaseLayer=291000 mSubLayer=0 mToken=WindowToken{6bd1718 android.os"
+ + " mActivityRecord=ActivityRecord{a3f066 u10 com.app3/MainActivity t1000031}\n"
+ + ".BinderProxy@99ccafb}\n"
+ + " mViewVisibility=0x4 mHaveFrame=true mObscured=false\n"
+ + " mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]\n"
+ + " isOnScreen=false\n"
+ + " isVisible=false\n"
+
+ + " Window #4 Window{1c5571 u0 HVAC Driver Temp}:\n"
+ + " mDisplayId=2 rootTaskId=1000006 mSession=Session{629ba4e 2235:u0a10120} "
+ + "mClient=android.os.BinderProxy@99ccafb\n"
+ + " mOwnerUid=10120 showForAllUsers=true package=com.app3 appop=NONE\n"
+ + " mAttrs={(24,0)(84x419) gr=BOTTOM LEFT CENTER sim={adjust=pan} "
+ + "ty=APPLICATION_STARTING fmt=TRANSLUCENT\n"
+ + " fl=NOT_FOCUSABLE LAYOUT_NO_LIMITS HARDWARE_ACCELERATED\n"
+ + " pfl=NO_MOVE_ANIMATION USE_BLAST INSET_PARENT_FRAME_BY_IME\n"
+ + " bhv=DEFAULT\n"
+ + " fitTypes=STATUS_BARS NAVIGATION_BARS CAPTION_BAR}\n"
+ + " Requested w=84 h=419 mLayoutSeq=143\n"
+ + " mBaseLayer=291000 mSubLayer=0 mToken=WindowToken{6bd1718 android.os"
+ + ".BinderProxy@99ccafb}\n"
+ + " mViewVisibility=0x4 mHaveFrame=true mObscured=false\n"
+ + " mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]\n"
+ + " isOnScreen=false\n"
+ + " isVisible=false\n";
+
+ private static final WindowDumpParser.Window APP_1_WINDOW = new WindowDumpParser.Window(
+ "com.app1", 0, null);
+ private static final WindowDumpParser.Window APP_2_WINDOW = new WindowDumpParser.Window(
+ "com.app2", 1, "2789ba8 u0 com.app2/.MainActivity t5");
+ private static final WindowDumpParser.Window APP_2_WINDOW_2 = new WindowDumpParser.Window(
+ "com.app2", 1, "b44066 u10 com.app2/SecondActivity t1000031");
+ private static final WindowDumpParser.Window APP_3_WINDOW = new WindowDumpParser.Window(
+ "com.app3", 2, "a3f066 u10 com.app3/MainActivity t1000031");
+
+ @Test
+ public void testWindowDumpParsing() {
+ assertThat(WindowDumpParser.getParsedAppWindows(WINDOW_DUMP, "com.app1")).containsExactly(
+ APP_1_WINDOW);
+ assertThat(WindowDumpParser.getParsedAppWindows(WINDOW_DUMP, "com.app2")).containsExactly(
+ APP_2_WINDOW, APP_2_WINDOW_2);
+ assertThat(WindowDumpParser.getParsedAppWindows(WINDOW_DUMP, "com.app3")).containsExactly(
+ APP_3_WINDOW);
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/power/SilentModeHandlerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/power/SilentModeHandlerUnitTest.java
index 6c3637b..2be3075 100644
--- a/tests/carservice_unit_test/src/com/android/car/power/SilentModeHandlerUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/power/SilentModeHandlerUnitTest.java
@@ -71,11 +71,11 @@
writeStringToFile(mFileHwStateMonitoring.getFile(), VALUE_SILENT_MODE);
- assertSilentMode(handler, /* expectedMode= */ true);
+ assertSilentMode(handler, /* isForcedMode= */ false, /* expectedMode= */ true);
writeStringToFile(mFileHwStateMonitoring.getFile(), VALUE_NON_SILENT_MODE);
- assertSilentMode(handler, /* expectedMode= */ false);
+ assertSilentMode(handler, /* isForcedMode= */ false, /* expectedMode= */ false);
}
@Test
@@ -169,14 +169,14 @@
writeStringToFile(mFileHwStateMonitoring.getFile(),
initSilentMode ? VALUE_SILENT_MODE : VALUE_NON_SILENT_MODE);
- assertSilentMode(handler, initSilentMode);
+ assertSilentMode(handler, /* isForcedMode= */ false, initSilentMode);
handler.setSilentMode(expectedSilentMode ? SilentModeHandler.SILENT_MODE_FORCED_SILENT
: SilentModeHandler.SILENT_MODE_FORCED_NON_SILENT);
writeStringToFile(mFileHwStateMonitoring.getFile(),
- expectedSilentMode ? VALUE_SILENT_MODE : VALUE_NON_SILENT_MODE);
+ expectedSilentMode ? VALUE_NON_SILENT_MODE : VALUE_SILENT_MODE);
- assertSilentMode(handler, expectedSilentMode);
+ assertSilentMode(handler, /* isForcedMode= */ true, expectedSilentMode);
}
private void testSetSilentMode_toNonForced(boolean initSilentMode) throws Exception {
@@ -191,7 +191,7 @@
writeStringToFile(mFileHwStateMonitoring.getFile(),
initSilentMode ? VALUE_NON_SILENT_MODE : VALUE_SILENT_MODE);
- assertSilentMode(handler, !initSilentMode);
+ assertSilentMode(handler, /* isForcedMode= */ false, !initSilentMode);
verify(mCarPowerManagementService, timeout(BUFFER_TIME_TO_AVOID_RACE_CONDITION))
.notifySilentModeChange(!initSilentMode);
}
@@ -212,12 +212,13 @@
}
}
- private void assertSilentMode(SilentModeHandler handler, boolean expectedMode)
- throws Exception {
+ private void assertSilentMode(SilentModeHandler handler, boolean isForcedMode,
+ boolean expectedMode) throws Exception {
String expectedValue = expectedMode ? VALUE_SILENT_MODE : VALUE_NON_SILENT_MODE;
for (int i = 0; i < MAX_POLLING_TRIES; i++) {
String contents = readFileAsString(mFileKernelSilentMode.getPath());
- if (handler.isSilentMode() == expectedMode && contents.equals(expectedValue)) {
+ if (handler.isSilentMode() == expectedMode
+ && (isForcedMode || contents.equals(expectedValue))) {
return;
}
SystemClock.sleep(POLLING_DELAY_MS);
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/CarTelemetryServiceTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/CarTelemetryServiceTest.java
index 0e9a168..dcd45fa 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/CarTelemetryServiceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/CarTelemetryServiceTest.java
@@ -17,107 +17,253 @@
package com.android.car.telemetry;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.car.telemetry.CarTelemetryManager;
-import android.car.telemetry.ManifestKey;
+import android.car.telemetry.ICarTelemetryServiceListener;
+import android.car.telemetry.MetricsConfigKey;
import android.content.Context;
+import android.os.Handler;
+import android.os.PersistableBundle;
import androidx.test.filters.SmallTest;
+import com.android.car.CarLocalServices;
+import com.android.car.CarPropertyService;
+import com.android.car.systeminterface.SystemInterface;
+import com.android.car.systeminterface.SystemStateInterface;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.MockitoRule;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.nio.file.Files;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(MockitoJUnitRunner.class)
@SmallTest
public class CarTelemetryServiceTest {
+ private static final long TIMEOUT_MS = 15_000L;
+ private static final String METRICS_CONFIG_NAME = "my_metrics_config";
+ private static final MetricsConfigKey KEY_V1 = new MetricsConfigKey(METRICS_CONFIG_NAME, 1);
+ private static final MetricsConfigKey KEY_V2 = new MetricsConfigKey(METRICS_CONFIG_NAME, 2);
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_V1 =
+ TelemetryProto.MetricsConfig.newBuilder()
+ .setName(METRICS_CONFIG_NAME).setVersion(1).setScript("no-op").build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_V2 =
+ TelemetryProto.MetricsConfig.newBuilder()
+ .setName(METRICS_CONFIG_NAME).setVersion(2).setScript("no-op").build();
+
+ private CountDownLatch mIdleHandlerLatch = new CountDownLatch(1);
+ private CarTelemetryService mService;
+ private File mTempSystemCarDir;
+ private Handler mTelemetryHandler;
+ private MetricsConfigStore mMetricsConfigStore;
+ private ResultStore mResultStore;
+
@Rule
public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock
+ private CarPropertyService mMockCarPropertyService;
+ @Mock
private Context mContext;
-
- private final ManifestKey mManifestKeyV1 = new ManifestKey("Name", 1);
- private final ManifestKey mManifestKeyV2 = new ManifestKey("Name", 2);
- private final TelemetryProto.MetricsConfig mMetricsConfig =
- TelemetryProto.MetricsConfig.newBuilder().setScript("no-op").build();
-
- private CarTelemetryService mService;
+ @Mock
+ private ICarTelemetryServiceListener mMockListener;
+ @Mock
+ private SystemInterface mMockSystemInterface;
+ @Mock
+ private SystemStateInterface mMockSystemStateInterface;
@Before
- public void setUp() {
- mService = new CarTelemetryService(mContext);
+ public void setUp() throws Exception {
+ CarLocalServices.removeServiceForTest(SystemInterface.class);
+ CarLocalServices.addService(SystemInterface.class, mMockSystemInterface);
+
+ mTempSystemCarDir = Files.createTempDirectory("telemetry_test").toFile();
+ when(mMockSystemInterface.getSystemCarDir()).thenReturn(mTempSystemCarDir);
+ when(mMockSystemInterface.getSystemStateInterface()).thenReturn(mMockSystemStateInterface);
+
+ mService = new CarTelemetryService(mContext, mMockCarPropertyService);
+ mService.init();
+ mService.setListener(mMockListener);
+
+ mTelemetryHandler = mService.getTelemetryHandler();
+ mTelemetryHandler.getLooper().getQueue().addIdleHandler(() -> {
+ mIdleHandlerLatch.countDown();
+ return true;
+ });
+ waitForHandlerThreadToFinish();
+
+ mMetricsConfigStore = mService.getMetricsConfigStore();
+ mResultStore = mService.getResultStore();
}
@Test
- public void testAddManifest_newManifest_shouldSucceed() {
- int result = mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+ public void testAddMetricsConfig_newMetricsConfig_shouldSucceed() throws Exception {
+ mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
- assertThat(result).isEqualTo(CarTelemetryManager.ERROR_NONE);
+ waitForHandlerThreadToFinish();
+ verify(mMockListener).onAddMetricsConfigStatus(
+ eq(KEY_V1), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_NONE));
}
@Test
- public void testAddManifest_duplicateManifest_shouldFail() {
- mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+ public void testAddMetricsConfig_duplicateMetricsConfig_shouldFail() throws Exception {
+ mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+ waitForHandlerThreadToFinish();
+ verify(mMockListener).onAddMetricsConfigStatus(
+ eq(KEY_V1), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_NONE));
- int result = mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+ mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
- assertThat(result).isEqualTo(CarTelemetryManager.ERROR_SAME_MANIFEST_EXISTS);
+ waitForHandlerThreadToFinish();
+ verify(mMockListener).onAddMetricsConfigStatus(
+ eq(KEY_V1), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_ALREADY_EXISTS));
}
@Test
- public void testAddManifest_invalidManifest_shouldFail() {
- int result = mService.addManifest(mManifestKeyV1, "bad manifest".getBytes());
+ public void testAddMetricsConfig_invalidMetricsConfig_shouldFail() throws Exception {
+ mService.addMetricsConfig(KEY_V1, "bad config".getBytes());
- assertThat(result).isEqualTo(CarTelemetryManager.ERROR_PARSE_MANIFEST_FAILED);
+ waitForHandlerThreadToFinish();
+ verify(mMockListener).onAddMetricsConfigStatus(
+ eq(KEY_V1), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_PARSE_FAILED));
}
@Test
- public void testAddManifest_olderManifest_shouldFail() {
- mService.addManifest(mManifestKeyV2, mMetricsConfig.toByteArray());
+ public void testAddMetricsConfig_olderMetricsConfig_shouldFail() throws Exception {
+ mService.addMetricsConfig(KEY_V2, METRICS_CONFIG_V2.toByteArray());
+ waitForHandlerThreadToFinish();
+ verify(mMockListener).onAddMetricsConfigStatus(
+ eq(KEY_V2), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_NONE));
- int result = mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+ mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
- assertThat(result).isEqualTo(CarTelemetryManager.ERROR_NEWER_MANIFEST_EXISTS);
+ waitForHandlerThreadToFinish();
+ verify(mMockListener).onAddMetricsConfigStatus(
+ eq(KEY_V1), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_VERSION_TOO_OLD));
}
@Test
- public void testAddManifest_newerManifest_shouldReplace() {
- mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+ public void testAddMetricsConfig_newerMetricsConfig_shouldReplaceAndDeleteOldResult()
+ throws Exception {
+ mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+ mResultStore.putInterimResult(KEY_V1.getName(), new PersistableBundle());
- int result = mService.addManifest(mManifestKeyV2, mMetricsConfig.toByteArray());
+ mService.addMetricsConfig(KEY_V2, METRICS_CONFIG_V2.toByteArray());
- assertThat(result).isEqualTo(CarTelemetryManager.ERROR_NONE);
+ waitForHandlerThreadToFinish();
+ verify(mMockListener).onAddMetricsConfigStatus(
+ eq(KEY_V2), eq(CarTelemetryManager.ERROR_METRICS_CONFIG_NONE));
+ assertThat(mMetricsConfigStore.getActiveMetricsConfigs())
+ .containsExactly(METRICS_CONFIG_V2);
+ assertThat(mResultStore.getInterimResult(KEY_V1.getName())).isNull();
}
@Test
- public void testRemoveManifest_manifestExists_shouldSucceed() {
- mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
+ public void testRemoveMetricsConfig_shouldDeleteConfigAndResult() throws Exception {
+ mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+ mResultStore.putInterimResult(KEY_V1.getName(), new PersistableBundle());
- boolean result = mService.removeManifest(mManifestKeyV1);
+ mService.removeMetricsConfig(KEY_V1);
- assertThat(result).isTrue();
+ waitForHandlerThreadToFinish();
+ assertThat(mMetricsConfigStore.getActiveMetricsConfigs()).isEmpty();
+ assertThat(mResultStore.getInterimResult(KEY_V1.getName())).isNull();
}
@Test
- public void testRemoveManifest_manifestDoesNotExist_shouldFail() {
- boolean result = mService.removeManifest(mManifestKeyV1);
+ public void testRemoveAllMetricsConfigs_shouldRemoveConfigsAndResults() throws Exception {
+ MetricsConfigKey key = new MetricsConfigKey("test config", 2);
+ TelemetryProto.MetricsConfig config =
+ TelemetryProto.MetricsConfig.newBuilder().setName(key.getName()).build();
+ mService.addMetricsConfig(key, config.toByteArray());
+ mService.addMetricsConfig(KEY_V1, METRICS_CONFIG_V1.toByteArray());
+ mResultStore.putInterimResult(KEY_V1.getName(), new PersistableBundle());
+ mResultStore.putFinalResult(key.getName(), new PersistableBundle());
- assertThat(result).isFalse();
+ mService.removeAllMetricsConfigs();
+
+ waitForHandlerThreadToFinish();
+ assertThat(mMetricsConfigStore.getActiveMetricsConfigs()).isEmpty();
+ assertThat(mResultStore.getInterimResult(KEY_V1.getName())).isNull();
+ assertThat(mResultStore.getFinalResult(key.getName(), /* deleteResult = */ false)).isNull();
}
@Test
- public void testRemoveAllManifests_shouldSucceed() {
- mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
- mService.addManifest(mManifestKeyV2, mMetricsConfig.toByteArray());
+ public void testSendFinishedReports_whenNoReport_shouldNotReceiveResponse() throws Exception {
+ mService.sendFinishedReports(KEY_V1);
- mService.removeAllManifests();
+ waitForHandlerThreadToFinish();
+ verify(mMockListener, never()).onResult(any(), any());
+ verify(mMockListener, never()).onError(any(), any());
+ }
- // verify that the manifests are cleared by adding them again, should succeed
- int result = mService.addManifest(mManifestKeyV1, mMetricsConfig.toByteArray());
- assertThat(result).isEqualTo(CarTelemetryManager.ERROR_NONE);
- result = mService.addManifest(mManifestKeyV2, mMetricsConfig.toByteArray());
- assertThat(result).isEqualTo(CarTelemetryManager.ERROR_NONE);
+ @Test
+ public void testSendFinishedReports_whenFinalResult_shouldReceiveResult() throws Exception {
+ PersistableBundle finalResult = new PersistableBundle();
+ finalResult.putBoolean("finished", true);
+ mResultStore.putFinalResult(KEY_V1.getName(), finalResult);
+
+ mService.sendFinishedReports(KEY_V1);
+
+ waitForHandlerThreadToFinish();
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ finalResult.writeToStream(bos);
+ verify(mMockListener).onResult(eq(KEY_V1), eq(bos.toByteArray()));
+ // result should have been deleted
+ assertThat(mResultStore.getFinalResult(KEY_V1.getName(), false)).isNull();
+ }
+
+ @Test
+ public void testSendFinishedReports_whenError_shouldReceiveError() throws Exception {
+ TelemetryProto.TelemetryError error = TelemetryProto.TelemetryError.newBuilder()
+ .setErrorType(TelemetryProto.TelemetryError.ErrorType.LUA_RUNTIME_ERROR)
+ .setMessage("test error")
+ .build();
+ mResultStore.putError(KEY_V1.getName(), error);
+
+ mService.sendFinishedReports(KEY_V1);
+
+ waitForHandlerThreadToFinish();
+ verify(mMockListener).onError(eq(KEY_V1), eq(error.toByteArray()));
+ // error should have been deleted
+ assertThat(mResultStore.getError(KEY_V1.getName(), false)).isNull();
+ }
+
+ @Test
+ public void testSendFinishedReports_whenListenerNotSet_shouldDoNothing() throws Exception {
+ PersistableBundle finalResult = new PersistableBundle();
+ finalResult.putBoolean("finished", true);
+ mResultStore.putFinalResult(KEY_V1.getName(), finalResult);
+ mService.clearListener(); // no listener = no way to send back results
+
+ mService.sendFinishedReports(KEY_V1);
+
+ waitForHandlerThreadToFinish();
+ // if listener is null, nothing should be done, result should still be in result store
+ assertThat(mResultStore.getFinalResult(KEY_V1.getName(), false).toString())
+ .isEqualTo(finalResult.toString());
+ }
+
+ private void waitForHandlerThreadToFinish() throws Exception {
+ assertWithMessage("handler not idle in %sms", TIMEOUT_MS)
+ .that(mIdleHandlerLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+ mIdleHandlerLatch = new CountDownLatch(1); // reset idle handler condition
+ mTelemetryHandler.runWithScissors(() -> { }, TIMEOUT_MS);
}
}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/JniUtilsTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/JniUtilsTest.java
deleted file mode 100644
index 67a5ac8..0000000
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/JniUtilsTest.java
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright (C) 2021 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.telemetry;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.Bundle;
-import android.util.Log;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-@RunWith(JUnit4.class)
-public final class JniUtilsTest {
-
- private static final String TAG = JniUtilsTest.class.getSimpleName();
-
- private static final String BOOLEAN_KEY = "boolean_key";
- private static final String INT_KEY = "int_key";
- private static final String STRING_KEY = "string_key";
- private static final String NUMBER_KEY = "number_key";
-
- private static final boolean BOOLEAN_VALUE = true;
- private static final double NUMBER_VALUE = 0.1;
- private static final int INT_VALUE = 10;
- private static final String STRING_VALUE = "test";
-
- // Pointer to Lua Engine instantiated in native space.
- private long mLuaEnginePtr = 0;
-
- static {
- System.loadLibrary("scriptexecutorjniutils-test");
- }
-
- @Before
- public void setUp() {
- mLuaEnginePtr = nativeCreateLuaEngine();
- }
-
- @After
- public void tearDown() {
- nativeDestroyLuaEngine(mLuaEnginePtr);
- }
-
- // Simply invokes PushBundleToLuaTable native method under test.
- private native void nativePushBundleToLuaTableCaller(long luaEnginePtr, Bundle bundle);
-
- // Creates an instance of LuaEngine on the heap and returns the pointer.
- private native long nativeCreateLuaEngine();
-
- // Destroys instance of LuaEngine on the native side at provided memory address.
- private native void nativeDestroyLuaEngine(long luaEnginePtr);
-
- // Returns size of a Lua object located at the specified position on the stack.
- private native int nativeGetObjectSize(long luaEnginePtr, int index);
-
- /*
- * Family of methods to check if the table on top of the stack has
- * the given value under provided key.
- */
- private native boolean nativeHasBooleanValue(long luaEnginePtr, String key, boolean value);
- private native boolean nativeHasStringValue(long luaEnginePtr, String key, String value);
- private native boolean nativeHasIntValue(long luaEnginePtr, String key, int value);
- private native boolean nativeHasDoubleValue(long luaEnginePtr, String key, double value);
-
- @Test
- public void pushBundleToLuaTable_nullBundleMakesEmptyLuaTable() {
- Log.d(TAG, "Using Lua Engine with address " + mLuaEnginePtr);
- nativePushBundleToLuaTableCaller(mLuaEnginePtr, null);
- // Get the size of the object on top of the stack,
- // which is where our table is supposed to be.
- assertThat(nativeGetObjectSize(mLuaEnginePtr, 1)).isEqualTo(0);
- }
-
- @Test
- public void pushBundleToLuaTable_valuesOfDifferentTypes() {
- Bundle bundle = new Bundle();
- bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
- bundle.putInt(INT_KEY, INT_VALUE);
- bundle.putDouble(NUMBER_KEY, NUMBER_VALUE);
- bundle.putString(STRING_KEY, STRING_VALUE);
-
- // Invokes the corresponding helper method to convert the bundle
- // to Lua table on Lua stack.
- nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
-
- // Check contents of Lua table.
- assertThat(nativeHasBooleanValue(mLuaEnginePtr, BOOLEAN_KEY, BOOLEAN_VALUE)).isTrue();
- assertThat(nativeHasIntValue(mLuaEnginePtr, INT_KEY, INT_VALUE)).isTrue();
- assertThat(nativeHasDoubleValue(mLuaEnginePtr, NUMBER_KEY, NUMBER_VALUE)).isTrue();
- assertThat(nativeHasStringValue(mLuaEnginePtr, STRING_KEY, STRING_VALUE)).isTrue();
- }
-
-
- @Test
- public void pushBundleToLuaTable_wrongKey() {
- Bundle bundle = new Bundle();
- bundle.putBoolean(BOOLEAN_KEY, BOOLEAN_VALUE);
-
- // Invokes the corresponding helper method to convert the bundle
- // to Lua table on Lua stack.
- nativePushBundleToLuaTableCaller(mLuaEnginePtr, bundle);
-
- // Check contents of Lua table.
- assertThat(nativeHasBooleanValue(mLuaEnginePtr, "wrong key", BOOLEAN_VALUE)).isFalse();
- }
-}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/MetricsConfigStoreTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/MetricsConfigStoreTest.java
new file mode 100644
index 0000000..6d4c5c6
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/MetricsConfigStoreTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2021 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.telemetry;
+
+import static android.car.telemetry.CarTelemetryManager.ERROR_METRICS_CONFIG_NONE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.car.telemetry.CarTelemetryManager;
+import android.car.telemetry.MetricsConfigKey;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class MetricsConfigStoreTest {
+ private static final String NAME_FOO = "Foo";
+ private static final String NAME_BAR = "Bar";
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_FOO =
+ TelemetryProto.MetricsConfig.newBuilder().setName(NAME_FOO).setVersion(1).build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_BAR =
+ TelemetryProto.MetricsConfig.newBuilder().setName(NAME_BAR).setVersion(1).build();
+ private static final MetricsConfigKey KEY_BAR = new MetricsConfigKey(
+ METRICS_CONFIG_BAR.getName(), METRICS_CONFIG_BAR.getVersion());
+
+ private File mTestRootDir;
+ private File mTestMetricsConfigDir;
+ private MetricsConfigStore mMetricsConfigStore;
+
+ @Before
+ public void setUp() throws Exception {
+ mTestRootDir = Files.createTempDirectory("car_telemetry_test").toFile();
+ mTestMetricsConfigDir = new File(mTestRootDir, MetricsConfigStore.METRICS_CONFIG_DIR);
+
+ mMetricsConfigStore = new MetricsConfigStore(mTestRootDir);
+ assertThat(mTestMetricsConfigDir.exists()).isTrue();
+ }
+
+ @Test
+ public void testRetrieveActiveMetricsConfigs_shouldSendConfigsToListener() throws Exception {
+ writeConfigToDisk(METRICS_CONFIG_FOO);
+ writeConfigToDisk(METRICS_CONFIG_BAR);
+ mMetricsConfigStore = new MetricsConfigStore(mTestRootDir); // reload data
+
+ List<TelemetryProto.MetricsConfig> result = mMetricsConfigStore.getActiveMetricsConfigs();
+
+ assertThat(result).containsExactly(METRICS_CONFIG_FOO, METRICS_CONFIG_BAR);
+ }
+
+ @Test
+ public void testAddMetricsConfig_shouldWriteConfigToDisk() throws Exception {
+ int status = mMetricsConfigStore.addMetricsConfig(METRICS_CONFIG_FOO);
+
+ assertThat(status).isEqualTo(CarTelemetryManager.ERROR_METRICS_CONFIG_NONE);
+ assertThat(readConfigFromFile(NAME_FOO)).isEqualTo(METRICS_CONFIG_FOO);
+ }
+
+ @Test
+ public void testAddMetricsConfig_whenInvalidVersion_shouldNotWriteToDisk() {
+ TelemetryProto.MetricsConfig invalidConfig = TelemetryProto.MetricsConfig.newBuilder()
+ .setName(NAME_BAR).setVersion(-1).build();
+
+ int status = mMetricsConfigStore.addMetricsConfig(invalidConfig);
+
+ assertThat(status).isEqualTo(CarTelemetryManager.ERROR_METRICS_CONFIG_VERSION_TOO_OLD);
+ assertThat(new File(mTestMetricsConfigDir, NAME_BAR).exists()).isFalse();
+ }
+
+ @Test
+ public void testRemoveMetricsConfig_shouldDeleteConfigFromDisk() {
+ int status = mMetricsConfigStore.addMetricsConfig(METRICS_CONFIG_BAR);
+ assertThat(status).isEqualTo(ERROR_METRICS_CONFIG_NONE);
+
+ mMetricsConfigStore.removeMetricsConfig(KEY_BAR);
+
+ assertThat(new File(mTestMetricsConfigDir, NAME_BAR).exists()).isFalse();
+ }
+
+ @Test
+ public void testRemoveMetricsConfig_whenConfigDoesNotExist_shouldDoNothing() {
+ boolean success = mMetricsConfigStore.removeMetricsConfig(KEY_BAR);
+
+ assertThat(success).isFalse();
+ }
+
+ @Test
+ public void testRemoveAllMetricsConfigs_shouldDeleteAll() throws Exception {
+ writeConfigToDisk(METRICS_CONFIG_FOO);
+ writeConfigToDisk(METRICS_CONFIG_BAR);
+
+ mMetricsConfigStore.removeAllMetricsConfigs();
+
+ assertThat(mTestMetricsConfigDir.listFiles()).isEmpty();
+ }
+
+ private void writeConfigToDisk(TelemetryProto.MetricsConfig config) throws Exception {
+ File file = new File(mTestMetricsConfigDir, config.getName());
+ Files.write(file.toPath(), config.toByteArray());
+ assertThat(file.exists()).isTrue();
+ }
+
+ private TelemetryProto.MetricsConfig readConfigFromFile(String fileName) throws Exception {
+ byte[] bytes = Files.readAllBytes(new File(mTestMetricsConfigDir, fileName).toPath());
+ return TelemetryProto.MetricsConfig.parseFrom(bytes);
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/ResultStoreTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/ResultStoreTest.java
new file mode 100644
index 0000000..9cedac9
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/ResultStoreTest.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2021 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.telemetry;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.car.telemetry.MetricsConfigKey;
+import android.os.PersistableBundle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ResultStoreTest {
+ private static final PersistableBundle TEST_INTERIM_BUNDLE = new PersistableBundle();
+ private static final PersistableBundle TEST_FINAL_BUNDLE = new PersistableBundle();
+ private static final TelemetryProto.TelemetryError TEST_TELEMETRY_ERROR =
+ TelemetryProto.TelemetryError.newBuilder().setMessage("test error").build();
+
+ private File mTestRootDir;
+ private File mTestInterimResultDir;
+ private File mTestErrorResultDir;
+ private File mTestFinalResultDir;
+ private ResultStore mResultStore;
+
+ @Before
+ public void setUp() throws Exception {
+ TEST_INTERIM_BUNDLE.putString("test key", "interim value");
+ TEST_FINAL_BUNDLE.putString("test key", "final value");
+
+ mTestRootDir = Files.createTempDirectory("car_telemetry_test").toFile();
+ mTestInterimResultDir = new File(mTestRootDir, ResultStore.INTERIM_RESULT_DIR);
+ mTestErrorResultDir = new File(mTestRootDir, ResultStore.ERROR_RESULT_DIR);
+ mTestFinalResultDir = new File(mTestRootDir, ResultStore.FINAL_RESULT_DIR);
+
+ mResultStore = new ResultStore(mTestRootDir);
+ }
+
+ @Test
+ public void testConstructor_shouldCreateResultsFolder() {
+ // constructor is called in setUp()
+ assertThat(mTestInterimResultDir.exists()).isTrue();
+ assertThat(mTestFinalResultDir.exists()).isTrue();
+ }
+
+ @Test
+ public void testConstructor_shouldLoadInterimResultsIntoMemory() throws Exception {
+ String testInterimFileName = "test_file_1";
+ writeBundleToFile(mTestInterimResultDir, testInterimFileName, TEST_INTERIM_BUNDLE);
+
+ mResultStore = new ResultStore(mTestRootDir);
+
+ // should compare value instead of reference
+ assertThat(mResultStore.getInterimResult(testInterimFileName).toString())
+ .isEqualTo(TEST_INTERIM_BUNDLE.toString());
+ }
+
+ @Test
+ public void testFlushToDisk_shouldRemoveStaleData() throws Exception {
+ File staleTestFile1 = new File(mTestInterimResultDir, "stale_test_file_1");
+ File staleTestFile2 = new File(mTestFinalResultDir, "stale_test_file_2");
+ File activeTestFile3 = new File(mTestInterimResultDir, "active_test_file_3");
+ writeBundleToFile(staleTestFile1, TEST_INTERIM_BUNDLE);
+ writeBundleToFile(staleTestFile2, TEST_FINAL_BUNDLE);
+ writeBundleToFile(activeTestFile3, TEST_INTERIM_BUNDLE);
+ long currTimeMs = System.currentTimeMillis();
+ staleTestFile1.setLastModified(0L); // stale
+ staleTestFile2.setLastModified(
+ currTimeMs - TimeUnit.MILLISECONDS.convert(31, TimeUnit.DAYS)); // stale
+ activeTestFile3.setLastModified(
+ currTimeMs - TimeUnit.MILLISECONDS.convert(29, TimeUnit.DAYS)); // active
+
+ mResultStore.flushToDisk();
+
+ assertThat(staleTestFile1.exists()).isFalse();
+ assertThat(staleTestFile2.exists()).isFalse();
+ assertThat(activeTestFile3.exists()).isTrue();
+ }
+
+
+ @Test
+ public void testPutInterimResultAndFlushToDisk_shouldReplaceExistingFile() throws Exception {
+ String newKey = "new key";
+ String newValue = "new value";
+ String metricsConfigName = "my_metrics_config";
+ writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
+ TEST_INTERIM_BUNDLE.putString(newKey, newValue);
+
+ mResultStore.putInterimResult(metricsConfigName, TEST_INTERIM_BUNDLE);
+ mResultStore.flushToDisk();
+
+ PersistableBundle bundle = readBundleFromFile(mTestInterimResultDir, metricsConfigName);
+ assertThat(bundle.getString(newKey)).isEqualTo(newValue);
+ assertThat(bundle.toString()).isEqualTo(TEST_INTERIM_BUNDLE.toString());
+ }
+
+ @Test
+ public void testPutInterimResultAndFlushToDisk_shouldWriteDirtyResultsOnly() throws Exception {
+ File fileFoo = new File(mTestInterimResultDir, "foo");
+ File fileBar = new File(mTestInterimResultDir, "bar");
+ writeBundleToFile(fileFoo, TEST_INTERIM_BUNDLE);
+ writeBundleToFile(fileBar, TEST_INTERIM_BUNDLE);
+ mResultStore = new ResultStore(mTestRootDir); // re-load data
+ PersistableBundle newData = new PersistableBundle();
+ newData.putDouble("pi", 3.1415926);
+
+ mResultStore.putInterimResult("bar", newData); // make bar dirty
+ fileFoo.delete(); // delete the clean file from the file system
+ mResultStore.flushToDisk(); // write dirty data
+
+ // foo is a clean file that should not be written in flushToDisk()
+ assertThat(fileFoo.exists()).isFalse();
+ assertThat(readBundleFromFile(fileBar).toString()).isEqualTo(newData.toString());
+ }
+
+ @Test
+ public void testGetFinalResult_whenNoData_shouldReceiveNull() throws Exception {
+ String metricsConfigName = "my_metrics_config";
+
+ PersistableBundle bundle = mResultStore.getFinalResult(metricsConfigName, true);
+
+ assertThat(bundle).isNull();
+ }
+
+ @Test
+ public void testGetFinalResult_whenDataCorrupt_shouldReceiveNull() throws Exception {
+ String metricsConfigName = "my_metrics_config";
+ Files.write(new File(mTestFinalResultDir, metricsConfigName).toPath(),
+ "not a bundle".getBytes(StandardCharsets.UTF_8));
+
+ PersistableBundle bundle = mResultStore.getFinalResult(metricsConfigName, true);
+
+ assertThat(bundle).isNull();
+ }
+
+ @Test
+ public void testGetFinalResult_whenDeleteFlagTrue_shouldDeleteData() throws Exception {
+ String testFinalFileName = "my_metrics_config";
+ writeBundleToFile(mTestFinalResultDir, testFinalFileName, TEST_FINAL_BUNDLE);
+
+ PersistableBundle bundle = mResultStore.getFinalResult(testFinalFileName, true);
+
+ // should compare value instead of reference
+ assertThat(bundle.toString()).isEqualTo(TEST_FINAL_BUNDLE.toString());
+ assertThat(new File(mTestFinalResultDir, testFinalFileName).exists()).isFalse();
+ }
+
+ @Test
+ public void testGetFinalResult_whenDeleteFlagFalse_shouldNotDeleteData() throws Exception {
+ String testFinalFileName = "my_metrics_config";
+ writeBundleToFile(mTestFinalResultDir, testFinalFileName, TEST_FINAL_BUNDLE);
+
+ PersistableBundle bundle = mResultStore.getFinalResult(testFinalFileName, false);
+
+ // should compare value instead of reference
+ assertThat(bundle.toString()).isEqualTo(TEST_FINAL_BUNDLE.toString());
+ assertThat(new File(mTestFinalResultDir, testFinalFileName).exists()).isTrue();
+ }
+
+ @Test
+ public void testGetError_whenNoError_shouldReceiveNull() {
+ String metricsConfigName = "my_metrics_config";
+
+ TelemetryProto.TelemetryError error = mResultStore.getError(metricsConfigName, true);
+
+ assertThat(error).isNull();
+ }
+
+ @Test
+ public void testGetError_shouldReceiveError() throws Exception {
+ String metricsConfigName = "my_metrics_config";
+ // write serialized error object to file
+ Files.write(
+ new File(mTestErrorResultDir, metricsConfigName).toPath(),
+ TEST_TELEMETRY_ERROR.toByteArray());
+
+ TelemetryProto.TelemetryError error = mResultStore.getError(metricsConfigName, true);
+
+ assertThat(error).isEqualTo(TEST_TELEMETRY_ERROR);
+ }
+
+ @Test
+ public void testPutFinalResult_shouldWriteResultAndRemoveInterim() throws Exception {
+ String metricsConfigName = "my_metrics_config";
+ writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
+
+ mResultStore.putFinalResult(metricsConfigName, TEST_FINAL_BUNDLE);
+
+ assertThat(mResultStore.getInterimResult(metricsConfigName)).isNull();
+ assertThat(new File(mTestInterimResultDir, metricsConfigName).exists()).isFalse();
+ assertThat(new File(mTestFinalResultDir, metricsConfigName).exists()).isTrue();
+ }
+
+ @Test
+ public void testPutError_shouldWriteErrorAndRemoveInterimResultFile() throws Exception {
+ String metricsConfigName = "my_metrics_config";
+ writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
+
+ mResultStore.putError(metricsConfigName, TEST_TELEMETRY_ERROR);
+
+ assertThat(new File(mTestInterimResultDir, metricsConfigName).exists()).isFalse();
+ assertThat(new File(mTestErrorResultDir, metricsConfigName).exists()).isTrue();
+ }
+
+ @Test
+ public void testRemoveResult_whenInterimResult_shouldDelete() throws Exception {
+ String metricsConfigName = "my_metrics_config";
+ MetricsConfigKey key = new MetricsConfigKey(metricsConfigName, 1);
+ writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
+
+ mResultStore.removeResult(key);
+
+ assertThat(new File(mTestInterimResultDir, metricsConfigName).exists()).isFalse();
+ }
+
+ @Test
+ public void testRemoveResult_whenFinalResult_shouldDelete() throws Exception {
+ String metricsConfigName = "my_metrics_config";
+ MetricsConfigKey key = new MetricsConfigKey(metricsConfigName, 1);
+ writeBundleToFile(mTestFinalResultDir, metricsConfigName, TEST_FINAL_BUNDLE);
+
+ mResultStore.removeResult(key);
+
+ assertThat(new File(mTestFinalResultDir, metricsConfigName).exists()).isFalse();
+ }
+
+ @Test
+ public void testRemoveAllResults_shouldDeleteAll() throws Exception {
+ mResultStore.putInterimResult("config 1", TEST_INTERIM_BUNDLE);
+ mResultStore.putFinalResult("config 2", TEST_FINAL_BUNDLE);
+ mResultStore.putError("config 3", TEST_TELEMETRY_ERROR);
+ mResultStore.flushToDisk();
+
+ mResultStore.removeAllResults();
+
+ assertThat(mTestInterimResultDir.listFiles()).isEmpty();
+ assertThat(mTestFinalResultDir.listFiles()).isEmpty();
+ }
+
+ private void writeBundleToFile(
+ File dir, String fileName, PersistableBundle persistableBundle) throws Exception {
+ writeBundleToFile(new File(dir, fileName), persistableBundle);
+ }
+
+ /**
+ * Writes a persistable bundle to the result directory with the given directory and file name,
+ * and verifies that it was successfully written.
+ */
+ private void writeBundleToFile(
+ File file, PersistableBundle persistableBundle) throws Exception {
+ try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
+ persistableBundle.writeToStream(byteArrayOutputStream);
+ Files.write(file.toPath(), byteArrayOutputStream.toByteArray());
+ }
+ assertWithMessage("bundle is not written to the result directory")
+ .that(file.exists()).isTrue();
+ }
+
+ private PersistableBundle readBundleFromFile(File dir, String fileName) throws Exception {
+ return readBundleFromFile(new File(dir, fileName));
+ }
+
+ /** Reads a persistable bundle from the given path. */
+ private PersistableBundle readBundleFromFile(File file) throws Exception {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ return PersistableBundle.readFromStream(fis);
+ }
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java
deleted file mode 100644
index 774da25..0000000
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/ScriptExecutorTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2021 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.telemetry;
-
-import static org.junit.Assert.fail;
-
-import android.car.telemetry.IScriptExecutor;
-import android.car.telemetry.IScriptExecutorListener;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.RemoteException;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@RunWith(JUnit4.class)
-public final class ScriptExecutorTest {
-
- private IScriptExecutor mScriptExecutor;
- private ScriptExecutor mInstance;
- private Context mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
-
-
- private static final class ScriptExecutorListener extends IScriptExecutorListener.Stub {
- @Override
- public void onScriptFinished(byte[] result) {
- }
-
- @Override
- public void onSuccess(Bundle stateToPersist) {
- }
-
- @Override
- public void onError(int errorType, String message, String stackTrace) {
- }
- }
-
- private final IScriptExecutorListener mFakeScriptExecutorListener =
- new ScriptExecutorListener();
-
- // TODO(b/189241508). Parsing of publishedData is not implemented yet.
- // Null is the only accepted input.
- private final Bundle mPublishedData = null;
- private final Bundle mSavedState = new Bundle();
-
- private static final String LUA_SCRIPT =
- "function hello(data, state)\n"
- + " print(\"Hello World\")\n"
- + "end\n";
-
- private static final String LUA_METHOD = "hello";
-
- private final CountDownLatch mBindLatch = new CountDownLatch(1);
-
- private static final int BIND_SERVICE_TIMEOUT_SEC = 5;
-
- private final ServiceConnection mScriptExecutorConnection =
- new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName className, IBinder service) {
- mScriptExecutor = IScriptExecutor.Stub.asInterface(service);
- mBindLatch.countDown();
- }
-
- @Override
- public void onServiceDisconnected(ComponentName className) {
- fail("Service unexpectedly disconnected");
- }
- };
-
- @Before
- public void setUp() throws InterruptedException {
- mContext.bindIsolatedService(new Intent(mContext, ScriptExecutor.class),
- Context.BIND_AUTO_CREATE, "scriptexecutor", mContext.getMainExecutor(),
- mScriptExecutorConnection);
- if (!mBindLatch.await(BIND_SERVICE_TIMEOUT_SEC, TimeUnit.SECONDS)) {
- fail("Failed to bind to ScripExecutor service");
- }
- }
-
- @Test
- public void invokeScript_helloWorld() throws RemoteException {
- // Expect to load "hello world" script successfully and push the function.
- mScriptExecutor.invokeScript(LUA_SCRIPT, LUA_METHOD, mPublishedData, mSavedState,
- mFakeScriptExecutorListener);
- // Sleep, otherwise the test case will complete before the script loads
- // because invokeScript is non-blocking.
- try {
- // TODO(b/192285332): Replace sleep logic with waiting for specific callbacks
- // to be called once they are implemented. Otherwise, this could be a flaky test.
- TimeUnit.SECONDS.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- fail(e.getMessage());
- }
- }
-}
-
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerTest.java
new file mode 100644
index 0000000..c056be5
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2021 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.telemetry.databroker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.car.telemetry.MetricsConfigKey;
+import android.os.Handler;
+
+import com.android.car.systeminterface.SystemStateInterface;
+import com.android.car.telemetry.MetricsConfigStore;
+import com.android.car.telemetry.TelemetryProto;
+import com.android.car.telemetry.systemmonitor.SystemMonitor;
+import com.android.car.telemetry.systemmonitor.SystemMonitorEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Arrays;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DataBrokerControllerTest {
+
+ @Mock private DataBroker mMockDataBroker;
+ @Mock private Handler mMockHandler;
+ @Mock private MetricsConfigStore mMockMetricsConfigStore;
+ @Mock private SystemMonitor mMockSystemMonitor;
+ @Mock private SystemStateInterface mMockSystemStateInterface;
+
+ @Captor ArgumentCaptor<TelemetryProto.MetricsConfig> mConfigCaptor;
+
+ @Captor ArgumentCaptor<Integer> mPriorityCaptor;
+
+ @InjectMocks private DataBrokerController mController;
+
+ private static final TelemetryProto.Publisher PUBLISHER =
+ TelemetryProto.Publisher.newBuilder()
+ .setVehicleProperty(
+ TelemetryProto.VehiclePropertyPublisher
+ .newBuilder()
+ .setReadRate(1)
+ .setVehiclePropertyId(1000))
+ .build();
+ private static final TelemetryProto.Subscriber SUBSCRIBER =
+ TelemetryProto.Subscriber.newBuilder()
+ .setHandler("handler_func")
+ .setPublisher(PUBLISHER)
+ .build();
+ private static final TelemetryProto.MetricsConfig CONFIG =
+ TelemetryProto.MetricsConfig.newBuilder()
+ .setName("config_name")
+ .setVersion(1)
+ .setScript("function init() end")
+ .addSubscribers(SUBSCRIBER)
+ .build();
+ private static final MetricsConfigKey CONFIG_KEY = new MetricsConfigKey(
+ CONFIG.getName(), CONFIG.getVersion());
+
+ @Before
+ public void setup() {
+ when(mMockHandler.post(any(Runnable.class))).thenAnswer(i -> {
+ Runnable runnable = i.getArgument(0);
+ runnable.run();
+ return true;
+ });
+ }
+
+ @Test
+ public void testOnInit_setsOnScriptFinishedCallback() {
+ // Checks that mMockDataBroker's setOnScriptFinishedCallback is called after it's injected
+ // into controller's constructor with @InjectMocks
+ verify(mMockDataBroker).setOnScriptFinishedCallback(
+ any(DataBroker.ScriptFinishedCallback.class));
+ }
+
+ @Test
+ public void testOnBootCompleted_shouldStartMetricsCollection() {
+ when(mMockMetricsConfigStore.getActiveMetricsConfigs()).thenReturn(Arrays.asList(CONFIG));
+ ArgumentCaptor<Runnable> mRunnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mMockSystemStateInterface).scheduleActionForBootCompleted(
+ mRunnableCaptor.capture(), any());
+
+ mRunnableCaptor.getValue().run(); // startMetricsCollection();
+
+ verify(mMockDataBroker).addMetricsConfig(eq(CONFIG_KEY), eq(CONFIG));
+ }
+
+ @Test
+ public void testOnScriptFinished_shouldRemoveConfig() {
+ mController.onScriptFinished(CONFIG_KEY);
+
+ verify(mMockMetricsConfigStore).removeMetricsConfig(eq(CONFIG_KEY));
+ verify(mMockDataBroker).removeMetricsConfig(eq(CONFIG_KEY));
+ }
+
+ @Test
+ public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForHighCpuUsage() {
+ SystemMonitorEvent highCpuEvent = new SystemMonitorEvent();
+ highCpuEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
+ highCpuEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+
+ mController.onSystemMonitorEvent(highCpuEvent);
+
+ verify(mMockDataBroker, atLeastOnce())
+ .setTaskExecutionPriority(mPriorityCaptor.capture());
+ assertThat(mPriorityCaptor.getValue())
+ .isEqualTo(DataBrokerController.TASK_PRIORITY_HI);
+ }
+
+ @Test
+ public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForHighMemUsage() {
+ SystemMonitorEvent highMemEvent = new SystemMonitorEvent();
+ highMemEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+ highMemEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
+
+ mController.onSystemMonitorEvent(highMemEvent);
+
+ verify(mMockDataBroker, atLeastOnce())
+ .setTaskExecutionPriority(mPriorityCaptor.capture());
+ assertThat(mPriorityCaptor.getValue())
+ .isEqualTo(DataBrokerController.TASK_PRIORITY_HI);
+ }
+
+ @Test
+ public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForMedCpuUsage() {
+ SystemMonitorEvent medCpuEvent = new SystemMonitorEvent();
+ medCpuEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
+ medCpuEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+
+ mController.onSystemMonitorEvent(medCpuEvent);
+
+ verify(mMockDataBroker, atLeastOnce())
+ .setTaskExecutionPriority(mPriorityCaptor.capture());
+ assertThat(mPriorityCaptor.getValue())
+ .isEqualTo(DataBrokerController.TASK_PRIORITY_MED);
+ }
+
+ @Test
+ public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForMedMemUsage() {
+ SystemMonitorEvent medMemEvent = new SystemMonitorEvent();
+ medMemEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+ medMemEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
+
+ mController.onSystemMonitorEvent(medMemEvent);
+
+ verify(mMockDataBroker, atLeastOnce())
+ .setTaskExecutionPriority(mPriorityCaptor.capture());
+ assertThat(mPriorityCaptor.getValue())
+ .isEqualTo(DataBrokerController.TASK_PRIORITY_MED);
+ }
+
+ @Test
+ public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForLowUsage() {
+ SystemMonitorEvent lowUsageEvent = new SystemMonitorEvent();
+ lowUsageEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+ lowUsageEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
+
+ mController.onSystemMonitorEvent(lowUsageEvent);
+
+ verify(mMockDataBroker, atLeastOnce())
+ .setTaskExecutionPriority(mPriorityCaptor.capture());
+ assertThat(mPriorityCaptor.getValue())
+ .isEqualTo(DataBrokerController.TASK_PRIORITY_LOW);
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerUnitTest.java
deleted file mode 100644
index 931a557..0000000
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerControllerUnitTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2021 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.telemetry.databroker;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.verify;
-
-import com.android.car.telemetry.TelemetryProto;
-import com.android.car.telemetry.systemmonitor.SystemMonitor;
-import com.android.car.telemetry.systemmonitor.SystemMonitorEvent;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-public class DataBrokerControllerUnitTest {
-
- @Mock private DataBroker mMockDataBroker;
-
- @Mock private SystemMonitor mMockSystemMonitor;
-
- @Captor ArgumentCaptor<TelemetryProto.MetricsConfig> mConfigCaptor;
-
- @Captor ArgumentCaptor<Integer> mPriorityCaptor;
-
- @InjectMocks private DataBrokerController mController;
-
- private static final TelemetryProto.Publisher PUBLISHER =
- TelemetryProto.Publisher.newBuilder()
- .setVehicleProperty(
- TelemetryProto.VehiclePropertyPublisher
- .newBuilder()
- .setReadRate(1)
- .setVehiclePropertyId(1000))
- .build();
- private static final TelemetryProto.Subscriber SUBSCRIBER =
- TelemetryProto.Subscriber.newBuilder()
- .setHandler("handler_func")
- .setPublisher(PUBLISHER)
- .build();
- private static final TelemetryProto.MetricsConfig CONFIG =
- TelemetryProto.MetricsConfig.newBuilder()
- .setName("config_name")
- .setVersion(1)
- .setScript("function init() end")
- .addSubscribers(SUBSCRIBER)
- .build();
-
- @Test
- public void testOnNewConfig_configPassedToDataBroker() {
- mController.onNewMetricsConfig(CONFIG);
-
- verify(mMockDataBroker).addMetricsConfiguration(mConfigCaptor.capture());
- assertThat(mConfigCaptor.getValue()).isEqualTo(CONFIG);
- }
-
- @Test
- public void testOnInit_setsOnScriptFinishedCallback() {
- // Checks that mMockDataBroker's setOnScriptFinishedCallback is called after it's injected
- // into controller's constructor with @InjectMocks
- verify(mMockDataBroker).setOnScriptFinishedCallback(
- any(DataBrokerController.ScriptFinishedCallback.class));
- }
-
- @Test
- public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForHighCpuUsage() {
- SystemMonitorEvent highCpuEvent = new SystemMonitorEvent();
- highCpuEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
- highCpuEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
-
- mController.onSystemMonitorEvent(highCpuEvent);
-
- verify(mMockDataBroker, atLeastOnce())
- .setTaskExecutionPriority(mPriorityCaptor.capture());
- assertThat(mPriorityCaptor.getValue())
- .isEqualTo(DataBrokerController.TASK_PRIORITY_HI);
- }
-
- @Test
- public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForHighMemUsage() {
- SystemMonitorEvent highMemEvent = new SystemMonitorEvent();
- highMemEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
- highMemEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_HI);
-
- mController.onSystemMonitorEvent(highMemEvent);
-
- verify(mMockDataBroker, atLeastOnce())
- .setTaskExecutionPriority(mPriorityCaptor.capture());
- assertThat(mPriorityCaptor.getValue())
- .isEqualTo(DataBrokerController.TASK_PRIORITY_HI);
- }
-
- @Test
- public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForMedCpuUsage() {
- SystemMonitorEvent medCpuEvent = new SystemMonitorEvent();
- medCpuEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
- medCpuEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
-
- mController.onSystemMonitorEvent(medCpuEvent);
-
- verify(mMockDataBroker, atLeastOnce())
- .setTaskExecutionPriority(mPriorityCaptor.capture());
- assertThat(mPriorityCaptor.getValue())
- .isEqualTo(DataBrokerController.TASK_PRIORITY_MED);
- }
-
- @Test
- public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForMedMemUsage() {
- SystemMonitorEvent medMemEvent = new SystemMonitorEvent();
- medMemEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
- medMemEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_MED);
-
- mController.onSystemMonitorEvent(medMemEvent);
-
- verify(mMockDataBroker, atLeastOnce())
- .setTaskExecutionPriority(mPriorityCaptor.capture());
- assertThat(mPriorityCaptor.getValue())
- .isEqualTo(DataBrokerController.TASK_PRIORITY_MED);
- }
-
- @Test
- public void testOnSystemEvent_setDataBrokerPriorityCorrectlyForLowUsage() {
- SystemMonitorEvent lowUsageEvent = new SystemMonitorEvent();
- lowUsageEvent.setCpuUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
- lowUsageEvent.setMemoryUsageLevel(SystemMonitorEvent.USAGE_LEVEL_LOW);
-
- mController.onSystemMonitorEvent(lowUsageEvent);
-
- verify(mMockDataBroker, atLeastOnce())
- .setTaskExecutionPriority(mPriorityCaptor.capture());
- assertThat(mPriorityCaptor.getValue())
- .isEqualTo(DataBrokerController.TASK_PRIORITY_LOW);
- }
-}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java
new file mode 100644
index 0000000..c5f30c2
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerTest.java
@@ -0,0 +1,641 @@
+/*
+ * Copyright (C) 2021 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.telemetry.databroker;
+
+import static com.android.car.telemetry.databroker.ScriptExecutionTask.APPROX_BUNDLE_SIZE_BYTES_KEY;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.car.AbstractExtendedMockitoCarServiceTestCase;
+import android.car.hardware.CarPropertyConfig;
+import android.car.telemetry.MetricsConfigKey;
+import android.content.Context;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+
+import com.android.car.CarPropertyService;
+import com.android.car.telemetry.ResultStore;
+import com.android.car.telemetry.TelemetryProto;
+import com.android.car.telemetry.publisher.PublisherFactory;
+import com.android.car.telemetry.publisher.StatsManagerProxy;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutor;
+import com.android.car.telemetry.scriptexecutorinterface.IScriptExecutorListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DataBrokerTest extends AbstractExtendedMockitoCarServiceTestCase {
+ private static final int PROP_ID = 100;
+ private static final int PROP_AREA = 200;
+ private static final int PRIORITY_HIGH = 1;
+ private static final int PRIORITY_LOW = 100;
+ private static final long TIMEOUT_MS = 15_000L;
+ private static final CarPropertyConfig<Integer> PROP_CONFIG =
+ CarPropertyConfig.newBuilder(Integer.class, PROP_ID, PROP_AREA).setAccess(
+ CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ).build();
+ private static final TelemetryProto.VehiclePropertyPublisher
+ VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION =
+ TelemetryProto.VehiclePropertyPublisher.newBuilder().setReadRate(
+ 1).setVehiclePropertyId(PROP_ID).build();
+ private static final TelemetryProto.Publisher PUBLISHER_CONFIGURATION =
+ TelemetryProto.Publisher.newBuilder().setVehicleProperty(
+ VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION).build();
+
+ /** MetricsConfig that contains a high priority subscriber. */
+ private static final TelemetryProto.Subscriber SUBSCRIBER_FOO =
+ TelemetryProto.Subscriber.newBuilder().setHandler("function_name_foo").setPublisher(
+ PUBLISHER_CONFIGURATION).setPriority(PRIORITY_HIGH).build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_FOO =
+ TelemetryProto.MetricsConfig.newBuilder().setName("Foo").setVersion(
+ 1).addSubscribers(SUBSCRIBER_FOO).build();
+ private static final MetricsConfigKey KEY_FOO = new MetricsConfigKey(
+ METRICS_CONFIG_FOO.getName(), METRICS_CONFIG_FOO.getVersion());
+
+ /** MetricsConfig that contains a low priority subscriber. */
+ private static final TelemetryProto.Subscriber SUBSCRIBER_BAR =
+ TelemetryProto.Subscriber.newBuilder().setHandler("function_name_bar").setPublisher(
+ PUBLISHER_CONFIGURATION).setPriority(PRIORITY_LOW).build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_BAR =
+ TelemetryProto.MetricsConfig.newBuilder().setName("Bar").setVersion(
+ 1).addSubscribers(SUBSCRIBER_BAR).build();
+ private static final MetricsConfigKey KEY_BAR = new MetricsConfigKey(
+ METRICS_CONFIG_BAR.getName(), METRICS_CONFIG_BAR.getVersion());
+
+
+ // when count reaches 0, all handler messages are scheduled to be dispatched after current time
+ private CountDownLatch mIdleHandlerLatch = new CountDownLatch(1);
+ private PersistableBundle mData = new PersistableBundle();
+ private DataBrokerImpl mDataBroker;
+ private FakeScriptExecutor mFakeScriptExecutor;
+ private ScriptExecutionTask mHighPriorityTask;
+ private ScriptExecutionTask mLowPriorityTask;
+
+ @Mock
+ private Context mMockContext;
+ @Mock
+ private CarPropertyService mMockCarPropertyService;
+ @Mock
+ private DataBroker.ScriptFinishedCallback mMockScriptFinishedCallback;
+ @Mock
+ private Handler mMockHandler;
+ @Mock
+ private StatsManagerProxy mMockStatsManager;
+ @Mock
+ private IBinder mMockScriptExecutorBinder;
+ @Mock
+ private ResultStore mMockResultStore;
+
+ @Before
+ public void setUp() throws Exception {
+ when(mMockCarPropertyService.getPropertyList())
+ .thenReturn(Collections.singletonList(PROP_CONFIG));
+ PublisherFactory factory = new PublisherFactory(
+ mMockCarPropertyService, mMockHandler, mMockStatsManager,
+ Files.createTempDirectory("telemetry_test").toFile());
+ mDataBroker = new DataBrokerImpl(mMockContext, factory, mMockResultStore);
+ mDataBroker.setOnScriptFinishedCallback(mMockScriptFinishedCallback);
+ // add IdleHandler to get notified when all messages and posts are handled
+ mDataBroker.getTelemetryHandler().getLooper().getQueue().addIdleHandler(() -> {
+ mIdleHandlerLatch.countDown();
+ return true;
+ });
+
+ mFakeScriptExecutor = new FakeScriptExecutor();
+ when(mMockScriptExecutorBinder.queryLocalInterface(anyString()))
+ .thenReturn(mFakeScriptExecutor);
+ when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(i -> {
+ ServiceConnection conn = i.getArgument(1);
+ conn.onServiceConnected(null, mMockScriptExecutorBinder);
+ return true;
+ });
+
+ mHighPriorityTask = new ScriptExecutionTask(
+ new DataSubscriber(mDataBroker, METRICS_CONFIG_FOO, SUBSCRIBER_FOO),
+ mData,
+ SystemClock.elapsedRealtime());
+ mLowPriorityTask = new ScriptExecutionTask(
+ new DataSubscriber(mDataBroker, METRICS_CONFIG_BAR, SUBSCRIBER_BAR),
+ mData,
+ SystemClock.elapsedRealtime());
+ }
+
+ @Override
+ protected void onSessionBuilder(CustomMockitoSessionBuilder builder) {
+ builder.spyStatic(ParcelFileDescriptor.class);
+ }
+
+ @Test
+ public void testSetTaskExecutionPriority_whenNoTask_shouldNotInvokeScriptExecutor()
+ throws Exception {
+ mDataBroker.setTaskExecutionPriority(PRIORITY_HIGH);
+
+ waitForTelemetryThreadToFinish();
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void testSetTaskExecutionPriority_whenNextTaskPriorityLow_shouldNotRunTask()
+ throws Exception {
+ mDataBroker.getTaskQueue().add(mLowPriorityTask);
+
+ mDataBroker.setTaskExecutionPriority(PRIORITY_HIGH);
+
+ waitForTelemetryThreadToFinish();
+ // task is not polled
+ assertThat(mDataBroker.getTaskQueue().peek()).isEqualTo(mLowPriorityTask);
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void testSetTaskExecutionPriority_whenNextTaskPriorityHigh_shouldInvokeScriptExecutor()
+ throws Exception {
+ mDataBroker.getTaskQueue().add(mHighPriorityTask);
+
+ mDataBroker.setTaskExecutionPriority(PRIORITY_HIGH);
+
+ waitForTelemetryThreadToFinish();
+ // task is polled and run
+ assertThat(mDataBroker.getTaskQueue().peek()).isNull();
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testScheduleNextTask_whenNoTask_shouldNotInvokeScriptExecutor() throws Exception {
+ mDataBroker.scheduleNextTask();
+
+ waitForTelemetryThreadToFinish();
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void testScheduleNextTask_whenTaskInProgress_shouldNotInvokeScriptExecutorAgain()
+ throws Exception {
+ PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+ taskQueue.add(mHighPriorityTask);
+ mDataBroker.scheduleNextTask(); // start a task
+ waitForTelemetryThreadToFinish();
+ assertThat(taskQueue.peek()).isNull(); // assert that task is polled and running
+ taskQueue.add(mHighPriorityTask); // add another task into the queue
+
+ mDataBroker.scheduleNextTask(); // schedule next task while the last task is in progress
+
+ waitForTelemetryThreadToFinish();
+ // verify task is not polled
+ assertThat(taskQueue.peek()).isEqualTo(mHighPriorityTask);
+ // expect one invocation for the task that is running
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testScheduleNextTask_whenTaskCompletes_shouldAutomaticallyScheduleNextTask()
+ throws Exception {
+ PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+ // add two tasks into the queue for execution
+ taskQueue.add(mHighPriorityTask);
+ taskQueue.add(mHighPriorityTask);
+
+ mDataBroker.scheduleNextTask(); // start a task
+ waitForTelemetryThreadToFinish();
+ // end a task, should automatically schedule the next task
+ mFakeScriptExecutor.notifyScriptSuccess(mData); // posts to telemetry handler
+
+ waitForTelemetryThreadToFinish();
+ // verify queue is empty, both tasks are polled and executed
+ assertThat(taskQueue.peek()).isNull();
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void testScheduleNextTask_onScriptSuccess_shouldStoreInterimResult() throws Exception {
+ mData.putBoolean("script is finished", false);
+ mData.putDouble("value of euler's number", 2.71828);
+ mDataBroker.getTaskQueue().add(mHighPriorityTask);
+
+ mDataBroker.scheduleNextTask();
+ waitForTelemetryThreadToFinish();
+ mFakeScriptExecutor.notifyScriptSuccess(mData); // posts to telemetry handler
+
+ waitForTelemetryThreadToFinish();
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(1);
+ verify(mMockResultStore).putInterimResult(
+ eq(mHighPriorityTask.getMetricsConfig().getName()), eq(mData));
+ }
+
+ @Test
+ public void testScheduleNextTask_onScriptError_shouldStoreErrorObject() throws Exception {
+ mDataBroker.getTaskQueue().add(mHighPriorityTask);
+ TelemetryProto.TelemetryError.ErrorType errorType =
+ TelemetryProto.TelemetryError.ErrorType.LUA_RUNTIME_ERROR;
+ String errorMessage = "test onError";
+ TelemetryProto.TelemetryError expectedError = TelemetryProto.TelemetryError.newBuilder()
+ .setErrorType(errorType)
+ .setMessage(errorMessage)
+ .build();
+
+ mDataBroker.scheduleNextTask();
+ waitForTelemetryThreadToFinish();
+ mFakeScriptExecutor.notifyScriptError(errorType.getNumber(), errorMessage);
+
+ waitForTelemetryThreadToFinish();
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(1);
+ verify(mMockResultStore).putError(eq(METRICS_CONFIG_FOO.getName()), eq(expectedError));
+ }
+
+ @Test
+ public void testScheduleNextTask_whenScriptFinishes_shouldStoreFinalResult()
+ throws Exception {
+ mData.putBoolean("script is finished", true);
+ mData.putDouble("value of pi", 3.14159265359);
+ mDataBroker.getTaskQueue().add(mHighPriorityTask);
+
+ mDataBroker.scheduleNextTask();
+ waitForTelemetryThreadToFinish();
+ mFakeScriptExecutor.notifyScriptFinish(mData); // posts to telemetry handler
+
+ waitForTelemetryThreadToFinish();
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(1);
+ verify(mMockResultStore).putFinalResult(
+ eq(mHighPriorityTask.getMetricsConfig().getName()), eq(mData));
+ verify(mMockScriptFinishedCallback).onScriptFinished(eq(KEY_FOO));
+ }
+
+ @Test
+ public void testScheduleNextTask_whenInterimDataExists_shouldPassToScriptExecutor()
+ throws Exception {
+ mData.putDouble("value of golden ratio", 1.618033);
+ mDataBroker.getTaskQueue().add(mHighPriorityTask);
+ when(mMockResultStore.getInterimResult(mHighPriorityTask.getMetricsConfig().getName()))
+ .thenReturn(mData);
+
+ mDataBroker.scheduleNextTask();
+
+ waitForTelemetryThreadToFinish();
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(1);
+ assertThat(mFakeScriptExecutor.getSavedState()).isEqualTo(mData);
+ }
+
+ @Test
+ public void testScheduleNextTask_largeInput_shouldPipeData() throws Exception {
+ PersistableBundle data = new PersistableBundle();
+ data.putBooleanArray("1 MB Array", new boolean [1024 * 1024]);
+ ScriptExecutionTask highPriorityTask = new ScriptExecutionTask(
+ new DataSubscriber(mDataBroker, METRICS_CONFIG_FOO, SUBSCRIBER_FOO),
+ data,
+ SystemClock.elapsedRealtime());
+ mDataBroker.getTaskQueue().add(highPriorityTask);
+
+ mDataBroker.scheduleNextTask();
+
+ waitForTelemetryThreadToFinish();
+ assertThat(mFakeScriptExecutor.getInvokeScriptForLargeInputCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testScheduleNextTask_largeReportWithApproxSize_shouldPipeData() throws Exception {
+ PersistableBundle data = new PersistableBundle();
+ data.putInt(APPROX_BUNDLE_SIZE_BYTES_KEY, 21000);
+ ScriptExecutionTask highPriorityTask = new ScriptExecutionTask(
+ new DataSubscriber(mDataBroker, METRICS_CONFIG_FOO, SUBSCRIBER_FOO),
+ data,
+ SystemClock.elapsedRealtime());
+ mDataBroker.getTaskQueue().add(highPriorityTask);
+
+ mDataBroker.scheduleNextTask();
+
+ waitForTelemetryThreadToFinish();
+ assertThat(mFakeScriptExecutor.getInvokeScriptForLargeInputCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testScheduleNextTask_smallReportWithApproxSize_doesNotPipeData() throws Exception {
+ PersistableBundle data = new PersistableBundle();
+ data.putInt(APPROX_BUNDLE_SIZE_BYTES_KEY, 1000);
+ ScriptExecutionTask highPriorityTask = new ScriptExecutionTask(
+ new DataSubscriber(mDataBroker, METRICS_CONFIG_FOO, SUBSCRIBER_FOO),
+ data,
+ SystemClock.elapsedRealtime());
+ mDataBroker.getTaskQueue().add(highPriorityTask);
+
+ mDataBroker.scheduleNextTask();
+
+ waitForTelemetryThreadToFinish();
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testScheduleNextTask_largeInputPipeIOException_shouldIgnoreCurrentTask()
+ throws Exception {
+ PersistableBundle data = new PersistableBundle();
+ data.putBooleanArray("1 MB Array", new boolean [1024 * 1024]);
+ PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+ ScriptExecutionTask highPriorityTask = new ScriptExecutionTask(
+ new DataSubscriber(mDataBroker, METRICS_CONFIG_FOO, SUBSCRIBER_FOO),
+ data,
+ SystemClock.elapsedRealtime());
+ taskQueue.add(highPriorityTask); // invokeScriptForLargeInput() path
+ taskQueue.add(new ScriptExecutionTask(
+ new DataSubscriber(mDataBroker, METRICS_CONFIG_FOO, SUBSCRIBER_FOO),
+ new PersistableBundle(),
+ SystemClock.elapsedRealtime())); // invokeScript() path
+ ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
+ when(ParcelFileDescriptor.createPipe()).thenReturn(fds);
+ fds[1].close(); // cause IO Exception in invokeScriptForLargeInput() path
+
+ mDataBroker.scheduleNextTask();
+
+ waitForTelemetryThreadToFinish();
+ assertThat(mFakeScriptExecutor.getInvokeScriptForLargeInputCount()).isEqualTo(1);
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(1);
+ assertThat(taskQueue).isEmpty();
+ }
+
+ @Test
+ public void testScheduleNextTask_bindScriptExecutorFailedOnce_shouldRebind()
+ throws Exception {
+ Mockito.reset(mMockContext);
+ when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(
+ new Answer() {
+ private int mCount = 0;
+
+ @Override
+ public Object answer(InvocationOnMock invocation) {
+ if (mCount++ == 1) {
+ return false; // fail first attempt
+ }
+ ServiceConnection conn = invocation.getArgument(1);
+ conn.onServiceConnected(null, mMockScriptExecutorBinder);
+ return true; // second attempt should succeed
+ }
+ });
+ mDataBroker.mBindScriptExecutorDelayMillis = 0L; // immediately rebind for testing purpose
+ mDataBroker.addMetricsConfig(KEY_FOO, METRICS_CONFIG_FOO);
+ PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+ taskQueue.add(mHighPriorityTask);
+
+ // will rebind to ScriptExecutor if it is null
+ mDataBroker.scheduleNextTask();
+
+ waitForTelemetryThreadToFinish();
+ assertThat(taskQueue.peek()).isNull();
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testScheduleNextTask_bindScriptExecutorFailedMultipleTimes_shouldDisableBroker()
+ throws Exception {
+ // fail 6 future attempts to bind to it
+ Mockito.reset(mMockContext);
+ when(mMockContext.bindServiceAsUser(any(), any(), anyInt(), any()))
+ .thenReturn(false, false, false, false, false, false);
+ mDataBroker.mBindScriptExecutorDelayMillis = 0L; // immediately rebind for testing purpose
+ mDataBroker.addMetricsConfig(KEY_FOO, METRICS_CONFIG_FOO);
+ PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+ taskQueue.add(mHighPriorityTask);
+
+ // will rebind to ScriptExecutor if it is null
+ mDataBroker.scheduleNextTask();
+
+ waitForTelemetryThreadToFinish();
+ // broker disabled, all subscribers should have been removed
+ assertThat(mDataBroker.getSubscriptionMap()).hasSize(0);
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void testScheduleNextTask_whenScriptExecutorThrowsException_shouldResetAndTryAgain()
+ throws Exception {
+ PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+ taskQueue.add(mHighPriorityTask);
+ mFakeScriptExecutor.failNextApiCalls(1); // fail the next invokeScript() call
+
+ mDataBroker.scheduleNextTask();
+
+ waitForTelemetryThreadToFinish();
+ // invokeScript() failed, task is re-queued and re-run
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(2);
+ assertThat(taskQueue).isEmpty();
+ }
+
+ @Test
+ public void testAddTaskToQueue_shouldInvokeScriptExecutor() throws Exception {
+ mDataBroker.addTaskToQueue(mHighPriorityTask);
+
+ waitForTelemetryThreadToFinish();
+ assertThat(mFakeScriptExecutor.getInvokeScriptCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testAddMetricsConfig_newMetricsConfig() {
+ mDataBroker.addMetricsConfig(KEY_BAR, METRICS_CONFIG_BAR);
+
+ assertThat(mDataBroker.getSubscriptionMap()).hasSize(1);
+ assertThat(mDataBroker.getSubscriptionMap()).containsKey(KEY_BAR);
+ // there should be one data subscriber in the subscription list of METRICS_CONFIG_BAR
+ assertThat(mDataBroker.getSubscriptionMap().get(KEY_BAR)).hasSize(1);
+ }
+
+
+ @Test
+ public void testAddMetricsConfig_duplicateMetricsConfig_shouldDoNothing() {
+ mDataBroker.addMetricsConfig(KEY_FOO, METRICS_CONFIG_FOO);
+ mDataBroker.addMetricsConfig(KEY_FOO, METRICS_CONFIG_FOO);
+
+ assertThat(mDataBroker.getSubscriptionMap()).hasSize(1);
+ assertThat(mDataBroker.getSubscriptionMap()).containsKey(KEY_FOO);
+ assertThat(mDataBroker.getSubscriptionMap().get(KEY_FOO)).hasSize(1);
+ }
+
+ @Test
+ public void testRemoveMetricsConfiguration_shouldRemoveAllAssociatedTasks() {
+ mDataBroker.addMetricsConfig(KEY_FOO, METRICS_CONFIG_FOO);
+ mDataBroker.addMetricsConfig(KEY_BAR, METRICS_CONFIG_BAR);
+ ScriptExecutionTask taskWithMetricsConfigFoo = new ScriptExecutionTask(
+ new DataSubscriber(mDataBroker, METRICS_CONFIG_FOO, SUBSCRIBER_FOO),
+ mData,
+ SystemClock.elapsedRealtime());
+ PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+ taskQueue.add(mHighPriorityTask); // associated with METRICS_CONFIG_FOO
+ taskQueue.add(mLowPriorityTask); // associated with METRICS_CONFIG_BAR
+ taskQueue.add(taskWithMetricsConfigFoo); // associated with METRICS_CONFIG_FOO
+ assertThat(taskQueue).hasSize(3);
+
+ mDataBroker.removeMetricsConfig(KEY_FOO);
+
+ assertThat(taskQueue).hasSize(1);
+ assertThat(taskQueue.poll()).isEqualTo(mLowPriorityTask);
+ }
+
+ @Test
+ public void testRemoveMetricsConfiguration_whenMetricsConfigNonExistent_shouldDoNothing() {
+ mDataBroker.removeMetricsConfig(KEY_BAR);
+
+ assertThat(mDataBroker.getSubscriptionMap()).hasSize(0);
+ }
+
+ @Test
+ public void testRemoveAllMetricsConfigs_shouldRemoveTasksAndClearSubscriptionMap() {
+ mDataBroker.addMetricsConfig(KEY_FOO, METRICS_CONFIG_FOO);
+ mDataBroker.addMetricsConfig(KEY_BAR, METRICS_CONFIG_BAR);
+ PriorityBlockingQueue<ScriptExecutionTask> taskQueue = mDataBroker.getTaskQueue();
+ taskQueue.add(mHighPriorityTask); // associated with METRICS_CONFIG_FOO
+ taskQueue.add(mLowPriorityTask); // associated with METRICS_CONFIG_BAR
+
+ mDataBroker.removeAllMetricsConfigs();
+
+ assertThat(taskQueue).isEmpty();
+ assertThat(mDataBroker.getSubscriptionMap()).isEmpty();
+ }
+
+ private void waitForTelemetryThreadToFinish() throws Exception {
+ assertWithMessage("handler not idle in %sms", TIMEOUT_MS)
+ .that(mIdleHandlerLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+ mIdleHandlerLatch = new CountDownLatch(1); // reset idle handler condition
+ }
+
+ private static class FakeScriptExecutor implements IScriptExecutor {
+ private IScriptExecutorListener mListener;
+ private int mInvokeScriptCount = 0;
+ private int mInvokeScriptForLargeInputCount = 0;
+ private int mFailApi = 0;
+ private PersistableBundle mSavedState = null;
+
+ @Override
+ public void invokeScript(String scriptBody, String functionName,
+ PersistableBundle publishedData, @Nullable PersistableBundle savedState,
+ IScriptExecutorListener listener)
+ throws RemoteException {
+ mInvokeScriptCount++;
+ mSavedState = savedState;
+ mListener = listener;
+ if (mFailApi > 0) {
+ mFailApi--;
+ throw new RemoteException("Simulated failure");
+ }
+ }
+
+ @Override
+ public void invokeScriptForLargeInput(String scriptBody, String functionName,
+ ParcelFileDescriptor publishedDataFileDescriptor,
+ @Nullable PersistableBundle savedState,
+ IScriptExecutorListener listener) throws RemoteException {
+ mInvokeScriptForLargeInputCount++;
+ mSavedState = savedState;
+ mListener = listener;
+ if (mFailApi > 0) {
+ mFailApi--;
+ throw new RemoteException("Simulated failure");
+ }
+ // Since DataBrokerImpl and FakeScriptExecutor are in the same process, they do not
+ // use real IPC and share the fd. When DataBroker closes the fd, it affects
+ // FakeScriptExecutor. Therefore FakeScriptExecutor must dup the fd before it is
+ // closed by DataBroker
+ ParcelFileDescriptor dup = null;
+ try {
+ dup = publishedDataFileDescriptor.dup();
+ } catch (IOException e) { }
+ final ParcelFileDescriptor fd = Objects.requireNonNull(dup);
+ // to prevent deadlock, read and write must happen on separate threads
+ Handler.getMain().post(() -> {
+ try (InputStream input = new ParcelFileDescriptor.AutoCloseInputStream(fd)) {
+ PersistableBundle.readFromStream(input);
+ } catch (IOException e) { }
+ });
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return null;
+ }
+
+ /** Mocks script temporary completion. */
+ public void notifyScriptSuccess(PersistableBundle bundle) {
+ try {
+ mListener.onSuccess(bundle);
+ } catch (RemoteException e) {
+ // nothing to do
+ }
+ }
+
+ /** Mocks script producing final result. */
+ public void notifyScriptFinish(PersistableBundle bundle) {
+ try {
+ mListener.onScriptFinished(bundle);
+ } catch (RemoteException e) {
+ // nothing to do
+ }
+ }
+
+ /** Mocks script finished with error. */
+ public void notifyScriptError(int errorType, String errorMessage) {
+ try {
+ mListener.onError(errorType, errorMessage, null);
+ } catch (RemoteException e) {
+ // nothing to do
+ }
+ }
+
+ /** Fails the next N invokeScript() call. */
+ public void failNextApiCalls(int n) {
+ mFailApi = n;
+ }
+
+ /** Returns number of times invokeScript() was called. */
+ public int getInvokeScriptCount() {
+ return mInvokeScriptCount;
+ }
+
+ /** Returns number of times invokeScriptForLargeInput() was called. */
+ public int getInvokeScriptForLargeInputCount() {
+ return mInvokeScriptForLargeInputCount;
+ }
+
+ /** Returns the interim data passed in invokeScript(). */
+ public PersistableBundle getSavedState() {
+ return mSavedState;
+ }
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerUnitTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerUnitTest.java
deleted file mode 100644
index 45c5a22..0000000
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataBrokerUnitTest.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (C) 2021 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.telemetry.databroker;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.when;
-
-import android.car.hardware.CarPropertyConfig;
-
-import com.android.car.CarPropertyService;
-import com.android.car.telemetry.TelemetryProto;
-import com.android.car.telemetry.publisher.PublisherFactory;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-
-import java.util.Collections;
-
-@RunWith(MockitoJUnitRunner.class)
-public class DataBrokerUnitTest {
- private static final int PROP_ID = 100;
- private static final int PROP_AREA = 200;
- private static final CarPropertyConfig<Integer> PROP_CONFIG =
- CarPropertyConfig.newBuilder(Integer.class, PROP_ID, PROP_AREA).setAccess(
- CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ).build();
- private static final TelemetryProto.VehiclePropertyPublisher
- VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION =
- TelemetryProto.VehiclePropertyPublisher.newBuilder().setReadRate(
- 1).setVehiclePropertyId(PROP_ID).build();
- private static final TelemetryProto.Publisher PUBLISHER_CONFIGURATION =
- TelemetryProto.Publisher.newBuilder().setVehicleProperty(
- VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION).build();
- private static final TelemetryProto.Subscriber SUBSCRIBER_FOO =
- TelemetryProto.Subscriber.newBuilder().setHandler("function_name_foo").setPublisher(
- PUBLISHER_CONFIGURATION).build();
- private static final TelemetryProto.MetricsConfig METRICS_CONFIG_FOO =
- TelemetryProto.MetricsConfig.newBuilder().setName("Foo").setVersion(
- 1).addSubscribers(SUBSCRIBER_FOO).build();
- private static final TelemetryProto.Subscriber SUBSCRIBER_BAR =
- TelemetryProto.Subscriber.newBuilder().setHandler("function_name_bar").setPublisher(
- PUBLISHER_CONFIGURATION).build();
- private static final TelemetryProto.MetricsConfig METRICS_CONFIG_BAR =
- TelemetryProto.MetricsConfig.newBuilder().setName("Bar").setVersion(
- 1).addSubscribers(SUBSCRIBER_BAR).build();
-
- @Mock
- private CarPropertyService mMockCarPropertyService;
-
- private DataBrokerImpl mDataBroker;
-
- @Before
- public void setUp() {
- when(mMockCarPropertyService.getPropertyList())
- .thenReturn(Collections.singletonList(PROP_CONFIG));
- PublisherFactory factory = new PublisherFactory(mMockCarPropertyService);
- mDataBroker = new DataBrokerImpl(factory);
- }
-
- @Test
- public void testAddMetricsConfiguration_newMetricsConfig() {
- mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
-
- assertThat(mDataBroker.getSubscriptionMap()).containsKey(METRICS_CONFIG_FOO.getName());
- // there should be one data subscriber in the subscription list of METRICS_CONFIG_FOO
- assertThat(mDataBroker.getSubscriptionMap().get(METRICS_CONFIG_FOO.getName())).hasSize(1);
- }
-
- @Test
- public void testAddMetricsConfiguration_multipleMetricsConfigsSamePublisher() {
- mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
- mDataBroker.addMetricsConfiguration(METRICS_CONFIG_BAR);
-
- assertThat(mDataBroker.getSubscriptionMap()).containsKey(METRICS_CONFIG_FOO.getName());
- assertThat(mDataBroker.getSubscriptionMap()).containsKey(METRICS_CONFIG_BAR.getName());
- }
-
- @Test
- public void testAddMetricsConfiguration_addSameMetricsConfigs() {
- mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
-
- boolean status = mDataBroker.addMetricsConfiguration(METRICS_CONFIG_FOO);
-
- assertThat(status).isFalse();
- }
-
- @Test
- public void testRemoveMetricsConfiguration_removeNonexistentMetricsConfig() {
- boolean status = mDataBroker.removeMetricsConfiguration(METRICS_CONFIG_FOO);
-
- assertThat(status).isFalse();
- }
-}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataSubscriberTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataSubscriberTest.java
new file mode 100644
index 0000000..69602f1
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/databroker/DataSubscriberTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 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.telemetry.databroker;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.car.telemetry.TelemetryProto;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DataSubscriberTest {
+
+ private static final TelemetryProto.VehiclePropertyPublisher
+ VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION =
+ TelemetryProto.VehiclePropertyPublisher.newBuilder().setReadRate(
+ 1).setVehiclePropertyId(100).build();
+ private static final TelemetryProto.Publisher PUBLISHER_CONFIGURATION =
+ TelemetryProto.Publisher.newBuilder().setVehicleProperty(
+ VEHICLE_PROPERTY_PUBLISHER_CONFIGURATION).build();
+ private static final TelemetryProto.Subscriber SUBSCRIBER_FOO =
+ TelemetryProto.Subscriber.newBuilder().setHandler("function_name_foo").setPublisher(
+ PUBLISHER_CONFIGURATION).setPriority(1).build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_FOO =
+ TelemetryProto.MetricsConfig.newBuilder().setName("Foo").setVersion(
+ 1).addSubscribers(SUBSCRIBER_FOO).build();
+ private static final TelemetryProto.Subscriber SUBSCRIBER_BAR =
+ TelemetryProto.Subscriber.newBuilder().setHandler("function_name_bar").setPublisher(
+ PUBLISHER_CONFIGURATION).setPriority(1).build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG_BAR =
+ TelemetryProto.MetricsConfig.newBuilder().setName("Bar").setVersion(
+ 1).addSubscribers(SUBSCRIBER_BAR).build();
+
+ @Mock
+ private DataBroker mMockDataBroker;
+
+ @Test
+ public void testEquals_whenSame_shouldBeEqual() {
+ DataSubscriber foo = new DataSubscriber(mMockDataBroker, METRICS_CONFIG_FOO,
+ SUBSCRIBER_FOO);
+ DataSubscriber bar = new DataSubscriber(mMockDataBroker, METRICS_CONFIG_FOO,
+ SUBSCRIBER_FOO);
+
+ assertThat(foo).isEqualTo(bar);
+ }
+
+ @Test
+ public void testEquals_whenDifferent_shouldNotBeEqual() {
+ DataSubscriber foo = new DataSubscriber(mMockDataBroker, METRICS_CONFIG_FOO,
+ SUBSCRIBER_FOO);
+ DataSubscriber bar = new DataSubscriber(mMockDataBroker, METRICS_CONFIG_BAR,
+ SUBSCRIBER_BAR);
+
+ assertThat(foo).isNotEqualTo(bar);
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/CarTelemetrydPublisherTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/CarTelemetrydPublisherTest.java
new file mode 100644
index 0000000..36edb67
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/CarTelemetrydPublisherTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.automotive.telemetry.internal.ICarDataListener;
+import android.automotive.telemetry.internal.ICarTelemetryInternal;
+import android.car.test.mocks.AbstractExtendedMockitoTestCase;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+import com.android.car.telemetry.TelemetryProto;
+import com.android.car.telemetry.databroker.DataSubscriber;
+import com.android.car.test.FakeHandlerWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.List;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CarTelemetrydPublisherTest extends AbstractExtendedMockitoTestCase {
+ private static final String SERVICE_NAME = ICarTelemetryInternal.DESCRIPTOR + "/default";
+ private static final int CAR_DATA_ID_1 = 1;
+ private static final TelemetryProto.Publisher PUBLISHER_PARAMS_1 =
+ TelemetryProto.Publisher.newBuilder()
+ .setCartelemetryd(TelemetryProto.CarTelemetrydPublisher.newBuilder()
+ .setId(CAR_DATA_ID_1))
+ .build();
+
+ private final FakeHandlerWrapper mFakeHandlerWrapper =
+ new FakeHandlerWrapper(Looper.getMainLooper(), FakeHandlerWrapper.Mode.IMMEDIATE);
+
+ @Mock private IBinder mMockBinder;
+ @Mock private DataSubscriber mMockDataSubscriber;
+
+ @Captor private ArgumentCaptor<IBinder.DeathRecipient> mLinkToDeathCallbackCaptor;
+
+ // These 2 variables are set in onPublisherFailure() callback.
+ @Nullable private Throwable mPublisherFailure;
+ @Nullable private List<TelemetryProto.MetricsConfig> mFailedConfigs;
+
+ private FakeCarTelemetryInternal mFakeCarTelemetryInternal;
+ private CarTelemetrydPublisher mPublisher;
+
+ @Before
+ public void setUp() throws Exception {
+ mPublisher = new CarTelemetrydPublisher(
+ this::onPublisherFailure, mFakeHandlerWrapper.getMockHandler());
+ mFakeCarTelemetryInternal = new FakeCarTelemetryInternal(mMockBinder);
+ when(mMockDataSubscriber.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
+ when(mMockBinder.queryLocalInterface(any())).thenReturn(mFakeCarTelemetryInternal);
+ doNothing().when(mMockBinder).linkToDeath(mLinkToDeathCallbackCaptor.capture(), anyInt());
+ doReturn(mMockBinder).when(() -> ServiceManager.checkService(SERVICE_NAME));
+ }
+
+ @Override
+ protected void onSessionBuilder(CustomMockitoSessionBuilder builder) {
+ builder.spyStatic(ServiceManager.class);
+ }
+
+ @Test
+ public void testAddDataSubscriber_registersNewListener() {
+ mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+ assertThat(mFakeCarTelemetryInternal.mListener).isNotNull();
+ assertThat(mPublisher.isConnectedToCarTelemetryd()).isTrue();
+ assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
+ }
+
+ @Test
+ public void testAddDataSubscriber_withInvalidId_fails() {
+ DataSubscriber invalidDataSubscriber = Mockito.mock(DataSubscriber.class);
+ when(invalidDataSubscriber.getPublisherParam()).thenReturn(
+ TelemetryProto.Publisher.newBuilder()
+ .setCartelemetryd(TelemetryProto.CarTelemetrydPublisher.newBuilder()
+ .setId(42000)) // invalid ID
+ .build());
+
+ Throwable error = assertThrows(IllegalArgumentException.class,
+ () -> mPublisher.addDataSubscriber(invalidDataSubscriber));
+
+ assertThat(error).hasMessageThat().contains("Invalid CarData ID");
+ assertThat(mFakeCarTelemetryInternal.mListener).isNull();
+ assertThat(mPublisher.isConnectedToCarTelemetryd()).isFalse();
+ assertThat(mPublisher.hasDataSubscriber(invalidDataSubscriber)).isFalse();
+ }
+
+ @Test
+ public void testRemoveDataSubscriber_ignoresIfNotFound() {
+ mPublisher.removeDataSubscriber(mMockDataSubscriber);
+ }
+
+ @Test
+ public void testRemoveDataSubscriber_removesOnlySingleSubscriber() {
+ DataSubscriber subscriber2 = Mockito.mock(DataSubscriber.class);
+ when(subscriber2.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
+ mPublisher.addDataSubscriber(mMockDataSubscriber);
+ mPublisher.addDataSubscriber(subscriber2);
+
+ mPublisher.removeDataSubscriber(subscriber2);
+
+ assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
+ assertThat(mPublisher.hasDataSubscriber(subscriber2)).isFalse();
+ assertThat(mFakeCarTelemetryInternal.mListener).isNotNull();
+ }
+
+ @Test
+ public void testRemoveDataSubscriber_disconnectsFromICarTelemetry() {
+ mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+ mPublisher.removeDataSubscriber(mMockDataSubscriber);
+
+ assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
+ assertThat(mFakeCarTelemetryInternal.mListener).isNull();
+ }
+
+ @Test
+ public void testRemoveAllDataSubscribers_succeeds() {
+ DataSubscriber subscriber2 = Mockito.mock(DataSubscriber.class);
+ when(subscriber2.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
+ mPublisher.addDataSubscriber(mMockDataSubscriber);
+ mPublisher.addDataSubscriber(subscriber2);
+
+ mPublisher.removeAllDataSubscribers();
+
+ assertThat(mPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
+ assertThat(mPublisher.hasDataSubscriber(subscriber2)).isFalse();
+ assertThat(mFakeCarTelemetryInternal.mListener).isNull();
+ }
+
+ @Test
+ public void testNotifiesFailureConsumer_whenBinderDies() {
+ mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+ mLinkToDeathCallbackCaptor.getValue().binderDied();
+
+ assertThat(mFakeCarTelemetryInternal.mSetListenerCallCount).isEqualTo(1);
+ assertThat(mPublisherFailure).hasMessageThat()
+ .contains("ICarTelemetryInternal binder died");
+ assertThat(mFailedConfigs).hasSize(1); // got all the failed configs
+ }
+
+ @Test
+ public void testNotifiesFailureConsumer_whenFailsConnectToService() {
+ mFakeCarTelemetryInternal.setApiFailure(new RemoteException("tough life"));
+
+ mPublisher.addDataSubscriber(mMockDataSubscriber);
+
+ assertThat(mPublisherFailure).hasMessageThat()
+ .contains("Cannot set CarData listener");
+ assertThat(mFailedConfigs).hasSize(1);
+ }
+
+ private void onPublisherFailure(AbstractPublisher publisher,
+ List<TelemetryProto.MetricsConfig> affectedConfigs, Throwable error) {
+ mPublisherFailure = error;
+ mFailedConfigs = affectedConfigs;
+ }
+
+ private static class FakeCarTelemetryInternal implements ICarTelemetryInternal {
+ @Nullable ICarDataListener mListener;
+ int mSetListenerCallCount = 0;
+ private final IBinder mBinder;
+ @Nullable private RemoteException mApiFailure = null;
+
+ FakeCarTelemetryInternal(IBinder binder) {
+ mBinder = binder;
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return mBinder;
+ }
+
+ @Override
+ public void setListener(ICarDataListener listener) throws RemoteException {
+ mSetListenerCallCount += 1;
+ if (mApiFailure != null) {
+ throw mApiFailure;
+ }
+ mListener = listener;
+ }
+
+ @Override
+ public void clearListener() throws RemoteException {
+ if (mApiFailure != null) {
+ throw mApiFailure;
+ }
+ mListener = null;
+ }
+
+ void setApiFailure(RemoteException e) {
+ mApiFailure = e;
+ }
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/HashUtilsTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/HashUtilsTest.java
new file mode 100644
index 0000000..7ece9a8
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/HashUtilsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class HashUtilsTest {
+ @Test
+ public void testSha256() {
+ assertThat(HashUtils.sha256("")).isEqualTo(1449310910991872227L);
+ assertThat(HashUtils.sha256("a")).isEqualTo(-3837880752741967926L);
+ assertThat(HashUtils.sha256("aa")).isEqualTo(-8157175689457624170L);
+ assertThat(HashUtils.sha256("b")).isEqualTo(5357375904281011006L);
+ }
+
+ @Test
+ public void testMurmur2Hash64() {
+ assertThat(HashUtils.murmur2Hash64("")).isEqualTo(-9117937525680267717L);
+ assertThat(HashUtils.murmur2Hash64("abcd")).isEqualTo(-4207384847647213435L);
+ assertThat(HashUtils.murmur2Hash64("com.sample.process.name"))
+ .isEqualTo(-5639285681030453830L);
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java
new file mode 100644
index 0000000..25076a1
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/StatsPublisherTest.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher;
+
+import static com.android.car.telemetry.AtomsProto.Atom.ACTIVITY_FOREGROUND_STATE_CHANGED_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.Atom.APP_START_MEMORY_STATE_CAPTURED_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.Atom.PROCESS_MEMORY_STATE_FIELD_NUMBER;
+import static com.android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.ACTIVITY_FOREGROUND_STATE_CHANGED;
+import static com.android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.APP_START_MEMORY_STATE_CAPTURED;
+import static com.android.car.telemetry.TelemetryProto.StatsPublisher.SystemMetric.PROCESS_MEMORY_STATE;
+import static com.android.car.telemetry.publisher.StatsPublisher.ACTIVITY_FOREGROUND_STATE_CHANGED_ATOM_MATCHER_ID;
+import static com.android.car.telemetry.publisher.StatsPublisher.ACTIVITY_FOREGROUND_STATE_CHANGED_EVENT_METRIC_ID;
+import static com.android.car.telemetry.publisher.StatsPublisher.APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID;
+import static com.android.car.telemetry.publisher.StatsPublisher.APP_START_MEMORY_STATE_CAPTURED_EVENT_METRIC_ID;
+import static com.android.car.telemetry.publisher.StatsPublisher.PROCESS_MEMORY_STATE_FIELDS_MATCHER;
+import static com.android.car.telemetry.publisher.StatsPublisher.PROCESS_MEMORY_STATE_GAUGE_METRIC_ID;
+import static com.android.car.telemetry.publisher.StatsPublisher.PROCESS_MEMORY_STATE_MATCHER_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.StatsManager;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.SystemClock;
+
+import com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured;
+import com.android.car.telemetry.AtomsProto.Atom;
+import com.android.car.telemetry.AtomsProto.ProcessMemoryState;
+import com.android.car.telemetry.StatsLogProto;
+import com.android.car.telemetry.StatsLogProto.ConfigMetricsReport;
+import com.android.car.telemetry.StatsLogProto.DimensionsValue;
+import com.android.car.telemetry.StatsLogProto.DimensionsValueTuple;
+import com.android.car.telemetry.StatsLogProto.EventMetricData;
+import com.android.car.telemetry.StatsLogProto.GaugeBucketInfo;
+import com.android.car.telemetry.StatsLogProto.GaugeMetricData;
+import com.android.car.telemetry.StatsLogProto.StatsLogReport;
+import com.android.car.telemetry.StatsdConfigProto;
+import com.android.car.telemetry.TelemetryProto;
+import com.android.car.telemetry.databroker.DataSubscriber;
+import com.android.car.test.FakeHandlerWrapper;
+
+import com.google.common.collect.Range;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(MockitoJUnitRunner.class)
+public class StatsPublisherTest {
+ private static final TelemetryProto.Publisher STATS_PUBLISHER_PARAMS_1 =
+ TelemetryProto.Publisher.newBuilder()
+ .setStats(TelemetryProto.StatsPublisher.newBuilder()
+ .setSystemMetric(APP_START_MEMORY_STATE_CAPTURED))
+ .build();
+ private static final TelemetryProto.Publisher STATS_PUBLISHER_PARAMS_2 =
+ TelemetryProto.Publisher.newBuilder()
+ .setStats(TelemetryProto.StatsPublisher.newBuilder()
+ .setSystemMetric(PROCESS_MEMORY_STATE))
+ .build();
+ private static final TelemetryProto.Publisher STATS_PUBLISHER_PARAMS_3 =
+ TelemetryProto.Publisher.newBuilder()
+ .setStats(TelemetryProto.StatsPublisher.newBuilder()
+ .setSystemMetric(ACTIVITY_FOREGROUND_STATE_CHANGED))
+ .build();
+ private static final TelemetryProto.Subscriber SUBSCRIBER_1 =
+ TelemetryProto.Subscriber.newBuilder()
+ .setHandler("handler_fn_1")
+ .setPublisher(STATS_PUBLISHER_PARAMS_1)
+ .build();
+ private static final TelemetryProto.Subscriber SUBSCRIBER_2 =
+ TelemetryProto.Subscriber.newBuilder()
+ .setHandler("handler_fn_2")
+ .setPublisher(STATS_PUBLISHER_PARAMS_2)
+ .build();
+ private static final TelemetryProto.Subscriber SUBSCRIBER_3 =
+ TelemetryProto.Subscriber.newBuilder()
+ .setHandler("handler_fn_3")
+ .setPublisher(STATS_PUBLISHER_PARAMS_3)
+ .build();
+ private static final TelemetryProto.MetricsConfig METRICS_CONFIG =
+ TelemetryProto.MetricsConfig.newBuilder()
+ .setName("myconfig")
+ .setVersion(1)
+ .addSubscribers(SUBSCRIBER_1)
+ .addSubscribers(SUBSCRIBER_2)
+ .addSubscribers(SUBSCRIBER_3)
+ .build();
+
+ private static final long SUBSCRIBER_1_HASH = -8101507323446050791L; // Used as configKey.
+ private static final long SUBSCRIBER_2_HASH = 2778197004730583271L; // Used as configKey.
+ private static final long SUBSCRIBER_3_HASH = 7046592220837963576L; // Used as configKey.
+
+ // This StatsdConfig is generated for SUBSCRIBER_1.
+ private static final StatsdConfigProto.StatsdConfig STATSD_CONFIG_1 =
+ StatsdConfigProto.StatsdConfig.newBuilder()
+ .setId(SUBSCRIBER_1_HASH)
+ .addAtomMatcher(StatsdConfigProto.AtomMatcher.newBuilder()
+ .setId(APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID)
+ .setSimpleAtomMatcher(
+ StatsdConfigProto.SimpleAtomMatcher.newBuilder()
+ .setAtomId(
+ APP_START_MEMORY_STATE_CAPTURED_FIELD_NUMBER)))
+ .addEventMetric(StatsdConfigProto.EventMetric.newBuilder()
+ .setId(APP_START_MEMORY_STATE_CAPTURED_EVENT_METRIC_ID)
+ .setWhat(APP_START_MEMORY_STATE_CAPTURED_ATOM_MATCHER_ID))
+ .addAllowedLogSource("AID_SYSTEM")
+ .build();
+
+ // This StatsdConfig is generated for SUBSCRIBER_2.
+ private static final StatsdConfigProto.StatsdConfig STATSD_CONFIG_2 =
+ StatsdConfigProto.StatsdConfig.newBuilder()
+ .setId(SUBSCRIBER_2_HASH)
+ .addAtomMatcher(StatsdConfigProto.AtomMatcher.newBuilder()
+ // The id must be unique within StatsdConfig/matchers
+ .setId(PROCESS_MEMORY_STATE_MATCHER_ID)
+ .setSimpleAtomMatcher(StatsdConfigProto.SimpleAtomMatcher.newBuilder()
+ .setAtomId(PROCESS_MEMORY_STATE_FIELD_NUMBER)))
+ .addGaugeMetric(StatsdConfigProto.GaugeMetric.newBuilder()
+ // The id must be unique within StatsdConfig/metrics
+ .setId(PROCESS_MEMORY_STATE_GAUGE_METRIC_ID)
+ .setWhat(PROCESS_MEMORY_STATE_MATCHER_ID)
+ .setDimensionsInWhat(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(PROCESS_MEMORY_STATE_FIELD_NUMBER)
+ .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(1)) // ProcessMemoryState.uid
+ .addChild(StatsdConfigProto.FieldMatcher.newBuilder()
+ .setField(2)) // ProcessMemoryState.process_name
+ )
+ .setGaugeFieldsFilter(StatsdConfigProto.FieldFilter.newBuilder()
+ .setFields(PROCESS_MEMORY_STATE_FIELDS_MATCHER))
+ .setSamplingType(
+ StatsdConfigProto.GaugeMetric.SamplingType.RANDOM_ONE_SAMPLE)
+ .setBucket(StatsdConfigProto.TimeUnit.FIVE_MINUTES)
+ )
+ .addAllowedLogSource("AID_SYSTEM")
+ .addPullAtomPackages(StatsdConfigProto.PullAtomPackages.newBuilder()
+ .setAtomId(PROCESS_MEMORY_STATE_FIELD_NUMBER)
+ .addPackages("AID_SYSTEM"))
+ .build();
+
+ // This StatsdConfig is generated for SUBSCRIBER_3.
+ private static final StatsdConfigProto.StatsdConfig STATSD_CONFIG_3 =
+ StatsdConfigProto.StatsdConfig.newBuilder()
+ .setId(SUBSCRIBER_3_HASH)
+ .addAtomMatcher(StatsdConfigProto.AtomMatcher.newBuilder()
+ .setId(ACTIVITY_FOREGROUND_STATE_CHANGED_ATOM_MATCHER_ID)
+ .setSimpleAtomMatcher(
+ StatsdConfigProto.SimpleAtomMatcher.newBuilder()
+ .setAtomId(
+ ACTIVITY_FOREGROUND_STATE_CHANGED_FIELD_NUMBER)
+ ))
+ .addEventMetric(StatsdConfigProto.EventMetric.newBuilder()
+ .setId(ACTIVITY_FOREGROUND_STATE_CHANGED_EVENT_METRIC_ID)
+ .setWhat(ACTIVITY_FOREGROUND_STATE_CHANGED_ATOM_MATCHER_ID))
+ .addAllowedLogSource("AID_SYSTEM")
+ .build();
+
+ private static final EventMetricData EVENT_DATA =
+ EventMetricData.newBuilder()
+ .setElapsedTimestampNanos(99999999L)
+ .setAtom(Atom.newBuilder()
+ .setAppStartMemoryStateCaptured(
+ AppStartMemoryStateCaptured.newBuilder()
+ .setUid(1000)
+ .setActivityName("activityName")
+ .setRssInBytes(1234L)))
+ .build();
+
+ private static final GaugeMetricData GAUGE_DATA =
+ GaugeMetricData.newBuilder()
+ .addBucketInfo(GaugeBucketInfo.newBuilder()
+ .addAtom(Atom.newBuilder()
+ .setProcessMemoryState(ProcessMemoryState.newBuilder()
+ .setRssInBytes(4567L)))
+ .addElapsedTimestampNanos(445678901L))
+ .addDimensionLeafValuesInWhat(DimensionsValue.newBuilder()
+ .setValueInt(234))
+ .build();
+
+ private static final StatsLogProto.ConfigMetricsReportList METRICS_REPORT =
+ StatsLogProto.ConfigMetricsReportList.newBuilder()
+ .addReports(ConfigMetricsReport.newBuilder()
+ .addMetrics(StatsLogReport.newBuilder()
+ .setMetricId(APP_START_MEMORY_STATE_CAPTURED_EVENT_METRIC_ID)
+ .setEventMetrics(
+ StatsLogReport.EventMetricDataWrapper.newBuilder()
+ .addData(EVENT_DATA))))
+ .addReports(ConfigMetricsReport.newBuilder()
+ .addMetrics(StatsLogReport.newBuilder()
+ .setMetricId(PROCESS_MEMORY_STATE_GAUGE_METRIC_ID)
+ .setGaugeMetrics(
+ StatsLogReport.GaugeMetricDataWrapper.newBuilder()
+ .addData(GAUGE_DATA))
+ .setDimensionsPathInWhat(DimensionsValue.newBuilder()
+ .setValueTuple(DimensionsValueTuple.newBuilder()
+ .addDimensionsValue(DimensionsValue.newBuilder()
+ .setField(1))))))
+ .build();
+
+ // By default the test assumes all the StatsdConfigs are valid.
+ private static final StatsLogProto.StatsdStatsReport CONFIG_STATS_REPORT =
+ StatsLogProto.StatsdStatsReport.newBuilder()
+ .addConfigStats(StatsLogProto.StatsdStatsReport.ConfigStats.newBuilder()
+ // in unit tests UID of test and app are the same
+ .setUid(Process.myUid())
+ .setId(SUBSCRIBER_1_HASH) // id is the same as configKey
+ .setIsValid(true))
+ .addConfigStats(StatsLogProto.StatsdStatsReport.ConfigStats.newBuilder()
+ // in unit tests UID of test and app are the same
+ .setUid(Process.myUid())
+ .setId(SUBSCRIBER_2_HASH) // id is the same as configKey
+ .setIsValid(true))
+ .build();
+
+ private static final StatsLogProto.ConfigMetricsReportList EMPTY_METRICS_REPORT =
+ StatsLogProto.ConfigMetricsReportList.newBuilder().build();
+
+ private static final DataSubscriber DATA_SUBSCRIBER_1 =
+ new DataSubscriber(null, METRICS_CONFIG, SUBSCRIBER_1);
+
+ private final FakeHandlerWrapper mFakeHandlerWrapper =
+ new FakeHandlerWrapper(Looper.getMainLooper(), FakeHandlerWrapper.Mode.QUEUEING);
+
+ private File mRootDirectory;
+ private StatsPublisher mPublisher; // subject
+
+ // These 2 variables are set in onPublisherFailure() callback. Defaults to null.
+ private Throwable mPublisherFailure;
+ private List<TelemetryProto.MetricsConfig> mFailedConfigs;
+
+ @Mock private StatsManagerProxy mStatsManager;
+
+ @Captor private ArgumentCaptor<PersistableBundle> mBundleCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ mRootDirectory = Files.createTempDirectory("telemetry_test").toFile();
+ mPublisher = createRestartedPublisher();
+ when(mStatsManager.getStatsMetadata()).thenReturn(CONFIG_STATS_REPORT.toByteArray());
+ }
+
+ /**
+ * Emulates a restart by creating a new StatsPublisher. StatsManager and PersistableBundle
+ * stays the same.
+ */
+ private StatsPublisher createRestartedPublisher() throws Exception {
+ return new StatsPublisher(
+ this::onPublisherFailure,
+ mStatsManager,
+ mRootDirectory,
+ mFakeHandlerWrapper.getMockHandler());
+ }
+
+ @Test
+ public void testAddDataSubscriber_registersNewListener() throws Exception {
+ mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+ verify(mStatsManager, times(1))
+ .addConfig(SUBSCRIBER_1_HASH, STATSD_CONFIG_1.toByteArray());
+ assertThat(mPublisher.hasDataSubscriber(DATA_SUBSCRIBER_1)).isTrue();
+ }
+
+ @Test
+ public void testAddDataSubscriber_sameVersion_addsToStatsdOnce() throws Exception {
+ mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+ mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+ verify(mStatsManager, times(1))
+ .addConfig(SUBSCRIBER_1_HASH, STATSD_CONFIG_1.toByteArray());
+ assertThat(mPublisher.hasDataSubscriber(DATA_SUBSCRIBER_1)).isTrue();
+ }
+
+ @Test
+ public void testAddDataSubscriber_whenRestarted_addsToStatsdOnce() throws Exception {
+ mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+ StatsPublisher publisher2 = createRestartedPublisher();
+
+ publisher2.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+ verify(mStatsManager, times(1))
+ .addConfig(SUBSCRIBER_1_HASH, STATSD_CONFIG_1.toByteArray());
+ assertThat(publisher2.hasDataSubscriber(DATA_SUBSCRIBER_1)).isTrue();
+ }
+
+ @Test
+ public void testAddDataSubscriber_forProcessMemoryState_generatesStatsdMetrics()
+ throws Exception {
+ DataSubscriber processMemoryStateSubscriber =
+ new DataSubscriber(null, METRICS_CONFIG, SUBSCRIBER_2);
+
+ mPublisher.addDataSubscriber(processMemoryStateSubscriber);
+
+ verify(mStatsManager, times(1))
+ .addConfig(SUBSCRIBER_2_HASH, STATSD_CONFIG_2.toByteArray());
+ assertThat(mPublisher.hasDataSubscriber(processMemoryStateSubscriber)).isTrue();
+ }
+
+ @Test
+ public void testAddDataSubscriber_forActivityForegroundState_generatesStatsdMetrics()
+ throws Exception {
+ DataSubscriber activityForegroundStateSubscriber =
+ new DataSubscriber(null, METRICS_CONFIG, SUBSCRIBER_3);
+
+ mPublisher.addDataSubscriber(activityForegroundStateSubscriber);
+
+ verify(mStatsManager, times(1))
+ .addConfig(SUBSCRIBER_3_HASH, STATSD_CONFIG_3.toByteArray());
+ assertThat(mPublisher.hasDataSubscriber(activityForegroundStateSubscriber)).isTrue();
+ }
+
+ @Test
+ public void testRemoveDataSubscriber_removesFromStatsd() throws Exception {
+ mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+ mPublisher.removeDataSubscriber(DATA_SUBSCRIBER_1);
+
+ verify(mStatsManager, times(1)).removeConfig(SUBSCRIBER_1_HASH);
+ assertThat(getSavedStatsConfigs().keySet()).isEmpty();
+ assertThat(mPublisher.hasDataSubscriber(DATA_SUBSCRIBER_1)).isFalse();
+ }
+
+ @Test
+ public void testRemoveDataSubscriber_ifNotFound_nothingHappensButCallsStatsdRemove()
+ throws Exception {
+ mPublisher.removeDataSubscriber(DATA_SUBSCRIBER_1);
+
+ // It should try removing StatsdConfig from StatsD, in case it was added there before and
+ // left dangled.
+ verify(mStatsManager, times(1)).removeConfig(SUBSCRIBER_1_HASH);
+ assertThat(mPublisher.hasDataSubscriber(DATA_SUBSCRIBER_1)).isFalse();
+ }
+
+ @Test
+ public void testRemoveAllDataSubscriber_whenRestarted_removesFromStatsdAndClears()
+ throws Exception {
+ mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+ StatsPublisher publisher2 = createRestartedPublisher();
+
+ publisher2.removeAllDataSubscribers();
+
+ verify(mStatsManager, times(1)).removeConfig(SUBSCRIBER_1_HASH);
+ assertThat(getSavedStatsConfigs().keySet()).isEmpty();
+ assertThat(publisher2.hasDataSubscriber(DATA_SUBSCRIBER_1)).isFalse();
+ }
+
+ @Test
+ public void testAddDataSubscriber_queuesPeriodicTaskInTheHandler() {
+ mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+ assertThat(mFakeHandlerWrapper.getQueuedMessages()).hasSize(1);
+ Message msg = mFakeHandlerWrapper.getQueuedMessages().get(0);
+ long expectedPullPeriodMillis = 10 * 60 * 1000; // 10 minutes
+ assertThatMessageIsScheduledWithGivenDelay(msg, expectedPullPeriodMillis);
+ }
+
+ @Test
+ public void testAddDataSubscriber_whenFails_notifiesFailureConsumer() throws Exception {
+ doThrow(new StatsManager.StatsUnavailableException("fail"))
+ .when(mStatsManager).addConfig(anyLong(), any());
+
+ mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+ assertThat(mPublisherFailure).hasMessageThat().contains("Failed to add config");
+ assertThat(mFailedConfigs).hasSize(1); // got all the failed configs
+ }
+
+ @Test
+ public void testRemoveDataSubscriber_removesPeriodicStatsdReportPull() {
+ mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+ mPublisher.removeDataSubscriber(DATA_SUBSCRIBER_1);
+
+ assertThat(mFakeHandlerWrapper.getQueuedMessages()).isEmpty();
+ }
+
+ @Test
+ public void testRemoveAllDataSubscriber_removesPeriodicStatsdReportPull() {
+ mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+
+ mPublisher.removeAllDataSubscribers();
+
+ assertThat(mFakeHandlerWrapper.getQueuedMessages()).isEmpty();
+ }
+
+ @Test
+ public void testAfterDispatchItSchedulesANewPullReportTask() throws Exception {
+ mPublisher.addDataSubscriber(DATA_SUBSCRIBER_1);
+ Message firstMessage = mFakeHandlerWrapper.getQueuedMessages().get(0);
+ when(mStatsManager.getReports(anyLong())).thenReturn(EMPTY_METRICS_REPORT.toByteArray());
+
+ mFakeHandlerWrapper.dispatchQueuedMessages();
+
+ assertThat(mFakeHandlerWrapper.getQueuedMessages()).hasSize(1);
+ Message newMessage = mFakeHandlerWrapper.getQueuedMessages().get(0);
+ assertThat(newMessage).isNotEqualTo(firstMessage);
+ long expectedPullPeriodMillis = 10 * 60 * 1000; // 10 minutes
+ assertThatMessageIsScheduledWithGivenDelay(newMessage, expectedPullPeriodMillis);
+ }
+
+ @Test
+ public void testPullStatsdReport_correctlyPushesBundlesToSubscribers() throws Exception {
+ DataSubscriber subscriber1 = Mockito.mock(DataSubscriber.class);
+ when(subscriber1.getSubscriber()).thenReturn(SUBSCRIBER_1);
+ when(subscriber1.getMetricsConfig()).thenReturn(METRICS_CONFIG);
+ when(subscriber1.getPublisherParam()).thenReturn(SUBSCRIBER_1.getPublisher());
+ mPublisher.addDataSubscriber(subscriber1);
+ DataSubscriber subscriber2 = Mockito.mock(DataSubscriber.class);
+ when(subscriber2.getSubscriber()).thenReturn(SUBSCRIBER_2);
+ when(subscriber2.getMetricsConfig()).thenReturn(METRICS_CONFIG);
+ when(subscriber2.getPublisherParam()).thenReturn(SUBSCRIBER_2.getPublisher());
+ mPublisher.addDataSubscriber(subscriber2);
+ when(mStatsManager.getReports(anyLong())).thenReturn(METRICS_REPORT.toByteArray());
+
+ mFakeHandlerWrapper.dispatchQueuedMessages();
+
+ verify(subscriber1).push(mBundleCaptor.capture());
+ PersistableBundle bundle1 = mBundleCaptor.getValue();
+ assertThat(bundle1.getLongArray("elapsed_timestamp_nanos"))
+ .asList().containsExactly(99999999L);
+ assertThat(bundle1.getIntArray("uid")).asList().containsExactly(1000);
+ assertThat(Arrays.asList(bundle1.getStringArray("activity_name")))
+ .containsExactly("activityName");
+ assertThat(bundle1.getLongArray("rss_in_bytes")).asList().containsExactly(1234L);
+ verify(subscriber2).push(mBundleCaptor.capture());
+ PersistableBundle bundle2 = mBundleCaptor.getValue();
+ assertThat(bundle2.getIntArray("uid")).asList().containsExactly(234);
+ assertThat(bundle2.getLongArray("rss_in_bytes")).asList().containsExactly(4567L);
+ assertThat(bundle2.getLongArray("elapsed_timestamp_nanos"))
+ .asList().containsExactly(445678901L);
+ }
+
+ @Test
+ public void testOnInvalidConfig_notifiesPublisherFailureListener() throws Exception {
+ DataSubscriber subscriber = spy(new DataSubscriber(null, METRICS_CONFIG, SUBSCRIBER_1));
+ mPublisher.addDataSubscriber(subscriber);
+ reset(mStatsManager);
+ when(mStatsManager.getStatsMetadata()).thenReturn(
+ StatsLogProto.StatsdStatsReport.newBuilder()
+ .addConfigStats(StatsLogProto.StatsdStatsReport.ConfigStats.newBuilder()
+ // in unit tests UID of test and app are the same
+ .setUid(Process.myUid())
+ .setId(SUBSCRIBER_1_HASH) // id is the same as configKey
+ .setIsValid(false))
+ .build().toByteArray());
+ when(mStatsManager.getReports(anyLong())).thenReturn(EMPTY_METRICS_REPORT.toByteArray());
+
+ mFakeHandlerWrapper.dispatchQueuedMessages();
+
+ // subscriber shouldn't get data, because of EMPTY_METRICS_REPORT.
+ verify(subscriber, times(0)).push(any());
+ assertThat(mFailedConfigs).containsExactly(METRICS_CONFIG);
+ assertThat(mPublisherFailure).hasMessageThat().contains("Found invalid configs");
+ }
+
+ private PersistableBundle getSavedStatsConfigs() throws Exception {
+ File savedConfigsFile = new File(mRootDirectory, StatsPublisher.SAVED_STATS_CONFIGS_FILE);
+ if (!savedConfigsFile.exists()) {
+ return new PersistableBundle();
+ }
+ try (FileInputStream fileInputStream = new FileInputStream(savedConfigsFile)) {
+ return PersistableBundle.readFromStream(fileInputStream);
+ }
+ }
+
+ private void onPublisherFailure(AbstractPublisher publisher,
+ List<TelemetryProto.MetricsConfig> affectedConfigs, Throwable error) {
+ mPublisherFailure = error;
+ mFailedConfigs = affectedConfigs;
+ }
+
+ private static void assertThatMessageIsScheduledWithGivenDelay(Message msg, long delayMillis) {
+ long expectedTimeMillis = SystemClock.uptimeMillis() + delayMillis;
+ long deltaMillis = 1000; // +/- 1 seconds is good enough for testing
+ assertThat(msg.getWhen()).isIn(Range
+ .closed(expectedTimeMillis - deltaMillis, expectedTimeMillis + deltaMillis));
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/VehiclePropertyPublisherTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/VehiclePropertyPublisherTest.java
index bcd408a..39009bd 100644
--- a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/VehiclePropertyPublisherTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/VehiclePropertyPublisherTest.java
@@ -27,6 +27,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -34,11 +35,13 @@
import android.car.hardware.CarPropertyValue;
import android.car.hardware.property.CarPropertyEvent;
import android.car.hardware.property.ICarPropertyEventListener;
-import android.os.Bundle;
+import android.os.Looper;
+import android.os.PersistableBundle;
import com.android.car.CarPropertyService;
import com.android.car.telemetry.TelemetryProto;
import com.android.car.telemetry.databroker.DataSubscriber;
+import com.android.car.test.FakeHandlerWrapper;
import org.junit.Before;
import org.junit.Test;
@@ -74,7 +77,7 @@
.setVehiclePropertyId(-200))
.build();
- // mMockCarPropertyService supported car property list.
+ // CarPropertyConfigs for mMockCarPropertyService.
private static final CarPropertyConfig<Integer> PROP_CONFIG_1 =
CarPropertyConfig.newBuilder(Integer.class, PROP_ID_1, AREA_ID).setAccess(
CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_READ).build();
@@ -82,16 +85,18 @@
CarPropertyConfig.newBuilder(Integer.class, PROP_ID_2, AREA_ID).setAccess(
CarPropertyConfig.VEHICLE_PROPERTY_ACCESS_WRITE).build();
+ private final FakeHandlerWrapper mFakeHandlerWrapper =
+ new FakeHandlerWrapper(Looper.getMainLooper(), FakeHandlerWrapper.Mode.IMMEDIATE);
+
@Mock
private DataSubscriber mMockDataSubscriber;
-
@Mock
private CarPropertyService mMockCarPropertyService;
@Captor
private ArgumentCaptor<ICarPropertyEventListener> mCarPropertyCallbackCaptor;
@Captor
- private ArgumentCaptor<Bundle> mBundleCaptor;
+ private ArgumentCaptor<PersistableBundle> mBundleCaptor;
private VehiclePropertyPublisher mVehiclePropertyPublisher;
@@ -100,7 +105,10 @@
when(mMockDataSubscriber.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
when(mMockCarPropertyService.getPropertyList())
.thenReturn(List.of(PROP_CONFIG_1, PROP_CONFIG_2_WRITE_ONLY));
- mVehiclePropertyPublisher = new VehiclePropertyPublisher(mMockCarPropertyService);
+ mVehiclePropertyPublisher = new VehiclePropertyPublisher(
+ mMockCarPropertyService,
+ this::onPublisherFailure,
+ mFakeHandlerWrapper.getMockHandler());
}
@Test
@@ -108,7 +116,21 @@
mVehiclePropertyPublisher.addDataSubscriber(mMockDataSubscriber);
verify(mMockCarPropertyService).registerListener(eq(PROP_ID_1), eq(PROP_READ_RATE), any());
- assertThat(mVehiclePropertyPublisher.getDataSubscribers()).hasSize(1);
+ assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
+ }
+
+ @Test
+ public void testAddDataSubscriber_withSamePropertyId_registersSingleListener() {
+ DataSubscriber subscriber2 = mock(DataSubscriber.class);
+ when(subscriber2.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
+
+ mVehiclePropertyPublisher.addDataSubscriber(mMockDataSubscriber);
+ mVehiclePropertyPublisher.addDataSubscriber(subscriber2);
+
+ verify(mMockCarPropertyService, times(1))
+ .registerListener(eq(PROP_ID_1), eq(PROP_READ_RATE), any());
+ assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isTrue();
+ assertThat(mVehiclePropertyPublisher.hasDataSubscriber(subscriber2)).isTrue();
}
@Test
@@ -125,7 +147,7 @@
() -> mVehiclePropertyPublisher.addDataSubscriber(invalidDataSubscriber));
assertThat(error).hasMessageThat().contains("No access.");
- assertThat(mVehiclePropertyPublisher.getDataSubscribers()).isEmpty();
+ assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
}
@Test
@@ -137,7 +159,7 @@
() -> mVehiclePropertyPublisher.addDataSubscriber(invalidDataSubscriber));
assertThat(error).hasMessageThat().contains("not found");
- assertThat(mVehiclePropertyPublisher.getDataSubscribers()).isEmpty();
+ assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
}
@Test
@@ -145,21 +167,28 @@
mVehiclePropertyPublisher.addDataSubscriber(mMockDataSubscriber);
mVehiclePropertyPublisher.removeDataSubscriber(mMockDataSubscriber);
- // TODO(b/189143814): add proper verification
+
+ verify(mMockCarPropertyService, times(1)).unregisterListener(eq(PROP_ID_1), any());
+ assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
}
@Test
- public void testRemoveDataSubscriber_failsIfNotFound() {
- Throwable error = assertThrows(IllegalArgumentException.class,
- () -> mVehiclePropertyPublisher.removeDataSubscriber(mMockDataSubscriber));
-
- assertThat(error).hasMessageThat().contains("subscriber not found");
+ public void testRemoveDataSubscriber_ignoresIfNotFound() {
+ mVehiclePropertyPublisher.removeDataSubscriber(mMockDataSubscriber);
}
@Test
public void testRemoveAllDataSubscribers_succeeds() {
+ DataSubscriber subscriber2 = mock(DataSubscriber.class);
+ when(subscriber2.getPublisherParam()).thenReturn(PUBLISHER_PARAMS_1);
+ mVehiclePropertyPublisher.addDataSubscriber(mMockDataSubscriber);
+ mVehiclePropertyPublisher.addDataSubscriber(subscriber2);
+
mVehiclePropertyPublisher.removeAllDataSubscribers();
- // TODO(b/189143814): add tests
+
+ assertThat(mVehiclePropertyPublisher.hasDataSubscriber(mMockDataSubscriber)).isFalse();
+ assertThat(mVehiclePropertyPublisher.hasDataSubscriber(subscriber2)).isFalse();
+ verify(mMockCarPropertyService, times(1)).unregisterListener(eq(PROP_ID_1), any());
}
@Test
@@ -171,8 +200,10 @@
mCarPropertyCallbackCaptor.getValue().onEvent(Collections.singletonList(PROP_EVENT_1));
verify(mMockDataSubscriber).push(mBundleCaptor.capture());
- CarPropertyEvent event = mBundleCaptor.getValue().getParcelable(
- VehiclePropertyPublisher.CAR_PROPERTY_EVENT_KEY);
- assertThat(event).isEqualTo(PROP_EVENT_1);
+ // TODO(b/197269115): add more assertions on the contents of
+ // PersistableBundle object.
}
+
+ private void onPublisherFailure(AbstractPublisher publisher,
+ List<TelemetryProto.MetricsConfig> affectedConfigs, Throwable error) { }
}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ActivityForegroundStateChangedConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ActivityForegroundStateChangedConverterTest.java
new file mode 100644
index 0000000..221c716
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ActivityForegroundStateChangedConverterTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import static com.android.car.telemetry.AtomsProto.ActivityForegroundStateChanged.CLASS_NAME_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ActivityForegroundStateChanged.PKG_NAME_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ActivityForegroundStateChanged.STATE_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ActivityForegroundStateChanged.UID_FIELD_NUMBER;
+import static com.android.car.telemetry.databroker.ScriptExecutionTask.APPROX_BUNDLE_SIZE_BYTES_KEY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.PersistableBundle;
+import android.util.SparseArray;
+
+import com.android.car.telemetry.AtomsProto.ActivityForegroundStateChanged;
+import com.android.car.telemetry.AtomsProto.ActivityForegroundStateChanged.State;
+import com.android.car.telemetry.AtomsProto.Atom;
+import com.android.car.telemetry.StatsLogProto.DimensionsValue;
+import com.android.car.telemetry.publisher.HashUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class ActivityForegroundStateChangedConverterTest {
+ private static final Atom ATOM_A =
+ Atom.newBuilder()
+ .setActivityForegroundStateChanged(ActivityForegroundStateChanged.newBuilder()
+ .setClassName("className1")
+ .setState(State.BACKGROUND))
+ .build();
+
+ private static final Atom ATOM_B =
+ Atom.newBuilder()
+ .setActivityForegroundStateChanged(ActivityForegroundStateChanged.newBuilder()
+ .setClassName("className2")
+ .setState(State.FOREGROUND))
+ .build();
+
+ private static final Atom ATOM_MISMATCH =
+ Atom.newBuilder()
+ .setActivityForegroundStateChanged(ActivityForegroundStateChanged.newBuilder()
+ // Some fields are not set, creating mismatch with above atoms
+ .setClassName("className2"))
+ .build();
+
+ private static final List<Integer> DIM_FIELDS_IDS = Arrays.asList(1, 2);
+ private static final Long HASH_1 = HashUtils.murmur2Hash64("package.name.1");
+ private static final Long HASH_2 = HashUtils.murmur2Hash64("package.name.2");
+ private static final Map<Long, String> HASH_STR_MAP = Map.of(
+ HASH_1, "package.name.1",
+ HASH_2, "package.name.2");
+
+ private static final List<DimensionsValue> DV_PAIR_A =
+ Arrays.asList(
+ DimensionsValue.newBuilder().setValueInt(1000).build(),
+ DimensionsValue.newBuilder().setValueStrHash(HASH_1).build());
+
+ private static final List<DimensionsValue> DV_PAIR_B =
+ Arrays.asList(
+ DimensionsValue.newBuilder().setValueInt(2000).build(),
+ DimensionsValue.newBuilder().setValueStrHash(HASH_2).build());
+
+ private static final List<DimensionsValue> DV_PAIR_MALFORMED =
+ Arrays.asList(
+ DimensionsValue.newBuilder().setValueInt(3000).build(),
+ // Wrong format since leaf level dimension value should set value, not field
+ DimensionsValue.newBuilder().setField(3).build());
+
+ // Subject of the test.
+ private ActivityForegroundStateChangedConverter mConverter =
+ new ActivityForegroundStateChangedConverter();
+
+ @Test
+ public void testConvertAtomsListWithDimensionValues_putsCorrectDataToPersistableBundle()
+ throws StatsConversionException {
+ List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_B);
+ List<List<DimensionsValue>> dimensionsValuesList = Arrays.asList(DV_PAIR_A, DV_PAIR_B);
+
+ SparseArray<AtomFieldAccessor<ActivityForegroundStateChanged>> accessorMap =
+ mConverter.getAtomFieldAccessorMap();
+
+ PersistableBundle bundle = mConverter.convert(atomsList, DIM_FIELDS_IDS,
+ dimensionsValuesList, HASH_STR_MAP);
+
+ assertThat(bundle.size()).isEqualTo(5);
+ assertThat(bundle.getInt(APPROX_BUNDLE_SIZE_BYTES_KEY)).isEqualTo(120);
+ assertThat(bundle.getIntArray(accessorMap.get(UID_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1000, 2000).inOrder();
+ assertThat(Arrays.asList(
+ bundle.getStringArray(accessorMap.get(PKG_NAME_FIELD_NUMBER).getFieldName())))
+ .containsExactly("package.name.1", "package.name.2").inOrder();
+ assertThat(Arrays.asList(
+ bundle.getStringArray(accessorMap.get(CLASS_NAME_FIELD_NUMBER).getFieldName())))
+ .containsExactly("className1", "className2").inOrder();
+ assertThat(bundle.getIntArray(accessorMap.get(STATE_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(0, 1).inOrder(); // States background=0 and foreground=1
+ }
+
+ @Test
+ public void testAtomSetFieldInconsistency_throwsException() {
+ List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_MISMATCH);
+ List<List<DimensionsValue>> dimensionsValuesList = Arrays.asList(DV_PAIR_A, DV_PAIR_B);
+
+ assertThrows(
+ StatsConversionException.class,
+ () -> mConverter.convert(
+ atomsList,
+ DIM_FIELDS_IDS,
+ dimensionsValuesList,
+ HASH_STR_MAP));
+ }
+
+ @Test
+ public void testMalformedDimensionValue_throwsException() {
+ List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_B);
+ List<List<DimensionsValue>> dimensionsValuesList =
+ Arrays.asList(DV_PAIR_A, DV_PAIR_MALFORMED);
+
+ assertThrows(
+ StatsConversionException.class,
+ () -> mConverter.convert(
+ atomsList,
+ DIM_FIELDS_IDS,
+ dimensionsValuesList,
+ HASH_STR_MAP));
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/AppStartMemoryStateCapturedConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/AppStartMemoryStateCapturedConverterTest.java
new file mode 100644
index 0000000..b309069
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/AppStartMemoryStateCapturedConverterTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import static com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured.ACTIVITY_NAME_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured.CACHE_IN_BYTES_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured.PAGE_FAULT_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured.PAGE_MAJOR_FAULT_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured.PROCESS_NAME_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured.RSS_IN_BYTES_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured.SWAP_IN_BYTES_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured.UID_FIELD_NUMBER;
+import static com.android.car.telemetry.databroker.ScriptExecutionTask.APPROX_BUNDLE_SIZE_BYTES_KEY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.PersistableBundle;
+import android.util.SparseArray;
+
+import com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured;
+import com.android.car.telemetry.AtomsProto.Atom;
+import com.android.car.telemetry.StatsLogProto.DimensionsValue;
+import com.android.car.telemetry.publisher.HashUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class AppStartMemoryStateCapturedConverterTest {
+ private static final Atom ATOM_A =
+ Atom.newBuilder()
+ .setAppStartMemoryStateCaptured(AppStartMemoryStateCaptured.newBuilder()
+ .setActivityName("activityName1")
+ .setPageFault(59L)
+ .setPageMajorFault(34L)
+ .setRssInBytes(1234L)
+ .setCacheInBytes(234L)
+ .setSwapInBytes(111L))
+ .build();
+
+ private static final Atom ATOM_B =
+ Atom.newBuilder()
+ .setAppStartMemoryStateCaptured(AppStartMemoryStateCaptured.newBuilder()
+ .setActivityName("activityName2")
+ .setPageFault(99L)
+ .setPageMajorFault(55L)
+ .setRssInBytes(2345L)
+ .setCacheInBytes(345L)
+ .setSwapInBytes(222L))
+ .build();
+
+ private static final Atom ATOM_MISMATCH =
+ Atom.newBuilder()
+ .setAppStartMemoryStateCaptured(AppStartMemoryStateCaptured.newBuilder()
+ // Some fields are not set, creating mismatch with above atoms
+ .setPageFault(100L)
+ .setPageMajorFault(66L)
+ .setCacheInBytes(456L)
+ .setSwapInBytes(333L))
+ .build();
+
+ private static final List<Integer> DIM_FIELDS_IDS = Arrays.asList(1, 2);
+ private static final Long HASH_1 = HashUtils.murmur2Hash64("process.name.1");
+ private static final Long HASH_2 = HashUtils.murmur2Hash64("process.name.2");
+ private static final Map<Long, String> HASH_STR_MAP = Map.of(
+ HASH_1, "process.name.1",
+ HASH_2, "process.name.2");
+
+ private static final List<DimensionsValue> DV_PAIR_A =
+ Arrays.asList(
+ DimensionsValue.newBuilder().setValueInt(1000).build(),
+ DimensionsValue.newBuilder().setValueStrHash(HASH_1).build());
+
+ private static final List<DimensionsValue> DV_PAIR_B =
+ Arrays.asList(
+ DimensionsValue.newBuilder().setValueInt(2000).build(),
+ DimensionsValue.newBuilder().setValueStrHash(HASH_2).build());
+
+ private static final List<DimensionsValue> DV_PAIR_MALFORMED =
+ Arrays.asList(
+ DimensionsValue.newBuilder().setValueInt(3000).build(),
+ // Wrong format since leaf level dimension value should set value, not field
+ DimensionsValue.newBuilder().setField(3).build());
+
+ // Subject of the test.
+ private AppStartMemoryStateCapturedConverter mConverter =
+ new AppStartMemoryStateCapturedConverter();
+
+ @Test
+ public void testConvertAtomsListWithDimensionValues_putsCorrectDataToPersistableBundle()
+ throws StatsConversionException {
+ List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_B);
+ List<List<DimensionsValue>> dimensionsValuesList = Arrays.asList(DV_PAIR_A, DV_PAIR_B);
+
+ SparseArray<AtomFieldAccessor<AppStartMemoryStateCaptured>> accessorMap =
+ mConverter.getAtomFieldAccessorMap();
+
+ PersistableBundle bundle = mConverter.convert(atomsList, DIM_FIELDS_IDS,
+ dimensionsValuesList, HASH_STR_MAP);
+
+ assertThat(bundle.size()).isEqualTo(9);
+ assertThat(bundle.getInt(APPROX_BUNDLE_SIZE_BYTES_KEY)).isEqualTo(204);
+ assertThat(bundle.getIntArray(accessorMap.get(UID_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1000, 2000).inOrder();
+ assertThat(Arrays.asList(
+ bundle.getStringArray(accessorMap.get(PROCESS_NAME_FIELD_NUMBER).getFieldName())))
+ .containsExactly("process.name.1", "process.name.2").inOrder();
+ assertThat(Arrays.asList(
+ bundle.getStringArray(accessorMap.get(ACTIVITY_NAME_FIELD_NUMBER).getFieldName())))
+ .containsExactly("activityName1", "activityName2").inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(PAGE_FAULT_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(59L, 99L).inOrder();
+ assertThat(bundle.getLongArray(
+ accessorMap.get(PAGE_MAJOR_FAULT_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(34L, 55L).inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(RSS_IN_BYTES_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1234L, 2345L).inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(CACHE_IN_BYTES_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(234L, 345L).inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(SWAP_IN_BYTES_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(111L, 222L).inOrder();
+ }
+
+ @Test
+ public void testAtomSetFieldInconsistency_throwsException() {
+ List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_MISMATCH);
+ List<List<DimensionsValue>> dimensionsValuesList = Arrays.asList(DV_PAIR_A, DV_PAIR_B);
+
+ assertThrows(
+ StatsConversionException.class,
+ () -> mConverter.convert(
+ atomsList,
+ DIM_FIELDS_IDS,
+ dimensionsValuesList,
+ HASH_STR_MAP));
+ }
+
+ @Test
+ public void testMalformedDimensionValue_throwsException() {
+ List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_B);
+ List<List<DimensionsValue>> dimensionsValuesList =
+ Arrays.asList(DV_PAIR_A, DV_PAIR_MALFORMED);
+
+ assertThrows(
+ StatsConversionException.class,
+ () -> mConverter.convert(
+ atomsList,
+ DIM_FIELDS_IDS,
+ dimensionsValuesList,
+ HASH_STR_MAP));
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/AtomListConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/AtomListConverterTest.java
new file mode 100644
index 0000000..4704966
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/AtomListConverterTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import static com.android.car.telemetry.databroker.ScriptExecutionTask.APPROX_BUNDLE_SIZE_BYTES_KEY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.PersistableBundle;
+import android.util.SparseArray;
+
+import com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured;
+import com.android.car.telemetry.AtomsProto.Atom;
+import com.android.car.telemetry.AtomsProto.ProcessMemoryState;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class AtomListConverterTest {
+ @Test
+ public void testConvertPushedAtomsListWithUnsetFields_putsCorrectDataToPersistableBundle()
+ throws StatsConversionException {
+ List<Atom> pushedAtomsList = Arrays.asList(
+ Atom.newBuilder()
+ .setAppStartMemoryStateCaptured(
+ AppStartMemoryStateCaptured.newBuilder()
+ .setUid(1000)
+ .setActivityName("activityName1")
+ .setRssInBytes(1234L))
+ .build(),
+ Atom.newBuilder()
+ .setAppStartMemoryStateCaptured(
+ AppStartMemoryStateCaptured.newBuilder()
+ .setUid(1100)
+ .setActivityName("activityName2")
+ .setRssInBytes(2345L))
+ .build()
+ );
+ SparseArray<AtomFieldAccessor<AppStartMemoryStateCaptured>> accessorMap =
+ new AppStartMemoryStateCapturedConverter().getAtomFieldAccessorMap();
+
+ PersistableBundle bundle = AtomListConverter.convert(pushedAtomsList, null, null, null);
+
+ assertThat(bundle.size()).isEqualTo(4);
+ assertThat(bundle.getInt(APPROX_BUNDLE_SIZE_BYTES_KEY)).isEqualTo(80);
+ assertThat(bundle.getIntArray(
+ accessorMap.get(AppStartMemoryStateCaptured.UID_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1000, 1100).inOrder();
+ assertThat(Arrays.asList(bundle.getStringArray(
+ accessorMap.get(AppStartMemoryStateCaptured.ACTIVITY_NAME_FIELD_NUMBER)
+ .getFieldName())))
+ .containsExactly("activityName1", "activityName2").inOrder();
+ assertThat(bundle.getLongArray(
+ accessorMap.get(AppStartMemoryStateCaptured.RSS_IN_BYTES_FIELD_NUMBER)
+ .getFieldName()))
+ .asList().containsExactly(1234L, 2345L).inOrder();
+ }
+
+ @Test
+ public void testConvertPulledAtomsListWithUnsetFields_putsCorrectDataToPersistableBundle()
+ throws StatsConversionException {
+ List<Atom> pulledAtomsList = Arrays.asList(
+ Atom.newBuilder()
+ .setProcessMemoryState(ProcessMemoryState.newBuilder()
+ .setUid(1000)
+ .setProcessName("processName1")
+ .setRssInBytes(1234L))
+ .build(),
+ Atom.newBuilder()
+ .setProcessMemoryState(ProcessMemoryState.newBuilder()
+ .setUid(1100)
+ .setProcessName("processName2")
+ .setRssInBytes(2345L))
+ .build()
+ );
+ SparseArray<AtomFieldAccessor<ProcessMemoryState>> accessorMap =
+ new ProcessMemoryStateConverter().getAtomFieldAccessorMap();
+
+ PersistableBundle bundle = AtomListConverter.convert(pulledAtomsList, null, null, null);
+
+ assertThat(bundle.size()).isEqualTo(4);
+ assertThat(bundle.getInt(APPROX_BUNDLE_SIZE_BYTES_KEY)).isEqualTo(76);
+ assertThat(bundle.getIntArray(
+ accessorMap.get(ProcessMemoryState.UID_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1000, 1100).inOrder();
+ assertThat(Arrays.asList(bundle.getStringArray(
+ accessorMap.get(ProcessMemoryState.PROCESS_NAME_FIELD_NUMBER).getFieldName())))
+ .containsExactly("processName1", "processName2").inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(
+ ProcessMemoryState.RSS_IN_BYTES_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1234L, 2345L).inOrder();
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ConfigMetricsReportListConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ConfigMetricsReportListConverterTest.java
new file mode 100644
index 0000000..63ee0b8
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ConfigMetricsReportListConverterTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.PersistableBundle;
+import android.util.SparseArray;
+
+import com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured;
+import com.android.car.telemetry.AtomsProto.Atom;
+import com.android.car.telemetry.AtomsProto.ProcessMemoryState;
+import com.android.car.telemetry.StatsLogProto.ConfigMetricsReport;
+import com.android.car.telemetry.StatsLogProto.ConfigMetricsReportList;
+import com.android.car.telemetry.StatsLogProto.DimensionsValue;
+import com.android.car.telemetry.StatsLogProto.DimensionsValueTuple;
+import com.android.car.telemetry.StatsLogProto.EventMetricData;
+import com.android.car.telemetry.StatsLogProto.GaugeBucketInfo;
+import com.android.car.telemetry.StatsLogProto.GaugeMetricData;
+import com.android.car.telemetry.StatsLogProto.StatsLogReport;
+import com.android.car.telemetry.publisher.HashUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class ConfigMetricsReportListConverterTest {
+ @Test
+ public void testConvertMultipleReports_correctlySetsPersistableBundles()
+ throws StatsConversionException {
+ String testGaugeMetricProcessName = "process.name";
+ Long hash = HashUtils.murmur2Hash64(testGaugeMetricProcessName);
+ EventMetricData eventData = EventMetricData.newBuilder()
+ .setElapsedTimestampNanos(99999999L)
+ .setAtom(Atom.newBuilder()
+ .setAppStartMemoryStateCaptured(
+ AppStartMemoryStateCaptured.newBuilder()
+ .setUid(1000)
+ .setActivityName("activityName")
+ .setRssInBytes(1234L)))
+ .build();
+
+ GaugeMetricData gaugeData = GaugeMetricData.newBuilder()
+ .addBucketInfo(GaugeBucketInfo.newBuilder()
+ .addAtom(Atom.newBuilder()
+ .setProcessMemoryState(ProcessMemoryState.newBuilder()
+ .setRssInBytes(4567L)))
+ .addElapsedTimestampNanos(445678901L))
+ .addDimensionLeafValuesInWhat(DimensionsValue.newBuilder()
+ .setValueInt(234))
+ .addDimensionLeafValuesInWhat(DimensionsValue.newBuilder()
+ .setValueStrHash(hash))
+ .build();
+
+ ConfigMetricsReportList reportList = ConfigMetricsReportList.newBuilder()
+ .addReports(ConfigMetricsReport.newBuilder()
+ .addMetrics(StatsLogReport.newBuilder()
+ .setMetricId(12345L)
+ .setEventMetrics(
+ StatsLogReport.EventMetricDataWrapper.newBuilder()
+ .addData(eventData))))
+ .addReports(ConfigMetricsReport.newBuilder()
+ .addMetrics(StatsLogReport.newBuilder()
+ .setMetricId(23456L)
+ .setGaugeMetrics(
+ StatsLogReport.GaugeMetricDataWrapper.newBuilder()
+ .addData(gaugeData))
+ .setDimensionsPathInWhat(DimensionsValue.newBuilder()
+ .setValueTuple(DimensionsValueTuple.newBuilder()
+ .addDimensionsValue(DimensionsValue.newBuilder()
+ .setField(1))
+ .addDimensionsValue(DimensionsValue.newBuilder()
+ .setField(2)))))
+ .addStrings(testGaugeMetricProcessName))
+ .build();
+ SparseArray<AtomFieldAccessor<AppStartMemoryStateCaptured>> appMemAccessorMap =
+ new AppStartMemoryStateCapturedConverter().getAtomFieldAccessorMap();
+ SparseArray<AtomFieldAccessor<ProcessMemoryState>> procMemAccessorMap =
+ new ProcessMemoryStateConverter().getAtomFieldAccessorMap();
+
+ Map<Long, PersistableBundle> map = ConfigMetricsReportListConverter.convert(reportList);
+
+ assertThat(new ArrayList<Long>(map.keySet())).containsExactly(12345L, 23456L);
+ PersistableBundle eventBundle = map.get(12345L);
+ assertThat(eventBundle.getLongArray(EventMetricDataConverter.ELAPSED_TIME_NANOS))
+ .asList().containsExactly(99999999L);
+ assertThat(eventBundle.getIntArray(
+ appMemAccessorMap.get(AppStartMemoryStateCaptured.UID_FIELD_NUMBER)
+ .getFieldName()))
+ .asList().containsExactly(1000);
+ assertThat(Arrays.asList(eventBundle.getStringArray(
+ appMemAccessorMap.get(AppStartMemoryStateCaptured.ACTIVITY_NAME_FIELD_NUMBER)
+ .getFieldName())))
+ .containsExactly("activityName");
+ assertThat(eventBundle.getLongArray(
+ appMemAccessorMap.get(AppStartMemoryStateCaptured.RSS_IN_BYTES_FIELD_NUMBER)
+ .getFieldName()))
+ .asList().containsExactly(1234L);
+ PersistableBundle gaugeBundle = map.get(23456L);
+ assertThat(gaugeBundle.getIntArray(
+ procMemAccessorMap.get(ProcessMemoryState.UID_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(234);
+ assertThat(Arrays.asList(gaugeBundle.getStringArray(
+ procMemAccessorMap.get(ProcessMemoryState.PROCESS_NAME_FIELD_NUMBER)
+ .getFieldName())))
+ .containsExactly("process.name");
+ assertThat(gaugeBundle.getLongArray(
+ procMemAccessorMap.get(ProcessMemoryState.RSS_IN_BYTES_FIELD_NUMBER)
+ .getFieldName()))
+ .asList().containsExactly(4567L);
+ assertThat(gaugeBundle.getLongArray(GaugeMetricDataConverter.ELAPSED_TIME_NANOS))
+ .asList().containsExactly(445678901L);
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/EventMetricDataConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/EventMetricDataConverterTest.java
new file mode 100644
index 0000000..0bac4cd
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/EventMetricDataConverterTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import static com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured.ACTIVITY_NAME_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured.RSS_IN_BYTES_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured.UID_FIELD_NUMBER;
+import static com.android.car.telemetry.databroker.ScriptExecutionTask.APPROX_BUNDLE_SIZE_BYTES_KEY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.PersistableBundle;
+import android.util.SparseArray;
+
+import com.android.car.telemetry.AtomsProto.AppStartMemoryStateCaptured;
+import com.android.car.telemetry.AtomsProto.Atom;
+import com.android.car.telemetry.StatsLogProto.EventMetricData;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class EventMetricDataConverterTest {
+ @Test
+ public void testConvertEventDataList_putsCorrectDataIntoPersistableBundle()
+ throws StatsConversionException {
+ List<EventMetricData> eventDataList = Arrays.asList(
+ EventMetricData.newBuilder()
+ .setElapsedTimestampNanos(12345678L)
+ .setAtom(Atom.newBuilder()
+ .setAppStartMemoryStateCaptured(
+ AppStartMemoryStateCaptured.newBuilder()
+ .setUid(1000)
+ .setActivityName("activityName1")
+ .setRssInBytes(1234L)))
+ .build(),
+ EventMetricData.newBuilder()
+ .setElapsedTimestampNanos(23456789L)
+ .setAtom(Atom.newBuilder()
+ .setAppStartMemoryStateCaptured(
+ AppStartMemoryStateCaptured.newBuilder()
+ .setUid(1100)
+ .setActivityName("activityName2")
+ .setRssInBytes(2345L)))
+ .build()
+ );
+ SparseArray<AtomFieldAccessor<AppStartMemoryStateCaptured>> accessorMap =
+ new AppStartMemoryStateCapturedConverter().getAtomFieldAccessorMap();
+
+ PersistableBundle bundle = EventMetricDataConverter.convertEventDataList(eventDataList);
+
+ assertThat(bundle.size()).isEqualTo(5);
+ assertThat(bundle.getInt(APPROX_BUNDLE_SIZE_BYTES_KEY)).isEqualTo(96);
+ assertThat(bundle.getLongArray(EventMetricDataConverter.ELAPSED_TIME_NANOS))
+ .asList().containsExactly(12345678L, 23456789L).inOrder();
+ assertThat(bundle.getIntArray(accessorMap.get(UID_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1000, 1100).inOrder();
+ assertThat(Arrays.asList(bundle.getStringArray(
+ accessorMap.get(ACTIVITY_NAME_FIELD_NUMBER).getFieldName())))
+ .containsExactly("activityName1", "activityName2").inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(RSS_IN_BYTES_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1234L, 2345L).inOrder();
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/GaugeMetricDataConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/GaugeMetricDataConverterTest.java
new file mode 100644
index 0000000..808a6a1
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/GaugeMetricDataConverterTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.PAGE_FAULT_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.PROCESS_NAME_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.RSS_IN_BYTES_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.SWAP_IN_BYTES_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.UID_FIELD_NUMBER;
+import static com.android.car.telemetry.databroker.ScriptExecutionTask.APPROX_BUNDLE_SIZE_BYTES_KEY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.PersistableBundle;
+import android.util.SparseArray;
+
+import com.android.car.telemetry.AtomsProto;
+import com.android.car.telemetry.StatsLogProto;
+import com.android.car.telemetry.publisher.HashUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class GaugeMetricDataConverterTest {
+ @Test
+ public void testConvertGaugeDataList_putsCorrectDataIntoPersistableBundle()
+ throws StatsConversionException {
+ Long hash1 = HashUtils.murmur2Hash64("process.name.A");
+ Long hash2 = HashUtils.murmur2Hash64("process.name.B");
+ List<StatsLogProto.GaugeMetricData> gaugeDataList = Arrays.asList(
+ StatsLogProto.GaugeMetricData.newBuilder()
+ .addBucketInfo(StatsLogProto.GaugeBucketInfo.newBuilder()
+ .addAtom(AtomsProto.Atom.newBuilder()
+ .setProcessMemoryState(
+ AtomsProto.ProcessMemoryState.newBuilder()
+ .setPageFault(1000L)
+ .setRssInBytes(1234L)))
+ .addElapsedTimestampNanos(12345678L)
+ .addAtom(AtomsProto.Atom.newBuilder()
+ .setProcessMemoryState(
+ AtomsProto.ProcessMemoryState.newBuilder()
+ .setPageFault(1100L)
+ .setRssInBytes(2345L)))
+ .addElapsedTimestampNanos(23456789L))
+ .addBucketInfo(StatsLogProto.GaugeBucketInfo.newBuilder()
+ .addAtom(AtomsProto.Atom.newBuilder()
+ .setProcessMemoryState(
+ AtomsProto.ProcessMemoryState.newBuilder()
+ .setPageFault(1200L)
+ .setRssInBytes(3456L)))
+ .addElapsedTimestampNanos(34567890L))
+ .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+ .setValueInt(123))
+ .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+ .setValueStrHash(hash1))
+ .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+ .setValueLong(11111111L))
+ .build(),
+ StatsLogProto.GaugeMetricData.newBuilder()
+ .addBucketInfo(StatsLogProto.GaugeBucketInfo.newBuilder()
+ .addAtom(AtomsProto.Atom.newBuilder()
+ .setProcessMemoryState(
+ AtomsProto.ProcessMemoryState.newBuilder()
+ .setPageFault(1300L)
+ .setRssInBytes(4567L)))
+ .addElapsedTimestampNanos(445678901L))
+ .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+ .setValueInt(234))
+ .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+ .setValueStrHash(hash2))
+ .addDimensionLeafValuesInWhat(StatsLogProto.DimensionsValue.newBuilder()
+ .setValueLong(22222222L))
+ .build()
+ );
+ List<Integer> dimensionsFieldsIds = Arrays.asList(1, 2, 8);
+ Map<Long, String> hashToStringMap = Map.of(
+ hash1, "process.name.1",
+ hash2, "process.name.2");
+ SparseArray<AtomFieldAccessor<AtomsProto.ProcessMemoryState>> accessorMap =
+ new ProcessMemoryStateConverter().getAtomFieldAccessorMap();
+
+ PersistableBundle bundle = GaugeMetricDataConverter.convertGaugeDataList(
+ gaugeDataList, dimensionsFieldsIds, hashToStringMap);
+
+ // For each atom 2 fields were set, additionally 3 fields were encoded in dimension values,
+ // and 1 elapsed time array, so 6 arrays are expected in the bundle.
+ assertThat(bundle.size()).isEqualTo(7);
+ assertThat(bundle.getInt(APPROX_BUNDLE_SIZE_BYTES_KEY)).isEqualTo(264);
+ assertThat(bundle.getIntArray(accessorMap.get(UID_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(123, 123, 123, 234).inOrder();
+ assertThat(Arrays.asList(bundle.getStringArray(
+ accessorMap.get(PROCESS_NAME_FIELD_NUMBER).getFieldName())))
+ .containsExactly("process.name.1", "process.name.1",
+ "process.name.1", "process.name.2").inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(PAGE_FAULT_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1000L, 1100L, 1200L, 1300L).inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(RSS_IN_BYTES_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1234L, 2345L, 3456L, 4567L).inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(SWAP_IN_BYTES_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(11111111L, 11111111L, 11111111L, 22222222L).inOrder();
+ assertThat(bundle.getLongArray(GaugeMetricDataConverter.ELAPSED_TIME_NANOS))
+ .asList().containsExactly(12345678L, 23456789L, 34567890L, 445678901L).inOrder();
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemoryStateConverterTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemoryStateConverterTest.java
new file mode 100644
index 0000000..050cb4c
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/publisher/statsconverters/ProcessMemoryStateConverterTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2021 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.telemetry.publisher.statsconverters;
+
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.CACHE_IN_BYTES_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.OOM_ADJ_SCORE_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.PAGE_FAULT_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.PAGE_MAJOR_FAULT_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.PROCESS_NAME_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.RSS_IN_BYTES_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.SWAP_IN_BYTES_FIELD_NUMBER;
+import static com.android.car.telemetry.AtomsProto.ProcessMemoryState.UID_FIELD_NUMBER;
+import static com.android.car.telemetry.databroker.ScriptExecutionTask.APPROX_BUNDLE_SIZE_BYTES_KEY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.PersistableBundle;
+import android.util.SparseArray;
+
+import com.android.car.telemetry.AtomsProto.Atom;
+import com.android.car.telemetry.AtomsProto.ProcessMemoryState;
+import com.android.car.telemetry.StatsLogProto.DimensionsValue;
+import com.android.car.telemetry.publisher.HashUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class ProcessMemoryStateConverterTest {
+ private static final Atom ATOM_A =
+ Atom.newBuilder()
+ .setProcessMemoryState(ProcessMemoryState.newBuilder()
+ .setOomAdjScore(100)
+ .setPageFault(59L)
+ .setPageMajorFault(34L)
+ .setRssInBytes(1234L)
+ .setCacheInBytes(234L)
+ .setSwapInBytes(111L))
+ .build();
+
+ private static final Atom ATOM_B =
+ Atom.newBuilder()
+ .setProcessMemoryState(ProcessMemoryState.newBuilder()
+ .setOomAdjScore(200)
+ .setPageFault(99L)
+ .setPageMajorFault(55L)
+ .setRssInBytes(2345L)
+ .setCacheInBytes(345L)
+ .setSwapInBytes(222L))
+ .build();
+
+ private static final Atom ATOM_MISMATCH =
+ Atom.newBuilder()
+ .setProcessMemoryState(ProcessMemoryState.newBuilder()
+ // Some fields are not set, creating mismatch with above atoms
+ .setPageFault(100L)
+ .setPageMajorFault(66L)
+ .setCacheInBytes(456L)
+ .setSwapInBytes(333L))
+ .build();
+
+ private static final List<Integer> DIM_FIELDS_IDS = Arrays.asList(1, 2);
+ private static final Long HASH_1 = HashUtils.murmur2Hash64("process.name.1");
+ private static final Long HASH_2 = HashUtils.murmur2Hash64("process.name.2");
+ private static final Map<Long, String> HASH_STR_MAP = Map.of(
+ HASH_1, "process.name.1",
+ HASH_2, "process.name.2");
+
+ private static final List<DimensionsValue> DV_PAIR_A =
+ Arrays.asList(
+ DimensionsValue.newBuilder().setValueInt(1000).build(),
+ DimensionsValue.newBuilder().setValueStrHash(HASH_1).build());
+
+ private static final List<DimensionsValue> DV_PAIR_B =
+ Arrays.asList(
+ DimensionsValue.newBuilder().setValueInt(2000).build(),
+ DimensionsValue.newBuilder().setValueStrHash(HASH_2).build());
+
+ private static final List<DimensionsValue> DV_PAIR_MALFORMED =
+ Arrays.asList(
+ DimensionsValue.newBuilder().setValueInt(3000).build(),
+ // Wrong format since leaf level dimension value should set value, not field
+ DimensionsValue.newBuilder().setField(3).build());
+
+ // Subject of the test.
+ private ProcessMemoryStateConverter mConverter = new ProcessMemoryStateConverter();
+
+ @Test
+ public void testConvertAtomsListWithDimensionValues_putsCorrectDataToPersistableBundle()
+ throws StatsConversionException {
+ List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_B);
+ List<List<DimensionsValue>> dimensionsValuesList = Arrays.asList(DV_PAIR_A, DV_PAIR_B);
+ SparseArray<AtomFieldAccessor<ProcessMemoryState>> accessorMap =
+ mConverter.getAtomFieldAccessorMap();
+
+ PersistableBundle bundle = mConverter.convert(atomsList, DIM_FIELDS_IDS,
+ dimensionsValuesList, HASH_STR_MAP);
+
+ assertThat(bundle.size()).isEqualTo(9);
+ assertThat(bundle.getInt(APPROX_BUNDLE_SIZE_BYTES_KEY)).isEqualTo(156);
+ assertThat(bundle.getIntArray(accessorMap.get(UID_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1000, 2000).inOrder();
+ assertThat(Arrays.asList(
+ bundle.getStringArray(accessorMap.get(PROCESS_NAME_FIELD_NUMBER).getFieldName())))
+ .containsExactly("process.name.1", "process.name.2").inOrder();
+ assertThat(bundle.getIntArray(accessorMap.get(OOM_ADJ_SCORE_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(100, 200).inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(PAGE_FAULT_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(59L, 99L).inOrder();
+ assertThat(bundle.getLongArray(
+ accessorMap.get(PAGE_MAJOR_FAULT_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(34L, 55L).inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(RSS_IN_BYTES_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(1234L, 2345L).inOrder();
+ assertThat(bundle.getLongArray(
+ accessorMap.get(CACHE_IN_BYTES_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(234L, 345L).inOrder();
+ assertThat(bundle.getLongArray(accessorMap.get(SWAP_IN_BYTES_FIELD_NUMBER).getFieldName()))
+ .asList().containsExactly(111L, 222L).inOrder();
+ }
+
+ @Test
+ public void testAtomSetFieldInconsistency_throwsException() {
+ List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_MISMATCH);
+ List<List<DimensionsValue>> dimensionsValuesList = Arrays.asList(DV_PAIR_A, DV_PAIR_B);
+
+ assertThrows(
+ StatsConversionException.class,
+ () -> mConverter.convert(
+ atomsList,
+ DIM_FIELDS_IDS,
+ dimensionsValuesList,
+ HASH_STR_MAP));
+ }
+
+ @Test
+ public void testMalformedDimensionValue_throwsException() {
+ List<Atom> atomsList = Arrays.asList(ATOM_A, ATOM_B);
+ List<List<DimensionsValue>> dimensionsValuesList =
+ Arrays.asList(DV_PAIR_A, DV_PAIR_MALFORMED);
+
+ assertThrows(
+ StatsConversionException.class,
+ () -> mConverter.convert(
+ atomsList,
+ DIM_FIELDS_IDS,
+ dimensionsValuesList,
+ HASH_STR_MAP));
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorTest.java b/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorTest.java
new file mode 100644
index 0000000..0e7ba3a
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/telemetry/systemmonitor/SystemMonitorTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2021 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.telemetry.systemmonitor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.MemoryInfo;
+import android.content.Context;
+import android.os.Handler;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SystemMonitorTest {
+
+ @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private static final String TEST_LOADAVG = "1.2 3.4 2.2 123/1452 21348";
+ private static final String TEST_LOADAVG_BAD_FORMAT = "1.2 3.4";
+ private static final String TEST_LOADAVG_NOT_FLOAT = "1.2 abc 2.1 12/231 2";
+
+ @Mock private Context mMockContext;
+ @Mock private Handler mMockHandler; // it promptly executes the runnable in the same thread
+ @Mock private ActivityManager mMockActivityManager;
+ @Mock private SystemMonitor.SystemMonitorCallback mMockCallback;
+
+ @Captor ArgumentCaptor<Runnable> mRunnableCaptor;
+ @Captor ArgumentCaptor<SystemMonitorEvent> mEventCaptor;
+
+ @Before
+ public void setup() {
+ when(mMockContext.getSystemService(anyString())).thenReturn(mMockActivityManager);
+ when(mMockHandler.post(any(Runnable.class))).thenAnswer(i -> {
+ Runnable runnable = i.getArgument(0);
+ runnable.run();
+ return true;
+ });
+ }
+
+ @Test
+ public void testSetEventCpuUsageLevel_setsCorrectUsageLevelForHighUsage() {
+ SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+ SystemMonitorEvent event = new SystemMonitorEvent();
+
+ systemMonitor.setEventCpuUsageLevel(event, /* cpuLoadPerCore= */ 1.5);
+
+ assertThat(event.getCpuUsageLevel())
+ .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_HI);
+ }
+
+ @Test
+ public void testSetEventCpuUsageLevel_setsCorrectUsageLevelForMedUsage() {
+ SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+ SystemMonitorEvent event = new SystemMonitorEvent();
+
+ systemMonitor.setEventCpuUsageLevel(event, /* cpuLoadPerCore= */ 0.6);
+
+ assertThat(event.getCpuUsageLevel())
+ .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_MED);
+ }
+
+ @Test
+ public void testSetEventCpuUsageLevel_setsCorrectUsageLevelForLowUsage() {
+ SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+ SystemMonitorEvent event = new SystemMonitorEvent();
+
+ systemMonitor.setEventCpuUsageLevel(event, /* cpuLoadPerCore= */ 0.5);
+
+ assertThat(event.getCpuUsageLevel())
+ .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_LOW);
+ }
+
+ @Test
+ public void testSetEventMemUsageLevel_setsCorrectUsageLevelForHighUsage() {
+ SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+ SystemMonitorEvent event = new SystemMonitorEvent();
+
+ systemMonitor.setEventMemUsageLevel(event, /* memLoadRatio= */ 0.98);
+
+ assertThat(event.getMemoryUsageLevel())
+ .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_HI);
+ }
+
+ @Test
+ public void testSetEventMemUsageLevel_setsCorrectUsageLevelForMedUsage() {
+ SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+ SystemMonitorEvent event = new SystemMonitorEvent();
+
+ systemMonitor.setEventMemUsageLevel(event, /* memLoadRatio= */ 0.85);
+
+ assertThat(event.getMemoryUsageLevel())
+ .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_MED);
+ }
+
+ @Test
+ public void testSetEventMemUsageLevel_setsCorrectUsageLevelForLowUsage() {
+ SystemMonitor systemMonitor = SystemMonitor.create(mMockContext, mMockHandler);
+ SystemMonitorEvent event = new SystemMonitorEvent();
+
+ systemMonitor.setEventMemUsageLevel(event, /* memLoadRatio= */ 0.80);
+
+ assertThat(event.getMemoryUsageLevel())
+ .isEqualTo(SystemMonitorEvent.USAGE_LEVEL_LOW);
+ }
+
+ @Test
+ public void testSetCallback_whenMemUsageLow_shouldInvokeCallback() throws IOException {
+ doAnswer(i -> {
+ MemoryInfo mi = i.getArgument(0); // memory usage is at 50%
+ mi.availMem = 5_000_000L;
+ mi.totalMem = 10_000_000;
+ return null;
+ }).when(mMockActivityManager).getMemoryInfo(any(MemoryInfo.class));
+ SystemMonitor systemMonitor = new SystemMonitor(
+ mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG));
+
+ systemMonitor.setSystemMonitorCallback(mMockCallback);
+
+ verify(mMockCallback, atLeastOnce()).onSystemMonitorEvent(mEventCaptor.capture());
+ SystemMonitorEvent event = mEventCaptor.getValue();
+ // from TEST_LOADAVG, cpu load = 1.2 / numProcessors, CPU usage should be low
+ assertThat(event.getCpuUsageLevel()).isEqualTo(SystemMonitorEvent.USAGE_LEVEL_LOW);
+ // 1 - 5_000_000 / 10_000_000 = 0.5, memory usage should be low
+ assertThat(event.getMemoryUsageLevel()).isEqualTo(SystemMonitorEvent.USAGE_LEVEL_LOW);
+ }
+
+ @Test
+ public void testSetCallback_whenMemUsageHigh_shouldInvokeCallback() throws IOException {
+ doAnswer(i -> {
+ MemoryInfo mi = i.getArgument(0); // memory usage is at 95%
+ mi.availMem = 500_000L;
+ mi.totalMem = 10_000_000L;
+ return null;
+ }).when(mMockActivityManager).getMemoryInfo(any(MemoryInfo.class));
+ SystemMonitor systemMonitor = new SystemMonitor(
+ mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG));
+
+ systemMonitor.setSystemMonitorCallback(mMockCallback);
+
+ verify(mMockCallback, atLeastOnce()).onSystemMonitorEvent(mEventCaptor.capture());
+ SystemMonitorEvent event = mEventCaptor.getValue();
+ // 1 - 500_000 / 10_000_000 = 0.95, memory usage should be high
+ assertThat(event.getMemoryUsageLevel()).isEqualTo(SystemMonitorEvent.USAGE_LEVEL_HI);
+ }
+
+ @Test
+ public void testWhenLoadavgIsBadFormat_getCpuLoadReturnsNull() throws IOException {
+ SystemMonitor systemMonitor = new SystemMonitor(
+ mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG_BAD_FORMAT));
+
+ assertThat(systemMonitor.getCpuLoad()).isNull();
+ }
+
+ @Test
+ public void testWhenLoadavgIsNotFloatParsable_getCpuLoadReturnsNull() throws IOException {
+ SystemMonitor systemMonitor = new SystemMonitor(
+ mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG_NOT_FLOAT));
+
+ assertThat(systemMonitor.getCpuLoad()).isNull();
+ }
+
+ @Test
+ public void testWhenUnsetCallback_sameCallbackFromSetCallbackIsRemoved() throws IOException {
+ SystemMonitor systemMonitor = new SystemMonitor(
+ mMockContext, mMockHandler, writeTempFile(TEST_LOADAVG));
+
+ systemMonitor.setSystemMonitorCallback(mMockCallback);
+ systemMonitor.unsetSystemMonitorCallback();
+
+ verify(mMockHandler, times(1)).post(mRunnableCaptor.capture());
+ Runnable setRunnable = mRunnableCaptor.getValue();
+ verify(mMockHandler, times(1)).removeCallbacks(mRunnableCaptor.capture());
+ Runnable unsetRunnable = mRunnableCaptor.getValue();
+ assertThat(setRunnable).isEqualTo(unsetRunnable);
+ }
+
+ /**
+ * Creates and writes to the temp file, returns its path.
+ */
+ private String writeTempFile(String content) throws IOException {
+ File tempFile = temporaryFolder.newFile();
+ try (FileWriter fw = new FileWriter(tempFile)) {
+ fw.write(content);
+ }
+ return tempFile.getAbsolutePath();
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/user/InitialUserSetterTest.java b/tests/carservice_unit_test/src/com/android/car/user/InitialUserSetterTest.java
index 3a7430b..1cb5d41 100644
--- a/tests/carservice_unit_test/src/com/android/car/user/InitialUserSetterTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/user/InitialUserSetterTest.java
@@ -593,6 +593,40 @@
}
@Test
+ public void testDefaultBehavior_firstBoot_ok_setEmptyLocale() throws Exception {
+ // no need to mock hasInitialUser(), it will return false by default
+ UserInfo newUser = expectCreateFullUser(USER_ID, OWNER_NAME, UserInfo.FLAG_ADMIN);
+ expectSwitchUser(USER_ID);
+
+ mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR)
+ .setUserLocales("")
+ .build());
+
+ verifyUserSwitched(USER_ID);
+ verifyFallbackDefaultBehaviorNeverCalled();
+ verifySystemUserUnlocked();
+ assertInitialUserSet(newUser);
+ assertSystemLocalesToBeNull();
+ }
+
+ @Test
+ public void testDefaultBehavior_firstBoot_ok_setBlankLocale() throws Exception {
+ // no need to mock hasInitialUser(), it will return false by default
+ UserInfo newUser = expectCreateFullUser(USER_ID, OWNER_NAME, UserInfo.FLAG_ADMIN);
+ expectSwitchUser(USER_ID);
+
+ mSetter.set(new Builder(InitialUserSetter.TYPE_DEFAULT_BEHAVIOR)
+ .setUserLocales(" ")
+ .build());
+
+ verifyUserSwitched(USER_ID);
+ verifyFallbackDefaultBehaviorNeverCalled();
+ verifySystemUserUnlocked();
+ assertInitialUserSet(newUser);
+ assertSystemLocalesToBeNull();
+ }
+
+ @Test
public void testDefaultBehavior_firstBoot_fail_createUserFailed() throws Exception {
// no need to mock hasInitialUser(), it will return false by default
// no need to mock createUser(), it will return null by default
@@ -1182,6 +1216,10 @@
assertThat(getSettingsString(Settings.System.SYSTEM_LOCALES)).isEqualTo(expected);
}
+ private void assertSystemLocalesToBeNull() {
+ assertThat(getSettingsString(Settings.System.SYSTEM_LOCALES)).isNull();
+ }
+
private final class MyListener implements Consumer<UserInfo> {
public int numberCalls;
public UserInfo initialUser;
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java b/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java
index b12aa27..e757536 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/CarWatchdogServiceUnitTest.java
@@ -16,21 +16,31 @@
package com.android.car.watchdog;
-import static android.automotive.watchdog.internal.ResourceOveruseActionType.KILLED;
+import static android.automotive.watchdog.internal.ResourceOveruseActionType.KILLED_RECURRING_OVERUSE;
import static android.automotive.watchdog.internal.ResourceOveruseActionType.NOT_KILLED;
-import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetAliveUsers;
+import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetAllUsers;
+import static android.car.test.mocks.AndroidMockitoHelper.mockUmGetUserHandles;
+import static android.car.test.mocks.AndroidMockitoHelper.mockUmIsUserRunning;
import static android.car.watchdog.CarWatchdogManager.TIMEOUT_CRITICAL;
+import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__KILL_REASON__KILLED_ON_IO_OVERUSE;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__GARAGE_MODE;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE;
+import static com.android.car.CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED__UID_STATE__UNKNOWN_UID_STATE;
+import static com.android.car.watchdog.WatchdogStorage.ZONE_OFFSET;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
+import static org.mockito.AdditionalMatchers.or;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyList;
@@ -39,9 +49,9 @@
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -52,15 +62,26 @@
import android.automotive.watchdog.internal.GarageMode;
import android.automotive.watchdog.internal.ICarWatchdog;
import android.automotive.watchdog.internal.ICarWatchdogServiceForSystem;
+import android.automotive.watchdog.internal.IoUsageStats;
import android.automotive.watchdog.internal.PackageIdentifier;
import android.automotive.watchdog.internal.PackageInfo;
import android.automotive.watchdog.internal.PackageIoOveruseStats;
import android.automotive.watchdog.internal.PackageMetadata;
import android.automotive.watchdog.internal.PackageResourceOveruseAction;
import android.automotive.watchdog.internal.PerStateIoOveruseThreshold;
+import android.automotive.watchdog.internal.PowerCycle;
import android.automotive.watchdog.internal.ResourceSpecificConfiguration;
import android.automotive.watchdog.internal.StateType;
import android.automotive.watchdog.internal.UidType;
+import android.automotive.watchdog.internal.UserPackageIoUsageStats;
+import android.automotive.watchdog.internal.UserState;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.ICarUxRestrictionsChangeListener;
+import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
+import android.car.hardware.power.CarPowerPolicy;
+import android.car.hardware.power.ICarPowerPolicyListener;
+import android.car.hardware.power.ICarPowerStateListener;
+import android.car.hardware.power.PowerComponent;
import android.car.test.mocks.AbstractExtendedMockitoTestCase;
import android.car.watchdog.CarWatchdogManager;
import android.car.watchdog.ICarWatchdogServiceCallback;
@@ -80,7 +101,9 @@
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.os.Binder;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
@@ -89,21 +112,28 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.SparseArray;
+import android.view.Display;
-import com.android.car.CarLog;
+import com.android.car.CarLocalServices;
import com.android.car.CarServiceUtils;
-import com.android.internal.util.function.TriConsumer;
+import com.android.car.CarStatsLog;
+import com.android.car.CarUxRestrictionsManagerService;
+import com.android.car.power.CarPowerManagementService;
import com.google.common.truth.Correspondence;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
-import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -112,57 +142,135 @@
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
/**
* <p>This class contains unit tests for the {@link CarWatchdogService}.
*/
@RunWith(MockitoJUnitRunner.class)
-public class CarWatchdogServiceUnitTest extends AbstractExtendedMockitoTestCase {
- static final String TAG = CarLog.tagFor(CarWatchdogService.class);
+public final class CarWatchdogServiceUnitTest extends AbstractExtendedMockitoTestCase {
private static final String CAR_WATCHDOG_DAEMON_INTERFACE = "carwatchdogd_system";
private static final int MAX_WAIT_TIME_MS = 3000;
private static final int INVALID_SESSION_ID = -1;
+ private static final int OVERUSE_HANDLING_DELAY_MILLS = 1000;
+ private static final int RECURRING_OVERUSE_THRESHOLD = 2;
+ private static final long STATS_DURATION_SECONDS = 3 * 60 * 60;
@Mock private Context mMockContext;
@Mock private PackageManager mMockPackageManager;
@Mock private UserManager mMockUserManager;
+ @Mock private CarPowerManagementService mMockCarPowerManagementService;
+ @Mock private CarUxRestrictionsManagerService mMockCarUxRestrictionsManagerService;
@Mock private IBinder mMockBinder;
@Mock private ICarWatchdog mMockCarWatchdogDaemon;
+ @Mock private WatchdogStorage mMockWatchdogStorage;
+
+
+ @Captor private ArgumentCaptor<ICarPowerStateListener> mICarPowerStateListenerCaptor;
+ @Captor private ArgumentCaptor<ICarPowerPolicyListener> mICarPowerPolicyListenerCaptor;
+ @Captor private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor;
+ @Captor private ArgumentCaptor<ICarUxRestrictionsChangeListener>
+ mICarUxRestrictionsChangeListener;
+ @Captor private ArgumentCaptor<IBinder.DeathRecipient> mDeathRecipientCaptor;
+ @Captor private ArgumentCaptor<ICarWatchdogServiceForSystem>
+ mICarWatchdogServiceForSystemCaptor;
+ @Captor private ArgumentCaptor<List<
+ android.automotive.watchdog.internal.ResourceOveruseConfiguration>>
+ mResourceOveruseConfigurationsCaptor;
+ @Captor private ArgumentCaptor<List<PackageResourceOveruseAction>>
+ mResourceOveruseActionsCaptor;
+ @Captor private ArgumentCaptor<int[]> mIntArrayCaptor;
+ @Captor private ArgumentCaptor<byte[]> mOveruseStatsCaptor;
+ @Captor private ArgumentCaptor<byte[]> mKilledStatsCaptor;
+ @Captor private ArgumentCaptor<Integer> mOverusingUidCaptor;
+ @Captor private ArgumentCaptor<Integer> mKilledUidCaptor;
+ @Captor private ArgumentCaptor<Integer> mUidStateCaptor;
+ @Captor private ArgumentCaptor<Integer> mSystemStateCaptor;
+ @Captor private ArgumentCaptor<Integer> mKillReasonCaptor;
+
private CarWatchdogService mCarWatchdogService;
private ICarWatchdogServiceForSystem mWatchdogServiceForSystemImpl;
private IBinder.DeathRecipient mCarWatchdogDaemonBinderDeathRecipient;
private BroadcastReceiver mBroadcastReceiver;
- private final SparseArray<String> mPackageNamesByUids = new SparseArray<>();
- private final SparseArray<String[]> mSharedPackagesByUids = new SparseArray<>();
- private final ArrayMap<String, ApplicationInfo> mApplicationInfosByPackages = new ArrayMap<>();
+ private boolean mIsDaemonCrashed;
+ private ICarPowerStateListener mCarPowerStateListener;
+ private ICarPowerPolicyListener mCarPowerPolicyListener;
+ private ICarUxRestrictionsChangeListener mCarUxRestrictionsChangeListener;
+ private TimeSourceInterface mTimeSource;
+
+ private final SparseArray<String> mGenericPackageNameByUid = new SparseArray<>();
+ private final SparseArray<List<String>> mPackagesBySharedUid = new SparseArray<>();
+ private final ArrayMap<String, android.content.pm.PackageInfo> mPmPackageInfoByUserPackage =
+ new ArrayMap<>();
+ private final ArraySet<String> mDisabledUserPackages = new ArraySet<>();
+ private final List<WatchdogStorage.UserPackageSettingsEntry> mUserPackageSettingsEntries =
+ new ArrayList<>();
+ private final List<WatchdogStorage.IoUsageStatsEntry> mIoUsageStatsEntries = new ArrayList<>();
+ private final IPackageManager mSpiedPackageManager = spy(ActivityThread.getPackageManager());
@Override
protected void onSessionBuilder(CustomMockitoSessionBuilder builder) {
builder
.spyStatic(ServiceManager.class)
.spyStatic(Binder.class)
- .spyStatic(ActivityThread.class);
+ .spyStatic(ActivityThread.class)
+ .spyStatic(CarLocalServices.class)
+ .spyStatic(CarStatsLog.class);
}
/**
* Initialize all of the objects with the @Mock annotation.
*/
@Before
- public void setUpMocks() throws Exception {
+ public void setUp() throws Exception {
when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockContext.getPackageName()).thenReturn(
CarWatchdogServiceUnitTest.class.getCanonicalName());
- mCarWatchdogService = new CarWatchdogService(mMockContext);
+ doReturn(mMockCarPowerManagementService)
+ .when(() -> CarLocalServices.getService(CarPowerManagementService.class));
+ doReturn(mMockCarUxRestrictionsManagerService)
+ .when(() -> CarLocalServices.getService(CarUxRestrictionsManagerService.class));
+ doReturn(mSpiedPackageManager).when(() -> ActivityThread.getPackageManager());
+
+ mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage);
+ mCarWatchdogService.setOveruseHandlingDelay(OVERUSE_HANDLING_DELAY_MILLS);
+ mCarWatchdogService.setRecurringOveruseThreshold(RECURRING_OVERUSE_THRESHOLD);
+ setDate(/* numDaysAgo= */ 0);
mockWatchdogDaemon();
+ mockWatchdogStorage();
setupUsers();
mCarWatchdogService.init();
- mWatchdogServiceForSystemImpl = registerCarWatchdogService();
+ captureCarPowerListeners();
captureBroadcastReceiver();
- captureDaemonBinderDeathRecipient();
+ captureCarUxRestrictionsChangeListener();
+ captureAndVerifyRegistrationWithDaemon(/* waitOnMain= */ true);
+ verifyDatabaseInit(/* wantedInvocations= */ 1);
mockPackageManager();
}
+ /**
+ * Releases resources.
+ */
+ @After
+ public void tearDown() throws Exception {
+ if (mIsDaemonCrashed) {
+ /* Note: On daemon crash, CarWatchdogService retries daemon connection on the main
+ * thread. This retry outlives the test and impacts other test runs. Thus always call
+ * restartWatchdogDaemonAndAwait after crashing the daemon and before completing
+ * teardown.
+ */
+ restartWatchdogDaemonAndAwait();
+ }
+ mUserPackageSettingsEntries.clear();
+ mIoUsageStatsEntries.clear();
+ mGenericPackageNameByUid.clear();
+ mPackagesBySharedUid.clear();
+ mPmPackageInfoByUserPackage.clear();
+ mDisabledUserPackages.clear();
+ }
+
@Test
public void testCarWatchdogServiceHealthCheck() throws Exception {
mWatchdogServiceForSystemImpl.checkIfAlive(123456, TIMEOUT_CRITICAL);
@@ -215,95 +323,371 @@
verify(mMockCarWatchdogDaemon)
.notifySystemStateChange(
eq(StateType.GARAGE_MODE), eq(GarageMode.GARAGE_MODE_ON), eq(-1));
+ verify(mMockWatchdogStorage).shrinkDatabase();
}
@Test
public void testGarageModeStateChangeToOff() throws Exception {
mBroadcastReceiver.onReceive(mMockContext,
new Intent().setAction(CarWatchdogService.ACTION_GARAGE_MODE_OFF));
- verify(mMockCarWatchdogDaemon)
+ // GARAGE_MODE_OFF is notified twice: Once during the initial daemon connect and once when
+ // the ACTION_GARAGE_MODE_OFF intent is received.
+ verify(mMockCarWatchdogDaemon, times(2))
.notifySystemStateChange(
eq(StateType.GARAGE_MODE), eq(GarageMode.GARAGE_MODE_OFF), eq(-1));
+ verify(mMockWatchdogStorage, never()).shrinkDatabase();
+ }
+
+ @Test
+ public void testWatchdogDaemonRestart() throws Exception {
+ crashWatchdogDaemon();
+
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ false, 101, 102);
+ mockUmIsUserRunning(mMockUserManager, /* userId= */ 101, /* isRunning= */ false);
+ mockUmIsUserRunning(mMockUserManager, /* userId= */ 102, /* isRunning= */ true);
+ setCarPowerState(CarPowerStateListener.SHUTDOWN_ENTER);
+ mBroadcastReceiver.onReceive(mMockContext,
+ new Intent().setAction(CarWatchdogService.ACTION_GARAGE_MODE_ON));
+
+ restartWatchdogDaemonAndAwait();
+
+ verify(mMockCarWatchdogDaemon, times(1)).notifySystemStateChange(
+ eq(StateType.USER_STATE), eq(101), eq(UserState.USER_STATE_STOPPED));
+ verify(mMockCarWatchdogDaemon, times(1)).notifySystemStateChange(
+ eq(StateType.USER_STATE), eq(102), eq(UserState.USER_STATE_STARTED));
+ verify(mMockCarWatchdogDaemon, times(1)).notifySystemStateChange(
+ eq(StateType.POWER_CYCLE), eq(PowerCycle.POWER_CYCLE_SHUTDOWN_ENTER), eq(-1));
+ verify(mMockCarWatchdogDaemon, times(1)).notifySystemStateChange(
+ eq(StateType.GARAGE_MODE), eq(GarageMode.GARAGE_MODE_ON), eq(-1));
+ }
+
+ @Test
+ public void testUserRemovedBroadcast() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 101, 102);
+ mBroadcastReceiver.onReceive(mMockContext,
+ new Intent().setAction(Intent.ACTION_USER_REMOVED)
+ .putExtra(Intent.EXTRA_USER, UserHandle.of(100)));
+ verify(mMockWatchdogStorage).syncUsers(new int[] {101, 102});
}
@Test
public void testGetResourceOveruseStats() throws Exception {
- mPackageNamesByUids.put(Binder.getCallingUid(), mMockContext.getPackageName());
+ int uid = Binder.getCallingUid();
+ injectPackageInfos(Collections.singletonList(
+ constructPackageManagerPackageInfo(
+ mMockContext.getPackageName(), uid, null, ApplicationInfo.FLAG_SYSTEM, 0)));
- List<PackageIoOveruseStats> packageIoOveruseStats = Collections.singletonList(
- constructPackageIoOveruseStats(
- Binder.getCallingUid(), /* shouldNotify= */false,
- constructInternalIoOveruseStats(/* killableOnOveruse= */false,
- /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
- /* writtenBytes= */constructPerStateBytes(100, 200, 300),
- /* totalOveruses= */2)));
- mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+ SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
+ injectIoOveruseStatsForPackages(
+ mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+ /* shouldNotifyPackages= */ new ArraySet<>());
ResourceOveruseStats expectedStats =
- constructResourceOveruseStats(mPackageNamesByUids.keyAt(0),
- mPackageNamesByUids.valueAt(0),
- packageIoOveruseStats.get(0).ioOveruseStats);
+ constructResourceOveruseStats(uid, mMockContext.getPackageName(),
+ packageIoOveruseStatsByUid.get(uid).ioOveruseStats);
ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
- assertWithMessage("Expected: " + expectedStats.toString() + "\nActual: "
- + actualStats.toString())
- .that(ResourceOveruseStatsSubject.isEquals(actualStats, expectedStats)).isTrue();
+ ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+
+ verifyNoMoreInteractions(mMockWatchdogStorage);
+ }
+
+ @Test
+ public void testGetResourceOveruseStatsForPast7days() throws Exception {
+ int uid = Binder.getCallingUid();
+ String packageName = mMockContext.getPackageName();
+ injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
+ packageName, uid, null, ApplicationInfo.FLAG_SYSTEM, 0)));
+
+ long startTime = mTimeSource.now().atZone(ZONE_OFFSET).minusDays(4).toEpochSecond();
+ long duration = mTimeSource.now().getEpochSecond() - startTime;
+ when(mMockWatchdogStorage.getHistoricalIoOveruseStats(
+ UserHandle.getUserId(uid), packageName, 6))
+ .thenReturn(new IoOveruseStats.Builder(startTime, duration).setTotalOveruses(5)
+ .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build());
+
+ injectIoOveruseStatsForPackages(mGenericPackageNameByUid,
+ /* killablePackages= */ Collections.singleton(packageName),
+ /* shouldNotifyPackages= */ new ArraySet<>());
+
+ ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+ CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+ IoOveruseStats ioOveruseStats =
+ new IoOveruseStats.Builder(startTime, duration + STATS_DURATION_SECONDS)
+ .setKillableOnOveruse(true).setTotalOveruses(8).setTotalBytesWritten(24_600)
+ .setTotalTimesKilled(2)
+ .setRemainingWriteBytes(new PerStateBytes(20, 20, 20)).build();
+
+ ResourceOveruseStats expectedStats =
+ new ResourceOveruseStats.Builder(packageName, UserHandle.getUserHandleForUid(uid))
+ .setIoOveruseStats(ioOveruseStats).build();
+
+ ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+ }
+
+ @Test
+ public void testGetResourceOveruseStatsForPast7daysWithNoHistory() throws Exception {
+ int uid = Binder.getCallingUid();
+ String packageName = mMockContext.getPackageName();
+ injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
+ packageName, uid, null, ApplicationInfo.FLAG_SYSTEM, 0)));
+
+ when(mMockWatchdogStorage.getHistoricalIoOveruseStats(
+ UserHandle.getUserId(uid), packageName, 6)).thenReturn(null);
+
+ injectIoOveruseStatsForPackages(mGenericPackageNameByUid,
+ /* killablePackages= */ Collections.singleton(packageName),
+ /* shouldNotifyPackages= */ new ArraySet<>());
+
+ ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+ CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+ ResourceOveruseStats expectedStats =
+ new ResourceOveruseStats.Builder(packageName, UserHandle.getUserHandleForUid(uid))
+ .setIoOveruseStats(new IoOveruseStats.Builder(
+ mTimeSource.now().getEpochSecond(), STATS_DURATION_SECONDS)
+ .setKillableOnOveruse(true).setTotalOveruses(3)
+ .setTotalBytesWritten(600)
+ .setRemainingWriteBytes(new PerStateBytes(20, 20, 20)).build())
+ .build();
+
+ ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+ }
+
+ @Test
+ public void testGetResourceOveruseStatsForPast7daysWithNoCurrentStats() throws Exception {
+ int uid = Binder.getCallingUid();
+ String packageName = mMockContext.getPackageName();
+ injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
+ packageName, uid, null, ApplicationInfo.FLAG_SYSTEM, 0)));
+
+ long startTime = mTimeSource.now().atZone(ZONE_OFFSET).minusDays(4).toEpochSecond();
+ long duration = mTimeSource.now().getEpochSecond() - startTime;
+ when(mMockWatchdogStorage.getHistoricalIoOveruseStats(
+ UserHandle.getUserId(uid), packageName, 6))
+ .thenReturn(new IoOveruseStats.Builder(startTime, duration).setTotalOveruses(5)
+ .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build());
+
+ ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+ CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+ ResourceOveruseStats expectedStats =
+ new ResourceOveruseStats.Builder(packageName, UserHandle.getUserHandleForUid(uid))
+ .build();
+
+ ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+ }
+
+ @Test
+ public void testGetResourceOveruseStatsForSharedUid() throws Exception {
+ int sharedUid = Binder.getCallingUid();
+ injectPackageInfos(Collections.singletonList(
+ constructPackageManagerPackageInfo(
+ mMockContext.getPackageName(), sharedUid, "system_shared_package",
+ ApplicationInfo.FLAG_SYSTEM, 0)));
+
+ SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
+ injectIoOveruseStatsForPackages(
+ mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+ /* shouldNotifyPackages= */ new ArraySet<>());
+
+ ResourceOveruseStats expectedStats =
+ constructResourceOveruseStats(sharedUid, "shared:system_shared_package",
+ packageIoOveruseStatsByUid.get(sharedUid).ioOveruseStats);
+
+ ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+ CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
+
+ ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
}
@Test
public void testFailsGetResourceOveruseStatsWithInvalidArgs() throws Exception {
assertThrows(IllegalArgumentException.class,
- () -> mCarWatchdogService.getResourceOveruseStats(/* resourceOveruseFlag= */0,
+ () -> mCarWatchdogService.getResourceOveruseStats(/* resourceOveruseFlag= */ 0,
CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
assertThrows(IllegalArgumentException.class,
() -> mCarWatchdogService.getResourceOveruseStats(
- CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* maxStatsPeriod= */0));
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* maxStatsPeriod= */ 0));
}
@Test
public void testGetAllResourceOveruseStatsWithNoMinimum() throws Exception {
- mPackageNamesByUids.put(1103456, "third_party_package");
- mPackageNamesByUids.put(1201278, "vendor_package.critical");
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
- constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(0),
- /* shouldNotify= */false,
- constructInternalIoOveruseStats(/* killableOnOveruse= */true,
- /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
- /* writtenBytes= */constructPerStateBytes(100, 200, 300),
- /* totalOveruses= */2)),
- constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(1),
- /* shouldNotify= */false,
- constructInternalIoOveruseStats(/* killableOnOveruse= */false,
- /* remainingWriteBytes= */constructPerStateBytes(450, 120, 340),
- /* writtenBytes= */constructPerStateBytes(5000, 6000, 9000),
- /* totalOveruses= */2)));
- mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+ constructPackageIoOveruseStats(1103456,
+ /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(100, 200, 300),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
+ constructPackageIoOveruseStats(1201278,
+ /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(5000, 6000, 9000),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+ /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+ /* writtenBytes= */ constructPerStateBytes(5000, 6000, 9000),
+ /* totalOveruses= */ 3)));
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
List<ResourceOveruseStats> expectedStats = Arrays.asList(
- constructResourceOveruseStats(mPackageNamesByUids.keyAt(0),
- mPackageNamesByUids.valueAt(0),
+ constructResourceOveruseStats(1103456, "third_party_package",
packageIoOveruseStats.get(0).ioOveruseStats),
- constructResourceOveruseStats(mPackageNamesByUids.keyAt(1),
- mPackageNamesByUids.valueAt(1),
+ constructResourceOveruseStats(1201278, "vendor_package.critical",
packageIoOveruseStats.get(1).ioOveruseStats));
List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
- CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */0,
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
ResourceOveruseStatsSubject.assertThat(actualStats)
.containsExactlyElementsIn(expectedStats);
+
+ verifyNoMoreInteractions(mMockWatchdogStorage);
+ }
+
+ @Test
+ public void testGetAllResourceOveruseStatsWithNoMinimumForPast7days() throws Exception {
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
+
+ List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
+ constructPackageIoOveruseStats(1103456,
+ /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(100, 200, 300),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
+ constructPackageIoOveruseStats(1201278,
+ /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+ /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+ /* writtenBytes= */ constructPerStateBytes(5000, 6000, 9000),
+ /* totalOveruses= */ 0)));
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ ZonedDateTime now = mTimeSource.now().atZone(ZONE_OFFSET);
+ long startTime = now.minusDays(4).toEpochSecond();
+ IoOveruseStats thirdPartyPkgOldStats = new IoOveruseStats.Builder(
+ startTime, now.toEpochSecond() - startTime).setTotalOveruses(5)
+ .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build();
+ when(mMockWatchdogStorage.getHistoricalIoOveruseStats(11, "third_party_package", 6))
+ .thenReturn(thirdPartyPkgOldStats);
+
+ startTime = now.minusDays(6).toEpochSecond();
+ IoOveruseStats vendorPkgOldStats = new IoOveruseStats.Builder(
+ startTime, now.toEpochSecond() - startTime).setTotalOveruses(2)
+ .setTotalTimesKilled(0).setTotalBytesWritten(35_000).build();
+ when(mMockWatchdogStorage.getHistoricalIoOveruseStats(12, "vendor_package.critical", 6))
+ .thenReturn(vendorPkgOldStats);
+
+
+ List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
+ CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+ IoOveruseStats thirdPartyIoStats = new IoOveruseStats.Builder(
+ thirdPartyPkgOldStats.getStartTime(),
+ thirdPartyPkgOldStats.getDurationInSeconds() + STATS_DURATION_SECONDS)
+ .setKillableOnOveruse(true).setTotalOveruses(8).setTotalBytesWritten(24_600)
+ .setTotalTimesKilled(2).setRemainingWriteBytes(new PerStateBytes(0, 0, 0))
+ .build();
+ IoOveruseStats vendorIoStats = new IoOveruseStats.Builder(
+ vendorPkgOldStats.getStartTime(),
+ vendorPkgOldStats.getDurationInSeconds() + STATS_DURATION_SECONDS)
+ .setKillableOnOveruse(false).setTotalOveruses(2).setTotalBytesWritten(55_000)
+ .setTotalTimesKilled(0).setRemainingWriteBytes(new PerStateBytes(450, 120, 340))
+ .build();
+
+ List<ResourceOveruseStats> expectedStats = Arrays.asList(
+ new ResourceOveruseStats.Builder("third_party_package", new UserHandle(11))
+ .setIoOveruseStats(thirdPartyIoStats).build(),
+ new ResourceOveruseStats.Builder("vendor_package.critical", new UserHandle(12))
+ .setIoOveruseStats(vendorIoStats).build());
+
+ ResourceOveruseStatsSubject.assertThat(actualStats)
+ .containsExactlyElementsIn(expectedStats);
}
@Test
+ public void testGetAllResourceOveruseStatsForSharedPackage() throws Exception {
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "vendor_package.A", 1103456, "vendor_shared_package"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.B", 1103456, "vendor_shared_package"),
+ constructPackageManagerPackageInfo(
+ "system_package.C", 1201000, "system_shared_package"),
+ constructPackageManagerPackageInfo(
+ "system_package.D", 1201000, "system_shared_package"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.A", 1303456, "vendor_shared_package"),
+ constructPackageManagerPackageInfo(
+ "vendor_package.B", 1303456, "vendor_shared_package")));
+
+ List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
+ constructPackageIoOveruseStats(1103456,
+ /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(50, 100, 150),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
+ constructPackageIoOveruseStats(1201000,
+ /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+ /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+ /* writtenBytes= */ constructPerStateBytes(5000, 6000, 9000),
+ /* totalOveruses= */ 0)),
+ constructPackageIoOveruseStats(1303456,
+ /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(80, 170, 260),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ /* writtenBytes= */ constructPerStateBytes(80, 170, 260),
+ /* totalOveruses= */ 1)));
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ List<ResourceOveruseStats> expectedStats = Arrays.asList(
+ constructResourceOveruseStats(1103456, "shared:vendor_shared_package",
+ packageIoOveruseStats.get(0).ioOveruseStats),
+ constructResourceOveruseStats(1201278, "shared:system_shared_package",
+ packageIoOveruseStats.get(1).ioOveruseStats),
+ constructResourceOveruseStats(1303456, "shared:vendor_shared_package",
+ packageIoOveruseStats.get(2).ioOveruseStats));
+
+ List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
+ CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
+
+ ResourceOveruseStatsSubject.assertThat(actualStats)
+ .containsExactlyElementsIn(expectedStats);
+
+ verifyNoMoreInteractions(mMockWatchdogStorage);
+ }
+
+ @Test
public void testFailsGetAllResourceOveruseStatsWithInvalidArgs() throws Exception {
assertThrows(IllegalArgumentException.class,
- () -> mCarWatchdogService.getAllResourceOveruseStats(0, /* minimumStatsFlag= */0,
+ () -> mCarWatchdogService.getAllResourceOveruseStats(0, /* minimumStatsFlag= */ 0,
CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
assertThrows(IllegalArgumentException.class,
@@ -315,38 +699,38 @@
assertThrows(IllegalArgumentException.class,
() -> mCarWatchdogService.getAllResourceOveruseStats(
- CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */1 << 5,
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 1 << 5,
CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
assertThrows(IllegalArgumentException.class,
() -> mCarWatchdogService.getAllResourceOveruseStats(
- CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */0,
- /* maxStatsPeriod= */0));
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
+ /* maxStatsPeriod= */ 0));
}
@Test
public void testGetAllResourceOveruseStatsWithMinimum() throws Exception {
- mPackageNamesByUids.put(1103456, "third_party_package");
- mPackageNamesByUids.put(1201278, "vendor_package.critical");
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
- constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(0),
- /* shouldNotify= */false,
- constructInternalIoOveruseStats(/* killableOnOveruse= */true,
- /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
- /* writtenBytes= */constructPerStateBytes(100, 200, 300),
- /* totalOveruses= */2)),
- constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(1),
- /* shouldNotify= */false,
- constructInternalIoOveruseStats(/* killableOnOveruse= */false,
- /* remainingWriteBytes= */constructPerStateBytes(450, 120, 340),
- /* writtenBytes= */constructPerStateBytes(7000000, 6000, 9000),
- /* totalOveruses= */2)));
- mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+ constructPackageIoOveruseStats(1103456, /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(80, 170, 260),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
+ constructPackageIoOveruseStats(1201278, /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(5_070_000, 4500, 7000),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+ /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+ /* writtenBytes= */ constructPerStateBytes(7_000_000, 6000, 9000),
+ /* totalOveruses= */ 3)));
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
List<ResourceOveruseStats> expectedStats = Collections.singletonList(
- constructResourceOveruseStats(mPackageNamesByUids.keyAt(1),
- mPackageNamesByUids.valueAt(1),
+ constructResourceOveruseStats(1201278, "vendor_package.critical",
packageIoOveruseStats.get(1).ioOveruseStats));
List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
@@ -356,31 +740,93 @@
ResourceOveruseStatsSubject.assertThat(actualStats)
.containsExactlyElementsIn(expectedStats);
+
+ verifyNoMoreInteractions(mMockWatchdogStorage);
+ }
+
+ @Test
+ public void testGetAllResourceOveruseStatsWithMinimumForPast7days() throws Exception {
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
+
+ List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
+ constructPackageIoOveruseStats(1103456,
+ /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(80, 170, 260),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
+ constructPackageIoOveruseStats(1201278,
+ /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+ /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+ /* writtenBytes= */ constructPerStateBytes(100_000, 6000, 9000),
+ /* totalOveruses= */ 0)));
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ ZonedDateTime now = mTimeSource.now().atZone(ZONE_OFFSET);
+ long startTime = now.minusDays(4).toEpochSecond();
+ IoOveruseStats thirdPartyPkgOldStats = new IoOveruseStats.Builder(
+ startTime, now.toEpochSecond() - startTime).setTotalOveruses(5)
+ .setTotalTimesKilled(2).setTotalBytesWritten(24_000).build();
+ when(mMockWatchdogStorage.getHistoricalIoOveruseStats(11, "third_party_package", 6))
+ .thenReturn(thirdPartyPkgOldStats);
+
+ startTime = now.minusDays(6).toEpochSecond();
+ IoOveruseStats vendorPkgOldStats = new IoOveruseStats.Builder(
+ startTime, now.toEpochSecond() - startTime).setTotalOveruses(2)
+ .setTotalTimesKilled(0).setTotalBytesWritten(6_900_000).build();
+ when(mMockWatchdogStorage.getHistoricalIoOveruseStats(12, "vendor_package.critical", 6))
+ .thenReturn(vendorPkgOldStats);
+
+ List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+ CarWatchdogManager.FLAG_MINIMUM_STATS_IO_1_MB,
+ CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+ IoOveruseStats vendorIoStats = new IoOveruseStats.Builder(
+ vendorPkgOldStats.getStartTime(),
+ vendorPkgOldStats.getDurationInSeconds() + STATS_DURATION_SECONDS)
+ .setKillableOnOveruse(false).setTotalOveruses(2).setTotalBytesWritten(7_015_000)
+ .setTotalTimesKilled(0).setRemainingWriteBytes(new PerStateBytes(450, 120, 340))
+ .build();
+
+ List<ResourceOveruseStats> expectedStats = Collections.singletonList(
+ new ResourceOveruseStats.Builder("vendor_package.critical", new UserHandle(12))
+ .setIoOveruseStats(vendorIoStats).build());
+
+ ResourceOveruseStatsSubject.assertThat(actualStats)
+ .containsExactlyElementsIn(expectedStats);
}
@Test
public void testGetResourceOveruseStatsForUserPackage() throws Exception {
- mPackageNamesByUids.put(1103456, "third_party_package");
- mPackageNamesByUids.put(1201278, "vendor_package.critical");
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
- constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(0),
- /* shouldNotify= */false,
- constructInternalIoOveruseStats(/* killableOnOveruse= */true,
- /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
- /* writtenBytes= */constructPerStateBytes(100, 200, 300),
- /* totalOveruses= */2)),
- constructPackageIoOveruseStats(mPackageNamesByUids.keyAt(1),
- /* shouldNotify= */false,
- constructInternalIoOveruseStats(/* killableOnOveruse= */false,
- /* remainingWriteBytes= */constructPerStateBytes(450, 120, 340),
- /* writtenBytes= */constructPerStateBytes(500, 600, 900),
- /* totalOveruses= */2)));
- mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+ constructPackageIoOveruseStats(1103456,
+ /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(80, 170, 260),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
+ constructPackageIoOveruseStats(1201278,
+ /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(300, 400, 700),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+ /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+ /* writtenBytes= */ constructPerStateBytes(500, 600, 900),
+ /* totalOveruses= */ 3)));
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
ResourceOveruseStats expectedStats =
- constructResourceOveruseStats(mPackageNamesByUids.keyAt(1),
- mPackageNamesByUids.valueAt(1),
+ constructResourceOveruseStats(1201278, "vendor_package.critical",
packageIoOveruseStats.get(1).ioOveruseStats);
ResourceOveruseStats actualStats =
@@ -389,22 +835,100 @@
CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
- assertWithMessage("Expected: " + expectedStats.toString() + "\nActual: "
- + actualStats.toString())
- .that(ResourceOveruseStatsSubject.isEquals(actualStats, expectedStats)).isTrue();
+ ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+ }
+
+ @Test
+ public void testGetResourceOveruseStatsForUserPackageForPast7days() throws Exception {
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
+
+ List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
+ constructPackageIoOveruseStats(1103456,
+ /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(80, 170, 260),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
+ constructPackageIoOveruseStats(1201278,
+ /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(300, 400, 700),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+ /* remainingWriteBytes= */ constructPerStateBytes(450, 120, 340),
+ /* writtenBytes= */ constructPerStateBytes(500, 600, 900),
+ /* totalOveruses= */ 3)));
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ ZonedDateTime now = mTimeSource.now().atZone(ZONE_OFFSET);
+ long startTime = now.minusDays(4).toEpochSecond();
+ IoOveruseStats vendorPkgOldStats = new IoOveruseStats.Builder(
+ startTime, now.toEpochSecond() - startTime).setTotalOveruses(2)
+ .setTotalTimesKilled(0).setTotalBytesWritten(6_900_000).build();
+ when(mMockWatchdogStorage.getHistoricalIoOveruseStats(12, "vendor_package.critical", 6))
+ .thenReturn(vendorPkgOldStats);
+
+ ResourceOveruseStats actualStats =
+ mCarWatchdogService.getResourceOveruseStatsForUserPackage(
+ "vendor_package.critical", new UserHandle(12),
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+ CarWatchdogManager.STATS_PERIOD_PAST_7_DAYS);
+
+ IoOveruseStats vendorIoStats = new IoOveruseStats.Builder(
+ vendorPkgOldStats.getStartTime(),
+ vendorPkgOldStats.getDurationInSeconds() + STATS_DURATION_SECONDS)
+ .setKillableOnOveruse(false).setTotalOveruses(5).setTotalBytesWritten(6_902_000)
+ .setTotalTimesKilled(0).setRemainingWriteBytes(new PerStateBytes(450, 120, 340))
+ .build();
+
+ ResourceOveruseStats expectedStats = new ResourceOveruseStats.Builder(
+ "vendor_package.critical", new UserHandle(12)).setIoOveruseStats(vendorIoStats)
+ .build();
+
+ ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+ }
+
+ @Test
+ public void testGetResourceOveruseStatsForUserPackageWithSharedUids() throws Exception {
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "third_party_package", 1103456, "vendor_shared_package"),
+ constructPackageManagerPackageInfo(
+ "vendor_package", 1103456, "vendor_shared_package"),
+ constructPackageManagerPackageInfo("system_package", 1101100,
+ "shared_system_package")));
+
+ SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
+ injectIoOveruseStatsForPackages(
+ mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(
+ Collections.singleton("shared:vendor_shared_package")),
+ /* shouldNotifyPackages= */ new ArraySet<>());
+
+ ResourceOveruseStats expectedStats =
+ constructResourceOveruseStats(1103456, "shared:vendor_shared_package",
+ packageIoOveruseStatsByUid.get(1103456).ioOveruseStats);
+
+ ResourceOveruseStats actualStats =
+ mCarWatchdogService.getResourceOveruseStatsForUserPackage(
+ "vendor_package", new UserHandle(11),
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+ CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
+
+ ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
}
@Test
public void testFailsGetResourceOveruseStatsForUserPackageWithInvalidArgs() throws Exception {
assertThrows(NullPointerException.class,
() -> mCarWatchdogService.getResourceOveruseStatsForUserPackage(
- /* packageName= */null, new UserHandle(10),
+ /* packageName= */ null, new UserHandle(10),
CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
assertThrows(NullPointerException.class,
() -> mCarWatchdogService.getResourceOveruseStatsForUserPackage("some.package",
- /* userHandle= */null, CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+ /* userHandle= */ null, CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
assertThrows(IllegalArgumentException.class,
@@ -414,13 +938,13 @@
assertThrows(IllegalArgumentException.class,
() -> mCarWatchdogService.getResourceOveruseStatsForUserPackage("some.package",
- new UserHandle(10), /* resourceOveruseFlag= */0,
+ new UserHandle(10), /* resourceOveruseFlag= */ 0,
CarWatchdogManager.STATS_PERIOD_CURRENT_DAY));
assertThrows(IllegalArgumentException.class,
() -> mCarWatchdogService.getResourceOveruseStatsForUserPackage("some.package",
new UserHandle(10), CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
- /* maxStatsPeriod= */0));
+ /* maxStatsPeriod= */ 0));
}
@Test
@@ -433,7 +957,7 @@
@Test
public void testResourceOveruseListener() throws Exception {
- mPackageNamesByUids.put(Binder.getCallingUid(), mMockContext.getPackageName());
+ mGenericPackageNameByUid.put(Binder.getCallingUid(), mMockContext.getPackageName());
IResourceOveruseListener mockListener = createMockResourceOveruseListener();
IBinder mockBinder = mockListener.asBinder();
@@ -443,9 +967,9 @@
verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
- injectIoOveruseStatsForPackages(mPackageNamesByUids,
- /* killablePackages= */new ArraySet<>(),
- /* shouldNotifyPackages= */new ArraySet<>(
+ injectIoOveruseStatsForPackages(
+ mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+ /* shouldNotifyPackages= */ new ArraySet<>(
Collections.singleton(mMockContext.getPackageName())));
verify(mockListener).onOveruse(any());
@@ -455,9 +979,9 @@
verify(mockListener, atLeastOnce()).asBinder();
verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
- injectIoOveruseStatsForPackages(mPackageNamesByUids,
- /* killablePackages= */new ArraySet<>(),
- /* shouldNotifyPackages= */new ArraySet<>(
+ injectIoOveruseStatsForPackages(
+ mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+ /* shouldNotifyPackages= */ new ArraySet<>(
Collections.singletonList(mMockContext.getPackageName())));
verifyNoMoreInteractions(mockListener);
@@ -465,7 +989,7 @@
@Test
public void testDuplicateAddResourceOveruseListener() throws Exception {
- mPackageNamesByUids.put(Binder.getCallingUid(), mMockContext.getPackageName());
+ mGenericPackageNameByUid.put(Binder.getCallingUid(), mMockContext.getPackageName());
IResourceOveruseListener mockListener = createMockResourceOveruseListener();
IBinder mockBinder = mockListener.asBinder();
@@ -489,7 +1013,7 @@
@Test
public void testAddMultipleResourceOveruseListeners() throws Exception {
- mPackageNamesByUids.put(Binder.getCallingUid(), mMockContext.getPackageName());
+ mGenericPackageNameByUid.put(Binder.getCallingUid(), mMockContext.getPackageName());
IResourceOveruseListener firstMockListener = createMockResourceOveruseListener();
IBinder firstMockBinder = firstMockListener.asBinder();
@@ -504,9 +1028,9 @@
verify(firstMockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
verify(secondMockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
- injectIoOveruseStatsForPackages(mPackageNamesByUids,
- /* killablePackages= */new ArraySet<>(),
- /* shouldNotifyPackages= */new ArraySet<>(
+ injectIoOveruseStatsForPackages(
+ mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+ /* shouldNotifyPackages= */ new ArraySet<>(
Collections.singleton(mMockContext.getPackageName())));
verify(firstMockListener).onOveruse(any());
@@ -516,9 +1040,9 @@
verify(firstMockListener, atLeastOnce()).asBinder();
verify(firstMockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
- injectIoOveruseStatsForPackages(mPackageNamesByUids,
- /* killablePackages= */new ArraySet<>(),
- /* shouldNotifyPackages= */new ArraySet<>(
+ injectIoOveruseStatsForPackages(
+ mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+ /* shouldNotifyPackages= */ new ArraySet<>(
Collections.singletonList(mMockContext.getPackageName())));
verify(secondMockListener, times(2)).onOveruse(any());
@@ -528,9 +1052,9 @@
verify(secondMockListener, atLeastOnce()).asBinder();
verify(secondMockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
- injectIoOveruseStatsForPackages(mPackageNamesByUids,
- /* killablePackages= */new ArraySet<>(),
- /* shouldNotifyPackages= */new ArraySet<>(
+ injectIoOveruseStatsForPackages(
+ mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+ /* shouldNotifyPackages= */ new ArraySet<>(
Collections.singletonList(mMockContext.getPackageName())));
verifyNoMoreInteractions(firstMockListener);
@@ -548,7 +1072,7 @@
@Test
public void testResourceOveruseListenerForSystem() throws Exception {
int callingUid = Binder.getCallingUid();
- mPackageNamesByUids.put(callingUid, "critical.system.package");
+ mGenericPackageNameByUid.put(callingUid, "system_package.critical");
IResourceOveruseListener mockListener = createMockResourceOveruseListener();
mCarWatchdogService.addResourceOveruseListenerForSystem(
@@ -558,13 +1082,14 @@
verify(mockBinder).linkToDeath(any(IBinder.DeathRecipient.class), anyInt());
List<PackageIoOveruseStats> packageIoOveruseStats = Collections.singletonList(
- constructPackageIoOveruseStats(callingUid, /* shouldNotify= */true,
- constructInternalIoOveruseStats(/* killableOnOveruse= */true,
- /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
- /* writtenBytes= */constructPerStateBytes(100, 200, 300),
- /* totalOveruses= */2)));
+ constructPackageIoOveruseStats(callingUid, /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(80, 170, 260),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)));
- mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
verify(mockListener).onOveruse(any());
@@ -573,118 +1098,136 @@
verify(mockListener, atLeastOnce()).asBinder();
verify(mockBinder).unlinkToDeath(any(IBinder.DeathRecipient.class), anyInt());
- mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
verifyNoMoreInteractions(mockListener);
}
@Test
- public void testSetKillablePackageAsUserWithPackageStats() throws Exception {
- mockUmGetAliveUsers(mMockUserManager, 11, 12);
- injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
-
- mPackageNamesByUids.put(1103456, "third_party_package");
- mPackageNamesByUids.put(1101278, "vendor_package.critical");
- injectIoOveruseStatsForPackages(mPackageNamesByUids,
- /* killablePackages= */new ArraySet<>(
- Collections.singletonList("third_party_package")),
- /* shouldNotifyPackages= */new ArraySet<>());
+ public void testSetKillablePackageAsUser() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 11, 12);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1101278, null),
+ constructPackageManagerPackageInfo("third_party_package", 1203456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
UserHandle userHandle = new UserHandle(11);
-
mCarWatchdogService.setKillablePackageAsUser("third_party_package", userHandle,
- /* isKillable= */ true);
+ /* isKillable= */ false);
+ mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
+ userHandle, /* isKillable= */ false);
+
+ PackageKillableStateSubject.assertThat(
+ mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
+ new PackageKillableState("third_party_package", 11,
+ PackageKillableState.KILLABLE_STATE_NO),
+ new PackageKillableState("vendor_package.critical", 11,
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("third_party_package", 12,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("vendor_package.critical", 12,
+ PackageKillableState.KILLABLE_STATE_NEVER));
+
assertThrows(IllegalArgumentException.class,
() -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
userHandle, /* isKillable= */ true));
- PackageKillableStateSubject.assertThat(
- mCarWatchdogService.getPackageKillableStatesAsUser(userHandle)).containsExactly(
- new PackageKillableState("third_party_package", 11,
- PackageKillableState.KILLABLE_STATE_YES),
- new PackageKillableState("vendor_package.critical", 11,
- PackageKillableState.KILLABLE_STATE_NEVER));
-
- mCarWatchdogService.setKillablePackageAsUser("third_party_package", userHandle,
- /* isKillable= */ false);
- assertThrows(IllegalArgumentException.class,
- () -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
- userHandle, /* isKillable= */ false));
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 11, 12, 13);
+ injectPackageInfos(Collections.singletonList(
+ constructPackageManagerPackageInfo("third_party_package", 1303456, null)));
PackageKillableStateSubject.assertThat(
- mCarWatchdogService.getPackageKillableStatesAsUser(userHandle)).containsExactly(
+ mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
new PackageKillableState("third_party_package", 11,
PackageKillableState.KILLABLE_STATE_NO),
new PackageKillableState("vendor_package.critical", 11,
- PackageKillableState.KILLABLE_STATE_NEVER));
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("third_party_package", 12,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("vendor_package.critical", 12,
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("third_party_package", 13,
+ PackageKillableState.KILLABLE_STATE_YES));
}
@Test
- public void testSetKillablePackageAsUserWithNoPackageStats() throws Exception {
- mockUmGetAliveUsers(mMockUserManager, 11, 12);
- injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
+ public void testSetKillablePackageAsUserWithSharedUids() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 11, 12);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "third_party_package.A", 1103456, "third_party_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.B", 1103456, "third_party_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.C", 1101356, "third_party_shared_package.B"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.D", 1101356, "third_party_shared_package.B")));
UserHandle userHandle = new UserHandle(11);
- mCarWatchdogService.setKillablePackageAsUser("third_party_package", userHandle,
- /* isKillable= */ true);
- mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
- userHandle, /* isKillable= */ true);
-
- PackageKillableStateSubject.assertThat(
- mCarWatchdogService.getPackageKillableStatesAsUser(userHandle)).containsExactly(
- new PackageKillableState("third_party_package", 11,
- PackageKillableState.KILLABLE_STATE_YES),
- new PackageKillableState("vendor_package.critical", 11,
- PackageKillableState.KILLABLE_STATE_NEVER));
-
- mCarWatchdogService.setKillablePackageAsUser("third_party_package", userHandle,
+ mCarWatchdogService.setKillablePackageAsUser("third_party_package.A", userHandle,
/* isKillable= */ false);
- assertThrows(IllegalArgumentException.class,
- () -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
- userHandle, /* isKillable= */ false));
PackageKillableStateSubject.assertThat(
- mCarWatchdogService.getPackageKillableStatesAsUser(userHandle)).containsExactly(
- new PackageKillableState("third_party_package", 11,
+ mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
+ new PackageKillableState("third_party_package.A", 11,
PackageKillableState.KILLABLE_STATE_NO),
- new PackageKillableState("vendor_package.critical", 11,
- PackageKillableState.KILLABLE_STATE_NEVER));
+ new PackageKillableState("third_party_package.B", 11,
+ PackageKillableState.KILLABLE_STATE_NO),
+ new PackageKillableState("third_party_package.C", 11,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.D", 11,
+ PackageKillableState.KILLABLE_STATE_YES));
+
+ mCarWatchdogService.setKillablePackageAsUser("third_party_package.B", userHandle,
+ /* isKillable= */ true);
+ mCarWatchdogService.setKillablePackageAsUser("third_party_package.C", userHandle,
+ /* isKillable= */ false);
+
+ PackageKillableStateSubject.assertThat(
+ mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
+ new PackageKillableState("third_party_package.A", 11,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.B", 11,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.C", 11,
+ PackageKillableState.KILLABLE_STATE_NO),
+ new PackageKillableState("third_party_package.D", 11,
+ PackageKillableState.KILLABLE_STATE_NO));
}
@Test
- public void testSetKillablePackageAsUserForAllUsersWithPackageStats() throws Exception {
- mockUmGetAliveUsers(mMockUserManager, 11, 12);
- injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
-
- mPackageNamesByUids.put(1103456, "third_party_package");
- mPackageNamesByUids.put(1101278, "vendor_package.critical");
- injectIoOveruseStatsForPackages(mPackageNamesByUids,
- /* killablePackages= */new ArraySet<>(
- Collections.singletonList("third_party_package")),
- /* shouldNotifyPackages= */new ArraySet<>());
+ public void testSetKillablePackageAsUserForAllUsers() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 11, 12);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1101278, null),
+ constructPackageManagerPackageInfo("third_party_package", 1203456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
mCarWatchdogService.setKillablePackageAsUser("third_party_package", UserHandle.ALL,
- /* isKillable= */ true);
+ /* isKillable= */ false);
+ mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
+ UserHandle.ALL, /* isKillable= */ false);
+
+ PackageKillableStateSubject.assertThat(
+ mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
+ new PackageKillableState("third_party_package", 11,
+ PackageKillableState.KILLABLE_STATE_NO),
+ new PackageKillableState("vendor_package.critical", 11,
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("third_party_package", 12,
+ PackageKillableState.KILLABLE_STATE_NO),
+ new PackageKillableState("vendor_package.critical", 12,
+ PackageKillableState.KILLABLE_STATE_NEVER));
+
assertThrows(IllegalArgumentException.class,
() -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
UserHandle.ALL, /* isKillable= */ true));
- PackageKillableStateSubject.assertThat(
- mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
- new PackageKillableState("third_party_package", 11,
- PackageKillableState.KILLABLE_STATE_YES),
- new PackageKillableState("vendor_package.critical", 11,
- PackageKillableState.KILLABLE_STATE_NEVER),
- new PackageKillableState("third_party_package", 12,
- PackageKillableState.KILLABLE_STATE_YES),
- new PackageKillableState("vendor_package.critical", 12,
- PackageKillableState.KILLABLE_STATE_NEVER));
-
- mCarWatchdogService.setKillablePackageAsUser("third_party_package", UserHandle.ALL,
- /* isKillable= */ false);
- assertThrows(IllegalArgumentException.class,
- () -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
- UserHandle.ALL, /* isKillable= */ false));
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 11, 12, 13);
+ injectPackageInfos(Collections.singletonList(
+ constructPackageManagerPackageInfo("third_party_package", 1303456, null)));
PackageKillableStateSubject.assertThat(
mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
@@ -695,52 +1238,71 @@
new PackageKillableState("third_party_package", 12,
PackageKillableState.KILLABLE_STATE_NO),
new PackageKillableState("vendor_package.critical", 12,
- PackageKillableState.KILLABLE_STATE_NEVER));
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("third_party_package", 13,
+ PackageKillableState.KILLABLE_STATE_NO));
}
@Test
- public void testSetKillablePackageAsUserForAllUsersWithNoPackageStats() throws Exception {
- mockUmGetAliveUsers(mMockUserManager, 11, 12);
- injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
+ public void testSetKillablePackageAsUsersForAllUsersWithSharedUids() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 11, 12);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "third_party_package.A", 1103456, "third_party_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.B", 1103456, "third_party_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.C", 1101356, "third_party_shared_package.B"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.D", 1101356, "third_party_shared_package.B"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.A", 1203456, "third_party_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.B", 1203456, "third_party_shared_package.A")));
- mCarWatchdogService.setKillablePackageAsUser("third_party_package", UserHandle.ALL,
- /* isKillable= */ true);
- mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
- UserHandle.ALL, /* isKillable= */ true);
-
- PackageKillableStateSubject.assertThat(
- mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
- new PackageKillableState("third_party_package", 11,
- PackageKillableState.KILLABLE_STATE_YES),
- new PackageKillableState("vendor_package.critical", 11,
- PackageKillableState.KILLABLE_STATE_NEVER),
- new PackageKillableState("third_party_package", 12,
- PackageKillableState.KILLABLE_STATE_YES),
- new PackageKillableState("vendor_package.critical", 12,
- PackageKillableState.KILLABLE_STATE_NEVER));
-
- mCarWatchdogService.setKillablePackageAsUser("third_party_package", UserHandle.ALL,
+ mCarWatchdogService.setKillablePackageAsUser("third_party_package.A", UserHandle.ALL,
/* isKillable= */ false);
- assertThrows(IllegalArgumentException.class,
- () -> mCarWatchdogService.setKillablePackageAsUser("vendor_package.critical",
- UserHandle.ALL, /* isKillable= */ false));
PackageKillableStateSubject.assertThat(
mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
- new PackageKillableState("third_party_package", 11,
+ new PackageKillableState("third_party_package.A", 11,
PackageKillableState.KILLABLE_STATE_NO),
- new PackageKillableState("vendor_package.critical", 11,
- PackageKillableState.KILLABLE_STATE_NEVER),
- new PackageKillableState("third_party_package", 12,
+ new PackageKillableState("third_party_package.B", 11,
PackageKillableState.KILLABLE_STATE_NO),
- new PackageKillableState("vendor_package.critical", 12,
- PackageKillableState.KILLABLE_STATE_NEVER));
+ new PackageKillableState("third_party_package.C", 11,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.D", 11,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.A", 12,
+ PackageKillableState.KILLABLE_STATE_NO),
+ new PackageKillableState("third_party_package.B", 12,
+ PackageKillableState.KILLABLE_STATE_NO));
+
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 11, 12, 13);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "third_party_package.A", 1303456, "third_party_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.B", 1303456, "third_party_shared_package.A")));
+
+ PackageKillableStateSubject.assertThat(
+ mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(13)))
+ .containsExactly(
+ new PackageKillableState("third_party_package.A", 13,
+ PackageKillableState.KILLABLE_STATE_NO),
+ new PackageKillableState("third_party_package.B", 13,
+ PackageKillableState.KILLABLE_STATE_NO));
}
@Test
public void testGetPackageKillableStatesAsUser() throws Exception {
- mockUmGetAliveUsers(mMockUserManager, 11, 12);
- injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 11, 12);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1101278, null),
+ constructPackageManagerPackageInfo("third_party_package", 1203456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
+
PackageKillableStateSubject.assertThat(
mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(11)))
.containsExactly(
@@ -751,9 +1313,147 @@
}
@Test
+ public void testGetPackageKillableStatesAsUserWithSafeToKillPackages() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 100, 101);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("system_package.non_critical.A", 10002459, null),
+ constructPackageManagerPackageInfo("third_party_package", 10003456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical.B", 10001278, null),
+ constructPackageManagerPackageInfo("vendor_package.non_critical.A", 10005573, null),
+ constructPackageManagerPackageInfo("third_party_package", 10103456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical.B", 10101278, null)));
+
+ PackageKillableStateSubject.assertThat(
+ mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL))
+ .containsExactly(
+ new PackageKillableState("system_package.non_critical.A", 100,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package", 100,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("vendor_package.critical.B", 100,
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("vendor_package.non_critical.A", 100,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package", 101,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("vendor_package.critical.B", 101,
+ PackageKillableState.KILLABLE_STATE_NEVER));
+ }
+
+ @Test
+ public void testGetPackageKillableStatesAsUserWithVendorPackagePrefixes() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 100);
+ injectPackageInfos(Collections.singletonList(constructPackageManagerPackageInfo(
+ "some_pkg_as_vendor_pkg", 10002459, /* sharedUserId= */ null, /* flags= */ 0,
+ ApplicationInfo.PRIVATE_FLAG_SYSTEM_EXT)));
+
+ List<PackageKillableState> killableStates =
+ mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(100));
+
+ // The vendor package prefixes in the resource overuse configs help identify vendor
+ // packages. The safe-to-kill list in the vendor configs helps identify safe-to-kill vendor
+ // packages. |system_package_as_vendor| is a critical system package by default but with
+ // the resource overuse configs, this package should be classified as a safe-to-kill vendor
+ // package.
+ PackageKillableStateSubject.assertThat(killableStates)
+ .containsExactly(new PackageKillableState("some_pkg_as_vendor_pkg", 100,
+ PackageKillableState.KILLABLE_STATE_YES));
+ }
+
+ @Test
+ public void testGetPackageKillableStatesAsUserWithSharedUids() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 11, 12);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "system_package.A", 1103456, "vendor_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "vendor_package.B", 1103456, "vendor_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.C", 1105678, "third_party_shared_package"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.D", 1105678, "third_party_shared_package"),
+ constructPackageManagerPackageInfo(
+ "system_package.A", 1203456, "vendor_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "vendor_package.B", 1203456, "vendor_shared_package.A")));
+
+ PackageKillableStateSubject.assertThat(
+ mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(11)))
+ .containsExactly(
+ new PackageKillableState("system_package.A", 11,
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("vendor_package.B", 11,
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("third_party_package.C", 11,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.D", 11,
+ PackageKillableState.KILLABLE_STATE_YES));
+ }
+
+ @Test
+ public void testGetPackageKillableStatesAsUserWithSharedUidsAndSafeToKillPackages()
+ throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 100);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "vendor_package.non_critical.A", 10003456, "vendor_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "system_package.A", 10003456, "vendor_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "vendor_package.B", 10003456, "vendor_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.C", 10005678, "third_party_shared_package"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.D", 10005678, "third_party_shared_package")));
+
+ PackageKillableStateSubject.assertThat(
+ mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(100)))
+ .containsExactly(
+ new PackageKillableState("vendor_package.non_critical.A", 100,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("system_package.A", 100,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("vendor_package.B", 100,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.C", 100,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.D", 100,
+ PackageKillableState.KILLABLE_STATE_YES));
+ }
+
+ @Test
+ public void testGetPackageKillableStatesAsUserWithSharedUidsAndSafeToKillSharedPackage()
+ throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 100);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "vendor_package.A", 10003456, "vendor_shared_package.non_critical.B"),
+ constructPackageManagerPackageInfo(
+ "system_package.A", 10003456, "vendor_shared_package.non_critical.B"),
+ constructPackageManagerPackageInfo(
+ "vendor_package.B", 10003456, "vendor_shared_package.non_critical.B")));
+
+
+ PackageKillableStateSubject.assertThat(
+ mCarWatchdogService.getPackageKillableStatesAsUser(new UserHandle(100)))
+ .containsExactly(
+ new PackageKillableState("vendor_package.A", 100,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("system_package.A", 100,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("vendor_package.B", 100,
+ PackageKillableState.KILLABLE_STATE_YES));
+ }
+
+ @Test
public void testGetPackageKillableStatesAsUserForAllUsers() throws Exception {
- mockUmGetAliveUsers(mMockUserManager, 11, 12);
- injectPackageInfos(Arrays.asList("third_party_package", "vendor_package.critical"));
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 11, 12);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("third_party_package", 1103456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1101278, null),
+ constructPackageManagerPackageInfo("third_party_package", 1203456, null),
+ constructPackageManagerPackageInfo("vendor_package.critical", 1201278, null)));
+
PackageKillableStateSubject.assertThat(
mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
new PackageKillableState("third_party_package", 11,
@@ -767,6 +1467,40 @@
}
@Test
+ public void testGetPackageKillableStatesAsUserForAllUsersWithSharedUids() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 11, 12);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "system_package.A", 1103456, "vendor_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "vendor_package.B", 1103456, "vendor_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.C", 1105678, "third_party_shared_package"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.D", 1105678, "third_party_shared_package"),
+ constructPackageManagerPackageInfo(
+ "system_package.A", 1203456, "vendor_shared_package.A"),
+ constructPackageManagerPackageInfo(
+ "vendor_package.B", 1203456, "vendor_shared_package.A")));
+
+ PackageKillableStateSubject.assertThat(
+ mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL))
+ .containsExactly(
+ new PackageKillableState("system_package.A", 11,
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("vendor_package.B", 11,
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("third_party_package.C", 11,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.D", 11,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("system_package.A", 12,
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("vendor_package.B", 12,
+ PackageKillableState.KILLABLE_STATE_NEVER));
+ }
+
+ @Test
public void testSetResourceOveruseConfigurations() throws Exception {
assertThat(mCarWatchdogService.setResourceOveruseConfigurations(
sampleResourceOveruseConfigurations(), CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO))
@@ -775,6 +1509,13 @@
InternalResourceOveruseConfigurationSubject
.assertThat(captureOnSetResourceOveruseConfigurations())
.containsExactlyElementsIn(sampleInternalResourceOveruseConfigurations());
+
+ // CarService fetches and syncs resource overuse configuration on the main thread by posting
+ // a new message. Wait until this completes.
+ CarServiceUtils.runOnMainSync(() -> {});
+
+ /* Expect two calls, the first is made at car watchdog service init */
+ verify(mMockCarWatchdogDaemon, times(2)).getResourceOveruseConfigurations();
}
@Test
@@ -886,6 +1627,64 @@
}
@Test
+ public void testFailsSetResourceOveruseConfigurationsOnZeroComponentLevelIoOveruseThresholds()
+ throws Exception {
+ List<ResourceOveruseConfiguration> resourceOveruseConfigs =
+ Collections.singletonList(
+ sampleResourceOveruseConfigurationBuilder(ComponentType.SYSTEM,
+ sampleIoOveruseConfigurationBuilder(ComponentType.SYSTEM)
+ .setComponentLevelThresholds(new PerStateBytes(200, 0, 200))
+ .build())
+ .build());
+ assertThrows(IllegalArgumentException.class,
+ () -> mCarWatchdogService.setResourceOveruseConfigurations(resourceOveruseConfigs,
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO));
+ }
+
+ @Test
+ public void testFailsSetResourceOveruseConfigurationsOnEmptyIoOveruseSystemWideThresholds()
+ throws Exception {
+ List<ResourceOveruseConfiguration> resourceOveruseConfigs =
+ Collections.singletonList(
+ sampleResourceOveruseConfigurationBuilder(ComponentType.SYSTEM,
+ sampleIoOveruseConfigurationBuilder(ComponentType.SYSTEM)
+ .setSystemWideThresholds(new ArrayList<>())
+ .build())
+ .build());
+ assertThrows(IllegalArgumentException.class,
+ () -> mCarWatchdogService.setResourceOveruseConfigurations(resourceOveruseConfigs,
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO));
+ }
+
+ @Test
+ public void testFailsSetResourceOveruseConfigurationsOnIoOveruseInvalidSystemWideThreshold()
+ throws Exception {
+ List<ResourceOveruseConfiguration> resourceOveruseConfigs = new ArrayList<>();
+ resourceOveruseConfigs.add(sampleResourceOveruseConfigurationBuilder(ComponentType.SYSTEM,
+ sampleIoOveruseConfigurationBuilder(ComponentType.SYSTEM)
+ .setSystemWideThresholds(Collections.singletonList(
+ new IoOveruseAlertThreshold(30, 0)))
+ .build())
+ .build());
+ assertThrows(IllegalArgumentException.class,
+ () -> mCarWatchdogService.setResourceOveruseConfigurations(
+ resourceOveruseConfigs,
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO));
+
+ resourceOveruseConfigs.set(0,
+ sampleResourceOveruseConfigurationBuilder(ComponentType.SYSTEM,
+ sampleIoOveruseConfigurationBuilder(ComponentType.SYSTEM)
+ .setSystemWideThresholds(Collections.singletonList(
+ new IoOveruseAlertThreshold(0, 300)))
+ .build())
+ .build());
+ assertThrows(IllegalArgumentException.class,
+ () -> mCarWatchdogService.setResourceOveruseConfigurations(
+ resourceOveruseConfigs,
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO));
+ }
+
+ @Test
public void testFailsSetResourceOveruseConfigurationsOnNullIoOveruseConfiguration()
throws Exception {
List<ResourceOveruseConfiguration> resourceOveruseConfigs = Collections.singletonList(
@@ -897,9 +1696,6 @@
@Test
public void testGetResourceOveruseConfigurations() throws Exception {
- when(mMockCarWatchdogDaemon.getResourceOveruseConfigurations())
- .thenReturn(sampleInternalResourceOveruseConfigurations());
-
List<ResourceOveruseConfiguration> actualConfigs =
mCarWatchdogService.getResourceOveruseConfigurations(
CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO);
@@ -916,7 +1712,8 @@
() -> mCarWatchdogService.getResourceOveruseConfigurations(
CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO));
- verify(mMockCarWatchdogDaemon, never()).getResourceOveruseConfigurations();
+ /* Method initially called in CarWatchdogService init */
+ verify(mMockCarWatchdogDaemon).getResourceOveruseConfigurations();
}
@Test
@@ -932,9 +1729,6 @@
.when(() -> ServiceManager.getService(CAR_WATCHDOG_DAEMON_INTERFACE));
mCarWatchdogDaemonBinderDeathRecipient.binderDied();
- when(mMockCarWatchdogDaemon.getResourceOveruseConfigurations())
- .thenReturn(sampleInternalResourceOveruseConfigurations());
-
List<ResourceOveruseConfiguration> actualConfigs =
mCarWatchdogService.getResourceOveruseConfigurations(
CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO);
@@ -990,15 +1784,22 @@
@Test
public void testLatestIoOveruseStats() throws Exception {
+ setRequiresDistractionOptimization(true);
+ setDisplayStateEnabled(false);
int criticalSysPkgUid = Binder.getCallingUid();
- int nonCriticalSysPkgUid = getUid(1056);
- int nonCriticalVndrPkgUid = getUid(2564);
- int thirdPartyPkgUid = getUid(2044);
+ int nonCriticalSysPkgUid = 1001056;
+ int nonCriticalVndrPkgUid = 1002564;
+ int thirdPartyPkgUid = 1002044;
- mPackageNamesByUids.put(criticalSysPkgUid, "critical.system.package");
- mPackageNamesByUids.put(nonCriticalSysPkgUid, "non_critical.system.package");
- mPackageNamesByUids.put(nonCriticalVndrPkgUid, "non_critical.vendor.package");
- mPackageNamesByUids.put(thirdPartyPkgUid, "third_party.package");
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "system_package.critical", criticalSysPkgUid, null),
+ constructPackageManagerPackageInfo(
+ "system_package.non_critical", nonCriticalSysPkgUid, null),
+ constructPackageManagerPackageInfo(
+ "vendor_package.non_critical", nonCriticalVndrPkgUid, null),
+ constructPackageManagerPackageInfo(
+ "third_party_package", thirdPartyPkgUid, null)));
IResourceOveruseListener mockSystemListener = createMockResourceOveruseListener();
mCarWatchdogService.addResourceOveruseListenerForSystem(
@@ -1008,308 +1809,1182 @@
mCarWatchdogService.addResourceOveruseListener(
CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, mockListener);
- IPackageManager packageManagerService = Mockito.spy(ActivityThread.getPackageManager());
- when(ActivityThread.getPackageManager()).thenReturn(packageManagerService);
- mockApplicationEnabledSettingAccessors(packageManagerService);
-
List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
/* Overuse occurred but cannot be killed/disabled. */
- constructPackageIoOveruseStats(criticalSysPkgUid, /* shouldNotify= */true,
- constructInternalIoOveruseStats(/* killableOnOveruse= */false,
- /* remainingWriteBytes= */constructPerStateBytes(0, 0, 0),
- /* writtenBytes= */constructPerStateBytes(100, 200, 300),
- /* totalOveruses= */2)),
+ constructPackageIoOveruseStats(criticalSysPkgUid, /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(100, 200, 300),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+ /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
/* No overuse occurred but should be notified. */
- constructPackageIoOveruseStats(nonCriticalSysPkgUid, /* shouldNotify= */true,
- constructInternalIoOveruseStats(/* killableOnOveruse= */true,
- /* remainingWriteBytes= */constructPerStateBytes(20, 30, 40),
- /* writtenBytes= */constructPerStateBytes(100, 200, 300),
- /* totalOveruses= */2)),
+ constructPackageIoOveruseStats(nonCriticalSysPkgUid, /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(50, 100, 150),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(20, 30, 40),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
/* Neither overuse occurred nor be notified. */
- constructPackageIoOveruseStats(nonCriticalVndrPkgUid, /* shouldNotify= */false,
- constructInternalIoOveruseStats(/* killableOnOveruse= */true,
- /* remainingWriteBytes= */constructPerStateBytes(200, 300, 400),
- /* writtenBytes= */constructPerStateBytes(100, 200, 300),
- /* totalOveruses= */2)),
+ constructPackageIoOveruseStats(nonCriticalVndrPkgUid, /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(25, 50, 75),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(200, 300, 400),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
/* Overuse occurred and can be killed/disabled. */
- constructPackageIoOveruseStats(thirdPartyPkgUid, /* shouldNotify= */true,
- constructInternalIoOveruseStats(/* killableOnOveruse= */true,
- /* remainingWriteBytes= */constructPerStateBytes(0, 0, 0),
- /* writtenBytes= */constructPerStateBytes(100, 200, 300),
- /* totalOveruses= */2)));
+ constructPackageIoOveruseStats(thirdPartyPkgUid, /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(100, 200, 300),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ /* writtenBytes= */ constructPerStateBytes(300, 600, 900),
+ /* totalOveruses= */ 3)));
- mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ assertThat(mDisabledUserPackages).containsExactlyElementsIn(Collections.singleton(
+ "10:third_party_package"));
List<ResourceOveruseStats> expectedStats = new ArrayList<>();
expectedStats.add(constructResourceOveruseStats(criticalSysPkgUid,
- mPackageNamesByUids.get(criticalSysPkgUid),
- packageIoOveruseStats.get(0).ioOveruseStats));
+ "system_package.critical", packageIoOveruseStats.get(0).ioOveruseStats));
verifyOnOveruseCalled(expectedStats, mockListener);
expectedStats.add(constructResourceOveruseStats(nonCriticalSysPkgUid,
- mPackageNamesByUids.get(nonCriticalSysPkgUid),
- packageIoOveruseStats.get(1).ioOveruseStats));
+ "system_package.non_critical", packageIoOveruseStats.get(1).ioOveruseStats));
- expectedStats.add(constructResourceOveruseStats(thirdPartyPkgUid,
- mPackageNamesByUids.get(thirdPartyPkgUid),
+ /*
+ * When the package receives overuse notification, the package is not yet killed so the
+ * totalTimesKilled counter is not yet incremented.
+ */
+ expectedStats.add(constructResourceOveruseStats(thirdPartyPkgUid, "third_party_package",
packageIoOveruseStats.get(3).ioOveruseStats));
verifyOnOveruseCalled(expectedStats, mockSystemListener);
- verify(packageManagerService).getApplicationEnabledSetting(
- mPackageNamesByUids.get(thirdPartyPkgUid), UserHandle.getUserId(thirdPartyPkgUid));
- verify(packageManagerService).setApplicationEnabledSetting(
- eq(mPackageNamesByUids.get(thirdPartyPkgUid)), anyInt(), anyInt(),
- eq(UserHandle.getUserId(thirdPartyPkgUid)), anyString());
+ List<AtomsProto.CarWatchdogIoOveruseStatsReported> expectedReportedOveruseStats =
+ new ArrayList<>();
+ expectedReportedOveruseStats.add(constructIoOveruseStatsReported(criticalSysPkgUid,
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(10, 20, 30),
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(100, 200, 300)));
+ expectedReportedOveruseStats.add(constructIoOveruseStatsReported(thirdPartyPkgUid,
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(30, 60, 90),
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(300, 600, 900)));
- List<PackageResourceOveruseAction> expectedActions = Arrays.asList(
- constructPackageResourceOveruseAction(mPackageNamesByUids.get(criticalSysPkgUid),
- criticalSysPkgUid, new int[]{ResourceType.IO}, NOT_KILLED),
- constructPackageResourceOveruseAction(mPackageNamesByUids.get(thirdPartyPkgUid),
- thirdPartyPkgUid, new int[]{ResourceType.IO}, KILLED));
- verifyActionsTakenOnResourceOveruse(expectedActions);
- }
+ captureAndVerifyIoOveruseStatsReported(expectedReportedOveruseStats);
- @Test
- public void testLatestIoOveruseStatsWithUserOptedOutPackage() throws Exception {
- // TODO(b/170741935): Test that the user opted out package is not killed on overuse.
- }
+ List<AtomsProto.CarWatchdogKillStatsReported> expectedReportedKillStats =
+ Collections.singletonList(constructIoOveruseKillStatsReported(thirdPartyPkgUid,
+ CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE,
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(30, 60, 90),
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(300, 600, 900)));
- @Test
- public void testLatestIoOveruseStatsWithRecurringOveruse() throws Exception {
- /*
- * TODO(b/170741935): Test that non-critical packages are killed on recurring overuse
- * regardless of user settings.
+ captureAndVerifyKillStatsReported(expectedReportedKillStats);
+
+ /* {@link thirdPartyPkgUid} action is sent twice. Once before it is killed and once after
+ * it is killed. This is done because the package is killed only after the display is turned
+ * off.
*/
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{criticalSysPkgUid, thirdPartyPkgUid},
+ /* killedRecurringOveruseUids= */ new int[]{thirdPartyPkgUid}));
}
@Test
- public void testResetResourceOveruseStats() throws Exception {
- mPackageNamesByUids.put(Binder.getCallingUid(), mMockContext.getPackageName());
- mPackageNamesByUids.put(1101278, "vendor_package.critical");
- injectIoOveruseStatsForPackages(mPackageNamesByUids,
- /* killablePackages= */new ArraySet<>(),
- /* shouldNotifyPackages= */new ArraySet<>());
+ public void testLatestIoOveruseStatsWithSharedUid() throws Exception {
+ setRequiresDistractionOptimization(true);
+ setDisplayStateEnabled(false);
+ int criticalSysSharedUid = Binder.getCallingUid();
+ int nonCriticalVndrSharedUid = 1002564;
+ int thirdPartySharedUid = 1002044;
- mWatchdogServiceForSystemImpl.resetResourceOveruseStats(
- Collections.singletonList(mMockContext.getPackageName()));
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "system_package.A", criticalSysSharedUid, "system_shared_package"),
+ constructPackageManagerPackageInfo(
+ "system_package.B", criticalSysSharedUid, "system_shared_package"),
+ constructPackageManagerPackageInfo("vendor_package.non_critical",
+ nonCriticalVndrSharedUid, "vendor_shared_package"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.A", thirdPartySharedUid, "third_party_shared_package"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.B", thirdPartySharedUid, "third_party_shared_package")
+ ));
- ResourceOveruseStats actualStats = mCarWatchdogService.getResourceOveruseStats(
- CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+ IResourceOveruseListener mockSystemListener = createMockResourceOveruseListener();
+ mCarWatchdogService.addResourceOveruseListenerForSystem(
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, mockSystemListener);
+
+ IResourceOveruseListener mockListener = createMockResourceOveruseListener();
+ mCarWatchdogService.addResourceOveruseListener(
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, mockListener);
+
+ List<PackageIoOveruseStats> packageIoOveruseStats = Arrays.asList(
+ /* Overuse occurred but cannot be killed/disabled. */
+ constructPackageIoOveruseStats(criticalSysSharedUid, /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(100, 200, 300),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ false,
+ /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
+ /* No overuse occurred but should be notified. */
+ constructPackageIoOveruseStats(nonCriticalVndrSharedUid, /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(50, 100, 150),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(200, 300, 400),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3)),
+ /* Overuse occurred and can be killed/disabled. */
+ constructPackageIoOveruseStats(thirdPartySharedUid, /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(100, 200, 300),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ /* writtenBytes= */ constructPerStateBytes(300, 600, 900),
+ /* totalOveruses= */ 3)));
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ assertThat(mDisabledUserPackages).containsExactlyElementsIn(Arrays.asList(
+ "10:third_party_package.A", "10:third_party_package.B"));
+
+ List<ResourceOveruseStats> expectedStats = new ArrayList<>();
+
+ expectedStats.add(constructResourceOveruseStats(criticalSysSharedUid,
+ "shared:system_shared_package", packageIoOveruseStats.get(0).ioOveruseStats));
+
+ verifyOnOveruseCalled(expectedStats, mockListener);
+
+ expectedStats.add(constructResourceOveruseStats(nonCriticalVndrSharedUid,
+ "shared:vendor_shared_package", packageIoOveruseStats.get(1).ioOveruseStats));
+
+ /*
+ * When the package receives overuse notification, the package is not yet killed so the
+ * totalTimesKilled counter is not yet incremented.
+ */
+ expectedStats.add(constructResourceOveruseStats(thirdPartySharedUid,
+ "shared:third_party_shared_package", packageIoOveruseStats.get(2).ioOveruseStats));
+
+ verifyOnOveruseCalled(expectedStats, mockSystemListener);
+
+ List<AtomsProto.CarWatchdogIoOveruseStatsReported> expectedReportedOveruseStats =
+ new ArrayList<>();
+ expectedReportedOveruseStats.add(constructIoOveruseStatsReported(criticalSysSharedUid,
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(10, 20, 30),
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(100, 200, 300)));
+ expectedReportedOveruseStats.add(constructIoOveruseStatsReported(thirdPartySharedUid,
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(30, 60, 90),
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(300, 600, 900)));
+
+ captureAndVerifyIoOveruseStatsReported(expectedReportedOveruseStats);
+
+ List<AtomsProto.CarWatchdogKillStatsReported> expectedReportedKillStats =
+ Collections.singletonList(constructIoOveruseKillStatsReported(thirdPartySharedUid,
+ CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE,
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(30, 60, 90),
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(300, 600, 900)));
+
+ captureAndVerifyKillStatsReported(expectedReportedKillStats);
+
+ /* {@link thirdPartySharedUid} action is sent twice. Once before it is killed and once after
+ * it is killed. This is done because the package is killed only after the display is turned
+ * off.
+ */
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{criticalSysSharedUid, thirdPartySharedUid},
+ /* killedRecurringOveruseUids= */ new int[]{thirdPartySharedUid}));
+ }
+
+ @Test
+ public void testGetTodayIoUsageStats() throws Exception {
+ List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = Arrays.asList(
+ WatchdogStorageUnitTest.constructIoUsageStatsEntry(
+ /* userId= */ 10, "system_package", /* startTime */ 0, /* duration= */ 1234,
+ /* remainingWriteBytes= */ constructPerStateBytes(200, 300, 400),
+ /* writtenBytes= */ constructPerStateBytes(1000, 2000, 3000),
+ /* forgivenWriteBytes= */ constructPerStateBytes(100, 100, 100),
+ /* totalOveruses= */ 2, /* totalTimesKilled= */ 1),
+ WatchdogStorageUnitTest.constructIoUsageStatsEntry(
+ /* userId= */ 11, "vendor_package", /* startTime */ 0, /* duration= */ 1234,
+ /* remainingWriteBytes= */ constructPerStateBytes(500, 600, 700),
+ /* writtenBytes= */ constructPerStateBytes(1100, 2300, 4300),
+ /* forgivenWriteBytes= */ constructPerStateBytes(100, 100, 100),
+ /* totalOveruses= */ 4, /* totalTimesKilled= */ 10));
+ when(mMockWatchdogStorage.getTodayIoUsageStats()).thenReturn(ioUsageStatsEntries);
+
+ List<UserPackageIoUsageStats> actualStats =
+ mWatchdogServiceForSystemImpl.getTodayIoUsageStats();
+
+ List<UserPackageIoUsageStats> expectedStats = Arrays.asList(
+ constructUserPackageIoUsageStats(/* userId= */ 10, "system_package",
+ /* writtenBytes= */ constructPerStateBytes(1000, 2000, 3000),
+ /* forgivenWriteBytes= */ constructPerStateBytes(100, 100, 100),
+ /* totalOveruses= */ 2),
+ constructUserPackageIoUsageStats(/* userId= */ 11, "vendor_package",
+ /* writtenBytes= */ constructPerStateBytes(1100, 2300, 4300),
+ /* forgivenWriteBytes= */ constructPerStateBytes(100, 100, 100),
+ /* totalOveruses= */ 4));
+
+ assertThat(actualStats).comparingElementsUsing(Correspondence.from(
+ CarWatchdogServiceUnitTest::isUserPackageIoUsageStatsEquals,
+ "is user package I/O usage stats equal to"))
+ .containsExactlyElementsIn(expectedStats);
+ }
+
+ @Test
+ public void testPersistStatsOnShutdownEnter() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 10, 11, 12);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "third_party_package", 1103456, "vendor_shared_package.critical"),
+ constructPackageManagerPackageInfo(
+ "vendor_package", 1103456, "vendor_shared_package.critical"),
+ constructPackageManagerPackageInfo("third_party_package.A", 1001100, null),
+ constructPackageManagerPackageInfo("third_party_package.A", 1201100, null)));
+
+ SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid =
+ injectIoOveruseStatsForPackages(
+ mGenericPackageNameByUid,
+ /* killablePackages= */ new ArraySet<>(Collections.singletonList(
+ "third_party_package.A")),
+ /* shouldNotifyPackages= */ new ArraySet<>());
+
+ mCarWatchdogService.setKillablePackageAsUser(
+ "third_party_package.A", new UserHandle(12), /* isKillable= */ false);
+
+ setCarPowerState(CarPowerStateListener.SHUTDOWN_ENTER);
+ verify(mMockWatchdogStorage).saveIoUsageStats(any());
+ verify(mMockWatchdogStorage).saveUserPackageSettings(any());
+ mCarWatchdogService.release();
+ verify(mMockWatchdogStorage).release();
+ mCarWatchdogService = new CarWatchdogService(mMockContext, mMockWatchdogStorage);
+ mCarWatchdogService.init();
+ verifyDatabaseInit(/* wantedInvocations= */ 2);
+
+ List<ResourceOveruseStats> actualStats = mCarWatchdogService.getAllResourceOveruseStats(
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
- ResourceOveruseStats expectedStats = new ResourceOveruseStats.Builder(
- mMockContext.getPackageName(),
- UserHandle.getUserHandleForUid(Binder.getCallingUid())).build();
+ List<ResourceOveruseStats> expectedStats = Arrays.asList(
+ constructResourceOveruseStats(
+ /* uid= */ 1103456, "shared:vendor_shared_package.critical",
+ packageIoOveruseStatsByUid.get(1103456).ioOveruseStats),
+ constructResourceOveruseStats(/* uid= */ 1001100, "third_party_package.A",
+ packageIoOveruseStatsByUid.get(1001100).ioOveruseStats),
+ constructResourceOveruseStats(/* uid= */ 1201100, "third_party_package.A",
+ packageIoOveruseStatsByUid.get(1201100).ioOveruseStats));
- assertWithMessage("Expected: " + expectedStats.toString() + "\nActual: "
- + actualStats.toString())
- .that(ResourceOveruseStatsSubject.isEquals(actualStats, expectedStats)).isTrue();
+ PackageKillableStateSubject.assertThat(
+ mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL))
+ .containsExactly(
+ new PackageKillableState("third_party_package", 11,
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("vendor_package", 11,
+ PackageKillableState.KILLABLE_STATE_NEVER),
+ new PackageKillableState("third_party_package.A", 10,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.A", 12,
+ PackageKillableState.KILLABLE_STATE_NO));
+
+ ResourceOveruseStatsSubject.assertThat(actualStats)
+ .containsExactlyElementsIn(expectedStats);
+
+ verifyNoMoreInteractions(mMockWatchdogStorage);
+ }
+
+ @Test
+ public void testPersistIoOveruseStatsOnDateChange() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 10);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("system_package", 1011200, null),
+ constructPackageManagerPackageInfo("third_party_package", 1001100, null)));
+
+ setDisplayStateEnabled(false);
+ setDate(1);
+ List<PackageIoOveruseStats> prevDayStats = Arrays.asList(
+ constructPackageIoOveruseStats(1011200, /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(600, 700, 800),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ /* writtenBytes= */ constructPerStateBytes(600, 700, 800),
+ /* totalOveruses= */ 3)),
+ constructPackageIoOveruseStats(1001100, /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(1050, 1100, 1200),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(50, 60, 70),
+ /* writtenBytes= */ constructPerStateBytes(1100, 1200, 1300),
+ /* totalOveruses= */ 5)));
+ pushLatestIoOveruseStatsAndWait(prevDayStats);
+
+ List<WatchdogStorage.IoUsageStatsEntry> expectedSavedEntries = Arrays.asList(
+ new WatchdogStorage.IoUsageStatsEntry(/* userId= */ 10, "system_package",
+ new WatchdogPerfHandler.PackageIoUsage(prevDayStats.get(0).ioOveruseStats,
+ /* forgivenWriteBytes= */ constructPerStateBytes(600, 700, 800),
+ /* totalTimesKilled= */ 1)),
+ new WatchdogStorage.IoUsageStatsEntry(/* userId= */ 10, "third_party_package",
+ new WatchdogPerfHandler.PackageIoUsage(prevDayStats.get(1).ioOveruseStats,
+ /* forgivenWriteBytes= */ constructPerStateBytes(1050, 1100, 1200),
+ /* totalTimesKilled= */ 0)));
+
+ setDisplayStateEnabled(true);
+ setDate(0);
+ List<PackageIoOveruseStats> currentDayStats = Arrays.asList(
+ constructPackageIoOveruseStats(1011200, /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(500, 550, 600),
+ /* writtenBytes= */ constructPerStateBytes(100, 150, 200),
+ /* totalOveruses= */ 0)),
+ constructPackageIoOveruseStats(1001100, /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ constructInternalIoOveruseStats(/* killableOnOveruse= */ true,
+ /* remainingWriteBytes= */ constructPerStateBytes(250, 360, 470),
+ /* writtenBytes= */ constructPerStateBytes(900, 900, 900),
+ /* totalOveruses= */ 0)));
+ pushLatestIoOveruseStatsAndWait(currentDayStats);
+
+ IoUsageStatsEntrySubject.assertThat(mIoUsageStatsEntries)
+ .containsExactlyElementsIn(expectedSavedEntries);
+
+ List<ResourceOveruseStats> actualCurrentDayStats =
+ mCarWatchdogService.getAllResourceOveruseStats(
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO, /* minimumStatsFlag= */ 0,
+ CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
+
+ List<ResourceOveruseStats> expectedCurrentDayStats = Arrays.asList(
+ constructResourceOveruseStats(/* uid= */ 1011200, "system_package",
+ currentDayStats.get(0).ioOveruseStats),
+ constructResourceOveruseStats(/* uid= */ 1001100, "third_party_package",
+ currentDayStats.get(1).ioOveruseStats));
+
+ ResourceOveruseStatsSubject.assertThat(actualCurrentDayStats)
+ .containsExactlyElementsIn(expectedCurrentDayStats);
+ }
+
+ /* TODO(b/197770456): Test user notification after implementing it.
+ * @Test
+ * public void testNoUserNotificationWithNoRecurrentOveruse() throws Exception {
+ * 1. Set display to enabled and UX restriction to not require distraction optimization.
+ * 2. Push some I/O stats without recurrent overuse and wait.
+ * 3. Verify no user notification.
+ * }
+ *
+ * @Test
+ * public void testNoUserNotificationOnRecurrentOveruseWithDistractionOptimization()
+ * throws Exception {
+ * 1. Set display to enabled and UX restriction to requires distraction optimization.
+ * 2. Push some I/O stats with recurrent overuse and wait.
+ * 3. Verify no user notification.
+ * }
+ *
+ * @Test
+ * public void testUserNotificationOnRecurrentOveruseAfterNoDistractionOptimization()
+ * throws Exception {
+ * 1. Set display to enabled and UX restriction to requires distraction optimization.
+ * 2. Push some I/O stats with recurrent overuse and wait.
+ * 3. Set UX restriction to not require distraction optimization.
+ * 4. Verify only current user is notified for only one app. Other notifications for
+ * the current user are posted on notification center.
+ * }
+ *
+ * @Test
+ * public void testImmediateUserNotificationOnRecurrentOveruseWhenNoDistractionOptimization()
+ * throws Exception {
+ * 1. Set display to enabled and UX restriction to not require distraction optimization.
+ * 2. Push some I/O stats with recurrent overuse and wait.
+ * 3. Verify user is notified for only one app and others posted on notification center.
+ * 4. Push more I/O stats with recurrent overuse and wait.
+ * 5. Verify notifications for the current user are posted only in notification center.
+ * }
+ *
+ * @Test
+ * public void testNoUserNotificationOnRecurrentOveruseByPrePrioritizedApp() throws Exception {
+ * 1. Set display to enabled and UX restriction to requires distraction optimization.
+ * 2. Set package killable state to NO.
+ * 3. Push some I/O stats with recurrent overuse and wait.
+ * 4. Set UX restriction to not require distraction optimization.
+ * 5. Verify no user notification.
+ * }
+ *
+ * @Test
+ * public void testNoUserNotificationOnRecurrentOveruseByPostPrioritizedApp() throws Exception {
+ * 1. Set display to enabled and UX restriction to requires distraction optimization.
+ * 2. Push some I/O stats with recurrent overuse and wait.
+ * 3. Set package killable state to NO.
+ * 4. Set UX restriction to without requires distraction optimization.
+ * 5. Verify no user notification.
+ * }
+ *
+ * @Test
+ * public void testUserNotificationOnRecurrentOveruseByPriorityResettedApp() throws Exception {
+ * 1. Set display to enabled and UX restriction to requires distraction optimization.
+ * 2. Set package killable state to NO.
+ * 3. Push some I/O stats with recurrent overuse and wait.
+ * 4. Set package killable state to YES.
+ * 5. Set UX restriction to not require distraction optimization.
+ * 6. Verify only current user is notified for only one app. Other notifications for
+ * the current user are posted on notification center.
+ * }
+ *
+ * @Test
+ * public void testUserNotificationOnHistoricalRecurrentOveruse() throws Exception {
+ * 1. Setup historical stats with RECURRING_OVERUSE_THRESHOLD overuses by mocking
+ * WatchdogStorage calls.
+ * 2. Set display to enabled and UX restriction to requires distraction optimization.
+ * 3. Push some I/O stats with non-recurrent overuse and wait.
+ * 4. Set UX restriction to not require distraction optimization.
+ * 5. Verify no user notification.
+ * }
+ *
+ * @Test
+ * public void testUserNotificationWithDisabledDisplay() throws Exception {
+ * 1. Set display to disabled and UX restriction to not require distraction optimization.
+ * 2. Push some I/O stats with recurrent overuse and wait.
+ * 3. Verify notifications for the current user are posted only in notification center.
+ * }
+ */
+
+ @Test
+ public void testNoDisableWithNoRecurrentOveruse() throws Exception {
+ setUpSampleUserAndPackages();
+ setRequiresDistractionOptimization(false);
+ setDisplayStateEnabled(false);
+
+ List<PackageIoOveruseStats> packageIoOveruseStats =
+ sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ false);
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ captureAndVerifyIoOveruseStatsReported(sampleReportedOveruseStats());
+
+ verify(() -> CarStatsLog.write(eq(CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED),
+ anyInt(), anyInt(), anyInt(), anyInt(), any(), any()), never());
+
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{
+ 10010001, 10110001, 10010004, 10110004, 10010005, 10110005},
+ /* killedRecurringOveruseUids= */ new int[]{}));
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).isEmpty();
+ }
+
+ @Test
+ public void testNoDisableRecurrentlyOverusingAppWithDistractionOptimization() throws Exception {
+ setUpSampleUserAndPackages();
+ setRequiresDistractionOptimization(true);
+ setDisplayStateEnabled(true);
+
+ List<PackageIoOveruseStats> packageIoOveruseStats =
+ sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ true);
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ captureAndVerifyIoOveruseStatsReported(sampleReportedOveruseStats());
+
+ verify(() -> CarStatsLog.write(eq(CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED),
+ anyInt(), anyInt(), anyInt(), anyInt(), any(), any()), never());
+
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{
+ 10010001, 10110001, 10010004, 10110004, 10010005, 10110005},
+ /* killedRecurringOveruseUids= */ new int[]{}));
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).isEmpty();
+ }
+
+ @Test
+ public void testNoDisableRecurrentlyOverusingAppWhenDisplayEnabled() throws Exception {
+ setUpSampleUserAndPackages();
+ setRequiresDistractionOptimization(false);
+ setDisplayStateEnabled(true);
+
+ List<PackageIoOveruseStats> packageIoOveruseStats =
+ sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ true);
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ captureAndVerifyIoOveruseStatsReported(sampleReportedOveruseStats());
+
+ verify(() -> CarStatsLog.write(eq(CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED),
+ anyInt(), anyInt(), anyInt(), anyInt(), any(), any()), never());
+
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{
+ 10010001, 10110001, 10010004, 10110004, 10010005, 10110005},
+ /* killedRecurringOveruseUids= */ new int[]{}));
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).isEmpty();
+ }
+
+ @Test
+ public void testDisableRecurrentlyOverusingAppAfterDisplayDisabled() throws Exception {
+ setUpSampleUserAndPackages();
+ setRequiresDistractionOptimization(true);
+ setDisplayStateEnabled(true);
+
+ List<PackageIoOveruseStats> packageIoOveruseStats =
+ sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ true);
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).isEmpty();
+
+ setRequiresDistractionOptimization(false);
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).isEmpty();
+
+ setDisplayStateEnabled(false);
+
+ captureAndVerifyIoOveruseStatsReported(sampleReportedOveruseStats());
+
+ captureAndVerifyKillStatsReported(sampleReportedKillStats(
+ CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE,
+ /* killedUids= */ new int[]{10010004, 10110004, 10010005, 10110005}));
+
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{
+ 10010001, 10110001, 10010004, 10110004, 10010005, 10110005},
+ /* killedRecurringOveruseUids= */ new int[]{10010004, 10110004, 10010005, 10110005}
+ ));
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).containsExactly(
+ "100:vendor_package.non_critical", "101:vendor_package.non_critical",
+ "100:third_party_package.A", "101:third_party_package.A",
+ "100:third_party_package.B", "101:third_party_package.B");
+ }
+
+ @Test
+ public void testImmediateDisableRecurrentlyOverusingAppDuringDisabledDisplay()
+ throws Exception {
+ setUpSampleUserAndPackages();
+ setRequiresDistractionOptimization(false);
+ setDisplayStateEnabled(false);
+
+ List<PackageIoOveruseStats> packageIoOveruseStats =
+ sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ true);
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ captureAndVerifyIoOveruseStatsReported(sampleReportedOveruseStats());
+
+ captureAndVerifyKillStatsReported(sampleReportedKillStats(
+ CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE,
+ /* killedUids= */ new int[]{10010004, 10110004, 10010005, 10110005}));
+
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{
+ 10010001, 10110001, 10010004, 10110004, 10010005, 10110005},
+ /* killedRecurringOveruseUids= */ new int[]{10010004, 10110004, 10010005, 10110005}
+ ));
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).containsExactly(
+ "100:vendor_package.non_critical", "101:vendor_package.non_critical",
+ "100:third_party_package.A", "101:third_party_package.A",
+ "100:third_party_package.B", "101:third_party_package.B");
+ }
+
+ @Test
+ public void testDisableRecurrentlyOverusingAppWhenDisplayDisabledAfterDateChange()
+ throws Exception {
+ setDate(1);
+ setUpSampleUserAndPackages();
+ setRequiresDistractionOptimization(true);
+ setDisplayStateEnabled(true);
+
+ List<PackageIoOveruseStats> packageIoOveruseStats =
+ sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ true);
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).isEmpty();
+
+ setDate(0);
+
+ pushLatestIoOveruseStatsAndWait(new ArrayList<>());
+
+ setRequiresDistractionOptimization(false);
+ setDisplayStateEnabled(false);
+
+ captureAndVerifyIoOveruseStatsReported(sampleReportedOveruseStats());
+
+ captureAndVerifyKillStatsReported(sampleReportedKillStats(
+ CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE,
+ /* killedUids= */ new int[]{10010004, 10110004, 10010005, 10110005}));
+
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{
+ 10010001, 10110001, 10010004, 10110004, 10010005, 10110005},
+ /* killedRecurringOveruseUids= */ new int[]{10010004, 10110004, 10010005, 10110005}
+ ));
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).containsExactly(
+ "100:vendor_package.non_critical", "101:vendor_package.non_critical",
+ "100:third_party_package.A", "101:third_party_package.A",
+ "100:third_party_package.B", "101:third_party_package.B");
+ }
+
+ @Test
+ public void testNoDisableRecurrentlyOverusingPrePrioritizedApp() throws Exception {
+ setUpSampleUserAndPackages();
+ setRequiresDistractionOptimization(true);
+ setDisplayStateEnabled(true);
+
+ mCarWatchdogService.setKillablePackageAsUser(
+ "vendor_package.non_critical", new UserHandle(100), /* isKillable= */ false);
+ mCarWatchdogService.setKillablePackageAsUser(
+ "third_party_package.A", new UserHandle(101), /* isKillable= */ false);
+
+ List<PackageIoOveruseStats> packageIoOveruseStats =
+ sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ true);
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).isEmpty();
+
+ setRequiresDistractionOptimization(false);
+ setDisplayStateEnabled(false);
+
+ captureAndVerifyIoOveruseStatsReported(sampleReportedOveruseStats());
+
+ captureAndVerifyKillStatsReported(sampleReportedKillStats(
+ CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE,
+ /* killedUids= */ new int[]{10110004, 10010005}));
+
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{
+ 10010001, 10110001, 10010004, 10110004, 10010005, 10110005},
+ /* killedRecurringOveruseUids= */ new int[]{10110004, 10010005}
+ ));
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages)
+ .containsExactly("101:vendor_package.non_critical", "100:third_party_package.A",
+ "100:third_party_package.B");
+ }
+
+ @Test
+ public void testNoDisableRecurrentlyOverusingPostPrioritizedApp() throws Exception {
+ setUpSampleUserAndPackages();
+ setRequiresDistractionOptimization(true);
+ setDisplayStateEnabled(true);
+
+ List<PackageIoOveruseStats> packageIoOveruseStats =
+ sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ true);
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).isEmpty();
+
+ mCarWatchdogService.setKillablePackageAsUser(
+ "vendor_package.non_critical", new UserHandle(100), /* isKillable= */ false);
+ mCarWatchdogService.setKillablePackageAsUser(
+ "third_party_package.A", new UserHandle(101), /* isKillable= */ false);
+
+ setRequiresDistractionOptimization(false);
+ setDisplayStateEnabled(false);
+
+ captureAndVerifyIoOveruseStatsReported(sampleReportedOveruseStats());
+
+ captureAndVerifyKillStatsReported(sampleReportedKillStats(
+ CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE,
+ /* killedUids= */ new int[]{10110004, 10010005}));
+
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{
+ 10010001, 10110001, 10010004, 10110004, 10010005, 10110005},
+ /* killedRecurringOveruseUids= */ new int[]{10110004, 10010005}
+ ));
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages)
+ .containsExactly("101:vendor_package.non_critical", "100:third_party_package.A",
+ "100:third_party_package.B");
+ }
+
+ @Test
+ public void testDisableRecurrentlyOverusingPriorityResettedApp() throws Exception {
+ setUpSampleUserAndPackages();
+ setRequiresDistractionOptimization(true);
+ setDisplayStateEnabled(true);
+
+ mCarWatchdogService.setKillablePackageAsUser(
+ "vendor_package.non_critical", new UserHandle(100), /* isKillable= */ false);
+
+ List<PackageIoOveruseStats> packageIoOveruseStats =
+ sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ true);
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).isEmpty();
+
+ mCarWatchdogService.setKillablePackageAsUser(
+ "vendor_package.non_critical", new UserHandle(100), /* isKillable= */ true);
+
+ setRequiresDistractionOptimization(false);
+ setDisplayStateEnabled(false);
+
+ captureAndVerifyIoOveruseStatsReported(sampleReportedOveruseStats());
+
+ captureAndVerifyKillStatsReported(sampleReportedKillStats(
+ CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__USER_NO_INTERACTION_MODE,
+ /* killedUids= */ new int[]{10010004, 10110004, 10010005, 10110005}));
+
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{
+ 10010001, 10110001, 10010004, 10110004, 10010005, 10110005},
+ /* killedRecurringOveruseUids= */ new int[]{10010004, 10110004, 10010005, 10110005}
+ ));
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).containsExactly(
+ "100:vendor_package.non_critical", "101:vendor_package.non_critical",
+ "100:third_party_package.A", "101:third_party_package.A",
+ "100:third_party_package.B", "101:third_party_package.B");
+ }
+
+ @Test
+ public void testImmediateDisableRecurrentlyOverusingAppDuringGarageMode()
+ throws Exception {
+ setUpSampleUserAndPackages();
+ setRequiresDistractionOptimization(false);
+ setDisplayStateEnabled(false);
+ mBroadcastReceiver.onReceive(mMockContext,
+ new Intent().setAction(CarWatchdogService.ACTION_GARAGE_MODE_ON));
+
+ List<PackageIoOveruseStats> packageIoOveruseStats =
+ sampleIoOveruseStats(/* requireRecurrentOveruseStats= */ true);
+
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+
+ captureAndVerifyIoOveruseStatsReported(sampleReportedOveruseStats());
+
+ captureAndVerifyKillStatsReported(sampleReportedKillStats(
+ CAR_WATCHDOG_KILL_STATS_REPORTED__SYSTEM_STATE__GARAGE_MODE,
+ /* killedUids= */ new int[]{10010004, 10110004, 10010005, 10110005}));
+
+ verifyActionsTakenOnResourceOveruse(sampleActionTakenOnOveruse(
+ /* notKilledUids= */ new int[]{
+ 10010001, 10110001, 10010004, 10110004, 10010005, 10110005},
+ /* killedRecurringOveruseUids= */ new int[]{10010004, 10110004, 10010005, 10110005}
+ ));
+
+ assertWithMessage("Disabled user packages").that(mDisabledUserPackages).containsExactly(
+ "100:vendor_package.non_critical", "101:vendor_package.non_critical",
+ "100:third_party_package.A", "101:third_party_package.A",
+ "100:third_party_package.B", "101:third_party_package.B");
+ }
+
+ /* TODO(b/195425666): Test recurrently overusing app with overuse history stored in the DB.
+ * @Test
+ * public void testDisableHistoricalRecurrentlyOverusingApp() throws Exception {}
+ */
+
+ @Test
+ public void testResetResourceOveruseStatsResetsStats() throws Exception {
+ UserHandle user = UserHandle.getUserHandleForUid(10003346);
+ String packageName = mMockContext.getPackageName();
+ mGenericPackageNameByUid.put(10003346, packageName);
+ mGenericPackageNameByUid.put(10101278, "vendor_package.critical");
+ mGenericPackageNameByUid.put(10103456, "third_party_package");
+ injectIoOveruseStatsForPackages(
+ mGenericPackageNameByUid, /* killablePackages= */ new ArraySet<>(),
+ /* shouldNotifyPackages= */ new ArraySet<>());
+
+ doReturn(COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED).when(mSpiedPackageManager)
+ .getApplicationEnabledSetting(
+ or(eq("third_party_package"), eq("vendor_package.critical")), eq(101));
+
+ mWatchdogServiceForSystemImpl.resetResourceOveruseStats(
+ Arrays.asList(packageName, "third_party_package"));
+
+ ResourceOveruseStats actualStats =
+ mCarWatchdogService.getResourceOveruseStatsForUserPackage(
+ packageName, user,
+ CarWatchdogManager.FLAG_RESOURCE_OVERUSE_IO,
+ CarWatchdogManager.STATS_PERIOD_CURRENT_DAY);
+
+ ResourceOveruseStats expectedStats = new ResourceOveruseStats.Builder(
+ packageName, user).build();
+
+ ResourceOveruseStatsSubject.assertEquals(actualStats, expectedStats);
+
+ verify(mMockWatchdogStorage).deleteUserPackage(eq(user.getIdentifier()), eq(packageName));
+
+ verify(mSpiedPackageManager).getApplicationEnabledSetting(packageName, 100);
+ verify(mSpiedPackageManager).getApplicationEnabledSetting("third_party_package", 101);
+ verify(mSpiedPackageManager).setApplicationEnabledSetting(eq("third_party_package"),
+ eq(COMPONENT_ENABLED_STATE_ENABLED), anyInt(), eq(101), anyString());
+
+ verifyNoMoreInteractions(mSpiedPackageManager);
+ }
+
+ @Test
+ public void testResetResourceOveruseStatsResetsUserPackageSettings() throws Exception {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 100, 101);
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("third_party_package.A", 10001278, null),
+ constructPackageManagerPackageInfo("third_party_package.A", 10101278, null),
+ constructPackageManagerPackageInfo("third_party_package.B", 10003346, null),
+ constructPackageManagerPackageInfo("third_party_package.B", 10103346, null)));
+ injectIoOveruseStatsForPackages(mGenericPackageNameByUid,
+ /* killablePackages= */ Set.of("third_party_package.A", "third_party_package.B"),
+ /* shouldNotifyPackages= */ new ArraySet<>());
+
+ mCarWatchdogService.setKillablePackageAsUser("third_party_package.A",
+ UserHandle.ALL, /* isKillable= */false);
+ mCarWatchdogService.setKillablePackageAsUser("third_party_package.B",
+ UserHandle.ALL, /* isKillable= */false);
+
+ mWatchdogServiceForSystemImpl.resetResourceOveruseStats(
+ Collections.singletonList("third_party_package.A"));
+
+ PackageKillableStateSubject.assertThat(
+ mCarWatchdogService.getPackageKillableStatesAsUser(UserHandle.ALL)).containsExactly(
+ new PackageKillableState("third_party_package.A", 100,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.A", 101,
+ PackageKillableState.KILLABLE_STATE_YES),
+ new PackageKillableState("third_party_package.B", 100,
+ PackageKillableState.KILLABLE_STATE_NO),
+ new PackageKillableState("third_party_package.B", 101,
+ PackageKillableState.KILLABLE_STATE_NO)
+ );
+
+ verify(mMockWatchdogStorage, times(2)).deleteUserPackage(anyInt(),
+ eq("third_party_package.A"));
}
@Test
public void testGetPackageInfosForUids() throws Exception {
- int[] uids = new int[]{6001, 6050, 5100, 110035, 120056, 120078, 1345678};
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "system_package.A", 6001, null, ApplicationInfo.FLAG_SYSTEM, 0),
+ constructPackageManagerPackageInfo(
+ "vendor_package.B", 5100, null, 0, ApplicationInfo.PRIVATE_FLAG_OEM),
+ constructPackageManagerPackageInfo(
+ "vendor_package.C", 1345678, null, 0, ApplicationInfo.PRIVATE_FLAG_ODM),
+ constructPackageManagerPackageInfo("third_party_package.D", 120056, null)));
+
+ int[] uids = new int[]{6001, 5100, 120056, 1345678};
+ List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
+ uids, new ArrayList<>());
+
List<PackageInfo> expectedPackageInfos = Arrays.asList(
- constructPackageInfo("system.package.A", 6001, new ArrayList<>(),
+ constructPackageInfo("system_package.A", 6001, new ArrayList<>(),
UidType.NATIVE, ComponentType.SYSTEM, ApplicationCategoryType.OTHERS),
- constructPackageInfo("shared:system.package", 6050,
- Arrays.asList("system.package.B", "third_party.package.C"),
- UidType.NATIVE, ComponentType.SYSTEM, ApplicationCategoryType.OTHERS),
- constructPackageInfo("vendor.package.D", 5100, new ArrayList<>(),
+ constructPackageInfo("vendor_package.B", 5100, new ArrayList<>(),
UidType.NATIVE, ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
- constructPackageInfo("shared:vendor.package", 110035,
- Arrays.asList("vendor.package.E", "system.package.F",
- "third_party.package.G"), UidType.APPLICATION,
- ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
- constructPackageInfo("third_party.package.H", 120056, new ArrayList<>(),
+ constructPackageInfo("third_party_package.D", 120056, new ArrayList<>(),
UidType.APPLICATION, ComponentType.THIRD_PARTY,
ApplicationCategoryType.OTHERS),
- constructPackageInfo("shared:third_party.package", 120078,
- Collections.singletonList("third_party.package.I"),
- UidType.APPLICATION, ComponentType.THIRD_PARTY,
- ApplicationCategoryType.OTHERS),
- constructPackageInfo("vendor.package.J", 1345678, new ArrayList<>(),
+ constructPackageInfo("vendor_package.C", 1345678, new ArrayList<>(),
UidType.APPLICATION, ComponentType.VENDOR,
ApplicationCategoryType.OTHERS));
- for (PackageInfo packageInfo : expectedPackageInfos) {
- mPackageNamesByUids.put(packageInfo.packageIdentifier.uid,
- packageInfo.packageIdentifier.name);
- mSharedPackagesByUids.put(packageInfo.packageIdentifier.uid,
- packageInfo.sharedUidPackages.toArray(new String[0]));
- }
+ assertPackageInfoEquals(actualPackageInfos, expectedPackageInfos);
+ }
- mApplicationInfosByPackages.put("system.package.A",
- constructApplicationInfo(ApplicationInfo.FLAG_SYSTEM, 0));
- mApplicationInfosByPackages.put("system.package.B",
- constructApplicationInfo(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP, 0));
- mApplicationInfosByPackages.put("system.package.F",
- constructApplicationInfo(0, ApplicationInfo.PRIVATE_FLAG_PRODUCT));
- mApplicationInfosByPackages.put("vendor.package.D",
- constructApplicationInfo(0, ApplicationInfo.PRIVATE_FLAG_OEM));
- mApplicationInfosByPackages.put("vendor.package.E",
- constructApplicationInfo(0, ApplicationInfo.PRIVATE_FLAG_VENDOR));
- mApplicationInfosByPackages.put("vendor.package.J",
- constructApplicationInfo(0, ApplicationInfo.PRIVATE_FLAG_ODM));
- mApplicationInfosByPackages.put("third_party.package.C", constructApplicationInfo(0, 0));
- mApplicationInfosByPackages.put("third_party.package.G", constructApplicationInfo(0, 0));
- mApplicationInfosByPackages.put("third_party.package.H", constructApplicationInfo(0, 0));
- mApplicationInfosByPackages.put("third_party.package.I", constructApplicationInfo(0, 0));
+ @Test
+ public void testGetPackageInfosWithSharedUids() throws Exception {
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo("system_package.A", 6050,
+ "system_shared_package", ApplicationInfo.FLAG_UPDATED_SYSTEM_APP, 0),
+ constructPackageManagerPackageInfo("system_package.B", 110035,
+ "vendor_shared_package", 0, ApplicationInfo.PRIVATE_FLAG_PRODUCT),
+ constructPackageManagerPackageInfo("vendor_package.C", 110035,
+ "vendor_shared_package", 0, ApplicationInfo.PRIVATE_FLAG_VENDOR),
+ constructPackageManagerPackageInfo(
+ "third_party_package.D", 6050, "system_shared_package"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.E", 110035, "vendor_shared_package"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.F", 120078, "third_party_shared_package")));
+ int[] uids = new int[]{6050, 110035, 120056, 120078};
List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
uids, new ArrayList<>());
+ List<PackageInfo> expectedPackageInfos = Arrays.asList(
+ constructPackageInfo("shared:system_shared_package", 6050,
+ Arrays.asList("system_package.A", "third_party_package.D"),
+ UidType.NATIVE, ComponentType.SYSTEM, ApplicationCategoryType.OTHERS),
+ constructPackageInfo("shared:vendor_shared_package", 110035,
+ Arrays.asList("vendor_package.C", "system_package.B",
+ "third_party_package.E"), UidType.APPLICATION,
+ ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
+ constructPackageInfo("shared:third_party_shared_package", 120078,
+ Collections.singletonList("third_party_package.F"),
+ UidType.APPLICATION, ComponentType.THIRD_PARTY,
+ ApplicationCategoryType.OTHERS));
+
assertPackageInfoEquals(actualPackageInfos, expectedPackageInfos);
}
@Test
public void testGetPackageInfosForUidsWithVendorPackagePrefixes() throws Exception {
- int[] uids = new int[]{110034, 110035, 123456, 120078};
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "vendor_package.A", 110034, null, 0, ApplicationInfo.PRIVATE_FLAG_PRODUCT),
+ constructPackageManagerPackageInfo("vendor_pkg.B", 110035,
+ "vendor_shared_package", ApplicationInfo.FLAG_SYSTEM, 0),
+ constructPackageManagerPackageInfo(
+ "third_party_package.C", 110035, "vendor_shared_package"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.D", 110035, "vendor_shared_package"),
+ constructPackageManagerPackageInfo(
+ "third_party_package.F", 120078, "third_party_shared_package"),
+ constructPackageManagerPackageInfo("vndr_pkg.G", 126345, "vendor_package.shared",
+ ApplicationInfo.FLAG_SYSTEM, 0),
+ /*
+ * A 3p package pretending to be a vendor package because 3p packages won't have the
+ * required flags.
+ */
+ constructPackageManagerPackageInfo("vendor_package.imposter", 123456, null, 0, 0)));
+
+ int[] uids = new int[]{110034, 110035, 120078, 126345, 123456};
+ List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
+ uids, Arrays.asList("vendor_package.", "vendor_pkg.", "shared:vendor_package."));
+
List<PackageInfo> expectedPackageInfos = Arrays.asList(
- constructPackageInfo("vendor.package.D", 110034, new ArrayList<>(),
+ constructPackageInfo("vendor_package.A", 110034, new ArrayList<>(),
UidType.APPLICATION, ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
- constructPackageInfo("shared:vendor.package", 110035,
- Arrays.asList("vendor.pkg.E", "third_party.package.F",
- "third_party.package.G"), UidType.APPLICATION,
+ constructPackageInfo("shared:vendor_shared_package", 110035,
+ Arrays.asList("vendor_pkg.B", "third_party_package.C",
+ "third_party_package.D"), UidType.APPLICATION,
ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
- constructPackageInfo("vendor.package.imposter", 123456,
- new ArrayList<>(), UidType.APPLICATION, ComponentType.THIRD_PARTY,
- ApplicationCategoryType.OTHERS),
- constructPackageInfo("third_party.package.H", 120078,
+ constructPackageInfo("shared:third_party_shared_package", 120078,
+ Collections.singletonList("third_party_package.F"), UidType.APPLICATION,
+ ComponentType.THIRD_PARTY, ApplicationCategoryType.OTHERS),
+ constructPackageInfo("shared:vendor_package.shared", 126345,
+ Collections.singletonList("vndr_pkg.G"), UidType.APPLICATION,
+ ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
+ constructPackageInfo("vendor_package.imposter", 123456,
new ArrayList<>(), UidType.APPLICATION, ComponentType.THIRD_PARTY,
ApplicationCategoryType.OTHERS));
- for (PackageInfo packageInfo : expectedPackageInfos) {
- mPackageNamesByUids.put(packageInfo.packageIdentifier.uid,
- packageInfo.packageIdentifier.name);
- mSharedPackagesByUids.put(packageInfo.packageIdentifier.uid,
- packageInfo.sharedUidPackages.toArray(new String[0]));
- }
-
- mApplicationInfosByPackages.put("vendor.package.D", constructApplicationInfo(0,
- ApplicationInfo.PRIVATE_FLAG_PRODUCT));
- mApplicationInfosByPackages.put("vendor.pkg.E",
- constructApplicationInfo(ApplicationInfo.FLAG_SYSTEM, 0));
- /*
- * A 3p package pretending to be a vendor package because 3p packages won't have the
- * required flags.
- */
- mApplicationInfosByPackages.put("vendor.package.imposter", constructApplicationInfo(0, 0));
- mApplicationInfosByPackages.put("third_party.package.F", constructApplicationInfo(0, 0));
- mApplicationInfosByPackages.put("third_party.package.G", constructApplicationInfo(0, 0));
- mApplicationInfosByPackages.put("third_party.package.H", constructApplicationInfo(0, 0));
-
- List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
- uids, Arrays.asList("vendor.package.", "vendor.pkg."));
-
assertPackageInfoEquals(actualPackageInfos, expectedPackageInfos);
}
@Test
public void testGetPackageInfosForUidsWithMissingApplicationInfos() throws Exception {
+ injectPackageInfos(Arrays.asList(
+ constructPackageManagerPackageInfo(
+ "vendor_package.A", 110034, null, 0, ApplicationInfo.PRIVATE_FLAG_OEM),
+ constructPackageManagerPackageInfo("vendor_package.B", 110035,
+ "vendor_shared_package", 0, ApplicationInfo.PRIVATE_FLAG_VENDOR),
+ constructPackageManagerPackageInfo(
+ "third_party_package.C", 110035, "vendor_shared_package")));
+
+ BiConsumer<Integer, String> addPackageToSharedUid = (uid, packageName) -> {
+ List<String> packages = mPackagesBySharedUid.get(uid);
+ if (packages == null) {
+ packages = new ArrayList<>();
+ }
+ packages.add(packageName);
+ mPackagesBySharedUid.put(uid, packages);
+ };
+
+ addPackageToSharedUid.accept(110035, "third_party.package.G");
+ mGenericPackageNameByUid.put(120056, "third_party.package.H");
+ mGenericPackageNameByUid.put(120078, "shared:third_party_shared_package");
+ addPackageToSharedUid.accept(120078, "third_party_package.I");
+
+
int[] uids = new int[]{110034, 110035, 120056, 120078};
+
+ List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
+ uids, new ArrayList<>());
+
List<PackageInfo> expectedPackageInfos = Arrays.asList(
- constructPackageInfo("vendor.package.D", 110034, new ArrayList<>(),
+ constructPackageInfo("vendor_package.A", 110034, new ArrayList<>(),
UidType.APPLICATION, ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
- constructPackageInfo("shared:vendor.package", 110035,
- Arrays.asList("vendor.package.E", "third_party.package.F",
+ constructPackageInfo("shared:vendor_shared_package", 110035,
+ Arrays.asList("vendor_package.B", "third_party_package.C",
"third_party.package.G"),
UidType.APPLICATION, ComponentType.VENDOR, ApplicationCategoryType.OTHERS),
constructPackageInfo("third_party.package.H", 120056, new ArrayList<>(),
UidType.APPLICATION, ComponentType.UNKNOWN,
ApplicationCategoryType.OTHERS),
- constructPackageInfo("shared:third_party.package", 120078,
- Collections.singletonList("third_party.package.I"),
+ constructPackageInfo("shared:third_party_shared_package", 120078,
+ Collections.singletonList("third_party_package.I"),
UidType.APPLICATION, ComponentType.UNKNOWN,
ApplicationCategoryType.OTHERS));
- for (PackageInfo packageInfo : expectedPackageInfos) {
- mPackageNamesByUids.put(packageInfo.packageIdentifier.uid,
- packageInfo.packageIdentifier.name);
- mSharedPackagesByUids.put(packageInfo.packageIdentifier.uid,
- packageInfo.sharedUidPackages.toArray(new String[0]));
- }
-
- mApplicationInfosByPackages.put("vendor.package.D", constructApplicationInfo(0,
- ApplicationInfo.PRIVATE_FLAG_VENDOR));
- mApplicationInfosByPackages.put("vendor.package.E", constructApplicationInfo(0,
- ApplicationInfo.PRIVATE_FLAG_VENDOR));
- mApplicationInfosByPackages.put("third_party.package.F", constructApplicationInfo(0, 0));
-
- List<PackageInfo> actualPackageInfos = mWatchdogServiceForSystemImpl.getPackageInfosForUids(
- uids, new ArrayList<>());
-
assertPackageInfoEquals(actualPackageInfos, expectedPackageInfos);
}
- private void mockWatchdogDaemon() {
+ @Test
+ public void testSetProcessHealthCheckEnabled() throws Exception {
+ mCarWatchdogService.controlProcessHealthCheck(true);
+
+ verify(mMockCarWatchdogDaemon).controlProcessHealthCheck(eq(true));
+ }
+
+ @Test
+ public void testSetProcessHealthCheckEnabledWithDisconnectedDaemon() throws Exception {
+ crashWatchdogDaemon();
+
+ assertThrows(IllegalStateException.class,
+ () -> mCarWatchdogService.controlProcessHealthCheck(false));
+
+ verify(mMockCarWatchdogDaemon, never()).controlProcessHealthCheck(anyBoolean());
+ }
+
+ @Test
+ public void testOveruseConfigurationCacheGetVendorPackagePrefixes() throws Exception {
+ OveruseConfigurationCache cache = new OveruseConfigurationCache();
+
+ cache.set(sampleInternalResourceOveruseConfigurations());
+
+ assertWithMessage("Vendor package prefixes").that(cache.getVendorPackagePrefixes())
+ .containsExactly("vendor_package", "some_pkg_as_vendor_pkg");
+ }
+
+ @Test
+ public void testOveruseConfigurationCacheFetchThreshold() throws Exception {
+ OveruseConfigurationCache cache = new OveruseConfigurationCache();
+
+ cache.set(sampleInternalResourceOveruseConfigurations());
+
+ InternalPerStateBytesSubject.assertWithMessage(
+ cache.fetchThreshold("system_package.non_critical.A", ComponentType.SYSTEM),
+ "System package with generic threshold")
+ .isEqualTo(constructPerStateBytes(10, 20, 30));
+
+ InternalPerStateBytesSubject.assertWithMessage(
+ cache.fetchThreshold("system_package.A", ComponentType.SYSTEM),
+ "System package with package specific threshold")
+ .isEqualTo(constructPerStateBytes(40, 50, 60));
+
+ InternalPerStateBytesSubject.assertWithMessage(
+ cache.fetchThreshold("system_package.MEDIA", ComponentType.SYSTEM),
+ "System package with media category threshold")
+ .isEqualTo(constructPerStateBytes(200, 400, 600));
+
+ InternalPerStateBytesSubject.assertWithMessage(
+ cache.fetchThreshold("vendor_package.non_critical.A", ComponentType.VENDOR),
+ "Vendor package with generic threshold")
+ .isEqualTo(constructPerStateBytes(20, 40, 60));
+
+ InternalPerStateBytesSubject.assertWithMessage(
+ cache.fetchThreshold("vendor_package.A", ComponentType.VENDOR),
+ "Vendor package with package specific threshold")
+ .isEqualTo(constructPerStateBytes(80, 100, 120));
+
+ InternalPerStateBytesSubject.assertWithMessage(
+ cache.fetchThreshold("vendor_package.MEDIA", ComponentType.VENDOR),
+ "Vendor package with media category threshold")
+ .isEqualTo(constructPerStateBytes(200, 400, 600));
+
+ InternalPerStateBytesSubject.assertWithMessage(
+ cache.fetchThreshold("third_party_package.A",
+ ComponentType.THIRD_PARTY),
+ "3p package with generic threshold").isEqualTo(constructPerStateBytes(30, 60, 90));
+
+ InternalPerStateBytesSubject.assertWithMessage(
+ cache.fetchThreshold("third_party_package.MAPS", ComponentType.VENDOR),
+ "3p package with maps category threshold")
+ .isEqualTo(constructPerStateBytes(2200, 4400, 6600));
+ }
+
+ @Test
+ public void testOveruseConfigurationCacheIsSafeToKill() throws Exception {
+ OveruseConfigurationCache cache = new OveruseConfigurationCache();
+
+ cache.set(sampleInternalResourceOveruseConfigurations());
+
+ assertWithMessage("isSafeToKill non-critical system package").that(cache.isSafeToKill(
+ "system_package.non_critical.A", ComponentType.SYSTEM, null)).isTrue();
+
+ assertWithMessage("isSafeToKill shared non-critical system package")
+ .that(cache.isSafeToKill("system_package.A", ComponentType.SYSTEM,
+ Collections.singletonList("system_package.non_critical.A"))).isTrue();
+
+ assertWithMessage("isSafeToKill non-critical vendor package").that(cache.isSafeToKill(
+ "vendor_package.non_critical.A", ComponentType.VENDOR, null)).isTrue();
+
+ assertWithMessage("isSafeToKill shared non-critical vendor package")
+ .that(cache.isSafeToKill("vendor_package.A", ComponentType.VENDOR,
+ Collections.singletonList("vendor_package.non_critical.A"))).isTrue();
+
+ assertWithMessage("isSafeToKill 3p package").that(cache.isSafeToKill(
+ "third_party_package.A", ComponentType.THIRD_PARTY, null)).isTrue();
+
+ assertWithMessage("isSafeToKill critical system package").that(cache.isSafeToKill(
+ "system_package.A", ComponentType.SYSTEM, null)).isFalse();
+
+ assertWithMessage("isSafeToKill critical vendor package").that(cache.isSafeToKill(
+ "vendor_package.A", ComponentType.VENDOR, null)).isFalse();
+ }
+
+ @Test
+ public void testOverwriteOveruseConfigurationCache() throws Exception {
+ OveruseConfigurationCache cache = new OveruseConfigurationCache();
+
+ cache.set(sampleInternalResourceOveruseConfigurations());
+
+ cache.set(new ArrayList<>());
+
+ assertWithMessage("Vendor package prefixes").that(cache.getVendorPackagePrefixes())
+ .isEmpty();
+
+ InternalPerStateBytesSubject.assertWithMessage(
+ cache.fetchThreshold("system_package.A", ComponentType.SYSTEM),
+ "System package with default threshold")
+ .isEqualTo(OveruseConfigurationCache.DEFAULT_THRESHOLD);
+
+ InternalPerStateBytesSubject.assertWithMessage(
+ cache.fetchThreshold("vendor_package.A", ComponentType.VENDOR),
+ "Vendor package with default threshold")
+ .isEqualTo(OveruseConfigurationCache.DEFAULT_THRESHOLD);
+
+ InternalPerStateBytesSubject.assertWithMessage(
+ cache.fetchThreshold("third_party_package.A", ComponentType.THIRD_PARTY),
+ "3p package with default threshold")
+ .isEqualTo(OveruseConfigurationCache.DEFAULT_THRESHOLD);
+
+ assertWithMessage("isSafeToKill any system package").that(cache.isSafeToKill(
+ "system_package.non_critical.A", ComponentType.SYSTEM, null)).isFalse();
+
+ assertWithMessage("isSafeToKill any vendor package").that(cache.isSafeToKill(
+ "vendor_package.non_critical.A", ComponentType.VENDOR, null)).isFalse();
+ }
+
+ public static android.automotive.watchdog.PerStateBytes constructPerStateBytes(
+ long fgBytes, long bgBytes, long gmBytes) {
+ android.automotive.watchdog.PerStateBytes perStateBytes =
+ new android.automotive.watchdog.PerStateBytes();
+ perStateBytes.foregroundBytes = fgBytes;
+ perStateBytes.backgroundBytes = bgBytes;
+ perStateBytes.garageModeBytes = gmBytes;
+ return perStateBytes;
+ }
+
+ private void mockWatchdogDaemon() throws Exception {
when(mMockBinder.queryLocalInterface(anyString())).thenReturn(mMockCarWatchdogDaemon);
when(mMockCarWatchdogDaemon.asBinder()).thenReturn(mMockBinder);
doReturn(mMockBinder).when(() -> ServiceManager.getService(CAR_WATCHDOG_DAEMON_INTERFACE));
+ when(mMockCarWatchdogDaemon.getResourceOveruseConfigurations()).thenReturn(
+ sampleInternalResourceOveruseConfigurations());
+ mIsDaemonCrashed = false;
}
- private void mockPackageManager() throws Exception {
- mPackageNamesByUids.clear();
- mSharedPackagesByUids.clear();
- mApplicationInfosByPackages.clear();
- when(mMockPackageManager.getNamesForUids(any())).thenAnswer(args -> {
- int[] uids = args.getArgument(0);
- String[] packageNames = new String[uids.length];
- for (int i = 0; i < uids.length; ++i) {
- packageNames[i] = mPackageNamesByUids.get(uids[i], null);
- }
- return packageNames;
+ private void mockWatchdogStorage() {
+ when(mMockWatchdogStorage.saveUserPackageSettings(any())).thenAnswer((args) -> {
+ mUserPackageSettingsEntries.addAll(args.getArgument(0));
+ return true;
});
- when(mMockPackageManager.getPackagesForUid(anyInt())).thenAnswer(
- args -> mSharedPackagesByUids.get(args.getArgument(0), null));
-
- when(mMockPackageManager.getApplicationInfoAsUser(any(), anyInt(), anyInt())).thenAnswer(
- args -> {
- String packageName = args.getArgument(0);
- ApplicationInfo applicationInfo = mApplicationInfosByPackages
- .getOrDefault(packageName, /* defaultValue= */ null);
- if (applicationInfo == null) {
- throw new PackageManager.NameNotFoundException(
- "Package " + packageName + " not found exception");
- }
- return applicationInfo;
- });
- }
-
- private void captureBroadcastReceiver() {
- ArgumentCaptor<BroadcastReceiver> receiverArgumentCaptor =
- ArgumentCaptor.forClass(BroadcastReceiver.class);
- verify(mMockContext)
- .registerReceiverForAllUsers(receiverArgumentCaptor.capture(), any(), any(), any());
- mBroadcastReceiver = receiverArgumentCaptor.getValue();
- assertWithMessage("Broadcast receiver must be non-null").that(mBroadcastReceiver)
- .isNotEqualTo(null);
- }
-
- private void captureDaemonBinderDeathRecipient() throws Exception {
- ArgumentCaptor<IBinder.DeathRecipient> deathRecipientCaptor =
- ArgumentCaptor.forClass(IBinder.DeathRecipient.class);
- verify(mMockBinder, timeout(MAX_WAIT_TIME_MS).atLeastOnce())
- .linkToDeath(deathRecipientCaptor.capture(), anyInt());
- mCarWatchdogDaemonBinderDeathRecipient = deathRecipientCaptor.getValue();
- }
-
- public void crashWatchdogDaemon() {
- doReturn(null).when(() -> ServiceManager.getService(CAR_WATCHDOG_DAEMON_INTERFACE));
- mCarWatchdogDaemonBinderDeathRecipient.binderDied();
- }
-
- public void restartWatchdogDaemonAndAwait() throws Exception {
- CountDownLatch latch = new CountDownLatch(1);
- doAnswer(args -> {
- latch.countDown();
- return null;
- }).when(mMockBinder).linkToDeath(any(), anyInt());
- mockWatchdogDaemon();
- latch.await(MAX_WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+ when(mMockWatchdogStorage.saveIoUsageStats(any())).thenAnswer((args) -> {
+ List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = args.getArgument(0);
+ for (WatchdogStorage.IoUsageStatsEntry entry : ioUsageStatsEntries) {
+ mIoUsageStatsEntries.add(
+ new WatchdogStorage.IoUsageStatsEntry(entry.userId, entry.packageName,
+ new WatchdogPerfHandler.PackageIoUsage(
+ entry.ioUsage.getInternalIoOveruseStats(),
+ entry.ioUsage.getForgivenWriteBytes(),
+ entry.ioUsage.getTotalTimesKilled())));
+ }
+ return true;
+ });
+ when(mMockWatchdogStorage.getUserPackageSettings()).thenReturn(mUserPackageSettingsEntries);
+ when(mMockWatchdogStorage.getTodayIoUsageStats()).thenReturn(mIoUsageStatsEntries);
}
private void setupUsers() {
@@ -1317,111 +2992,432 @@
mockUmGetAllUsers(mMockUserManager, new UserInfo[0]);
}
- private ICarWatchdogServiceForSystem registerCarWatchdogService() throws Exception {
- /* Registering to daemon is done on the main thread. To ensure the registration completes
- * before verification, execute an empty block on the main thread.
- */
+ private void captureCarPowerListeners() {
+ verify(mMockCarPowerManagementService).registerListener(
+ mICarPowerStateListenerCaptor.capture());
+ mCarPowerStateListener = mICarPowerStateListenerCaptor.getValue();
+ assertWithMessage("Car power state listener").that(mCarPowerStateListener).isNotNull();
+
+ verify(mMockCarPowerManagementService).addPowerPolicyListener(
+ any(), mICarPowerPolicyListenerCaptor.capture());
+ mCarPowerPolicyListener = mICarPowerPolicyListenerCaptor.getValue();
+ assertWithMessage("Car power policy listener").that(mCarPowerPolicyListener).isNotNull();
+ }
+
+ private void captureBroadcastReceiver() {
+ verify(mMockContext)
+ .registerReceiverForAllUsers(mBroadcastReceiverCaptor.capture(), any(), any(),
+ any());
+ mBroadcastReceiver = mBroadcastReceiverCaptor.getValue();
+ assertWithMessage("Broadcast receiver").that(mBroadcastReceiver).isNotNull();
+ }
+
+ private void captureCarUxRestrictionsChangeListener() {
+ verify(mMockCarUxRestrictionsManagerService).registerUxRestrictionsChangeListener(
+ mICarUxRestrictionsChangeListener.capture(), eq(Display.DEFAULT_DISPLAY));
+ mCarUxRestrictionsChangeListener = mICarUxRestrictionsChangeListener.getValue();
+ assertWithMessage("UX restrictions change listener").that(mCarUxRestrictionsChangeListener)
+ .isNotNull();
+ }
+
+ private void captureAndVerifyRegistrationWithDaemon(boolean waitOnMain) throws Exception {
+ if (waitOnMain) {
+ // Registering to daemon is done on the main thread. To ensure the registration
+ // completes before verification, execute an empty block on the main thread.
+ CarServiceUtils.runOnMainSync(() -> {});
+ }
+
+ verify(mMockCarWatchdogDaemon, atLeastOnce()).asBinder();
+
+ verify(mMockBinder, atLeastOnce()).linkToDeath(mDeathRecipientCaptor.capture(), anyInt());
+ mCarWatchdogDaemonBinderDeathRecipient = mDeathRecipientCaptor.getValue();
+ assertWithMessage("Watchdog daemon binder death recipient")
+ .that(mCarWatchdogDaemonBinderDeathRecipient).isNotNull();
+
+ verify(mMockCarWatchdogDaemon, atLeastOnce()).registerCarWatchdogService(
+ mICarWatchdogServiceForSystemCaptor.capture());
+ mWatchdogServiceForSystemImpl = mICarWatchdogServiceForSystemCaptor.getValue();
+ assertWithMessage("Car watchdog service for system")
+ .that(mWatchdogServiceForSystemImpl).isNotNull();
+
+ verify(mMockCarWatchdogDaemon, atLeastOnce()).notifySystemStateChange(
+ eq(StateType.GARAGE_MODE), eq(GarageMode.GARAGE_MODE_OFF), eq(-1));
+
+ // Once registration with daemon completes, the service post a new message on the main
+ // thread to fetch and sync resource overuse configs.
CarServiceUtils.runOnMainSync(() -> {});
- ArgumentCaptor<ICarWatchdogServiceForSystem> watchdogServiceForSystemImplCaptor =
- ArgumentCaptor.forClass(ICarWatchdogServiceForSystem.class);
- verify(mMockCarWatchdogDaemon, atLeastOnce()).registerCarWatchdogService(
- watchdogServiceForSystemImplCaptor.capture());
- return watchdogServiceForSystemImplCaptor.getValue();
+ verify(mMockCarWatchdogDaemon, atLeastOnce()).getResourceOveruseConfigurations();
}
+ private void verifyDatabaseInit(int wantedInvocations) throws Exception {
+ /*
+ * Database read is posted on a separate handler thread. Wait until the handler thread has
+ * processed the database read request before verifying.
+ */
+ CarServiceUtils.getHandlerThread(CarWatchdogService.class.getSimpleName())
+ .getThreadHandler().post(() -> {});
+ verify(mMockWatchdogStorage, times(wantedInvocations)).syncUsers(any());
+ verify(mMockWatchdogStorage, times(wantedInvocations)).getUserPackageSettings();
+ verify(mMockWatchdogStorage, times(wantedInvocations)).getTodayIoUsageStats();
+ }
+
+ private void mockPackageManager() throws Exception {
+ when(mMockPackageManager.getNamesForUids(any())).thenAnswer(args -> {
+ int[] uids = args.getArgument(0);
+ String[] names = new String[uids.length];
+ for (int i = 0; i < uids.length; ++i) {
+ names[i] = mGenericPackageNameByUid.get(uids[i], null);
+ }
+ return names;
+ });
+ when(mMockPackageManager.getPackagesForUid(anyInt())).thenAnswer(args -> {
+ int uid = args.getArgument(0);
+ List<String> packages = mPackagesBySharedUid.get(uid);
+ return packages.toArray(new String[0]);
+ });
+ when(mMockPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+ .thenAnswer(args -> {
+ int userId = args.getArgument(2);
+ String userPackageId = userId + ":" + args.getArgument(0);
+ android.content.pm.PackageInfo packageInfo =
+ mPmPackageInfoByUserPackage.get(userPackageId);
+ if (packageInfo == null) {
+ throw new PackageManager.NameNotFoundException(
+ "User package id '" + userPackageId + "' not found");
+ }
+ return packageInfo.applicationInfo;
+ });
+ when(mMockPackageManager.getPackageInfoAsUser(anyString(), anyInt(), anyInt())).thenAnswer(
+ args -> {
+ int userId = args.getArgument(2);
+ String userPackageId = userId + ":" + args.getArgument(0);
+ android.content.pm.PackageInfo packageInfo =
+ mPmPackageInfoByUserPackage.get(userPackageId);
+ if (packageInfo == null) {
+ throw new PackageManager.NameNotFoundException(
+ "User package id '" + userPackageId + "' not found");
+ }
+ return packageInfo;
+ });
+ when(mMockPackageManager.getInstalledPackagesAsUser(anyInt(), anyInt())).thenAnswer(
+ args -> {
+ int userId = args.getArgument(1);
+ List<android.content.pm.PackageInfo> packageInfos = new ArrayList<>();
+ for (android.content.pm.PackageInfo packageInfo :
+ mPmPackageInfoByUserPackage.values()) {
+ if (UserHandle.getUserId(packageInfo.applicationInfo.uid) == userId) {
+ packageInfos.add(packageInfo);
+ }
+ }
+ return packageInfos;
+ });
+ doAnswer((args) -> {
+ String value = args.getArgument(3) + ":" + args.getArgument(0);
+ mDisabledUserPackages.add(value);
+ return null;
+ }).when(mSpiedPackageManager).setApplicationEnabledSetting(
+ anyString(), eq(COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED), anyInt(),
+ anyInt(), anyString());
+ doReturn(COMPONENT_ENABLED_STATE_ENABLED).when(mSpiedPackageManager)
+ .getApplicationEnabledSetting(anyString(), anyInt());
+ }
+
+ private void setCarPowerState(int powerState) throws Exception {
+ when(mMockCarPowerManagementService.getPowerState()).thenReturn(powerState);
+ mCarPowerStateListener.onStateChanged(powerState);
+ }
+
+ private void setDisplayStateEnabled(boolean isEnabled) throws Exception {
+ int[] enabledComponents = new int[]{};
+ int[] disabledComponents = new int[]{};
+ if (isEnabled) {
+ enabledComponents = new int[]{PowerComponent.DISPLAY};
+ } else {
+ disabledComponents = new int[]{PowerComponent.DISPLAY};
+ }
+ mCarPowerPolicyListener.onPolicyChanged(
+ new CarPowerPolicy(/* policyId= */ "", enabledComponents, disabledComponents),
+ /* accumulatedPolicy= */ null);
+ }
+
+ private void setRequiresDistractionOptimization(boolean isRequires) throws Exception {
+ CarUxRestrictions.Builder builder = new CarUxRestrictions.Builder(
+ isRequires, UX_RESTRICTIONS_BASELINE, /* time= */ 0);
+ mCarUxRestrictionsChangeListener.onUxRestrictionsChanged(builder.build());
+ }
+
+ private void crashWatchdogDaemon() {
+ doReturn(null).when(() -> ServiceManager.getService(CAR_WATCHDOG_DAEMON_INTERFACE));
+ mCarWatchdogDaemonBinderDeathRecipient.binderDied();
+ mIsDaemonCrashed = true;
+ }
+
+ private void restartWatchdogDaemonAndAwait() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+ doAnswer(args -> {
+ latch.countDown();
+ return null;
+ }).when(mMockBinder).linkToDeath(any(), anyInt());
+ mockWatchdogDaemon();
+ latch.await(MAX_WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+ captureAndVerifyRegistrationWithDaemon(/* waitOnMain= */ false);
+ }
+
+ private void setDate(int numDaysAgo) {
+ TimeSourceInterface timeSource = new TimeSourceInterface() {
+ @Override
+ public Instant now() {
+ /* Return the same time, so the tests are deterministic. */
+ return mNow;
+ }
+
+ @Override
+ public String toString() {
+ return "Mocked date to " + now();
+ }
+
+ private final Instant mNow = Instant.now().minus(numDaysAgo, ChronoUnit.DAYS);
+ };
+ mCarWatchdogService.setTimeSource(timeSource);
+ mTimeSource = timeSource;
+ }
+
+
private void testClientHealthCheck(TestClient client, int badClientCount) throws Exception {
mCarWatchdogService.registerClient(client, TIMEOUT_CRITICAL);
mWatchdogServiceForSystemImpl.checkIfAlive(123456, TIMEOUT_CRITICAL);
- ArgumentCaptor<int[]> notRespondingClients = ArgumentCaptor.forClass(int[].class);
verify(mMockCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS)).tellCarWatchdogServiceAlive(
- eq(mWatchdogServiceForSystemImpl), notRespondingClients.capture(), eq(123456));
- assertThat(notRespondingClients.getValue().length).isEqualTo(0);
+ eq(mWatchdogServiceForSystemImpl), mIntArrayCaptor.capture(), eq(123456));
+ assertThat(mIntArrayCaptor.getValue()).isEmpty();
mWatchdogServiceForSystemImpl.checkIfAlive(987654, TIMEOUT_CRITICAL);
verify(mMockCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS)).tellCarWatchdogServiceAlive(
- eq(mWatchdogServiceForSystemImpl), notRespondingClients.capture(), eq(987654));
- assertThat(notRespondingClients.getValue().length).isEqualTo(badClientCount);
+ eq(mWatchdogServiceForSystemImpl), mIntArrayCaptor.capture(), eq(987654));
+ assertThat(mIntArrayCaptor.getValue().length).isEqualTo(badClientCount);
}
private List<android.automotive.watchdog.internal.ResourceOveruseConfiguration>
captureOnSetResourceOveruseConfigurations() throws Exception {
- ArgumentCaptor<List<android.automotive.watchdog.internal.ResourceOveruseConfiguration>>
- resourceOveruseConfigurationsCaptor = ArgumentCaptor.forClass(List.class);
verify(mMockCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS))
- .updateResourceOveruseConfigurations(resourceOveruseConfigurationsCaptor.capture());
- return resourceOveruseConfigurationsCaptor.getValue();
+ .updateResourceOveruseConfigurations(
+ mResourceOveruseConfigurationsCaptor.capture());
+ return mResourceOveruseConfigurationsCaptor.getValue();
}
- private void injectIoOveruseStatsForPackages(SparseArray<String> packageNamesByUid,
- Set<String> killablePackages, Set<String> shouldNotifyPackages) throws Exception {
+ private SparseArray<PackageIoOveruseStats> injectIoOveruseStatsForPackages(
+ SparseArray<String> genericPackageNameByUid, Set<String> killablePackages,
+ Set<String> shouldNotifyPackages) throws Exception {
+ SparseArray<PackageIoOveruseStats> packageIoOveruseStatsByUid = new SparseArray<>();
List<PackageIoOveruseStats> packageIoOveruseStats = new ArrayList<>();
- for (int i = 0; i < packageNamesByUid.size(); ++i) {
- String packageName = packageNamesByUid.valueAt(i);
- int uid = packageNamesByUid.keyAt(i);
- packageIoOveruseStats.add(constructPackageIoOveruseStats(uid,
- shouldNotifyPackages.contains(packageName),
- constructInternalIoOveruseStats(killablePackages.contains(packageName),
- /* remainingWriteBytes= */constructPerStateBytes(20, 20, 20),
- /* writtenBytes= */constructPerStateBytes(100, 200, 300),
- /* totalOveruses= */2)));
+ for (int i = 0; i < genericPackageNameByUid.size(); ++i) {
+ String name = genericPackageNameByUid.valueAt(i);
+ int uid = genericPackageNameByUid.keyAt(i);
+ PackageIoOveruseStats stats = constructPackageIoOveruseStats(uid,
+ shouldNotifyPackages.contains(name),
+ constructPerStateBytes(80, 147, 213),
+ constructInternalIoOveruseStats(killablePackages.contains(name),
+ /* remainingWriteBytes= */ constructPerStateBytes(20, 20, 20),
+ /* writtenBytes= */ constructPerStateBytes(100, 200, 300),
+ /* totalOveruses= */ 3));
+ packageIoOveruseStatsByUid.put(uid, stats);
+ packageIoOveruseStats.add(stats);
}
- mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
+ pushLatestIoOveruseStatsAndWait(packageIoOveruseStats);
+ return packageIoOveruseStatsByUid;
}
- private void injectPackageInfos(List<String> packageNames) {
- List<android.content.pm.PackageInfo> packageInfos = new ArrayList<>();
- TriConsumer<String, Integer, Integer> addPackageInfo =
- (packageName, flags, privateFlags) -> {
- android.content.pm.PackageInfo packageInfo =
- new android.content.pm.PackageInfo();
- packageInfo.packageName = packageName;
- packageInfo.applicationInfo = new ApplicationInfo();
- packageInfo.applicationInfo.flags = flags;
- packageInfo.applicationInfo.privateFlags = privateFlags;
- packageInfos.add(packageInfo);
- };
- for (String packageName : packageNames) {
- if (packageName.startsWith("system")) {
- addPackageInfo.accept(packageName, ApplicationInfo.FLAG_SYSTEM, 0);
- } else if (packageName.startsWith("vendor")) {
- addPackageInfo.accept(packageName, ApplicationInfo.FLAG_SYSTEM,
- ApplicationInfo.PRIVATE_FLAG_OEM);
- } else {
- addPackageInfo.accept(packageName, 0, 0);
+ private void injectPackageInfos(
+ List<android.content.pm.PackageInfo> packageInfos) {
+ for (android.content.pm.PackageInfo packageInfo : packageInfos) {
+ String genericPackageName = packageInfo.packageName;
+ int uid = packageInfo.applicationInfo.uid;
+ int userId = UserHandle.getUserId(uid);
+ if (packageInfo.sharedUserId != null) {
+ genericPackageName =
+ PackageInfoHandler.SHARED_PACKAGE_PREFIX + packageInfo.sharedUserId;
+ List<String> packages = mPackagesBySharedUid.get(uid);
+ if (packages == null) {
+ packages = new ArrayList<>();
+ }
+ packages.add(packageInfo.packageName);
+ mPackagesBySharedUid.put(uid, packages);
}
+ String userPackageId = userId + ":" + packageInfo.packageName;
+ assertWithMessage("Duplicate package infos provided for user package id: %s",
+ userPackageId).that(mPmPackageInfoByUserPackage.containsKey(userPackageId))
+ .isFalse();
+ assertWithMessage("Mismatch generic package names for the same uid '%s'",
+ uid).that(mGenericPackageNameByUid.get(uid, genericPackageName))
+ .isEqualTo(genericPackageName);
+ mPmPackageInfoByUserPackage.put(userPackageId, packageInfo);
+ mGenericPackageNameByUid.put(uid, genericPackageName);
}
- when(mMockPackageManager.getInstalledPackagesAsUser(eq(0), anyInt()))
- .thenReturn(packageInfos);
}
- private void mockApplicationEnabledSettingAccessors(IPackageManager pm) throws Exception {
- doReturn(COMPONENT_ENABLED_STATE_ENABLED).when(pm)
- .getApplicationEnabledSetting(anyString(), eq(UserHandle.myUserId()));
+ private void pushLatestIoOveruseStatsAndWait(
+ List<PackageIoOveruseStats> packageIoOveruseStats) throws Exception {
+ mWatchdogServiceForSystemImpl.latestIoOveruseStats(packageIoOveruseStats);
- doNothing().when(pm).setApplicationEnabledSetting(anyString(), anyInt(),
- anyInt(), eq(UserHandle.myUserId()), anyString());
+ // Resource overuse handling is done on the main thread by posting a new message with
+ // OVERUSE_HANDLING_DELAY_MILLS delay. Wait until the below message is processed before
+ // returning, so the resource overuse handling is completed.
+ delayedRunOnMainSync(() -> {}, OVERUSE_HANDLING_DELAY_MILLS * 2);
}
private void verifyActionsTakenOnResourceOveruse(List<PackageResourceOveruseAction> expected)
throws Exception {
- ArgumentCaptor<List<PackageResourceOveruseAction>> resourceOveruseActionsCaptor =
- ArgumentCaptor.forClass((Class) List.class);
+ // notifyActionsTakenOnOveruse is posted on the main thread after taking action, so wait for
+ // this to complete by posting a task on the main thread.
+ CarServiceUtils.runOnMainSync(() -> {});
- verify(mMockCarWatchdogDaemon, timeout(MAX_WAIT_TIME_MS)).actionTakenOnResourceOveruse(
- resourceOveruseActionsCaptor.capture());
- List<PackageResourceOveruseAction> actual = resourceOveruseActionsCaptor.getValue();
+ verify(mMockCarWatchdogDaemon,
+ timeout(MAX_WAIT_TIME_MS).atLeastOnce()).actionTakenOnResourceOveruse(
+ mResourceOveruseActionsCaptor.capture());
- assertThat(actual).comparingElementsUsing(
- Correspondence.from(
- CarWatchdogServiceUnitTest::isPackageResourceOveruseActionEquals,
- "is overuse action equal to")).containsExactlyElementsIn(expected);
+ List<PackageResourceOveruseAction> actual = new ArrayList<>();
+ for (List<PackageResourceOveruseAction> actions :
+ mResourceOveruseActionsCaptor.getAllValues()) {
+ actual.addAll(actions);
+ }
+
+ PackageResourceOveruseActionSubject.assertThat(actual).containsExactlyElementsIn(expected);
}
- public static boolean isPackageResourceOveruseActionEquals(PackageResourceOveruseAction actual,
- PackageResourceOveruseAction expected) {
- return isEquals(actual.packageIdentifier, expected.packageIdentifier)
- && Arrays.equals(actual.resourceTypes, expected.resourceTypes)
- && actual.resourceOveruseActionType == expected.resourceOveruseActionType;
+ private void setUpSampleUserAndPackages() {
+ mockUmGetUserHandles(mMockUserManager, /* excludeDying= */ true, 100, 101);
+ int[] users = new int[]{100, 101};
+ List<android.content.pm.PackageInfo> packageInfos = new ArrayList<>();
+ for (int i = 0; i < users.length; ++i) {
+ packageInfos.add(constructPackageManagerPackageInfo(
+ "system_package.critical", UserHandle.getUid(users[i], 10001), null));
+ packageInfos.add(constructPackageManagerPackageInfo(
+ "system_package.non_critical", UserHandle.getUid(users[i], 10002), null));
+ packageInfos.add(constructPackageManagerPackageInfo(
+ "vendor_package.critical", UserHandle.getUid(users[i], 10003), null));
+ packageInfos.add(constructPackageManagerPackageInfo(
+ "vendor_package.non_critical", UserHandle.getUid(users[i], 10004), null));
+ packageInfos.add(constructPackageManagerPackageInfo(
+ "third_party_package.A", UserHandle.getUid(users[i], 10005),
+ "third_party_shared_package"));
+ packageInfos.add(constructPackageManagerPackageInfo(
+ "third_party_package.B", UserHandle.getUid(users[i], 10005),
+ "third_party_shared_package"));
+ }
+ injectPackageInfos(packageInfos);
+ }
+
+ private List<PackageIoOveruseStats> sampleIoOveruseStats(boolean requireRecurrentOveruseStats)
+ throws Exception {
+ int[] users = new int[]{100, 101};
+ int totalOveruses = requireRecurrentOveruseStats ? RECURRING_OVERUSE_THRESHOLD + 1 : 0;
+ List<PackageIoOveruseStats> packageIoOveruseStats = new ArrayList<>();
+ android.automotive.watchdog.PerStateBytes zeroRemainingBytes =
+ constructPerStateBytes(0, 0, 0);
+ android.automotive.watchdog.PerStateBytes nonZeroRemainingBytes =
+ constructPerStateBytes(20, 30, 40);
+ android.automotive.watchdog.PerStateBytes writtenBytes =
+ constructPerStateBytes(100, 200, 300);
+ for (int i = 0; i < users.length; ++i) {
+ // Overuse occurred but cannot be killed/disabled.
+ packageIoOveruseStats.add(constructPackageIoOveruseStats(
+ UserHandle.getUid(users[i], 10001), /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ writtenBytes,
+ constructInternalIoOveruseStats(
+ /* killableOnOveruse= */ false, zeroRemainingBytes, writtenBytes,
+ totalOveruses)));
+ // No overuse occurred but the package should be notified.
+ packageIoOveruseStats.add(constructPackageIoOveruseStats(
+ UserHandle.getUid(users[i], 10002), /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ constructInternalIoOveruseStats(
+ /* killableOnOveruse= */ true, nonZeroRemainingBytes, writtenBytes,
+ totalOveruses)));
+ // Neither overuse occurred nor be notified.
+ packageIoOveruseStats.add(constructPackageIoOveruseStats(
+ UserHandle.getUid(users[i], 10003), /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ constructPerStateBytes(0, 0, 0),
+ constructInternalIoOveruseStats(
+ /* killableOnOveruse= */ false, nonZeroRemainingBytes, writtenBytes,
+ totalOveruses)));
+ // Overuse occurred and can be killed/disabled.
+ packageIoOveruseStats.add(constructPackageIoOveruseStats(
+ UserHandle.getUid(users[i], 10004), /* shouldNotify= */ false,
+ /* forgivenWriteBytes= */ writtenBytes,
+ constructInternalIoOveruseStats(
+ /* killableOnOveruse= */ true, zeroRemainingBytes, writtenBytes,
+ totalOveruses)));
+ // Overuse occurred and can be killed/disabled.
+ packageIoOveruseStats.add(constructPackageIoOveruseStats(
+ UserHandle.getUid(users[i], 10005), /* shouldNotify= */ true,
+ /* forgivenWriteBytes= */ writtenBytes,
+ constructInternalIoOveruseStats(
+ /* killableOnOveruse= */ true, zeroRemainingBytes, writtenBytes,
+ totalOveruses)));
+ }
+ return packageIoOveruseStats;
+ }
+
+ private static List<AtomsProto.CarWatchdogIoOveruseStatsReported> sampleReportedOveruseStats() {
+ // The below thresholds are from {@link sampleInternalResourceOveruseConfiguration} and
+ // UID/stat are from {@link sampleIoOveruseStats}.
+ AtomsProto.CarWatchdogPerStateBytes systemThreshold =
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(10, 20, 30);
+ AtomsProto.CarWatchdogPerStateBytes vendorThreshold =
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(20, 40, 60);
+ AtomsProto.CarWatchdogPerStateBytes thirdPartyThreshold =
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(30, 60, 90);
+ AtomsProto.CarWatchdogPerStateBytes writtenBytes =
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(100, 200, 300);
+ List<AtomsProto.CarWatchdogIoOveruseStatsReported> reportedOveruseStats = new ArrayList<>();
+ reportedOveruseStats.add(constructIoOveruseStatsReported(
+ 10010001, systemThreshold, writtenBytes));
+ reportedOveruseStats.add(constructIoOveruseStatsReported(
+ 10110001, systemThreshold, writtenBytes));
+ reportedOveruseStats.add(constructIoOveruseStatsReported(
+ 10010004, vendorThreshold, writtenBytes));
+ reportedOveruseStats.add(constructIoOveruseStatsReported(
+ 10110004, vendorThreshold, writtenBytes));
+ reportedOveruseStats.add(constructIoOveruseStatsReported(
+ 10010005, thirdPartyThreshold, writtenBytes));
+ reportedOveruseStats.add(constructIoOveruseStatsReported(
+ 10110005, thirdPartyThreshold, writtenBytes));
+ return reportedOveruseStats;
+ }
+
+ private static List<AtomsProto.CarWatchdogKillStatsReported> sampleReportedKillStats(
+ int systemState, int[] killedUids) {
+ // The below thresholds are from {@link sampleInternalResourceOveruseConfiguration} and
+ // UID/stat are from {@link sampleIoOveruseStats}.
+ AtomsProto.CarWatchdogPerStateBytes vendorThreshold =
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(20, 40, 60);
+ AtomsProto.CarWatchdogPerStateBytes thirdPartyThreshold =
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(30, 60, 90);
+ AtomsProto.CarWatchdogPerStateBytes writtenBytes =
+ WatchdogPerfHandler.constructCarWatchdogPerStateBytes(100, 200, 300);
+ List<AtomsProto.CarWatchdogKillStatsReported> reportedKillStats = new ArrayList<>();
+ for (int uid : killedUids) {
+ AtomsProto.CarWatchdogPerStateBytes threshold =
+ UserHandle.getAppId(uid) == 10004 ? vendorThreshold : thirdPartyThreshold;
+ reportedKillStats.add(constructIoOveruseKillStatsReported(
+ uid, systemState, threshold, writtenBytes));
+ }
+ return reportedKillStats;
+ }
+
+ private List<PackageResourceOveruseAction> sampleActionTakenOnOveruse(
+ int[] notKilledUids, int[] killedRecurringOveruseUids) {
+ List<PackageResourceOveruseAction> actions = new ArrayList<>();
+ for (int uid : notKilledUids) {
+ actions.add(constructPackageResourceOveruseAction(
+ mGenericPackageNameByUid.get(uid), uid, new int[]{ResourceType.IO}, NOT_KILLED)
+ );
+ }
+ for (int uid : killedRecurringOveruseUids) {
+ actions.add(constructPackageResourceOveruseAction(
+ mGenericPackageNameByUid.get(uid), uid, new int[]{ResourceType.IO},
+ KILLED_RECURRING_OVERUSE));
+ }
+ return actions;
}
private static void verifyOnOveruseCalled(List<ResourceOveruseStats> expectedStats,
@@ -1436,10 +3432,6 @@
.containsExactlyElementsIn(expectedStats);
}
- private static int getUid(int appId) {
- return UserHandle.getUid(UserHandle.myUserId(), appId);
- }
-
private static List<ResourceOveruseConfiguration> sampleResourceOveruseConfigurations() {
return Arrays.asList(
sampleResourceOveruseConfigurationBuilder(ComponentType.SYSTEM,
@@ -1463,12 +3455,20 @@
}
private static ResourceOveruseConfiguration.Builder sampleResourceOveruseConfigurationBuilder(
- int componentType, IoOveruseConfiguration ioOveruseConfig) {
- String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType);
- List<String> safeToKill = Arrays.asList(prefix + "_package.A", prefix + "_pkg.B");
- List<String> vendorPrefixes = Arrays.asList(prefix + "_package", prefix + "_pkg");
+ @ComponentType int componentType, IoOveruseConfiguration ioOveruseConfig) {
+ String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType).toLowerCase();
+ List<String> safeToKill = Arrays.asList(prefix + "_package.non_critical.A",
+ prefix + "_pkg.non_critical.B",
+ "shared:" + prefix + "_shared_package.non_critical.B",
+ "some_pkg_as_" + prefix + "_pkg");
+ List<String> vendorPrefixes = Arrays.asList(
+ prefix + "_package", "some_pkg_as_" + prefix + "_pkg");
Map<String, String> pkgToAppCategory = new ArrayMap<>();
- pkgToAppCategory.put(prefix + "_package.A", "android.car.watchdog.app.category.MEDIA");
+ pkgToAppCategory.put("system_package.MEDIA", "android.car.watchdog.app.category.MEDIA");
+ pkgToAppCategory.put("system_package.A", "android.car.watchdog.app.category.MAPS");
+ pkgToAppCategory.put("vendor_package.MEDIA", "android.car.watchdog.app.category.MEDIA");
+ pkgToAppCategory.put("vendor_package.A", "android.car.watchdog.app.category.MAPS");
+ pkgToAppCategory.put("third_party_package.MAPS", "android.car.watchdog.app.category.MAPS");
ResourceOveruseConfiguration.Builder configBuilder =
new ResourceOveruseConfiguration.Builder(componentType, safeToKill,
vendorPrefixes, pkgToAppCategory);
@@ -1477,48 +3477,57 @@
}
private static IoOveruseConfiguration.Builder sampleIoOveruseConfigurationBuilder(
- int componentType) {
- String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType);
+ @ComponentType int componentType) {
+ String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType).toLowerCase();
PerStateBytes componentLevelThresholds = new PerStateBytes(
- /* foregroundModeBytes= */10, /* backgroundModeBytes= */20,
- /* garageModeBytes= */30);
+ /* foregroundModeBytes= */ componentType * 10L,
+ /* backgroundModeBytes= */ componentType * 20L,
+ /* garageModeBytes= */ componentType * 30L);
Map<String, PerStateBytes> packageSpecificThresholds = new ArrayMap<>();
packageSpecificThresholds.put(prefix + "_package.A", new PerStateBytes(
- /* foregroundModeBytes= */40, /* backgroundModeBytes= */50,
- /* garageModeBytes= */60));
+ /* foregroundModeBytes= */ componentType * 40L,
+ /* backgroundModeBytes= */ componentType * 50L,
+ /* garageModeBytes= */ componentType * 60L));
Map<String, PerStateBytes> appCategorySpecificThresholds = new ArrayMap<>();
appCategorySpecificThresholds.put(
ResourceOveruseConfiguration.APPLICATION_CATEGORY_TYPE_MEDIA,
- new PerStateBytes(/* foregroundModeBytes= */100, /* backgroundModeBytes= */200,
- /* garageModeBytes= */300));
+ new PerStateBytes(/* foregroundModeBytes= */ componentType * 100L,
+ /* backgroundModeBytes= */ componentType * 200L,
+ /* garageModeBytes= */ componentType * 300L));
appCategorySpecificThresholds.put(
ResourceOveruseConfiguration.APPLICATION_CATEGORY_TYPE_MAPS,
- new PerStateBytes(/* foregroundModeBytes= */1100, /* backgroundModeBytes= */2200,
- /* garageModeBytes= */3300));
+ new PerStateBytes(/* foregroundModeBytes= */ componentType * 1100L,
+ /* backgroundModeBytes= */ componentType * 2200L,
+ /* garageModeBytes= */ componentType * 3300L));
List<IoOveruseAlertThreshold> systemWideThresholds = Collections.singletonList(
- new IoOveruseAlertThreshold(/* durationInSeconds= */10,
- /* writtenBytesPerSecond= */200));
+ new IoOveruseAlertThreshold(/* durationInSeconds= */ componentType * 10L,
+ /* writtenBytesPerSecond= */ componentType * 200L));
return new IoOveruseConfiguration.Builder(componentLevelThresholds,
packageSpecificThresholds, appCategorySpecificThresholds, systemWideThresholds);
}
private static android.automotive.watchdog.internal.ResourceOveruseConfiguration
- sampleInternalResourceOveruseConfiguration(int componentType,
+ sampleInternalResourceOveruseConfiguration(@ComponentType int componentType,
android.automotive.watchdog.internal.IoOveruseConfiguration ioOveruseConfig) {
- String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType);
+ String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType).toLowerCase();
android.automotive.watchdog.internal.ResourceOveruseConfiguration config =
new android.automotive.watchdog.internal.ResourceOveruseConfiguration();
config.componentType = componentType;
- config.safeToKillPackages = Arrays.asList(prefix + "_package.A", prefix + "_pkg.B");
- config.vendorPackagePrefixes = Arrays.asList(prefix + "_package", prefix + "_pkg");
-
- PackageMetadata metadata = new PackageMetadata();
- metadata.packageName = prefix + "_package.A";
- metadata.appCategoryType = ApplicationCategoryType.MEDIA;
- config.packageMetadata = Collections.singletonList(metadata);
+ config.safeToKillPackages = Arrays.asList(prefix + "_package.non_critical.A",
+ prefix + "_pkg.non_critical.B",
+ "shared:" + prefix + "_shared_package.non_critical.B",
+ "some_pkg_as_" + prefix + "_pkg");
+ config.vendorPackagePrefixes = Arrays.asList(
+ prefix + "_package", "some_pkg_as_" + prefix + "_pkg");
+ config.packageMetadata = Arrays.asList(
+ constructPackageMetadata("system_package.MEDIA", ApplicationCategoryType.MEDIA),
+ constructPackageMetadata("system_package.A", ApplicationCategoryType.MAPS),
+ constructPackageMetadata("vendor_package.MEDIA", ApplicationCategoryType.MEDIA),
+ constructPackageMetadata("vendor_package.A", ApplicationCategoryType.MAPS),
+ constructPackageMetadata("third_party_package.MAPS", ApplicationCategoryType.MAPS));
ResourceSpecificConfiguration resourceSpecificConfig = new ResourceSpecificConfiguration();
resourceSpecificConfig.setIoOveruseConfiguration(ioOveruseConfig);
@@ -1528,27 +3537,41 @@
}
private static android.automotive.watchdog.internal.IoOveruseConfiguration
- sampleInternalIoOveruseConfiguration(int componentType) {
- String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType);
+ sampleInternalIoOveruseConfiguration(@ComponentType int componentType) {
+ String prefix = WatchdogPerfHandler.toComponentTypeStr(componentType).toLowerCase();
android.automotive.watchdog.internal.IoOveruseConfiguration config =
new android.automotive.watchdog.internal.IoOveruseConfiguration();
- config.componentLevelThresholds = constructPerStateIoOveruseThreshold(prefix,
- /* fgBytes= */10, /* bgBytes= */20, /* gmBytes= */30);
+ config.componentLevelThresholds = constructPerStateIoOveruseThreshold(
+ WatchdogPerfHandler.toComponentTypeStr(componentType),
+ /* fgBytes= */ componentType * 10L, /* bgBytes= */ componentType * 20L,
+ /*gmBytes= */ componentType * 30L);
config.packageSpecificThresholds = Collections.singletonList(
- constructPerStateIoOveruseThreshold(prefix + "_package.A", /* fgBytes= */40,
- /* bgBytes= */50, /* gmBytes= */60));
+ constructPerStateIoOveruseThreshold(prefix + "_package.A",
+ /* fgBytes= */ componentType * 40L, /* bgBytes= */ componentType * 50L,
+ /* gmBytes= */ componentType * 60L));
config.categorySpecificThresholds = Arrays.asList(
constructPerStateIoOveruseThreshold(
WatchdogPerfHandler.INTERNAL_APPLICATION_CATEGORY_TYPE_MEDIA,
- /* fgBytes= */100, /* bgBytes= */200, /* gmBytes= */300),
+ /* fgBytes= */ componentType * 100L, /* bgBytes= */ componentType * 200L,
+ /* gmBytes= */ componentType * 300L),
constructPerStateIoOveruseThreshold(
WatchdogPerfHandler.INTERNAL_APPLICATION_CATEGORY_TYPE_MAPS,
- /* fgBytes= */1100, /* bgBytes= */2200, /* gmBytes= */3300));
+ /* fgBytes= */ componentType * 1100L, /* bgBytes= */ componentType * 2200L,
+ /* gmBytes= */ componentType * 3300L));
config.systemWideThresholds = Collections.singletonList(
- constructInternalIoOveruseAlertThreshold(/* duration= */10, /* writeBPS= */200));
+ constructInternalIoOveruseAlertThreshold(
+ /* duration= */ componentType * 10L, /* writeBPS= */ componentType * 200L));
return config;
}
+ private static PackageMetadata constructPackageMetadata(
+ String packageName, @ApplicationCategoryType int appCategoryType) {
+ PackageMetadata metadata = new PackageMetadata();
+ metadata.packageName = packageName;
+ metadata.appCategoryType = appCategoryType;
+ return metadata;
+ }
+
private static PerStateIoOveruseThreshold constructPerStateIoOveruseThreshold(String name,
long fgBytes, long bgBytes, long gmBytes) {
PerStateIoOveruseThreshold threshold = new PerStateIoOveruseThreshold();
@@ -1570,30 +3593,73 @@
}
private static PackageIoOveruseStats constructPackageIoOveruseStats(int uid,
- boolean shouldNotify, android.automotive.watchdog.IoOveruseStats ioOveruseStats) {
+ boolean shouldNotify, android.automotive.watchdog.PerStateBytes forgivenWriteBytes,
+ android.automotive.watchdog.IoOveruseStats ioOveruseStats) {
PackageIoOveruseStats stats = new PackageIoOveruseStats();
stats.uid = uid;
stats.shouldNotify = shouldNotify;
+ stats.forgivenWriteBytes = forgivenWriteBytes;
stats.ioOveruseStats = ioOveruseStats;
return stats;
}
private static ResourceOveruseStats constructResourceOveruseStats(int uid, String packageName,
android.automotive.watchdog.IoOveruseStats internalIoOveruseStats) {
- IoOveruseStats ioOveruseStats =
- WatchdogPerfHandler.toIoOveruseStatsBuilder(internalIoOveruseStats)
- .setKillableOnOveruse(internalIoOveruseStats.killableOnOveruse).build();
+ IoOveruseStats ioOveruseStats = WatchdogPerfHandler.toIoOveruseStatsBuilder(
+ internalIoOveruseStats, /* totalTimesKilled= */ 0,
+ internalIoOveruseStats.killableOnOveruse).build();
return new ResourceOveruseStats.Builder(packageName, UserHandle.getUserHandleForUid(uid))
.setIoOveruseStats(ioOveruseStats).build();
}
- private static android.automotive.watchdog.IoOveruseStats constructInternalIoOveruseStats(
+ private static UserPackageIoUsageStats constructUserPackageIoUsageStats(
+ int userId, String packageName, android.automotive.watchdog.PerStateBytes writtenBytes,
+ android.automotive.watchdog.PerStateBytes forgivenWriteBytes, int totalOveruses) {
+ UserPackageIoUsageStats stats = new UserPackageIoUsageStats();
+ stats.userId = userId;
+ stats.packageName = packageName;
+ stats.ioUsageStats = new IoUsageStats();
+ stats.ioUsageStats.writtenBytes = writtenBytes;
+ stats.ioUsageStats.forgivenWriteBytes = forgivenWriteBytes;
+ stats.ioUsageStats.totalOveruses = totalOveruses;
+ return stats;
+ }
+
+ public static boolean isUserPackageIoUsageStatsEquals(UserPackageIoUsageStats actual,
+ UserPackageIoUsageStats expected) {
+ return actual.userId == expected.userId && actual.packageName.equals(expected.packageName)
+ && isInternalPerStateBytesEquals(
+ actual.ioUsageStats.writtenBytes, expected.ioUsageStats.writtenBytes)
+ && isInternalPerStateBytesEquals(actual.ioUsageStats.forgivenWriteBytes,
+ expected.ioUsageStats.forgivenWriteBytes)
+ && actual.ioUsageStats.totalOveruses == expected.ioUsageStats.totalOveruses;
+ }
+
+ public static boolean isInternalPerStateBytesEquals(
+ android.automotive.watchdog.PerStateBytes actual,
+ android.automotive.watchdog.PerStateBytes expected) {
+ return actual.foregroundBytes == expected.foregroundBytes
+ && actual.backgroundBytes == expected.backgroundBytes
+ && actual.garageModeBytes == expected.garageModeBytes;
+ }
+
+ private android.automotive.watchdog.IoOveruseStats constructInternalIoOveruseStats(
boolean killableOnOveruse,
android.automotive.watchdog.PerStateBytes remainingWriteBytes,
android.automotive.watchdog.PerStateBytes writtenBytes, int totalOveruses) {
+ return constructInternalIoOveruseStats(killableOnOveruse, STATS_DURATION_SECONDS,
+ remainingWriteBytes, writtenBytes, totalOveruses);
+ }
+
+ private android.automotive.watchdog.IoOveruseStats constructInternalIoOveruseStats(
+ boolean killableOnOveruse, long durationInSecs,
+ android.automotive.watchdog.PerStateBytes remainingWriteBytes,
+ android.automotive.watchdog.PerStateBytes writtenBytes, int totalOveruses) {
android.automotive.watchdog.IoOveruseStats stats =
new android.automotive.watchdog.IoOveruseStats();
+ stats.startTime = mTimeSource.now().getEpochSecond();
+ stats.durationInSeconds = durationInSecs;
stats.killableOnOveruse = killableOnOveruse;
stats.remainingWriteBytes = remainingWriteBytes;
stats.writtenBytes = writtenBytes;
@@ -1601,16 +3667,6 @@
return stats;
}
- private static android.automotive.watchdog.PerStateBytes constructPerStateBytes(long fgBytes,
- long bgBytes, long gmBytes) {
- android.automotive.watchdog.PerStateBytes perStateBytes =
- new android.automotive.watchdog.PerStateBytes();
- perStateBytes.foregroundBytes = fgBytes;
- perStateBytes.backgroundBytes = bgBytes;
- perStateBytes.garageModeBytes = gmBytes;
- return perStateBytes;
- }
-
private static PackageResourceOveruseAction constructPackageResourceOveruseAction(
String packageName, int uid, int[] resourceTypes, int resourceOveruseActionType) {
PackageResourceOveruseAction action = new PackageResourceOveruseAction();
@@ -1622,6 +3678,162 @@
return action;
}
+ private static void delayedRunOnMainSync(Runnable action, long delayMillis)
+ throws InterruptedException {
+ AtomicBoolean isComplete = new AtomicBoolean();
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.postDelayed(() -> {
+ action.run();
+ synchronized (action) {
+ isComplete.set(true);
+ action.notifyAll();
+ }
+ }, delayMillis);
+ synchronized (action) {
+ while (!isComplete.get()) {
+ action.wait();
+ }
+ }
+ }
+
+ private static AtomsProto.CarWatchdogIoOveruseStatsReported
+ constructIoOveruseStatsReported(int uid, AtomsProto.CarWatchdogPerStateBytes threshold,
+ AtomsProto.CarWatchdogPerStateBytes writtenBytes) {
+ return constructCarWatchdogIoOveruseStatsReported(
+ uid, WatchdogPerfHandler.constructCarWatchdogIoOveruseStats(
+ AtomsProto.CarWatchdogIoOveruseStats.Period.DAILY, threshold, writtenBytes)
+ );
+ }
+
+ private static AtomsProto.CarWatchdogIoOveruseStatsReported
+ constructCarWatchdogIoOveruseStatsReported(
+ int uid, AtomsProto.CarWatchdogIoOveruseStats ioOveruseStats) {
+ return AtomsProto.CarWatchdogIoOveruseStatsReported.newBuilder()
+ .setUid(uid)
+ .setIoOveruseStats(ioOveruseStats)
+ .build();
+ }
+
+ private static AtomsProto.CarWatchdogKillStatsReported constructIoOveruseKillStatsReported(
+ int uid, int systemState, AtomsProto.CarWatchdogPerStateBytes threshold,
+ AtomsProto.CarWatchdogPerStateBytes writtenBytes) {
+ return constructCarWatchdogKillStatsReported(uid,
+ CAR_WATCHDOG_KILL_STATS_REPORTED__UID_STATE__UNKNOWN_UID_STATE, systemState,
+ CAR_WATCHDOG_KILL_STATS_REPORTED__KILL_REASON__KILLED_ON_IO_OVERUSE,
+ WatchdogPerfHandler.constructCarWatchdogIoOveruseStats(
+ AtomsProto.CarWatchdogIoOveruseStats.Period.DAILY, threshold, writtenBytes)
+ );
+ }
+
+ private static AtomsProto.CarWatchdogKillStatsReported constructCarWatchdogKillStatsReported(
+ int uid, int uidState, int systemState, int killReason,
+ AtomsProto.CarWatchdogIoOveruseStats ioOveruseStats) {
+ return AtomsProto.CarWatchdogKillStatsReported.newBuilder()
+ .setUid(uid)
+ .setUidState(AtomsProto.CarWatchdogKillStatsReported.UidState.forNumber(uidState))
+ .setSystemState(AtomsProto.CarWatchdogKillStatsReported.SystemState.forNumber(
+ systemState))
+ .setKillReason(AtomsProto.CarWatchdogKillStatsReported.KillReason.forNumber(
+ killReason))
+ .setIoOveruseStats(ioOveruseStats)
+ .build();
+ }
+
+ private void captureAndVerifyIoOveruseStatsReported(
+ List<AtomsProto.CarWatchdogIoOveruseStatsReported> expected) throws Exception {
+ verify(() -> CarStatsLog.write(eq(CarStatsLog.CAR_WATCHDOG_IO_OVERUSE_STATS_REPORTED),
+ mOverusingUidCaptor.capture(), mOveruseStatsCaptor.capture()),
+ times(expected.size()));
+
+ List<Integer> allUidValues = mOverusingUidCaptor.getAllValues();
+ List<byte[]> allOveruseStatsValues = mOveruseStatsCaptor.getAllValues();
+ List<AtomsProto.CarWatchdogIoOveruseStatsReported> actual = new ArrayList<>();
+ for (int i = 0; i < expected.size(); ++i) {
+ actual.add(constructCarWatchdogIoOveruseStatsReported(allUidValues.get(i),
+ AtomsProto.CarWatchdogIoOveruseStats.parseFrom(allOveruseStatsValues.get(i))));
+ }
+ assertWithMessage("I/O overuse stats reported to statsd").that(actual)
+ .comparingElementsUsing(Correspondence.from(
+ CarWatchdogServiceUnitTest::isCarWatchdogIoOveruseStatsReportedEquals,
+ "is equal to")).containsExactlyElementsIn(expected);
+ }
+
+ private void captureAndVerifyKillStatsReported(
+ List<AtomsProto.CarWatchdogKillStatsReported> expected) throws Exception {
+ // Overuse handling task is posted on the main thread and this task performs disabling and
+ // uploading metrics. Wait for this task to complete.
+ CarServiceUtils.runOnMainSync(() -> {});
+
+ verify(() -> CarStatsLog.write(eq(CarStatsLog.CAR_WATCHDOG_KILL_STATS_REPORTED),
+ mKilledUidCaptor.capture(), mUidStateCaptor.capture(),
+ mSystemStateCaptor.capture(), mKillReasonCaptor.capture(), eq(null),
+ mKilledStatsCaptor.capture()), times(expected.size()));
+
+ List<Integer> allUidValues = mKilledUidCaptor.getAllValues();
+ List<Integer> allUidStateValues = mUidStateCaptor.getAllValues();
+ List<Integer> allSystemStateValues = mSystemStateCaptor.getAllValues();
+ List<Integer> allKillReasonValues = mKillReasonCaptor.getAllValues();
+ List<byte[]> allIoOveruseStatsValues = mKilledStatsCaptor.getAllValues();
+ List<AtomsProto.CarWatchdogKillStatsReported> actual = new ArrayList<>();
+ for (int i = 0; i < expected.size(); ++i) {
+ actual.add(constructCarWatchdogKillStatsReported(allUidValues.get(i),
+ allUidStateValues.get(i), allSystemStateValues.get(i),
+ allKillReasonValues.get(i),
+ AtomsProto.CarWatchdogIoOveruseStats.parseFrom(
+ allIoOveruseStatsValues.get(i))));
+ }
+ assertWithMessage("I/O overuse kill stats reported to statsd").that(actual)
+ .comparingElementsUsing(Correspondence.from(
+ CarWatchdogServiceUnitTest::isCarWatchdogKillStatsReported, "is equal to"))
+ .containsExactlyElementsIn(expected);
+ }
+
+ public static boolean isCarWatchdogIoOveruseStatsReportedEquals(
+ AtomsProto.CarWatchdogIoOveruseStatsReported actual,
+ AtomsProto.CarWatchdogIoOveruseStatsReported expected) {
+ return actual.getUid() == expected.getUid()
+ && isCarWatchdogIoOveruseStatsEquals(
+ actual.getIoOveruseStats(), expected.getIoOveruseStats());
+ }
+
+ public static boolean isCarWatchdogKillStatsReported(
+ AtomsProto.CarWatchdogKillStatsReported actual,
+ AtomsProto.CarWatchdogKillStatsReported expected) {
+ return actual.getUid() == expected.getUid()
+ && actual.getUidState() == expected.getUidState()
+ && actual.getSystemState() == expected.getSystemState()
+ && actual.getKillReason() == expected.getKillReason()
+ // Process stats is null on I/O overuse kill stats, so equality checking on the
+ // objects is sufficient.
+ && actual.getProcessStats().equals(expected.getProcessStats())
+ && isCarWatchdogIoOveruseStatsEquals(
+ actual.getIoOveruseStats(), expected.getIoOveruseStats());
+ }
+
+ private static boolean isCarWatchdogIoOveruseStatsEquals(
+ AtomsProto.CarWatchdogIoOveruseStats actual,
+ AtomsProto.CarWatchdogIoOveruseStats expected) {
+ if (actual == null || expected == null) {
+ return (actual == null) && (expected == null);
+ }
+ return actual.getPeriod() == expected.getPeriod()
+ && isCarWatchdogPerStateBytesEquals(actual.getThreshold(), expected.getThreshold())
+ && isCarWatchdogPerStateBytesEquals(
+ actual.getWrittenBytes(), expected.getWrittenBytes())
+ && actual.getUptimeMillis() == expected.getUptimeMillis();
+ }
+
+ private static boolean isCarWatchdogPerStateBytesEquals(
+ AtomsProto.CarWatchdogPerStateBytes actual,
+ AtomsProto.CarWatchdogPerStateBytes expected) {
+ if (actual == null || expected == null) {
+ return (actual == null) && (expected == null);
+ }
+ return actual.getForegroundBytes() == expected.getForegroundBytes()
+ && actual.getBackgroundBytes() == expected.getBackgroundBytes()
+ && actual.getGarageModeBytes() == expected.getGarageModeBytes();
+ }
+
private class TestClient extends ICarWatchdogServiceCallback.Stub {
protected int mLastSessionId = INVALID_SESSION_ID;
@@ -1675,15 +3887,16 @@
return applicationInfo;
}
- private static String toString(List<PackageInfo> packageInfos) {
+ private static String toPackageInfosString(List<PackageInfo> packageInfos) {
StringBuilder builder = new StringBuilder();
for (PackageInfo packageInfo : packageInfos) {
- builder = toString(builder, packageInfo).append('\n');
+ builder = packageInfoStringBuilder(builder, packageInfo).append('\n');
}
return builder.toString();
}
- private static StringBuilder toString(StringBuilder builder, PackageInfo packageInfo) {
+ private static StringBuilder packageInfoStringBuilder(
+ StringBuilder builder, PackageInfo packageInfo) {
if (packageInfo == null) {
return builder.append("Null package info\n");
}
@@ -1711,14 +3924,17 @@
private static void assertPackageInfoEquals(List<PackageInfo> actual,
List<PackageInfo> expected) throws Exception {
assertWithMessage("Package infos for UIDs:\nExpected: %s\nActual: %s",
- toString(expected), toString(actual)).that(actual).comparingElementsUsing(
- Correspondence.from(CarWatchdogServiceUnitTest::isPackageInfoEquals,
- "is package info equal to")).containsExactlyElementsIn(expected);
+ CarWatchdogServiceUnitTest.toPackageInfosString(expected),
+ CarWatchdogServiceUnitTest.toPackageInfosString(actual))
+ .that(actual)
+ .comparingElementsUsing(
+ Correspondence.from(CarWatchdogServiceUnitTest::isPackageInfoEquals,
+ "is package info equal to")).containsExactlyElementsIn(expected);
}
private static boolean isPackageInfoEquals(PackageInfo lhs, PackageInfo rhs) {
return isEquals(lhs.packageIdentifier, rhs.packageIdentifier)
- && lhs.sharedUidPackages.equals(rhs.sharedUidPackages)
+ && lhs.sharedUidPackages.containsAll(rhs.sharedUidPackages)
&& lhs.componentType == rhs.componentType
&& lhs.appCategoryType == rhs.appCategoryType;
}
@@ -1726,4 +3942,31 @@
private static boolean isEquals(PackageIdentifier lhs, PackageIdentifier rhs) {
return lhs.name.equals(rhs.name) && lhs.uid == rhs.uid;
}
+
+ private static android.content.pm.PackageInfo constructPackageManagerPackageInfo(
+ String packageName, int uid, String sharedUserId) {
+ if (packageName.startsWith("system")) {
+ return constructPackageManagerPackageInfo(
+ packageName, uid, sharedUserId, ApplicationInfo.FLAG_SYSTEM, 0);
+ }
+ if (packageName.startsWith("vendor")) {
+ return constructPackageManagerPackageInfo(
+ packageName, uid, sharedUserId, ApplicationInfo.FLAG_SYSTEM,
+ ApplicationInfo.PRIVATE_FLAG_OEM);
+ }
+ return constructPackageManagerPackageInfo(packageName, uid, sharedUserId, 0, 0);
+ }
+
+ private static android.content.pm.PackageInfo constructPackageManagerPackageInfo(
+ String packageName, int uid, String sharedUserId, int flags, int privateFlags) {
+ android.content.pm.PackageInfo packageInfo = new android.content.pm.PackageInfo();
+ packageInfo.packageName = packageName;
+ packageInfo.sharedUserId = sharedUserId;
+ packageInfo.applicationInfo = new ApplicationInfo();
+ packageInfo.applicationInfo.packageName = packageName;
+ packageInfo.applicationInfo.uid = uid;
+ packageInfo.applicationInfo.flags = flags;
+ packageInfo.applicationInfo.privateFlags = privateFlags;
+ return packageInfo;
+ }
}
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/InternalIoOveruseConfigurationSubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/InternalIoOveruseConfigurationSubject.java
index 47ad645..47e496e 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/InternalIoOveruseConfigurationSubject.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/InternalIoOveruseConfigurationSubject.java
@@ -19,7 +19,6 @@
import static com.google.common.truth.Truth.assertAbout;
import android.annotation.Nullable;
-import android.automotive.watchdog.PerStateBytes;
import android.automotive.watchdog.internal.IoOveruseAlertThreshold;
import android.automotive.watchdog.internal.IoOveruseConfiguration;
import android.automotive.watchdog.internal.PerStateIoOveruseThreshold;
@@ -38,7 +37,7 @@
// Boiler-plate Subject.Factory for InternalIoOveruseConfigurationSubject
private static final Subject.Factory<
com.android.car.watchdog.InternalIoOveruseConfigurationSubject,
- Iterable<IoOveruseConfiguration>> Io_OVERUSE_CONFIG_SUBJECT_FACTORY =
+ Iterable<IoOveruseConfiguration>> IO_OVERUSE_CONFIG_SUBJECT_FACTORY =
com.android.car.watchdog.InternalIoOveruseConfigurationSubject::new;
private final Iterable<IoOveruseConfiguration> mActual;
@@ -46,12 +45,12 @@
// User-defined entry point
public static InternalIoOveruseConfigurationSubject assertThat(
@Nullable Iterable<IoOveruseConfiguration> stats) {
- return assertAbout(Io_OVERUSE_CONFIG_SUBJECT_FACTORY).that(stats);
+ return assertAbout(IO_OVERUSE_CONFIG_SUBJECT_FACTORY).that(stats);
}
public static Subject.Factory<InternalIoOveruseConfigurationSubject,
- Iterable<IoOveruseConfiguration>> resourceOveruseStats() {
- return Io_OVERUSE_CONFIG_SUBJECT_FACTORY;
+ Iterable<IoOveruseConfiguration>> ioOveruseConfigurations() {
+ return IO_OVERUSE_CONFIG_SUBJECT_FACTORY;
}
public void containsExactly(IoOveruseConfiguration... stats) {
@@ -71,9 +70,10 @@
if (actual == null || expected == null) {
return (actual == null) && (expected == null);
}
- return actual.componentLevelThresholds.name == expected.componentLevelThresholds.name
- && isPerStateBytesEquals(actual.componentLevelThresholds.perStateWriteBytes,
- expected.componentLevelThresholds.perStateWriteBytes)
+ return actual.componentLevelThresholds.name.equals(expected.componentLevelThresholds.name)
+ && InternalPerStateBytesSubject.isEquals(
+ actual.componentLevelThresholds.perStateWriteBytes,
+ expected.componentLevelThresholds.perStateWriteBytes)
&& isPerStateThresholdEquals(actual.packageSpecificThresholds,
expected.packageSpecificThresholds)
&& isPerStateThresholdEquals(actual.categorySpecificThresholds,
@@ -104,10 +104,9 @@
public static String toString(PerStateIoOveruseThreshold threshold) {
StringBuilder builder = new StringBuilder();
- builder.append("{Name: ").append(threshold.name).append(", WriteBytes: {fgBytes: ")
- .append(threshold.perStateWriteBytes.foregroundBytes).append(", bgBytes: ")
- .append(threshold.perStateWriteBytes.backgroundBytes).append(", gmBytes: ")
- .append(threshold.perStateWriteBytes.garageModeBytes).append("}}");
+ builder.append("{Name: ").append(threshold.name).append(", WriteBytes: ");
+ InternalPerStateBytesSubject.toStringBuilder(builder, threshold.perStateWriteBytes);
+ builder.append("}");
return builder.toString();
}
@@ -139,12 +138,6 @@
return actualStr.equals(expectedStr);
}
- private static boolean isPerStateBytesEquals(PerStateBytes acutal, PerStateBytes expected) {
- return acutal.foregroundBytes == expected.foregroundBytes
- && acutal.backgroundBytes == expected.backgroundBytes
- && acutal.garageModeBytes == expected.garageModeBytes;
- }
-
private static Set<String> toPerStateThresholdStrings(
List<PerStateIoOveruseThreshold> thresholds) {
return thresholds.stream().map(x -> String.format("%s:{%d,%d,%d}", x.name,
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/InternalPerStateBytesSubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/InternalPerStateBytesSubject.java
new file mode 100644
index 0000000..2f97e13
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/InternalPerStateBytesSubject.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2021 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.watchdog;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import android.annotation.Nullable;
+import android.automotive.watchdog.PerStateBytes;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+
+public final class InternalPerStateBytesSubject extends Subject {
+ // Boiler-plate Subject.Factory for PerStateBytesSubject
+ private static final Subject.Factory<com.android.car.watchdog.InternalPerStateBytesSubject,
+ PerStateBytes> PER_STATE_BYTES_SUBJECT_FACTORY =
+ com.android.car.watchdog.InternalPerStateBytesSubject::new;
+ private static final String NULL_ENTRY_STRING = "{NULL}";
+
+ private final PerStateBytes mActual;
+
+ // User-defined entry point
+ public static InternalPerStateBytesSubject assertThat(@Nullable PerStateBytes perStateBytes) {
+ return assertAbout(PER_STATE_BYTES_SUBJECT_FACTORY).that(perStateBytes);
+ }
+
+ // Static method for getting the subject factory (for use with assertAbout())
+ public static Subject.Factory<InternalPerStateBytesSubject, PerStateBytes> perStateBytes() {
+ return PER_STATE_BYTES_SUBJECT_FACTORY;
+ }
+
+ public static InternalPerStateBytesSubject assertWithMessage(
+ @Nullable PerStateBytes perStateBytes, String format, Object... args) {
+ return Truth.assertWithMessage(format, args).about(PER_STATE_BYTES_SUBJECT_FACTORY)
+ .that(perStateBytes);
+ }
+
+ private InternalPerStateBytesSubject(FailureMetadata failureMetadata,
+ @Nullable PerStateBytes subject) {
+ super(failureMetadata, subject);
+ this.mActual = subject;
+ }
+
+ // User-defined test assertion SPI below this point
+ public void isEqualTo(PerStateBytes expected) {
+ if (mActual == expected) {
+ return;
+ }
+ check("foregroundBytes").that(mActual.foregroundBytes).isEqualTo(expected.foregroundBytes);
+ check("backgroundBytes").that(mActual.backgroundBytes).isEqualTo(expected.backgroundBytes);
+ check("garageModeBytes").that(mActual.garageModeBytes).isEqualTo(expected.garageModeBytes);
+ }
+
+ public static boolean isEquals(PerStateBytes actual, PerStateBytes expected) {
+ if (actual == null || expected == null) {
+ return (actual == null) && (expected == null);
+ }
+ return actual.foregroundBytes == expected.foregroundBytes
+ && actual.backgroundBytes == expected.backgroundBytes
+ && actual.garageModeBytes == expected.garageModeBytes;
+ }
+
+ public static StringBuilder toStringBuilder(
+ StringBuilder builder, PerStateBytes perStateBytes) {
+ if (perStateBytes == null) {
+ return builder.append(NULL_ENTRY_STRING);
+ }
+ return builder.append("{Foreground bytes: ").append(perStateBytes.foregroundBytes)
+ .append(", Background bytes: ").append(perStateBytes.backgroundBytes)
+ .append(", Garage mode bytes: ").append(perStateBytes.garageModeBytes)
+ .append('}');
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/InternalResourceOveruseConfigurationSubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/InternalResourceOveruseConfigurationSubject.java
index d324efe..b2a4df2 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/InternalResourceOveruseConfigurationSubject.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/InternalResourceOveruseConfigurationSubject.java
@@ -61,7 +61,7 @@
}
public static Subject.Factory<InternalResourceOveruseConfigurationSubject,
- Iterable<ResourceOveruseConfiguration>> resourceOveruseStats() {
+ Iterable<ResourceOveruseConfiguration>> resourceOveruseConfigurations() {
return RESOURCE_OVERUSE_CONFIG_SUBJECT_FACTORY;
}
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/IoOveruseStatsSubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/IoOveruseStatsSubject.java
index 48b632f..d5afaf1 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/IoOveruseStatsSubject.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/IoOveruseStatsSubject.java
@@ -17,28 +17,34 @@
package com.android.car.watchdog;
import static com.google.common.truth.Truth.assertAbout;
-import static com.google.common.truth.Truth.assertWithMessage;
import android.annotation.Nullable;
import android.car.watchdog.IoOveruseStats;
import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.SimpleSubjectBuilder;
import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
public final class IoOveruseStatsSubject extends Subject {
- // Boiler-plate Subject.Factory for IoOveruseStatsSubject
+ /* Boiler-plate Subject.Factory for IoOveruseStatsSubject. */
private static final Subject.Factory<com.android.car.watchdog.IoOveruseStatsSubject,
IoOveruseStats> IO_OVERUSE_STATS_SUBJECT_FACTORY =
com.android.car.watchdog.IoOveruseStatsSubject::new;
private final IoOveruseStats mActual;
- // User-defined entry point
+ /* User-defined entry point. */
public static IoOveruseStatsSubject assertThat(@Nullable IoOveruseStats stats) {
return assertAbout(IO_OVERUSE_STATS_SUBJECT_FACTORY).that(stats);
}
- // Static method for getting the subject factory (for use with assertAbout())
+ public static SimpleSubjectBuilder<IoOveruseStatsSubject, IoOveruseStats> assertWithMessage(
+ String format, Object... args) {
+ return Truth.assertWithMessage(format, args).about(IO_OVERUSE_STATS_SUBJECT_FACTORY);
+ }
+
+ /* Static method for getting the subject factory (for use with assertAbout()). */
public static Subject.Factory<IoOveruseStatsSubject, IoOveruseStats> ioOveruseStats() {
return IO_OVERUSE_STATS_SUBJECT_FACTORY;
}
@@ -49,7 +55,7 @@
this.mActual = subject;
}
- // User-defined test assertion SPI below this point
+ /* User-defined test assertion SPI below this point. */
public void isEqualTo(IoOveruseStats expected) {
if (mActual == expected) {
return;
@@ -65,8 +71,8 @@
.isEqualTo(expected.getTotalBytesWritten());
check("isKillableOnOveruse()").that(mActual.isKillableOnOveruse())
.isEqualTo(expected.isKillableOnOveruse());
- assertWithMessage("getRemainingWriteBytes()").about(PerStateBytesSubject.perStateBytes())
- .that(mActual.getRemainingWriteBytes())
+ Truth.assertWithMessage("getRemainingWriteBytes()")
+ .about(PerStateBytesSubject.perStateBytes()).that(mActual.getRemainingWriteBytes())
.isEqualTo(expected.getRemainingWriteBytes());
}
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/IoUsageStatsEntrySubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/IoUsageStatsEntrySubject.java
new file mode 100644
index 0000000..0479c93
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/IoUsageStatsEntrySubject.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2021 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.watchdog;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import android.annotation.Nullable;
+import android.automotive.watchdog.IoOveruseStats;
+
+import com.google.common.truth.Correspondence;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+
+import java.util.Arrays;
+
+public final class IoUsageStatsEntrySubject extends Subject {
+ /* Boiler-plate Subject.Factory for IoUsageStatsEntrySubject. */
+ private static final Subject.Factory<
+ com.android.car.watchdog.IoUsageStatsEntrySubject,
+ Iterable<WatchdogStorage.IoUsageStatsEntry>> IO_OVERUSE_STATS_ENTRY_SUBJECT_FACTORY =
+ com.android.car.watchdog.IoUsageStatsEntrySubject::new;
+ private static final String NULL_ENTRY_STRING = "{NULL}";
+
+ private final Iterable<WatchdogStorage.IoUsageStatsEntry> mActual;
+ private String mMessagePrefix;
+
+ /* User-defined entry point. */
+ public static IoUsageStatsEntrySubject assertThat(
+ @Nullable Iterable<WatchdogStorage.IoUsageStatsEntry> stats) {
+ return assertAbout(IO_OVERUSE_STATS_ENTRY_SUBJECT_FACTORY).that(stats);
+ }
+
+ public static IoUsageStatsEntrySubject assertWithMessage(
+ @Nullable Iterable<WatchdogStorage.IoUsageStatsEntry> stats,
+ String format, Object... args) {
+ IoUsageStatsEntrySubject subject =
+ Truth.assertAbout(IO_OVERUSE_STATS_ENTRY_SUBJECT_FACTORY).that(stats);
+
+ subject.mMessagePrefix = String.format(format + ": ", args);
+ return subject;
+ }
+
+ public static Subject.Factory<IoUsageStatsEntrySubject,
+ Iterable<WatchdogStorage.IoUsageStatsEntry>> ioUsageStatsEntries() {
+ return IO_OVERUSE_STATS_ENTRY_SUBJECT_FACTORY;
+ }
+
+ public void containsExactly(WatchdogStorage.IoUsageStatsEntry... stats) {
+ containsExactlyElementsIn(Arrays.asList(stats));
+ }
+
+ public void containsExactlyElementsIn(Iterable<WatchdogStorage.IoUsageStatsEntry> expected) {
+ Truth.assertWithMessage("%sExpected stats(%s) equals to actual stats(%s)",
+ mMessagePrefix != null ? mMessagePrefix : "", toString(expected),
+ toString(mActual)).that(mActual).comparingElementsUsing(Correspondence.from(
+ IoUsageStatsEntrySubject::isEquals, "is equal to"))
+ .containsExactlyElementsIn(expected);
+ }
+
+ public static boolean isEquals(WatchdogStorage.IoUsageStatsEntry actual,
+ WatchdogStorage.IoUsageStatsEntry expected) {
+ if (actual == null || expected == null) {
+ return (actual == null) && (expected == null);
+ }
+ return actual.userId == expected.userId && actual.packageName.equals(expected.packageName)
+ && actual.ioUsage.getTotalTimesKilled() == expected.ioUsage.getTotalTimesKilled()
+ && InternalPerStateBytesSubject.isEquals(actual.ioUsage.getForgivenWriteBytes(),
+ expected.ioUsage.getForgivenWriteBytes())
+ && isEqualsIoOveruseStats(actual.ioUsage.getInternalIoOveruseStats(),
+ expected.ioUsage.getInternalIoOveruseStats());
+ }
+
+ private static boolean isEqualsIoOveruseStats(android.automotive.watchdog.IoOveruseStats actual,
+ android.automotive.watchdog.IoOveruseStats expected) {
+ if (actual == null || expected == null) {
+ return (actual == null) && (expected == null);
+ }
+ return actual.killableOnOveruse == expected.killableOnOveruse
+ && InternalPerStateBytesSubject.isEquals(
+ actual.remainingWriteBytes, expected.remainingWriteBytes)
+ && actual.startTime == expected.startTime
+ && actual.durationInSeconds == expected.durationInSeconds
+ && InternalPerStateBytesSubject.isEquals(actual.writtenBytes, expected.writtenBytes)
+ && actual.totalOveruses == expected.totalOveruses;
+ }
+
+ private static String toString(Iterable<WatchdogStorage.IoUsageStatsEntry> entries) {
+ StringBuilder builder = new StringBuilder();
+ builder.append('[');
+ for (WatchdogStorage.IoUsageStatsEntry entry : entries) {
+ toStringBuilder(builder, entry).append(", ");
+ }
+ if (builder.length() > 1) {
+ builder.delete(builder.length() - 2, builder.length());
+ }
+ builder.append(']');
+ return builder.toString();
+ }
+
+ private static StringBuilder toStringBuilder(StringBuilder builder,
+ WatchdogStorage.IoUsageStatsEntry entry) {
+ builder.append("{UserId: ").append(entry.userId)
+ .append(", Package name: ").append(entry.packageName)
+ .append(", IoUsage: ");
+ toStringBuilder(builder, entry.ioUsage);
+ return builder.append('}');
+ }
+
+ private static StringBuilder toStringBuilder(StringBuilder builder,
+ WatchdogPerfHandler.PackageIoUsage ioUsage) {
+ builder.append("{IoOveruseStats: ");
+ toStringBuilder(builder, ioUsage.getInternalIoOveruseStats());
+ builder.append(", ForgivenWriteBytes: ");
+ InternalPerStateBytesSubject.toStringBuilder(builder, ioUsage.getForgivenWriteBytes());
+ return builder.append(", Total times killed: ").append(ioUsage.getTotalTimesKilled())
+ .append('}');
+ }
+
+ private static StringBuilder toStringBuilder(StringBuilder builder,
+ IoOveruseStats stats) {
+ if (stats == null) {
+ return builder.append(NULL_ENTRY_STRING);
+ }
+ builder.append("{Killable on overuse: ").append(stats.killableOnOveruse)
+ .append(", Remaining write bytes: ");
+ InternalPerStateBytesSubject.toStringBuilder(builder, stats.remainingWriteBytes);
+ builder.append(", Start time: ").append(stats.startTime)
+ .append(", Duration: ").append(stats.durationInSeconds).append(" seconds")
+ .append(", Total overuses: ").append(stats.totalOveruses)
+ .append(", Written bytes: ");
+ InternalPerStateBytesSubject.toStringBuilder(builder, stats.writtenBytes);
+ return builder.append('}');
+ }
+
+ private IoUsageStatsEntrySubject(FailureMetadata failureMetadata,
+ @Nullable Iterable<WatchdogStorage.IoUsageStatsEntry> iterableSubject) {
+ super(failureMetadata, iterableSubject);
+
+ mActual = iterableSubject;
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/PackageKillableStateSubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/PackageKillableStateSubject.java
index 953d1df..f33c503 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/PackageKillableStateSubject.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/PackageKillableStateSubject.java
@@ -44,7 +44,7 @@
}
public static Subject.Factory<PackageKillableStateSubject,
- Iterable<PackageKillableState>> resourceOveruseStats() {
+ Iterable<PackageKillableState>> packageKillableStates() {
return PACKAGE_KILLABLE_STATE_SUBJECT_FACTORY;
}
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/PackageResourceOveruseActionSubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/PackageResourceOveruseActionSubject.java
new file mode 100644
index 0000000..d0911a3
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/PackageResourceOveruseActionSubject.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2021 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.watchdog;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.annotation.Nullable;
+import android.automotive.watchdog.internal.PackageIdentifier;
+import android.automotive.watchdog.internal.PackageResourceOveruseAction;
+
+import com.google.common.truth.Correspondence;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+
+import java.util.Arrays;
+
+public final class PackageResourceOveruseActionSubject extends Subject {
+ /* Boiler-plate Subject.Factory for PackageResourceOveruseActionSubject. */
+ private static final Subject.Factory<
+ com.android.car.watchdog.PackageResourceOveruseActionSubject,
+ Iterable<PackageResourceOveruseAction>>
+ PACKAGE_RESOURCE_OVERUSE_ACTION_SUBJECT_FACTORY =
+ com.android.car.watchdog.PackageResourceOveruseActionSubject::new;
+
+ private final Iterable<PackageResourceOveruseAction> mActual;
+
+ /* User-defined entry point. */
+ public static PackageResourceOveruseActionSubject assertThat(
+ @Nullable Iterable<PackageResourceOveruseAction> actions) {
+ return assertAbout(PACKAGE_RESOURCE_OVERUSE_ACTION_SUBJECT_FACTORY).that(actions);
+ }
+
+ public static Subject.Factory<PackageResourceOveruseActionSubject,
+ Iterable<PackageResourceOveruseAction>> packageResourceOveruseActions() {
+ return PACKAGE_RESOURCE_OVERUSE_ACTION_SUBJECT_FACTORY;
+ }
+
+ public void containsExactly(PackageResourceOveruseAction... actions) {
+ containsExactlyElementsIn(Arrays.asList(actions));
+ }
+
+ public void containsExactlyElementsIn(
+ Iterable<PackageResourceOveruseAction> expected) {
+ assertWithMessage("Package resource overuse actions:\nExpected: %s\nActual: %s\n",
+ toString(expected), toString(mActual)).that(mActual)
+ .comparingElementsUsing(Correspondence.from(
+ PackageResourceOveruseActionSubject::isEquals, "is equal to"))
+ .containsExactlyElementsIn(expected);
+ }
+
+ public static boolean isEquals(PackageResourceOveruseAction actual,
+ PackageResourceOveruseAction expected) {
+ if (actual == null || expected == null) {
+ return (actual == null) && (expected == null);
+ }
+ return isPackageIdentifierEquals(actual.packageIdentifier, expected.packageIdentifier)
+ && Arrays.equals(actual.resourceTypes, expected.resourceTypes)
+ && actual.resourceOveruseActionType == expected.resourceOveruseActionType;
+ }
+
+ private static boolean isPackageIdentifierEquals(PackageIdentifier lhs, PackageIdentifier rhs) {
+ return lhs.name.equals(rhs.name) && lhs.uid == rhs.uid;
+ }
+
+ public static String toString(Iterable<PackageResourceOveruseAction> actions) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("[");
+ for (PackageResourceOveruseAction action : actions) {
+ builder = toStringBuilder(builder, action);
+ builder.append(", ");
+ }
+ if (builder.length() > 1) {
+ builder.delete(builder.length() - 2, builder.length());
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+
+ public static StringBuilder toStringBuilder(
+ StringBuilder builder, PackageResourceOveruseAction action) {
+ return builder.append("{Package Identifier: {Name: ")
+ .append(action.packageIdentifier.name)
+ .append(", UID: ").append(action.packageIdentifier.uid)
+ .append("}, Resource Types: ").append(Arrays.toString(action.resourceTypes))
+ .append(", Action Type: ").append(action.resourceOveruseActionType).append("}");
+ }
+
+ private PackageResourceOveruseActionSubject(FailureMetadata failureMetadata,
+ @Nullable Iterable<PackageResourceOveruseAction> iterableSubject) {
+ super(failureMetadata, iterableSubject);
+ this.mActual = iterableSubject;
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseConfigurationSubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseConfigurationSubject.java
index 5bfdf17..9e849d2 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseConfigurationSubject.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseConfigurationSubject.java
@@ -46,7 +46,7 @@
}
public static Subject.Factory<ResourceOveruseConfigurationSubject,
- Iterable<ResourceOveruseConfiguration>> resourceOveruseStats() {
+ Iterable<ResourceOveruseConfiguration>> resourceOveruseConfigurations() {
return RESOURCE_OVERUSE_CONFIG_SUBJECT_FACTORY;
}
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseStatsSubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseStatsSubject.java
index 6b7b7ba..b0ce702 100644
--- a/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseStatsSubject.java
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/ResourceOveruseStatsSubject.java
@@ -17,6 +17,7 @@
package com.android.car.watchdog;
import static com.google.common.truth.Truth.assertAbout;
+import static com.google.common.truth.Truth.assertWithMessage;
import android.annotation.Nullable;
import android.car.watchdog.ResourceOveruseStats;
@@ -42,6 +43,11 @@
return assertAbout(RESOURCE_OVERUSE_STATS_SUBJECT_FACTORY).that(stats);
}
+ public static void assertEquals(ResourceOveruseStats actual, ResourceOveruseStats expected) {
+ assertWithMessage("Expected stats (%s) equals to actual stats (%s)", expected, actual)
+ .that(isEquals(actual, expected)).isTrue();
+ }
+
public static Subject.Factory<ResourceOveruseStatsSubject, Iterable<ResourceOveruseStats>>
resourceOveruseStats() {
return RESOURCE_OVERUSE_STATS_SUBJECT_FACTORY;
@@ -71,7 +77,7 @@
if (actual == null || expected == null) {
return false;
}
- return actual.getPackageName() == expected.getPackageName()
+ return actual.getPackageName().equals(expected.getPackageName())
&& actual.getUserHandle().equals(expected.getUserHandle())
&& IoOveruseStatsSubject.isEquals(actual.getIoOveruseStats(),
expected.getIoOveruseStats());
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/UserPackageSettingsEntrySubject.java b/tests/carservice_unit_test/src/com/android/car/watchdog/UserPackageSettingsEntrySubject.java
new file mode 100644
index 0000000..aa1df9e
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/UserPackageSettingsEntrySubject.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2021 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.watchdog;
+
+import static com.google.common.truth.Truth.assertAbout;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.annotation.Nullable;
+
+import com.google.common.truth.Correspondence;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+
+import java.util.Arrays;
+
+public final class UserPackageSettingsEntrySubject extends Subject {
+ /* Boiler-plate Subject.Factory for UserPackageSettingsEntrySubject. */
+ private static final Subject.Factory<
+ com.android.car.watchdog.UserPackageSettingsEntrySubject,
+ Iterable<WatchdogStorage.UserPackageSettingsEntry>>
+ USER_PACKAGE_SETTINGS_ENTRY_SUBJECT_FACTORY =
+ com.android.car.watchdog.UserPackageSettingsEntrySubject::new;
+
+ private final Iterable<WatchdogStorage.UserPackageSettingsEntry> mActual;
+
+ /* User-defined entry point. */
+ public static UserPackageSettingsEntrySubject assertThat(
+ @Nullable Iterable<WatchdogStorage.UserPackageSettingsEntry> stats) {
+ return assertAbout(USER_PACKAGE_SETTINGS_ENTRY_SUBJECT_FACTORY).that(stats);
+ }
+
+ public static Subject.Factory<UserPackageSettingsEntrySubject,
+ Iterable<WatchdogStorage.UserPackageSettingsEntry>> userPackageSettingsEntries() {
+ return USER_PACKAGE_SETTINGS_ENTRY_SUBJECT_FACTORY;
+ }
+
+ public void containsExactly(WatchdogStorage.UserPackageSettingsEntry... stats) {
+ containsExactlyElementsIn(Arrays.asList(stats));
+ }
+
+ public void containsExactlyElementsIn(
+ Iterable<WatchdogStorage.UserPackageSettingsEntry> expected) {
+ assertWithMessage("Expected entries (%s) equals to actual entries (%s)",
+ toString(expected), toString(mActual)).that(mActual)
+ .comparingElementsUsing(Correspondence.from(
+ UserPackageSettingsEntrySubject::isEquals, "is equal to"))
+ .containsExactlyElementsIn(expected);
+ }
+
+ public static boolean isEquals(WatchdogStorage.UserPackageSettingsEntry actual,
+ WatchdogStorage.UserPackageSettingsEntry expected) {
+ if (actual == null || expected == null) {
+ return (actual == null) && (expected == null);
+ }
+ return actual.userId == expected.userId && actual.packageName.equals(expected.packageName)
+ && actual.killableState == expected.killableState;
+ }
+
+ private static String toString(Iterable<WatchdogStorage.UserPackageSettingsEntry> entries) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("[");
+ for (WatchdogStorage.UserPackageSettingsEntry entry : entries) {
+ toStringBuilder(builder, entry).append(", ");
+ }
+ if (builder.length() > 1) {
+ builder.delete(builder.length() - 2, builder.length());
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+
+ private static StringBuilder toStringBuilder(StringBuilder builder,
+ WatchdogStorage.UserPackageSettingsEntry entry) {
+ return builder.append("{UserId: ").append(entry.userId)
+ .append(", Package name: ").append(entry.packageName)
+ .append(", Killable state: ").append(entry.killableState).append("}");
+ }
+
+ private UserPackageSettingsEntrySubject(FailureMetadata failureMetadata,
+ @Nullable Iterable<WatchdogStorage.UserPackageSettingsEntry> iterableSubject) {
+ super(failureMetadata, iterableSubject);
+ this.mActual = iterableSubject;
+ }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java b/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java
new file mode 100644
index 0000000..c84225d
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/watchdog/WatchdogStorageUnitTest.java
@@ -0,0 +1,578 @@
+/*
+ * Copyright (C) 2021 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.watchdog;
+
+import static android.car.watchdog.PackageKillableState.KILLABLE_STATE_NEVER;
+import static android.car.watchdog.PackageKillableState.KILLABLE_STATE_NO;
+import static android.car.watchdog.PackageKillableState.KILLABLE_STATE_YES;
+
+import static com.android.car.watchdog.WatchdogStorage.RETENTION_PERIOD;
+import static com.android.car.watchdog.WatchdogStorage.STATS_TEMPORAL_UNIT;
+import static com.android.car.watchdog.WatchdogStorage.WatchdogDbHelper.DATABASE_NAME;
+import static com.android.car.watchdog.WatchdogStorage.ZONE_OFFSET;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.automotive.watchdog.PerStateBytes;
+import android.car.watchdog.IoOveruseStats;
+import android.content.Context;
+import android.util.Slog;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * <p>This class contains unit tests for the {@link WatchdogStorage}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class WatchdogStorageUnitTest {
+ private static final String TAG = WatchdogStorageUnitTest.class.getSimpleName();
+
+ private Context mContext;
+ private WatchdogStorage mService;
+ private File mDatabaseFile;
+ private TimeSourceInterface mTimeSource;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext().createDeviceProtectedStorageContext();
+ mDatabaseFile = mContext.createDeviceProtectedStorageContext()
+ .getDatabasePath(DATABASE_NAME);
+ mService = new WatchdogStorage(mContext, /* useDataSystemCarDir= */ false);
+ setDate(/* numDaysAgo= */ 0);
+ }
+
+ @After
+ public void tearDown() {
+ mService.release();
+ if (!mDatabaseFile.delete()) {
+ Slog.e(TAG, "Failed to delete the database file: " + mDatabaseFile.getAbsolutePath());
+ }
+ }
+
+ @Test
+ public void testSaveUserPackageSettings() throws Exception {
+ List<WatchdogStorage.UserPackageSettingsEntry> expected = sampleSettings();
+
+ assertThat(mService.saveUserPackageSettings(expected)).isTrue();
+
+ UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+ .containsExactlyElementsIn(expected);
+ }
+
+ @Test
+ public void testOverwriteUserPackageSettings() throws Exception {
+ List<WatchdogStorage.UserPackageSettingsEntry> expected = Arrays.asList(
+ new WatchdogStorage.UserPackageSettingsEntry(
+ /* userId= */ 100, "system_package.non_critical.A", KILLABLE_STATE_YES),
+ new WatchdogStorage.UserPackageSettingsEntry(
+ /* userId= */ 100, "system_package.non_critical.B", KILLABLE_STATE_NO));
+
+ assertThat(mService.saveUserPackageSettings(expected)).isTrue();
+
+ expected = Arrays.asList(
+ new WatchdogStorage.UserPackageSettingsEntry(
+ /* userId= */ 100, "system_package.non_critical.A", KILLABLE_STATE_NEVER),
+ new WatchdogStorage.UserPackageSettingsEntry(
+ /* userId= */ 100, "system_package.non_critical.B", KILLABLE_STATE_NO));
+
+ assertThat(mService.saveUserPackageSettings(expected)).isTrue();
+
+ UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+ .containsExactlyElementsIn(expected);
+ }
+
+ @Test
+ public void testSaveAndGetIoOveruseStats() throws Exception {
+ injectSampleUserPackageSettings();
+ /* Start time aligned to the beginning of the day. */
+ long startTime = mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT)
+ .toEpochSecond();
+
+ assertWithMessage("Saved I/O usage stats successfully")
+ .that(mService.saveIoUsageStats(sampleStatsForDate(startTime, /* duration= */ 60)))
+ .isTrue();
+
+ long expectedDuration =
+ mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond() - startTime;
+ List<WatchdogStorage.IoUsageStatsEntry> expected = sampleStatsForDate(
+ startTime, expectedDuration);
+
+ IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+ .containsExactlyElementsIn(expected);
+ }
+
+ @Test
+ public void testSaveAndGetIoOveruseStatsWithOffsettedStartTime() throws Exception {
+ injectSampleUserPackageSettings();
+ /* Start time in the middle of the day. */
+ long startTime = mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT)
+ .plusHours(12).toEpochSecond();
+ List<WatchdogStorage.IoUsageStatsEntry> entries = sampleStatsForDate(
+ startTime, /* duration= */ 60);
+
+ assertWithMessage("Saved I/O usage stats successfully")
+ .that(mService.saveIoUsageStats(entries)).isTrue();
+
+ long expectedStartTime = mTimeSource.now().atZone(ZONE_OFFSET)
+ .truncatedTo(STATS_TEMPORAL_UNIT).toEpochSecond();
+ long expectedDuration =
+ mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond() - expectedStartTime;
+ List<WatchdogStorage.IoUsageStatsEntry> expected = sampleStatsForDate(
+ expectedStartTime, expectedDuration);
+
+ IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+ .containsExactlyElementsIn(expected);
+ }
+
+ @Test
+ public void testOverwriteIoOveruseStats() throws Exception {
+ injectSampleUserPackageSettings();
+ long startTime = mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT)
+ .toEpochSecond();
+ long duration = mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond() - startTime;
+
+ List<WatchdogStorage.IoUsageStatsEntry> statsBeforeOverwrite = Collections.singletonList(
+ constructIoUsageStatsEntry(
+ /* userId= */ 100, "system_package.non_critical.A", startTime, duration,
+ /* remainingWriteBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(200, 300, 400),
+ /* writtenBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(1000, 2000, 3000),
+ /* forgivenWriteBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(100, 100, 100),
+ /* totalOveruses= */ 2, /* totalTimesKilled= */ 1));
+
+ assertWithMessage("Saved I/O usage stats successfully")
+ .that(mService.saveIoUsageStats(statsBeforeOverwrite)).isTrue();
+
+ IoUsageStatsEntrySubject.assertWithMessage(
+ mService.getTodayIoUsageStats(), "I/O usage stats fetched from database")
+ .containsExactlyElementsIn(statsBeforeOverwrite);
+
+ List<WatchdogStorage.IoUsageStatsEntry> statsAfterOverwrite = Collections.singletonList(
+ constructIoUsageStatsEntry(
+ /* userId= */ 100, "system_package.non_critical.A", startTime, duration,
+ /* remainingWriteBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(400, 600, 800),
+ /* writtenBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(2000, 3000, 4000),
+ /* forgivenWriteBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(1200, 2300, 3400),
+ /* totalOveruses= */ 4, /* totalTimesKilled= */ 2));
+
+ assertWithMessage("Saved I/O usage stats successfully")
+ .that(mService.saveIoUsageStats(statsAfterOverwrite)).isTrue();
+
+ IoUsageStatsEntrySubject.assertWithMessage(
+ mService.getTodayIoUsageStats(), "Cached in memory I/O usage stats")
+ .containsExactlyElementsIn(statsBeforeOverwrite);
+
+ mService.release();
+ mService = new WatchdogStorage(mContext, /* useDataSystemCarDir= */ false);
+ setDate(/* numDaysAgo= */ 0);
+
+ assertWithMessage("User packages settings").that(mService.getUserPackageSettings())
+ .isNotEmpty();
+
+ IoUsageStatsEntrySubject.assertWithMessage(mService.getTodayIoUsageStats(),
+ "I/O usage stats fetched from database after restart")
+ .containsExactlyElementsIn(statsAfterOverwrite);
+ }
+
+ @Test
+ public void testSaveIoOveruseStatsOutsideRetentionPeriod() throws Exception {
+ injectSampleUserPackageSettings();
+ int retentionDaysAgo = RETENTION_PERIOD.getDays();
+
+ assertWithMessage("Saved I/O usage stats successfully")
+ .that(mService.saveIoUsageStats(sampleStatsBetweenDates(
+ /* includingStartDaysAgo= */ retentionDaysAgo,
+ /* excludingEndDaysAgo= */ retentionDaysAgo + 1))).isTrue();
+
+ assertWithMessage("Didn't fetch I/O overuse stats outside retention period")
+ .that(mService.getHistoricalIoOveruseStats(
+ /* userId= */ 100, "system_package.non_critical.A", retentionDaysAgo))
+ .isNull();
+ }
+
+ @Test
+ public void testGetHistoricalIoOveruseStats() throws Exception {
+ injectSampleUserPackageSettings();
+
+ assertThat(mService.saveIoUsageStats(sampleStatsBetweenDates(
+ /* includingStartDaysAgo= */ 0, /* excludingEndDaysAgo= */ 5))).isTrue();
+
+ IoOveruseStats actual = mService.getHistoricalIoOveruseStats(
+ /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 7);
+
+ assertWithMessage("Fetched I/O overuse stats").that(actual).isNotNull();
+
+ /*
+ * Returned stats shouldn't include stats for the current date as WatchdogPerfHandler fills
+ * the current day's stats.
+ */
+ ZonedDateTime currentDate = mTimeSource.now().atZone(ZONE_OFFSET)
+ .truncatedTo(STATS_TEMPORAL_UNIT);
+ long startTime = currentDate.minus(4, STATS_TEMPORAL_UNIT).toEpochSecond();
+ long duration = currentDate.toEpochSecond() - startTime;
+ IoOveruseStats expected = new IoOveruseStats.Builder(startTime, duration)
+ .setTotalOveruses(8).setTotalTimesKilled(4).setTotalBytesWritten(24_000).build();
+
+ IoOveruseStatsSubject.assertWithMessage(
+ "Fetched stats only for 4 days. Expected stats (%s) equals actual stats (%s)",
+ expected.toString(), actual.toString()).that(actual)
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testGetHistoricalIoOveruseStatsWithNoRecentStats() throws Exception {
+ injectSampleUserPackageSettings();
+
+ assertThat(mService.saveIoUsageStats(sampleStatsBetweenDates(
+ /* includingStartDaysAgo= */ 3, /* excludingEndDaysAgo= */ 5))).isTrue();
+
+ IoOveruseStats actual = mService.getHistoricalIoOveruseStats(
+ /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 7);
+
+ assertWithMessage("Fetched I/O overuse stats").that(actual).isNotNull();
+
+ /*
+ * Returned stats shouldn't include stats for the current date as WatchdogPerfHandler fills
+ * the current day's stats.
+ */
+ ZonedDateTime currentDate = mTimeSource.now().atZone(ZONE_OFFSET)
+ .truncatedTo(STATS_TEMPORAL_UNIT);
+ long startTime = currentDate.minus(4, STATS_TEMPORAL_UNIT).toEpochSecond();
+ long duration = currentDate.toEpochSecond() - startTime;
+ IoOveruseStats expected = new IoOveruseStats.Builder(startTime, duration)
+ .setTotalOveruses(4).setTotalTimesKilled(2).setTotalBytesWritten(12_000).build();
+
+ IoOveruseStatsSubject.assertWithMessage(
+ "Fetched stats only for 2 days. Expected stats (%s) equals actual stats (%s)",
+ expected.toString(), actual.toString()).that(actual)
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testDeleteUserPackage() throws Exception {
+ ArrayList<WatchdogStorage.UserPackageSettingsEntry> settingsEntries = sampleSettings();
+ List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = sampleStatsForToday();
+
+ assertThat(mService.saveUserPackageSettings(settingsEntries)).isTrue();
+ assertThat(mService.saveIoUsageStats(ioUsageStatsEntries)).isTrue();
+
+ int deleteUserId = 100;
+ String deletePackageName = "system_package.non_critical.A";
+
+ mService.deleteUserPackage(deleteUserId, deletePackageName);
+
+ settingsEntries.removeIf(
+ (s) -> s.userId == deleteUserId && s.packageName.equals(deletePackageName));
+
+ UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+ .containsExactlyElementsIn(settingsEntries);
+
+ ioUsageStatsEntries.removeIf(
+ (e) -> e.userId == deleteUserId && e.packageName.equals(deletePackageName));
+
+ IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+ .containsExactlyElementsIn(ioUsageStatsEntries);
+ }
+
+ @Test
+ public void testDeleteUserPackageWithNonexistentPackage() throws Exception {
+ injectSampleUserPackageSettings();
+ List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = sampleStatsForToday();
+
+ assertThat(mService.saveIoUsageStats(ioUsageStatsEntries)).isTrue();
+
+ int deleteUserId = 100;
+ String deletePackageName = "system_package.non_existent.A";
+
+ mService.deleteUserPackage(deleteUserId, deletePackageName);
+
+ UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+ .containsExactlyElementsIn(sampleSettings());
+
+ ioUsageStatsEntries.removeIf(
+ (e) -> e.userId == deleteUserId && e.packageName.equals(deletePackageName));
+
+ IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+ .containsExactlyElementsIn(ioUsageStatsEntries);
+ }
+
+ @Test
+ public void testDeleteUserPackageWithHistoricalIoOveruseStats()
+ throws Exception {
+ ArrayList<WatchdogStorage.UserPackageSettingsEntry> settingsEntries = sampleSettings();
+
+ assertThat(mService.saveUserPackageSettings(settingsEntries)).isTrue();
+ assertThat(mService.saveIoUsageStats(sampleStatsBetweenDates(
+ /* includingStartDaysAgo= */ 1, /* excludingEndDaysAgo= */ 6))).isTrue();
+
+ int deleteUserId = 100;
+ String deletePackageName = "system_package.non_critical.A";
+
+ mService.deleteUserPackage(deleteUserId, deletePackageName);
+
+ settingsEntries.removeIf(
+ (s) -> s.userId == deleteUserId && s.packageName.equals(deletePackageName));
+
+ UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+ .containsExactlyElementsIn(settingsEntries);
+
+ IoOveruseStats actual = mService.getHistoricalIoOveruseStats(
+ /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 7);
+
+ assertWithMessage("Fetched historical I/O overuse stats").that(actual).isNull();
+ }
+
+ @Test
+ public void testSyncUsers() throws Exception {
+ List<WatchdogStorage.UserPackageSettingsEntry> settingsEntries = sampleSettings();
+ List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = sampleStatsForToday();
+
+ assertThat(mService.saveUserPackageSettings(settingsEntries)).isTrue();
+ assertThat(mService.saveIoUsageStats(ioUsageStatsEntries)).isTrue();
+
+ mService.syncUsers(/* aliveUserIds= */ new int[] {101});
+
+ settingsEntries.removeIf((s) -> s.userId == 100);
+ ioUsageStatsEntries.removeIf((e) -> e.userId == 100);
+
+ UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+ .containsExactlyElementsIn(settingsEntries);
+
+ IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+ .containsExactlyElementsIn(ioUsageStatsEntries);
+ }
+
+ @Test
+ public void testSyncUsersWithHistoricalIoOveruseStats() throws Exception {
+ List<WatchdogStorage.UserPackageSettingsEntry> settingsEntries = sampleSettings();
+
+ assertThat(mService.saveUserPackageSettings(settingsEntries)).isTrue();
+ assertThat(mService.saveIoUsageStats(sampleStatsBetweenDates(
+ /* includingStartDaysAgo= */ 1, /* excludingEndDaysAgo= */ 6))).isTrue();
+
+ mService.syncUsers(/* aliveUserIds= */ new int[] {101});
+
+ settingsEntries.removeIf((s) -> s.userId == 100);
+
+ UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+ .containsExactlyElementsIn(settingsEntries);
+
+ IoOveruseStats actualSystemPackage = mService.getHistoricalIoOveruseStats(
+ /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 7);
+ IoOveruseStats actualVendorPackage = mService.getHistoricalIoOveruseStats(
+ /* userId= */ 100, "vendor_package.critical.C", /* numDaysAgo= */ 7);
+
+ assertWithMessage("System I/O overuse stats for deleted user")
+ .that(actualSystemPackage).isNull();
+ assertWithMessage("Vendor I/O overuse stats for deleted user")
+ .that(actualVendorPackage).isNull();
+ }
+
+ @Test
+ public void testSyncUsersWithNoDataForDeletedUser() throws Exception {
+ List<WatchdogStorage.UserPackageSettingsEntry> settingsEntries = sampleSettings();
+ List<WatchdogStorage.IoUsageStatsEntry> ioUsageStatsEntries = sampleStatsForToday();
+
+ assertThat(mService.saveUserPackageSettings(settingsEntries)).isTrue();
+ assertThat(mService.saveIoUsageStats(ioUsageStatsEntries)).isTrue();
+
+ mService.syncUsers(/* aliveUserIds= */ new int[] {100, 101});
+
+ UserPackageSettingsEntrySubject.assertThat(mService.getUserPackageSettings())
+ .containsExactlyElementsIn(settingsEntries);
+ IoUsageStatsEntrySubject.assertThat(mService.getTodayIoUsageStats())
+ .containsExactlyElementsIn(ioUsageStatsEntries);
+ }
+
+ @Test
+ public void testTruncateStatsOutsideRetentionPeriodOnDateChange() throws Exception {
+ injectSampleUserPackageSettings();
+ setDate(/* numDaysAgo= */ 1);
+
+ assertThat(mService.saveIoUsageStats(sampleStatsBetweenDates(
+ /* includingStartDaysAgo= */ 0, /* excludingEndDaysAgo= */ 40),
+ /* shouldCheckRetention= */ false)).isTrue();
+
+ IoOveruseStats actual = mService.getHistoricalIoOveruseStats(
+ /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 40);
+
+ assertWithMessage("Fetched I/O overuse stats").that(actual).isNotNull();
+
+ ZonedDateTime currentDate = mTimeSource.now().atZone(ZONE_OFFSET)
+ .truncatedTo(STATS_TEMPORAL_UNIT);
+ long startTime = currentDate.minus(39, STATS_TEMPORAL_UNIT).toEpochSecond();
+ long duration = currentDate.toEpochSecond() - startTime;
+ IoOveruseStats expected = new IoOveruseStats.Builder(startTime, duration)
+ .setTotalOveruses(78).setTotalTimesKilled(39).setTotalBytesWritten(234_000).build();
+
+ IoOveruseStatsSubject.assertWithMessage(
+ "Fetched stats only for 39 days. Expected stats (%s) equals actual stats (%s)",
+ expected.toString(), actual.toString()).that(actual)
+ .isEqualTo(expected);
+
+ setDate(/* numDaysAgo= */ 0);
+ mService.shrinkDatabase();
+
+ actual = mService.getHistoricalIoOveruseStats(
+ /* userId= */ 100, "system_package.non_critical.A", /* numDaysAgo= */ 40);
+
+ assertWithMessage("Fetched I/O overuse stats").that(actual).isNotNull();
+
+ currentDate = mTimeSource.now().atZone(ZONE_OFFSET).truncatedTo(STATS_TEMPORAL_UNIT);
+ startTime = currentDate.minus(RETENTION_PERIOD.minusDays(1)).toEpochSecond();
+ duration = currentDate.toEpochSecond() - startTime;
+ expected = new IoOveruseStats.Builder(startTime, duration)
+ .setTotalOveruses(58).setTotalTimesKilled(29).setTotalBytesWritten(174_000).build();
+
+ IoOveruseStatsSubject.assertWithMessage("Fetched stats only within retention period. "
+ + "Expected stats (%s) equals actual stats (%s)",
+ expected.toString(), actual.toString()).that(actual).isEqualTo(expected);
+ }
+
+ private void setDate(int numDaysAgo) {
+ TimeSourceInterface timeSource = new TimeSourceInterface() {
+ @Override
+ public Instant now() {
+ /* Return the same time, so the tests are deterministic. */
+ return mNow;
+ }
+
+ @Override
+ public String toString() {
+ return "Mocked date to " + now();
+ }
+
+ private final Instant mNow = Instant.now().minus(numDaysAgo, ChronoUnit.DAYS);
+ };
+ mService.setTimeSource(timeSource);
+ mTimeSource = timeSource;
+ }
+
+ private void injectSampleUserPackageSettings() throws Exception {
+ List<WatchdogStorage.UserPackageSettingsEntry> expected = sampleSettings();
+
+ assertThat(mService.saveUserPackageSettings(expected)).isTrue();
+ }
+
+ private static ArrayList<WatchdogStorage.UserPackageSettingsEntry> sampleSettings() {
+ return new ArrayList<>(Arrays.asList(
+ new WatchdogStorage.UserPackageSettingsEntry(
+ /* userId= */ 100, "system_package.non_critical.A", KILLABLE_STATE_YES),
+ new WatchdogStorage.UserPackageSettingsEntry(
+ /* userId= */ 100, "system_package.non_critical.B", KILLABLE_STATE_NO),
+ new WatchdogStorage.UserPackageSettingsEntry(
+ /* userId= */ 100, "vendor_package.critical.C", KILLABLE_STATE_NEVER),
+ new WatchdogStorage.UserPackageSettingsEntry(
+ /* userId= */ 101, "system_package.non_critical.A", KILLABLE_STATE_NO),
+ new WatchdogStorage.UserPackageSettingsEntry(
+ /* userId= */ 101, "system_package.non_critical.B", KILLABLE_STATE_YES),
+ new WatchdogStorage.UserPackageSettingsEntry(
+ /* userId= */ 101, "vendor_package.critical.C", KILLABLE_STATE_NEVER)));
+ }
+
+ private ArrayList<WatchdogStorage.IoUsageStatsEntry> sampleStatsBetweenDates(
+ int includingStartDaysAgo, int excludingEndDaysAgo) {
+ ZonedDateTime currentDate = mTimeSource.now().atZone(ZONE_OFFSET)
+ .truncatedTo(STATS_TEMPORAL_UNIT);
+ ArrayList<WatchdogStorage.IoUsageStatsEntry> entries = new ArrayList<>();
+ for (int i = includingStartDaysAgo; i < excludingEndDaysAgo; ++i) {
+ entries.addAll(sampleStatsForDate(
+ currentDate.minus(i, STATS_TEMPORAL_UNIT).toEpochSecond(),
+ STATS_TEMPORAL_UNIT.getDuration().toSeconds()));
+ }
+ return entries;
+ }
+
+ private ArrayList<WatchdogStorage.IoUsageStatsEntry> sampleStatsForToday() {
+ long currentTime = mTimeSource.now().atZone(ZONE_OFFSET)
+ .truncatedTo(STATS_TEMPORAL_UNIT).toEpochSecond();
+ long duration = mTimeSource.now().atZone(ZONE_OFFSET).toEpochSecond() - currentTime;
+ return sampleStatsForDate(currentTime, duration);
+ }
+
+ private static ArrayList<WatchdogStorage.IoUsageStatsEntry> sampleStatsForDate(
+ long statsDateEpoch, long duration) {
+ ArrayList<WatchdogStorage.IoUsageStatsEntry> entries = new ArrayList<>();
+ for (int i = 100; i <= 101; ++i) {
+ entries.add(constructIoUsageStatsEntry(
+ /* userId= */ i, "system_package.non_critical.A", statsDateEpoch, duration,
+ /* remainingWriteBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(200, 300, 400),
+ /* writtenBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(1000, 2000, 3000),
+ /* forgivenWriteBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(100, 100, 100),
+ /* totalOveruses= */ 2, /* totalTimesKilled= */ 1));
+ entries.add(constructIoUsageStatsEntry(
+ /* userId= */ i, "vendor_package.critical.C", statsDateEpoch, duration,
+ /* remainingWriteBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(500, 600, 700),
+ /* writtenBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(4000, 5000, 6000),
+ /* forgivenWriteBytes= */
+ CarWatchdogServiceUnitTest.constructPerStateBytes(200, 200, 200),
+ /* totalOveruses= */ 1, /* totalTimesKilled= */ 0));
+ }
+ return entries;
+ }
+
+ static WatchdogStorage.IoUsageStatsEntry constructIoUsageStatsEntry(
+ int userId, String packageName, long startTime, long duration,
+ PerStateBytes remainingWriteBytes, PerStateBytes writtenBytes,
+ PerStateBytes forgivenWriteBytes, int totalOveruses, int totalTimesKilled) {
+ WatchdogPerfHandler.PackageIoUsage ioUsage = new WatchdogPerfHandler.PackageIoUsage(
+ constructInternalIoOveruseStats(startTime, duration, remainingWriteBytes,
+ writtenBytes, totalOveruses), forgivenWriteBytes, totalTimesKilled);
+ return new WatchdogStorage.IoUsageStatsEntry(userId, packageName, ioUsage);
+ }
+
+ private static android.automotive.watchdog.IoOveruseStats constructInternalIoOveruseStats(
+ long startTime, long duration, PerStateBytes remainingWriteBytes,
+ PerStateBytes writtenBytes, int totalOveruses) {
+ android.automotive.watchdog.IoOveruseStats stats =
+ new android.automotive.watchdog.IoOveruseStats();
+ stats.startTime = startTime;
+ stats.durationInSeconds = duration;
+ stats.remainingWriteBytes = remainingWriteBytes;
+ stats.writtenBytes = writtenBytes;
+ stats.totalOveruses = totalOveruses;
+ return stats;
+ }
+}
diff --git a/tests/common_utils/src/com/android/car/test/FakeHandlerWrapper.java b/tests/common_utils/src/com/android/car/test/FakeHandlerWrapper.java
new file mode 100644
index 0000000..fe3071f
--- /dev/null
+++ b/tests/common_utils/src/com/android/car/test/FakeHandlerWrapper.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 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.test;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+
+/**
+ * A handler that allows control over when to dispatch messages and callbacks.
+ *
+ * <p>NOTE: Currently only supports {@link Runnable} messages. It doesn't dispatch regular messages.
+ *
+ * <p>Usage: Create an instance of {@link FakeHandlerWrapper}, and use {@link #getMockHandler()}
+ * in your test classes.
+ *
+ * <p>The implementation uses {@link Mockito} to bypass {@code final} keywords.
+ */
+public class FakeHandlerWrapper {
+ private Mode mMode;
+ private ArrayList<Message> mQueuedMessages = new ArrayList<>();
+
+ private final Handler mMockHandler;
+
+ public FakeHandlerWrapper(Looper looper, Mode mode) {
+ mMockHandler = Mockito.spy(new Handler(looper));
+ mMode = mode;
+ // Stubbing #sendMessageAtTime(Message, long).
+ Mockito.doAnswer(invocation -> {
+ Message msg = invocation.getArgument(0);
+ msg.when = invocation.getArgument(1); // uptimeMillis
+ mQueuedMessages.add(msg);
+ if (mMode == Mode.IMMEDIATE) {
+ dispatchQueuedMessages();
+ }
+ return true;
+ }).when(mMockHandler).sendMessageAtTime(Mockito.any(), Mockito.anyLong());
+ // Stubbing #removeCallbacks(Runnable).
+ Mockito.doAnswer(invocation -> {
+ Runnable callback = invocation.getArgument(0);
+ return mQueuedMessages.removeIf(msg -> msg.getCallback() == callback);
+ }).when(mMockHandler).removeCallbacks(Mockito.any());
+ }
+
+ public Handler getMockHandler() {
+ return mMockHandler;
+ }
+
+ public void setMode(Mode mode) {
+ mMode = mode;
+ }
+
+ /** Dispatch any messages that have been queued on the calling thread. */
+ public void dispatchQueuedMessages() {
+ ArrayList<Message> messages = new ArrayList<>(mQueuedMessages);
+ mQueuedMessages.clear();
+ for (Message msg : messages) {
+ Runnable callback = msg.getCallback();
+ if (callback != null) {
+ callback.run();
+ }
+ }
+ }
+
+ /** Returns the queued messages list. */
+ public ArrayList<Message> getQueuedMessages() {
+ return new ArrayList<>(mQueuedMessages);
+ }
+
+ public enum Mode {
+ /** Messages are dispatched immediately on the calling thread. */
+ IMMEDIATE,
+ /** Messages are queued until {@link #dispatchQueuedMessages()} is called. */
+ QUEUEING,
+ }
+}
diff --git a/tests/common_utils/src/com/android/car/test/FakeSharedPreferences.java b/tests/common_utils/src/com/android/car/test/FakeSharedPreferences.java
new file mode 100644
index 0000000..ac85e52
--- /dev/null
+++ b/tests/common_utils/src/com/android/car/test/FakeSharedPreferences.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2021 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.test;
+
+import android.content.SharedPreferences;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/** Fake class for {@link SharedPreferences} to be used in tests. */
+public class FakeSharedPreferences implements SharedPreferences, SharedPreferences.Editor {
+ private final HashMap<String, Object> mValues = new HashMap<>();
+ private final HashMap<String, Object> mTempValues = new HashMap<>();
+
+ @Override
+ public Editor edit() {
+ return this;
+ }
+
+ @Override
+ public boolean contains(String key) {
+ return mValues.containsKey(key);
+ }
+
+ @Override
+ public Map<String, ?> getAll() {
+ return new HashMap<>(mValues);
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Boolean) mValues.get(key)).booleanValue();
+ }
+ return defValue;
+ }
+
+ @Override
+ public float getFloat(String key, float defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Float) mValues.get(key)).floatValue();
+ }
+ return defValue;
+ }
+
+ @Override
+ public int getInt(String key, int defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Integer) mValues.get(key)).intValue();
+ }
+ return defValue;
+ }
+
+ @Override
+ public long getLong(String key, long defValue) {
+ if (mValues.containsKey(key)) {
+ return ((Long) mValues.get(key)).longValue();
+ }
+ return defValue;
+ }
+
+ @Override
+ public String getString(String key, String defValue) {
+ if (mValues.containsKey(key)) {
+ return (String) mValues.get(key);
+ }
+ return defValue;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Set<String> getStringSet(String key, Set<String> defValues) {
+ if (mValues.containsKey(key)) {
+ return (Set<String>) mValues.get(key);
+ }
+ return defValues;
+ }
+
+ @Override
+ public void registerOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void unregisterOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Editor putBoolean(String key, boolean value) {
+ mTempValues.put(key, Boolean.valueOf(value));
+ return this;
+ }
+
+ @Override
+ public Editor putFloat(String key, float value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putInt(String key, int value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putLong(String key, long value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putString(String key, String value) {
+ mTempValues.put(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putStringSet(String key, Set<String> values) {
+ mTempValues.put(key, values);
+ return this;
+ }
+
+ @Override
+ public Editor remove(String key) {
+ mTempValues.remove(key);
+ return this;
+ }
+
+ @Override
+ public Editor clear() {
+ mTempValues.clear();
+ return this;
+ }
+
+ @Override
+ public boolean commit() {
+ mValues.clear();
+ mValues.putAll(mTempValues);
+ return true;
+ }
+
+ @Override
+ public void apply() {
+ commit();
+ }
+}