Merge "Add a flag to guard new MessageQueue test apis" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5c0b227..b7a82cd 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -5293,6 +5293,11 @@
method @NonNull public android.hardware.contexthub.HubServiceInfo build();
}
+ @FlaggedApi("android.chre.flags.offload_api") public interface IHubEndpointDiscoveryCallback {
+ method public void onEndpointsStarted(@NonNull java.util.List<android.hardware.contexthub.HubDiscoveryInfo>);
+ method public void onEndpointsStopped(@NonNull java.util.List<android.hardware.contexthub.HubDiscoveryInfo>);
+ }
+
@FlaggedApi("android.chre.flags.offload_api") public interface IHubEndpointLifecycleCallback {
method public void onSessionClosed(@NonNull android.hardware.contexthub.HubEndpointSession, int);
method @NonNull public android.hardware.contexthub.HubEndpointSessionResult onSessionOpenRequest(@NonNull android.hardware.contexthub.HubEndpointInfo, @Nullable android.hardware.contexthub.HubServiceInfo);
@@ -6315,11 +6320,16 @@
method @Deprecated public int registerCallback(@NonNull android.hardware.location.ContextHubManager.Callback);
method @Deprecated public int registerCallback(android.hardware.location.ContextHubManager.Callback, android.os.Handler);
method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpoint(@NonNull android.hardware.contexthub.HubEndpoint);
+ method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(long, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback);
+ method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(long, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback, @NonNull java.util.concurrent.Executor);
+ method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(@NonNull String, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback);
+ method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(@NonNull String, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback, @NonNull java.util.concurrent.Executor);
method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int sendMessage(int, int, @NonNull android.hardware.location.ContextHubMessage);
method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int unloadNanoApp(int);
method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubTransaction<java.lang.Void> unloadNanoApp(@NonNull android.hardware.location.ContextHubInfo, long);
method @Deprecated public int unregisterCallback(@NonNull android.hardware.location.ContextHubManager.Callback);
method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void unregisterEndpoint(@NonNull android.hardware.contexthub.HubEndpoint);
+ method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void unregisterEndpointDiscoveryCallback(@NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback);
field public static final int AUTHORIZATION_DENIED = 0; // 0x0
field public static final int AUTHORIZATION_DENIED_GRACE_PERIOD = 1; // 0x1
field public static final int AUTHORIZATION_GRANTED = 2; // 0x2
diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java
index 5ad2348..db080fc 100644
--- a/core/java/android/companion/CompanionDeviceService.java
+++ b/core/java/android/companion/CompanionDeviceService.java
@@ -249,7 +249,7 @@
// TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
/**
- * Called by system whenever a device associated with this app is connected.
+ * Called by the system when an associated device is nearby or connected.
*
* @param associationInfo A record for the companion device.
*/
@@ -262,7 +262,7 @@
// TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
/**
- * Called by system whenever a device associated with this app is disconnected.
+ * Called by the system when an associated device is out of range or disconnected.
*
* @param associationInfo A record for the companion device.
*/
@@ -274,7 +274,7 @@
}
/**
- * Called by the system during device events.
+ * Called by the system when an associated device's presence state changes.
*
* @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest)
*/
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 02eed1a..3d2d487 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -7720,7 +7720,6 @@
@IntDef(flag = true, prefix = { "EXTENDED_FLAG_" }, value = {
EXTENDED_FLAG_FILTER_MISMATCH,
EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN,
- EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ExtendedFlags {}
@@ -7741,13 +7740,6 @@
*/
public static final int EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN = 1 << 1;
- /**
- * This flag indicates this intent called {@link #collectExtraIntentKeys()}.
- *
- * @hide
- */
- public static final int EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED = 1 << 2;
-
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// toUri() and parseUri() options.
@@ -12336,8 +12328,7 @@
}
private void collectNestedIntentKeysRecur(Set<Intent> visited) {
- addExtendedFlags(EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED);
- if (mExtras != null && !mExtras.isEmpty()) {
+ if (mExtras != null && !mExtras.isParcelled() && !mExtras.isEmpty()) {
for (String key : mExtras.keySet()) {
Object value = mExtras.get(key);
diff --git a/core/java/android/hardware/contexthub/IContextHubEndpointDiscoveryCallback.aidl b/core/java/android/hardware/contexthub/IContextHubEndpointDiscoveryCallback.aidl
new file mode 100644
index 0000000..85775c0
--- /dev/null
+++ b/core/java/android/hardware/contexthub/IContextHubEndpointDiscoveryCallback.aidl
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.hardware.contexthub;
+
+import android.hardware.contexthub.HubEndpointInfo;
+
+/**
+ * @hide
+ */
+oneway interface IContextHubEndpointDiscoveryCallback {
+ /**
+ * Called when endpoint(s) start.
+ * @param hubEndpointInfoList The list of endpoints that started.
+ */
+ void onEndpointsStarted(in HubEndpointInfo[] hubEndpointInfoList);
+
+ /**
+ * Called when endpoint(s) stopped.
+ * @param hubEndpointInfoList The list of endpoints that started.
+ */
+ void onEndpointsStopped(in HubEndpointInfo[] hubEndpointInfoList);
+}
diff --git a/core/java/android/hardware/contexthub/IHubEndpointDiscoveryCallback.java b/core/java/android/hardware/contexthub/IHubEndpointDiscoveryCallback.java
new file mode 100644
index 0000000..0b77ddb
--- /dev/null
+++ b/core/java/android/hardware/contexthub/IHubEndpointDiscoveryCallback.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.hardware.contexthub;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.chre.flags.Flags;
+
+import java.util.List;
+
+/**
+ * Interface for listening to updates about endpoint availability.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_OFFLOAD_API)
+public interface IHubEndpointDiscoveryCallback {
+ /**
+ * Called when a list of hub endpoints have started.
+ *
+ * @param discoveryInfoList The list containing hub discovery information.
+ */
+ void onEndpointsStarted(@NonNull List<HubDiscoveryInfo> discoveryInfoList);
+
+ /**
+ * Called when a list of hub endpoints have stopped.
+ *
+ * @param discoveryInfoList The list containing hub discovery information.
+ */
+ // TODO(b/375487784): Add endpoint stop reason
+ void onEndpointsStopped(@NonNull List<HubDiscoveryInfo> discoveryInfoList);
+}
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 426cd69f..5e8a187 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -38,6 +38,8 @@
import android.hardware.contexthub.HubEndpoint;
import android.hardware.contexthub.HubEndpointInfo;
import android.hardware.contexthub.HubServiceInfo;
+import android.hardware.contexthub.IContextHubEndpointDiscoveryCallback;
+import android.hardware.contexthub.IHubEndpointDiscoveryCallback;
import android.hardware.contexthub.IHubEndpointLifecycleCallback;
import android.os.Handler;
import android.os.HandlerExecutor;
@@ -49,7 +51,9 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
/**
@@ -202,6 +206,10 @@
private Callback mCallback;
private Handler mCallbackHandler;
+ /** A map of endpoint discovery callbacks currently registered */
+ private Map<IHubEndpointDiscoveryCallback, IContextHubEndpointDiscoveryCallback>
+ mDiscoveryCallbacks = new ConcurrentHashMap<>();
+
/**
* @deprecated Use {@code mCallback} instead.
*/
@@ -694,8 +702,6 @@
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@NonNull
public List<HubDiscoveryInfo> findEndpoints(long endpointId) {
- // TODO(b/379323274): Consider improving these getters to avoid racing with nano app load
- // timing.
try {
List<HubEndpointInfo> endpointInfos = mService.findEndpoints(endpointId);
List<HubDiscoveryInfo> results = new ArrayList<>(endpointInfos.size());
@@ -720,8 +726,6 @@
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@NonNull
public List<HubDiscoveryInfo> findEndpoints(@NonNull String serviceDescriptor) {
- // TODO(b/379323274): Consider improving these getters to avoid racing with nano app load
- // timing.
if (serviceDescriptor.isBlank()) {
throw new IllegalArgumentException("Invalid service descriptor: " + serviceDescriptor);
}
@@ -744,6 +748,188 @@
}
/**
+ * Creates an interface to invoke endpoint discovery callbacks to send down to the service.
+ *
+ * @param callback the callback to invoke at the client process
+ * @param executor the executor to invoke callbacks for this client
+ * @return the callback interface
+ */
+ private IContextHubEndpointDiscoveryCallback createDiscoveryCallback(
+ IHubEndpointDiscoveryCallback callback,
+ Executor executor,
+ @Nullable String serviceDescriptor) {
+ return new IContextHubEndpointDiscoveryCallback.Stub() {
+ @Override
+ public void onEndpointsStarted(HubEndpointInfo[] hubEndpointInfoList) {
+ if (hubEndpointInfoList.length == 0) {
+ Log.w(TAG, "onEndpointsStarted: received empty discovery list");
+ return;
+ }
+ executor.execute(
+ () -> {
+ // TODO(b/380293951): Refactor
+ List<HubDiscoveryInfo> discoveryList =
+ new ArrayList<>(hubEndpointInfoList.length);
+ for (HubEndpointInfo info : hubEndpointInfoList) {
+ if (serviceDescriptor != null) {
+ for (HubServiceInfo sInfo : info.getServiceInfoCollection()) {
+ if (sInfo.getServiceDescriptor()
+ .equals(serviceDescriptor)) {
+ discoveryList.add(new HubDiscoveryInfo(info, sInfo));
+ }
+ }
+ } else {
+ discoveryList.add(new HubDiscoveryInfo(info));
+ }
+ }
+ if (discoveryList.isEmpty()) {
+ Log.w(TAG, "onEndpointsStarted: no matching service descriptor");
+ } else {
+ callback.onEndpointsStarted(discoveryList);
+ }
+ });
+ }
+
+ @Override
+ public void onEndpointsStopped(HubEndpointInfo[] hubEndpointInfoList) {
+ if (hubEndpointInfoList.length == 0) {
+ Log.w(TAG, "onEndpointsStopped: received empty discovery list");
+ return;
+ }
+ executor.execute(
+ () -> {
+ List<HubDiscoveryInfo> discoveryList =
+ new ArrayList<>(hubEndpointInfoList.length);
+ for (HubEndpointInfo info : hubEndpointInfoList) {
+ if (serviceDescriptor != null) {
+ for (HubServiceInfo sInfo : info.getServiceInfoCollection()) {
+ if (sInfo.getServiceDescriptor()
+ .equals(serviceDescriptor)) {
+ discoveryList.add(new HubDiscoveryInfo(info, sInfo));
+ }
+ }
+ } else {
+ discoveryList.add(new HubDiscoveryInfo(info));
+ }
+ }
+ if (discoveryList.isEmpty()) {
+ Log.w(TAG, "onEndpointsStopped: no matching service descriptor");
+ } else {
+ callback.onEndpointsStopped(discoveryList);
+ }
+ });
+ }
+ };
+ }
+
+ /**
+ * Equivalent to {@link #registerEndpointDiscoveryCallback(long, IHubEndpointDiscoveryCallback,
+ * Executor)} with the default executor in the main thread.
+ */
+ @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+ @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+ public void registerEndpointDiscoveryCallback(
+ long endpointId, @NonNull IHubEndpointDiscoveryCallback callback) {
+ registerEndpointDiscoveryCallback(
+ endpointId, callback, new HandlerExecutor(Handler.getMain()));
+ }
+
+ /**
+ * Registers a callback to be notified when the hub endpoint with the corresponding endpoint ID
+ * has started or stopped.
+ *
+ * @param endpointId The identifier of the hub endpoint.
+ * @param callback The callback to be invoked.
+ * @param executor The executor to invoke the callback on.
+ */
+ @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+ @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+ public void registerEndpointDiscoveryCallback(
+ long endpointId,
+ @NonNull IHubEndpointDiscoveryCallback callback,
+ @NonNull Executor executor) {
+ Objects.requireNonNull(callback, "callback cannot be null");
+ Objects.requireNonNull(executor, "executor cannot be null");
+ IContextHubEndpointDiscoveryCallback iCallback =
+ createDiscoveryCallback(callback, executor, null);
+ try {
+ mService.registerEndpointDiscoveryCallbackId(endpointId, iCallback);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+
+ mDiscoveryCallbacks.put(callback, iCallback);
+ }
+
+ /**
+ * Equivalent to {@link #registerEndpointDiscoveryCallback(String,
+ * IHubEndpointDiscoveryCallback, Executor)} with the default executor in the main thread.
+ */
+ @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+ @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+ public void registerEndpointDiscoveryCallback(
+ @NonNull String serviceDescriptor, @NonNull IHubEndpointDiscoveryCallback callback) {
+ registerEndpointDiscoveryCallback(
+ serviceDescriptor, callback, new HandlerExecutor(Handler.getMain()));
+ }
+
+ /**
+ * Registers a callback to be notified when the hub endpoint with the corresponding service
+ * descriptor has started or stopped.
+ *
+ * @param serviceDescriptor The service descriptor of the hub endpoint.
+ * @param callback The callback to be invoked.
+ * @param executor The executor to invoke the callback on.
+ * @throws IllegalArgumentException if the serviceDescriptor is empty.
+ */
+ @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+ @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+ public void registerEndpointDiscoveryCallback(
+ @NonNull String serviceDescriptor,
+ @NonNull IHubEndpointDiscoveryCallback callback,
+ @NonNull Executor executor) {
+ Objects.requireNonNull(serviceDescriptor, "serviceDescriptor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ Objects.requireNonNull(executor, "executor cannot be null");
+ if (serviceDescriptor.isBlank()) {
+ throw new IllegalArgumentException("Invalid service descriptor: " + serviceDescriptor);
+ }
+
+ IContextHubEndpointDiscoveryCallback iCallback =
+ createDiscoveryCallback(callback, executor, serviceDescriptor);
+ try {
+ mService.registerEndpointDiscoveryCallbackDescriptor(serviceDescriptor, iCallback);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+
+ mDiscoveryCallbacks.put(callback, iCallback);
+ }
+
+ /**
+ * Unregisters a previously registered endpoint discovery callback.
+ *
+ * @param callback The callback previously registered.
+ * @throws IllegalArgumentException If the callback was not previously registered.
+ */
+ @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+ @FlaggedApi(Flags.FLAG_OFFLOAD_API)
+ public void unregisterEndpointDiscoveryCallback(
+ @NonNull IHubEndpointDiscoveryCallback callback) {
+ Objects.requireNonNull(callback, "callback cannot be null");
+ IContextHubEndpointDiscoveryCallback iCallback = mDiscoveryCallbacks.remove(callback);
+ if (iCallback == null) {
+ throw new IllegalArgumentException("Callback not previously registered");
+ }
+
+ try {
+ mService.unregisterEndpointDiscoveryCallback(iCallback);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Set a callback to receive messages from the context hub
*
* @param callback Callback object
diff --git a/core/java/android/hardware/location/IContextHubService.aidl b/core/java/android/hardware/location/IContextHubService.aidl
index f9f41244..f14aadc 100644
--- a/core/java/android/hardware/location/IContextHubService.aidl
+++ b/core/java/android/hardware/location/IContextHubService.aidl
@@ -21,6 +21,7 @@
import android.hardware.contexthub.HubEndpointInfo;
import android.hardware.contexthub.IContextHubEndpoint;
import android.hardware.contexthub.IContextHubEndpointCallback;
+import android.hardware.contexthub.IContextHubEndpointDiscoveryCallback;
import android.hardware.location.ContextHubInfo;
import android.hardware.location.ContextHubMessage;
import android.hardware.location.HubInfo;
@@ -137,4 +138,16 @@
// Register an endpoint with the context hub
@EnforcePermission("ACCESS_CONTEXT_HUB")
IContextHubEndpoint registerEndpoint(in HubEndpointInfo pendingEndpointInfo, in IContextHubEndpointCallback callback);
+
+ // Register an endpoint discovery callback (id)
+ @EnforcePermission("ACCESS_CONTEXT_HUB")
+ void registerEndpointDiscoveryCallbackId(long endpointId, in IContextHubEndpointDiscoveryCallback callback);
+
+ // Register an endpoint discovery callback (descriptor)
+ @EnforcePermission("ACCESS_CONTEXT_HUB")
+ void registerEndpointDiscoveryCallbackDescriptor(String serviceDescriptor, in IContextHubEndpointDiscoveryCallback callback);
+
+ // Unregister an endpoint with the context hub
+ @EnforcePermission("ACCESS_CONTEXT_HUB")
+ void unregisterEndpointDiscoveryCallback(in IContextHubEndpointDiscoveryCallback callback);
}
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 55ba4af..a653e0a 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -58,7 +58,7 @@
is_fixed_read_only: true
namespace: "permissions"
description: "enable enhanced confirmation incall apis"
- bug: "310220212"
+ bug: "364535720"
}
flag {
@@ -67,7 +67,7 @@
is_fixed_read_only: true
namespace: "permissions"
description: "enable the blocking of certain app installs during an unknown call"
- bug: "310220212"
+ bug: "364535720"
}
flag {
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 357aba3..6c92991 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -96,21 +96,6 @@
}
flag {
- name: "prevent_intent_redirect_throw_exception_if_nested_keys_not_collected"
- namespace: "responsible_apis"
- description: "Prevent intent redirect attacks by throwing exception if the intent does not collect nested keys"
- bug: "361143368"
-}
-
-flag {
- name: "prevent_intent_redirect_collect_nested_keys_on_server_if_not_collected"
- namespace: "responsible_apis"
- description: "Prevent intent redirect attacks by collecting nested keys on server if not yet collected"
- bug: "361143368"
- is_fixed_read_only: true
-}
-
-flag {
name: "enable_intent_matching_flags"
is_exported: true
namespace: "permissions"
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 80d39d1..595eb26 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -9333,7 +9333,11 @@
Set<Integer> bitmapIdSet = getBitmapIdsUsedByActions(new HashSet<>());
int result = 0;
for (int bitmapId: bitmapIdSet) {
- result += mBitmapCache.getBitmapForId(bitmapId).getAllocationByteCount();
+ Bitmap currentBitmap = mBitmapCache.getBitmapForId(bitmapId);
+ if (currentBitmap == null) {
+ continue;
+ }
+ result += currentBitmap.getAllocationByteCount();
}
return result;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a13e334..7c2a30d 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -848,10 +848,10 @@
<protected-broadcast android:name="android.app.action.CONSOLIDATED_NOTIFICATION_POLICY_CHANGED" />
<protected-broadcast android:name="android.intent.action.MAIN_USER_LOCKSCREEN_KNOWLEDGE_FACTOR_CHANGED" />
<protected-broadcast android:name="com.android.uwb.uwbcountrycode.GEOCODE_RETRY" />
- <protected-broadcast android:name="android.telephony.action.ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED" />
+ <protected-broadcast android:name="android.telephony.satellite.action.SATELLITE_SUBSCRIBER_ID_LIST_CHANGED" />
<protected-broadcast android:name="android.service.ondeviceintelligence.MODEL_LOADED" />
<protected-broadcast android:name="android.service.ondeviceintelligence.MODEL_UNLOADED" />
- <protected-broadcast android:name="android.telephony.action.ACTION_SATELLITE_START_NON_EMERGENCY_SESSION" />
+ <protected-broadcast android:name="android.telephony.satellite.action.SATELLITE_START_NON_EMERGENCY_SESSION" />
<!-- ====================================================================== -->
@@ -9351,6 +9351,17 @@
</intent-filter>
</service>
+ <service android:name="com.android.ecm.EnhancedConfirmationCallTrackerService"
+ android:permission="android.permission.BIND_INCALL_SERVICE"
+ android:featureFlag="android.permission.flags.enhanced_confirmation_in_call_apis_enabled"
+ android:exported="true">
+ <meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS"
+ android:value="true" />
+ <intent-filter>
+ <action android:name="android.telecom.InCallService"/>
+ </intent-filter>
+ </service>
+
<service android:name="com.android.server.companion.datatransfer.contextsync.CallMetadataSyncConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
diff --git a/core/res/res/drawable-watch-v36/dialog_alert_button_background_negative.xml b/core/res/res/drawable-watch-v36/dialog_alert_button_background_negative.xml
index b6b8eac3..0314bbe 100644
--- a/core/res/res/drawable-watch-v36/dialog_alert_button_background_negative.xml
+++ b/core/res/res/drawable-watch-v36/dialog_alert_button_background_negative.xml
@@ -18,7 +18,7 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/btn_material_filled_tonal_background_color"/>
- <corners android:radius="@dimen/config_bottomDialogCornerRadius" />
+ <corners android:radius="@dimen/config_wearMaterial3_bottomDialogCornerRadius" />
<size
android:width="@dimen/dialog_btn_negative_width"
android:height="@dimen/dialog_btn_negative_height" />
diff --git a/core/res/res/layout-watch-v36/alert_dialog_icon_button_wear_material3.xml b/core/res/res/layout-watch-v36/alert_dialog_icon_button_wear_material3.xml
new file mode 100644
index 0000000..407ec7a
--- /dev/null
+++ b/core/res/res/layout-watch-v36/alert_dialog_icon_button_wear_material3.xml
@@ -0,0 +1,123 @@
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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 layout is the AlertDialog template. It overrides the system layout with the same name.
+ Make sure to include all the existing id of the overridden alert_dialog_material.-->
+<com.android.internal.widget.WatchListDecorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/parentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <ScrollView
+ android:id="@+id/scrollView"
+ android:fillViewport="true"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <!-- Top Panel -->
+ <FrameLayout
+ android:paddingLeft="?dialogPreferredPadding"
+ android:paddingRight="?dialogPreferredPadding"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/topPanel"
+ android:minHeight="@dimen/dialog_list_padding_top_no_title">
+ <include android:id="@+id/title_template"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ layout="@layout/alert_dialog_title_material"/>
+ </FrameLayout>
+
+ <!-- Content Panel -->
+ <FrameLayout android:id="@+id/contentPanel"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false">
+ <TextView android:id="@+id/message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal|top"
+ android:textAppearance="@style/TextAppearance.DeviceDefault.Body1"
+ android:paddingStart="?dialogPreferredPadding"
+ android:paddingEnd="?dialogPreferredPadding"
+ android:paddingTop="8dip"
+ android:paddingBottom="8dip"/>
+ </FrameLayout>
+
+ <!-- Custom Panel, to replace content panel if needed -->
+ <FrameLayout android:id="@+id/customPanel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:minHeight="64dp">
+ <FrameLayout android:id="@+android:id/custom"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </FrameLayout>
+
+ <!-- Button Panel -->
+ <FrameLayout
+ android:id="@+id/buttonPanel"
+ android:minHeight="@dimen/dialog_list_padding_bottom_no_buttons"
+ android:layout_weight="1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center">
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="?dialogPreferredPadding"
+ style="?android:attr/buttonBarStyle"
+ android:measureWithLargestChild="true">
+ <Button android:id="@+id/button2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:layout_weight="1"
+ style="@style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3.Negative" />
+ <Button android:id="@+id/button3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:layout_weight="1"
+ style="?android:attr/buttonBarButtonStyle"/>
+ <FrameLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <Button android:id="@+id/button1"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:layout_weight="1"
+ style="@style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3.Confirm" />
+ <!-- This works as background. -->
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:src="@drawable/dialog_alert_button_positive"/>
+ </FrameLayout>
+ </LinearLayout>
+ </FrameLayout>
+ </LinearLayout>
+ </ScrollView>
+</com.android.internal.widget.WatchListDecorLayout>
diff --git a/core/res/res/layout-watch-v36/alert_dialog_material.xml b/core/res/res/layout-watch-v36/alert_dialog_material.xml
index 900102f..8f75456 100644
--- a/core/res/res/layout-watch-v36/alert_dialog_material.xml
+++ b/core/res/res/layout-watch-v36/alert_dialog_material.xml
@@ -82,9 +82,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
- android:orientation="horizontal"
+ android:orientation="vertical"
android:paddingBottom="?dialogPreferredPadding"
- style="?android:attr/buttonBarStyle"
android:measureWithLargestChild="true">
<Button android:id="@+id/button2"
android:layout_width="wrap_content"
@@ -92,7 +91,7 @@
android:layout_gravity="center"
android:gravity="center"
android:layout_weight="1"
- style="@style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog.Negative" />
+ style="@*android:style/Widget.DeviceDefault.Button.WearMaterial3"/>
<Button android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -100,22 +99,13 @@
android:gravity="center"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle"/>
- <FrameLayout
- android:layout_width="wrap_content"
- android:layout_height="wrap_content">
- <Button android:id="@+id/button1"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_gravity="center"
- android:gravity="center"
- android:layout_weight="1"
- style="@style/Widget.DeviceDefault.Button.ButtonBar.AlertDialog.Confirm"/>
- <!-- This works as background. -->
- <ImageView
+ <Button android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:src="@drawable/dialog_alert_button_positive"/>
- </FrameLayout>
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:layout_weight="1"
+ style="@*android:style/Widget.DeviceDefault.Button.Filled"/>
</LinearLayout>
</FrameLayout>
</LinearLayout>
diff --git a/core/res/res/values-watch-v36/config.xml b/core/res/res/values-watch-v36/config.xml
index 1143ae3..679dc70 100644
--- a/core/res/res/values-watch-v36/config.xml
+++ b/core/res/res/values-watch-v36/config.xml
@@ -16,5 +16,5 @@
<resources>
<dimen name="config_wearMaterial3_buttonCornerRadius">26dp</dimen>
- <dimen name="config_bottomDialogCornerRadius">18dp</dimen>
+ <dimen name="config_wearMaterial3_bottomDialogCornerRadius">18dp</dimen>
</resources>
diff --git a/core/res/res/values-watch-v36/styles_material.xml b/core/res/res/values-watch-v36/styles_material.xml
index 00f3f09..7da7435 100644
--- a/core/res/res/values-watch-v36/styles_material.xml
+++ b/core/res/res/values-watch-v36/styles_material.xml
@@ -57,7 +57,7 @@
</style>
<!-- AlertDialog Styles -->
- <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog" parent="Widget.DeviceDefault.Button">
+ <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3" parent="Widget.DeviceDefault.Button">
<item name="android:textSize">0sp</item>
<item name="android:gravity">center</item>
<item name="android:paddingStart">0dp</item>
@@ -65,14 +65,14 @@
<item name="android:drawablePadding">0dp</item>
</style>
- <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog.Confirm" parent="Widget.DeviceDefault.Button.ButtonBar.AlertDialog">
+ <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3.Confirm">
<!-- Use a ImageView as background -->
<item name="background">@android:color/transparent</item>
<item name="minWidth">@dimen/dialog_btn_confirm_width</item>
<item name="minHeight">@dimen/dialog_btn_confirm_height</item>
</style>
- <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog.Negative" parent="Widget.DeviceDefault.Button.ButtonBar.AlertDialog">
+ <style name="Widget.DeviceDefault.Button.ButtonBar.AlertDialog.WearMaterial3.Negative">
<item name="background">@drawable/dialog_alert_button_negative</item>
<item name="minWidth">@dimen/dialog_btn_negative_width</item>
<item name="minHeight">@dimen/dialog_btn_negative_height</item>
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index af690f4..897fc54 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -677,8 +677,4 @@
<permission name="android.permission.BATTERY_STATS"/>
<permission name="android.permission.ENTER_TRADE_IN_MODE"/>
</privapp-permissions>
-
- <privapp-permissions package="com.android.multiuser">
- <permission name="android.permission.MANAGE_USERS"/>
- </privapp-permissions>
</permissions>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 1ba29a2..8ee087b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -910,6 +910,11 @@
"Unexpected bundle for " + mPipTransitionState);
break;
case PipTransitionState.EXITED_PIP:
+ // Save the PiP bounds in case, we re-enter the PiP with the same component.
+ float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
+ mPipBoundsState.getBounds());
+ mPipBoundsState.saveReentryState(snapFraction);
+
mPipTransitionState.setPipTaskToken(null);
mPipTransitionState.setPinnedTaskLeash(null);
break;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 1913e93..f482269 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -1575,8 +1575,6 @@
final DesktopModeTouchEventListener touchEventListener =
new DesktopModeTouchEventListener(taskInfo, taskPositioner);
- InputMethod inputMethod = DesktopModeEventLogger.getInputMethodFromMotionEvent(
- touchEventListener.mMotionEvent);
windowDecoration.setOnMaximizeOrRestoreClickListener(() -> {
onMaximizeOrRestore(taskInfo.taskId, "maximize_menu", ResizeTrigger.MAXIMIZE_MENU,
touchEventListener.mMotionEvent);
@@ -1587,11 +1585,15 @@
return Unit.INSTANCE;
});
windowDecoration.setOnLeftSnapClickListener(() -> {
- onSnapResize(taskInfo.taskId, /* isLeft= */ true, inputMethod);
+ onSnapResize(taskInfo.taskId, /* isLeft= */ true,
+ DesktopModeEventLogger.getInputMethodFromMotionEvent(
+ touchEventListener.mMotionEvent));
return Unit.INSTANCE;
});
windowDecoration.setOnRightSnapClickListener(() -> {
- onSnapResize(taskInfo.taskId, /* isLeft= */ false, inputMethod);
+ onSnapResize(taskInfo.taskId, /* isLeft= */ false,
+ DesktopModeEventLogger.getInputMethodFromMotionEvent(
+ touchEventListener.mMotionEvent));
return Unit.INSTANCE;
});
windowDecoration.setOnToDesktopClickListener(desktopModeTransitionSource -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
index 6f72d34..c8aff78 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -74,8 +74,7 @@
mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize, disabledEdge);
// Save touch areas for each edge.
- mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mResizeHandleEdgeInset,
- mDisabledEdge);
+ mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleEdgeOutset, mDisabledEdge);
}
/**
@@ -459,7 +458,7 @@
private final @NonNull DisabledEdge mDisabledEdge;
private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness,
- int resizeHandleEdgeInset, DisabledEdge disabledEdge) {
+ DisabledEdge disabledEdge) {
// Save touch areas for each edge.
mDisabledEdge = disabledEdge;
// Save touch areas for each edge.
@@ -471,16 +470,16 @@
mLeftEdgeBounds = new Rect(
-resizeHandleThickness,
0,
- resizeHandleEdgeInset,
+ resizeHandleThickness,
taskSize.getHeight());
mRightEdgeBounds = new Rect(
- taskSize.getWidth() - resizeHandleEdgeInset,
+ taskSize.getWidth() - resizeHandleThickness,
0,
taskSize.getWidth() + resizeHandleThickness,
taskSize.getHeight());
mBottomEdgeBounds = new Rect(
-resizeHandleThickness,
- taskSize.getHeight() - resizeHandleEdgeInset,
+ taskSize.getHeight() - resizeHandleThickness,
taskSize.getWidth() + resizeHandleThickness,
taskSize.getHeight() + resizeHandleThickness);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
index e7d328e..479f156 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
@@ -63,6 +63,7 @@
private static final int EDGE_RESIZE_HANDLE_INSET = 4;
private static final int FINE_CORNER_SIZE = EDGE_RESIZE_THICKNESS * 2 + 10;
private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10;
+ private static final int SMALL_OFFSET = 10;
private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry(
TASK_CORNER_RADIUS, TASK_SIZE, EDGE_RESIZE_THICKNESS, EDGE_RESIZE_HANDLE_INSET,
FINE_CORNER_SIZE, LARGE_CORNER_SIZE, DragResizeWindowGeometry.DisabledEdge.NONE);
@@ -147,15 +148,19 @@
assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isTrue();
assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isTrue();
// Vertically along the edge is not contained.
- assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isFalse();
- assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS + 10)).isFalse();
+ assertThat(
+ region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS - SMALL_OFFSET)).isFalse();
+ assertThat(
+ region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS + SMALL_OFFSET)).isFalse();
}
private static void verifyVerticalEdge(@NonNull Region region, @NonNull Point point) {
assertThat(region.contains(point.x, point.y)).isTrue();
// Horizontally along the edge is not contained.
- assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isFalse();
- assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isFalse();
+ assertThat(
+ region.contains(point.x + EDGE_RESIZE_THICKNESS + SMALL_OFFSET, point.y)).isFalse();
+ assertThat(
+ region.contains(point.x - EDGE_RESIZE_THICKNESS - SMALL_OFFSET, point.y)).isFalse();
// Vertically along the edge is contained.
assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isTrue();
assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS)).isTrue();
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index d8a8c8b..bbe8e4e 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -141,6 +141,14 @@
}
flag {
+ name: "enable_route_visibility_control_api"
+ namespace: "media_better_together"
+ description: "API changes to allow more control over route visibility by route providers"
+ bug: "367799834"
+ is_exported: true
+}
+
+flag {
name: "enable_screen_off_scanning"
is_exported: true
namespace: "media_solutions"
diff --git a/packages/StatementService/Android.bp b/packages/StatementService/Android.bp
index 90e1808..39b0302 100644
--- a/packages/StatementService/Android.bp
+++ b/packages/StatementService/Android.bp
@@ -38,8 +38,10 @@
"StatementServiceParser",
"androidx.appcompat_appcompat",
"androidx.collection_collection-ktx",
+ "androidx.room_room-runtime",
"androidx.work_work-runtime",
"androidx.work_work-runtime-ktx",
"kotlinx-coroutines-android",
],
+ plugins: ["androidx.room_room-compiler-plugin"],
}
diff --git a/packages/StatementService/src/com/android/statementservice/database/Converters.kt b/packages/StatementService/src/com/android/statementservice/database/Converters.kt
new file mode 100644
index 0000000..21ecc8b
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/database/Converters.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.statementservice.database
+
+import android.content.UriRelativeFilter
+import android.content.UriRelativeFilterGroup
+import android.util.JsonReader
+import androidx.room.TypeConverter
+import org.json.JSONArray
+import org.json.JSONObject
+import java.io.StringReader
+import java.util.ArrayList
+
+class Converters {
+ companion object {
+ private const val ACTION_NAME = "action"
+ private const val FILTERS_NAME = "filters"
+ private const val URI_PART_NAME = "uriPart"
+ private const val PATTERN_TYPE_NAME = "patternType"
+ private const val FILTER_NAME = "filter"
+ }
+
+ @TypeConverter
+ fun groupsToJson(groups: List<UriRelativeFilterGroup>): String {
+ val json = JSONArray()
+ for (group in groups) {
+ json.put(groupToJson(group))
+ }
+ return json.toString()
+ }
+
+ @TypeConverter
+ fun stringToGroups(json: String): List<UriRelativeFilterGroup> {
+ val groups = ArrayList<UriRelativeFilterGroup>()
+ StringReader(json).use { stringReader ->
+ JsonReader(stringReader).use { reader ->
+ reader.beginArray()
+ while (reader.hasNext()) {
+ groups.add(parseGroup(reader))
+ }
+ reader.endArray()
+ }
+ }
+ return groups
+ }
+
+ private fun groupToJson(group: UriRelativeFilterGroup): JSONObject {
+ val jsonObject = JSONObject()
+ jsonObject.put(ACTION_NAME, group.action)
+ val filters = JSONArray()
+ for (filter in group.uriRelativeFilters) {
+ filters.put(filterToJson(filter))
+ }
+ jsonObject.put(FILTERS_NAME, filters)
+ return jsonObject
+ }
+
+ private fun filterToJson(filter: UriRelativeFilter): JSONObject {
+ val jsonObject = JSONObject()
+ jsonObject.put(URI_PART_NAME, filter.uriPart)
+ jsonObject.put(PATTERN_TYPE_NAME, filter.patternType)
+ jsonObject.put(FILTER_NAME, filter.filter)
+ return jsonObject
+ }
+
+ private fun parseGroup(reader: JsonReader): UriRelativeFilterGroup {
+ val jsonObject = JSONObject()
+ reader.beginObject()
+ while (reader.hasNext()) {
+ val name = reader.nextName()
+ when (name) {
+ ACTION_NAME -> jsonObject.put(ACTION_NAME, reader.nextInt())
+ FILTERS_NAME -> jsonObject.put(FILTERS_NAME, parseFilters(reader))
+ else -> reader.skipValue()
+ }
+ }
+ reader.endObject()
+
+ val group = UriRelativeFilterGroup(jsonObject.getInt(ACTION_NAME))
+ val filters = jsonObject.getJSONArray(FILTERS_NAME)
+ for (i in 0 until filters.length()) {
+ val filter = filters.getJSONObject(i)
+ group.addUriRelativeFilter(UriRelativeFilter(
+ filter.getInt(URI_PART_NAME),
+ filter.getInt(PATTERN_TYPE_NAME),
+ filter.getString(FILTER_NAME)
+ ))
+ }
+ return group
+ }
+
+ private fun parseFilters(reader: JsonReader): JSONArray {
+ val filters = JSONArray()
+ reader.beginArray()
+ while (reader.hasNext()) {
+ filters.put(parseFilter(reader))
+ }
+ reader.endArray()
+ return filters
+ }
+
+ private fun parseFilter(reader: JsonReader): JSONObject {
+ reader.beginObject()
+ val jsonObject = JSONObject()
+ while (reader.hasNext()) {
+ val name = reader.nextName()
+ when (name) {
+ URI_PART_NAME, PATTERN_TYPE_NAME -> jsonObject.put(name, reader.nextInt())
+ FILTER_NAME -> jsonObject.put(name, reader.nextString())
+ else -> reader.skipValue()
+ }
+ }
+ reader.endObject()
+ return jsonObject
+ }
+}
\ No newline at end of file
diff --git a/packages/StatementService/src/com/android/statementservice/database/DomainGroups.kt b/packages/StatementService/src/com/android/statementservice/database/DomainGroups.kt
new file mode 100644
index 0000000..c616669
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/database/DomainGroups.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.statementservice.database
+
+import android.content.UriRelativeFilterGroup
+import androidx.room.Entity
+
+@Entity(primaryKeys = ["packageName", "domain"])
+data class DomainGroups(
+ val packageName: String,
+ val domain: String,
+ val groups: List<UriRelativeFilterGroup>
+)
\ No newline at end of file
diff --git a/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDao.kt b/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDao.kt
new file mode 100644
index 0000000..3b4dcea
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDao.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.statementservice.database
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+
+@Dao
+interface DomainGroupsDao {
+ @Query("SELECT * FROM DomainGroups WHERE packageName = :packageName")
+ fun getDomainGroups(packageName: String): List<DomainGroups>
+
+ @Insert
+ fun insertDomainGroups(vararg domainGroups: DomainGroups)
+
+ @Query("DELETE FROM DomainGroups WHERE packageName = :packageName AND domain = :domain")
+ fun clear(packageName: String, domain: String)
+
+ @Query("DELETE FROM DomainGroups WHERE packageName = :packageName")
+ fun clear(packageName: String)
+}
\ No newline at end of file
diff --git a/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDatabase.kt b/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDatabase.kt
new file mode 100644
index 0000000..39833f6
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/database/DomainGroupsDatabase.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.statementservice.database
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+
+@Database(entities = [DomainGroups::class], version = 1)
+@TypeConverters(Converters::class)
+abstract class DomainGroupsDatabase : RoomDatabase() {
+ companion object {
+ private const val DATABASE_NAME = "domain-groups"
+ @Volatile
+ private var instance: DomainGroupsDatabase? = null
+
+ fun getInstance(context: Context) = instance ?: synchronized(this) {
+ instance ?: Room.databaseBuilder(
+ context,
+ DomainGroupsDatabase::class.java, DATABASE_NAME
+ ).build().also { instance = it }
+ }
+ }
+ abstract fun domainGroupsDao(): DomainGroupsDao
+}
\ No newline at end of file
diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt
index acb54f6..0d7a1fd 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerificationReceiverV1.kt
@@ -22,6 +22,7 @@
import androidx.work.ExistingWorkPolicy
import androidx.work.WorkManager
import com.android.statementservice.domain.worker.CollectV1Worker
+import com.android.statementservice.domain.worker.GroupUpdateV1Worker
import com.android.statementservice.domain.worker.SingleV1RequestWorker
/**
@@ -67,7 +68,7 @@
}
}
- //clear sp before enqueue unique work since policy is REPLACE
+ // clear sp before enqueue unique work since policy is REPLACE
val deContext = context.createDeviceProtectedStorageContext()
val editor = deContext?.getSharedPreferences(packageName, Context.MODE_PRIVATE)?.edit()
editor?.clear()?.apply()
@@ -78,6 +79,7 @@
workRequests
)
.then(CollectV1Worker.buildRequest(verificationId, packageName))
+ .then(GroupUpdateV1Worker.buildRequest(packageName))
.enqueue()
}
}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt b/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt
index 29f844f..6914347 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/DomainVerifier.kt
@@ -24,6 +24,7 @@
import com.android.statementservice.network.retriever.StatementRetriever
import com.android.statementservice.retriever.AbstractAsset
import com.android.statementservice.retriever.AbstractAssetMatcher
+import com.android.statementservice.retriever.Statement
import com.android.statementservice.utils.Result
import com.android.statementservice.utils.StatementUtils
import com.android.statementservice.utils.component1
@@ -87,10 +88,10 @@
host: String,
packageName: String,
network: Network? = null
- ): Pair<WorkResult, VerifyStatus> {
+ ): Triple<WorkResult, VerifyStatus, Statement?> {
val assetMatcher = synchronized(targetAssetCache) { targetAssetCache[packageName] }
.takeIf { it!!.isPresent }
- ?: return WorkResult.failure() to VerifyStatus.FAILURE_PACKAGE_MANAGER
+ ?: return Triple(WorkResult.failure(), VerifyStatus.FAILURE_PACKAGE_MANAGER, null)
return verifyHost(host, assetMatcher.get(), network)
}
@@ -98,34 +99,34 @@
host: String,
assetMatcher: AbstractAssetMatcher,
network: Network? = null
- ): Pair<WorkResult, VerifyStatus> {
+ ): Triple<WorkResult, VerifyStatus, Statement?> {
var exception: Exception? = null
val resultAndStatus = try {
val sourceAsset = StatementUtils.createWebAssetString(host)
.let(AbstractAsset::create)
val result = retriever.retrieve(sourceAsset, network)
- ?: return WorkResult.success() to VerifyStatus.FAILURE_UNKNOWN
+ ?: return Triple(WorkResult.success(), VerifyStatus.FAILURE_UNKNOWN, null)
when (result.responseCode) {
HttpURLConnection.HTTP_MOVED_PERM,
HttpURLConnection.HTTP_MOVED_TEMP -> {
- WorkResult.failure() to VerifyStatus.FAILURE_REDIRECT
+ Triple(WorkResult.failure(), VerifyStatus.FAILURE_REDIRECT, null)
}
else -> {
- val isVerified = result.statements.any { statement ->
+ val statement = result.statements.firstOrNull { statement ->
(StatementUtils.RELATION.matches(statement.relation) &&
assetMatcher.matches(statement.target))
}
- if (isVerified) {
- WorkResult.success() to VerifyStatus.SUCCESS
+ if (statement != null) {
+ Triple(WorkResult.success(), VerifyStatus.SUCCESS, statement)
} else {
- WorkResult.failure() to VerifyStatus.FAILURE_REJECTED_BY_SERVER
+ Triple(WorkResult.failure(), VerifyStatus.FAILURE_REJECTED_BY_SERVER, statement)
}
}
}
} catch (e: Exception) {
exception = e
- WorkResult.retry() to VerifyStatus.FAILURE_UNKNOWN
+ Triple(WorkResult.retry(), VerifyStatus.FAILURE_UNKNOWN, null)
}
if (DEBUG) {
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt
index a17f9c9..64d2d98 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/BaseRequestWorker.kt
@@ -17,9 +17,12 @@
package com.android.statementservice.domain.worker
import android.content.Context
+import android.content.UriRelativeFilterGroup
+import android.content.pm.verify.domain.DomainVerificationInfo
import android.content.pm.verify.domain.DomainVerificationManager
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
+import com.android.statementservice.database.DomainGroupsDatabase
import com.android.statementservice.domain.DomainVerifier
abstract class BaseRequestWorker(
@@ -27,8 +30,19 @@
protected val params: WorkerParameters
) : CoroutineWorker(appContext, params) {
+ protected val database = DomainGroupsDatabase.getInstance(appContext).domainGroupsDao()
+
protected val verificationManager =
appContext.getSystemService(DomainVerificationManager::class.java)!!
protected val verifier = DomainVerifier.getInstance(appContext)
+
+ protected fun updateUriRelativeFilterGroups(packageName: String, domainGroupUpdates: Map<String, List<UriRelativeFilterGroup>>) {
+ val verifiedDomains = verificationManager.getDomainVerificationInfo(packageName)?.hostToStateMap?.filterValues {
+ it == DomainVerificationInfo.STATE_SUCCESS || it == DomainVerificationInfo.STATE_MODIFIABLE_VERIFIED
+ }?.keys?.toList() ?: emptyList()
+ val domainGroups = verificationManager.getUriRelativeFilterGroups(packageName, verifiedDomains)
+ domainGroupUpdates.forEach { (domain, groups) -> domainGroups[domain] = groups }
+ verificationManager.setUriRelativeFilterGroups(packageName, domainGroups)
+ }
}
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/GroupUpdateV1Worker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/GroupUpdateV1Worker.kt
new file mode 100644
index 0000000..f53dfc4
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/GroupUpdateV1Worker.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.statementservice.domain.worker
+
+import android.content.Context
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.coroutineScope
+
+class GroupUpdateV1Worker(appContext: Context, params: WorkerParameters) :
+ BaseRequestWorker(appContext, params) {
+
+ companion object {
+
+ private const val PACKAGE_NAME_KEY = "packageName"
+
+ fun buildRequest(packageName: String) = OneTimeWorkRequestBuilder<GroupUpdateV1Worker>()
+ .setInputData(
+ Data.Builder()
+ .putString(PACKAGE_NAME_KEY, packageName)
+ .build()
+ )
+ .build()
+ }
+
+ override suspend fun doWork() = coroutineScope {
+ val packageName = params.inputData.getString(PACKAGE_NAME_KEY)!!
+ updateUriRelativeFilterGroups(packageName)
+ Result.success()
+ }
+
+ private fun updateUriRelativeFilterGroups(packageName: String) {
+ val groupUpdates = database.getDomainGroups(packageName)
+ updateUriRelativeFilterGroups(
+ packageName,
+ groupUpdates.associateBy({it.domain}, {it.groups})
+ )
+ database.clear(packageName)
+ }
+}
\ No newline at end of file
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt
index 61ab2c2..f83601a 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/RetryRequestWorker.kt
@@ -17,10 +17,13 @@
package com.android.statementservice.domain.worker
import android.content.Context
+import android.content.UriRelativeFilterGroup
+import android.content.pm.verify.domain.DomainVerificationManager
import androidx.work.NetworkType
import androidx.work.WorkerParameters
import com.android.statementservice.domain.VerifyStatus
import com.android.statementservice.utils.AndroidUtils
+import com.android.statementservice.utils.StatementUtils
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
@@ -36,7 +39,13 @@
params: WorkerParameters
) : BaseRequestWorker(appContext, params) {
- data class VerifyResult(val domainSetId: UUID, val host: String, val status: VerifyStatus)
+ data class VerifyResult(
+ val domainSetId: UUID,
+ val host: String,
+ val status: VerifyStatus,
+ val packageName: String,
+ val groups: List<UriRelativeFilterGroup>
+ )
override suspend fun doWork() = coroutineScope {
if (!AndroidUtils.isReceiverV2Enabled(appContext)) {
@@ -49,8 +58,11 @@
.map { (domainSetId, packageName, host) ->
async {
if (isActive && !isStopped) {
- val (_, status) = verifier.verifyHost(host, packageName, params.network)
- VerifyResult(domainSetId, host, status)
+ val (_, status, statement) = verifier.verifyHost(host, packageName, params.network)
+ val groups = statement?.dynamicAppLinkComponents.orEmpty().map {
+ StatementUtils.createUriRelativeFilterGroup(it)
+ }
+ VerifyResult(domainSetId, host, status, packageName, groups)
} else {
// If the job gets cancelled, stop the remaining hosts, but continue the
// job to commit the results for hosts that were already requested.
@@ -60,17 +72,25 @@
}
.awaitAll()
.filterNotNull() // TODO(b/159952358): Fast fail packages which can't be retrieved.
- .groupBy { it.domainSetId }
- .forEach { (domainSetId, resultsById) ->
- resultsById.groupBy { it.status }
- .mapValues { it.value.map(VerifyResult::host).toSet() }
- .forEach { (status, hosts) ->
- verificationManager.setDomainVerificationStatus(
- domainSetId,
- hosts,
- status.value
- )
+ .groupBy { it.packageName }
+ .forEach { (packageName, resultsByName) ->
+ val groupUpdates = mutableMapOf<String, List<UriRelativeFilterGroup>>()
+ resultsByName.groupBy { it.domainSetId }
+ .forEach { (domainSetId, resultsById) ->
+ resultsById.groupBy { it.status }
+ .forEach { (status, verifyResults) ->
+ val error = verificationManager.setDomainVerificationStatus(
+ domainSetId,
+ verifyResults.map(VerifyResult::host).toSet(),
+ status.value
+ )
+ if (error == DomainVerificationManager.STATUS_OK
+ && status == VerifyStatus.SUCCESS) {
+ verifyResults.forEach { groupUpdates[it.host] = it.groups }
+ }
+ }
}
+ updateUriRelativeFilterGroups(packageName, groupUpdates)
}
// Succeed regardless of results since this retry is best effort and not required
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt
index 7a198cb..253a162 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV1RequestWorker.kt
@@ -22,7 +22,9 @@
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters
+import com.android.statementservice.database.DomainGroups
import com.android.statementservice.utils.AndroidUtils
+import com.android.statementservice.utils.StatementUtils
import kotlinx.coroutines.coroutineScope
class SingleV1RequestWorker(appContext: Context, params: WorkerParameters) :
@@ -60,7 +62,9 @@
val packageName = params.inputData.getString(PACKAGE_NAME_KEY)!!
val host = params.inputData.getString(HOST_KEY)!!
- val (result, status) = verifier.verifyHost(host, packageName, params.network)
+ database.clear(packageName, host)
+
+ val (result, status, statement) = verifier.verifyHost(host, packageName, params.network)
if (DEBUG) {
Log.d(
@@ -75,6 +79,10 @@
val deContext = appContext.createDeviceProtectedStorageContext()
val sp = deContext?.getSharedPreferences(packageName, Context.MODE_PRIVATE)
sp?.edit()?.putInt("$HOST_SUCCESS_PREFIX$host", status.value)?.apply()
+ val groups = statement?.dynamicAppLinkComponents.orEmpty().map {
+ StatementUtils.createUriRelativeFilterGroup(it)
+ }
+ database.insertDomainGroups(DomainGroups(packageName, host, groups))
Result.success()
}
is Result.Failure -> {
diff --git a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt
index 562b132..8b1347a 100644
--- a/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt
+++ b/packages/StatementService/src/com/android/statementservice/domain/worker/SingleV2RequestWorker.kt
@@ -22,6 +22,7 @@
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters
import com.android.statementservice.utils.AndroidUtils
+import com.android.statementservice.utils.StatementUtils
import kotlinx.coroutines.coroutineScope
import java.util.UUID
@@ -59,9 +60,13 @@
val packageName = params.inputData.getString(PACKAGE_NAME_KEY)!!
val host = params.inputData.getString(HOST_KEY)!!
- val (result, status) = verifier.verifyHost(host, packageName, params.network)
+ val (result, status, statement) = verifier.verifyHost(host, packageName, params.network)
verificationManager.setDomainVerificationStatus(domainSetId, setOf(host), status.value)
+ val groups = statement?.dynamicAppLinkComponents.orEmpty().map {
+ StatementUtils.createUriRelativeFilterGroup(it)
+ }
+ updateUriRelativeFilterGroups(packageName, mapOf(host to groups))
result
}
diff --git a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt
index ad137400..d10cb0f 100644
--- a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt
+++ b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt
@@ -39,6 +39,11 @@
private const val FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string."
private const val FIELD_NOT_ARRAY_FORMAT_STRING = "Expected %s to be array."
+ private const val COMMENTS_NAME = "comments"
+ private const val EXCLUDE_NAME = "exclude"
+ private const val FRAGMENT_NAME = "#"
+ private const val QUERY_NAME = "?"
+ private const val PATH_NAME = "/"
/**
* Parses a JSON array of statements.
@@ -99,9 +104,7 @@
FIELD_NOT_ARRAY_FORMAT_STRING.format(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION)
)
val target = AssetFactory.create(targetObject)
- val dynamicAppLinkComponents = parseDynamicAppLinkComponents(
- statement.optJSONObject(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION_EXTENSIONS)
- )
+ val dynamicAppLinkComponents = parseDynamicAppLinkComponents(statement)
val statements = (0 until relations.length())
.map { relations.getString(it) }
@@ -129,13 +132,13 @@
}
private fun parseComponent(component: JSONObject): DynamicAppLinkComponent {
- val query = component.optJSONObject("?")
+ val query = component.optJSONObject(QUERY_NAME)
return DynamicAppLinkComponent.create(
- component.optBoolean("exclude", false),
- component.optString("#"),
- component.optString("/"),
+ component.optBoolean(EXCLUDE_NAME, false),
+ if (component.has(FRAGMENT_NAME)) component.getString(FRAGMENT_NAME) else null,
+ if (component.has(PATH_NAME)) component.getString(PATH_NAME) else null,
query?.keys()?.asSequence()?.associateWith { query.getString(it) },
- component.optString("comments")
+ component.optString(COMMENTS_NAME)
)
}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java b/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java
index dc27e12..c32f194 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java
@@ -130,7 +130,7 @@
@Override
public String toString() {
StringBuilder statement = new StringBuilder();
- statement.append("HandleAllUriRule: ");
+ statement.append("DynamicAppLinkComponent: ");
statement.append(mExclude);
statement.append(", ");
statement.append(mFragment);
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java b/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java
index 7635e82..ab1853c 100644
--- a/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java
+++ b/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java
@@ -24,8 +24,6 @@
import org.json.JSONObject;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
/**
* A helper class that creates a {@link JSONObject} from a {@link JsonReader}.
@@ -48,7 +46,7 @@
JsonToken token = reader.peek();
if (token.equals(JsonToken.BEGIN_ARRAY)) {
- output.put(fieldName, new JSONArray(parseArray(reader)));
+ output.put(fieldName, parseArray(reader));
} else if (token.equals(JsonToken.STRING)) {
output.put(fieldName, reader.nextString());
} else if (token.equals(JsonToken.BEGIN_OBJECT)) {
@@ -57,9 +55,11 @@
} catch (JSONException e) {
errorMsg = e.getMessage();
}
+ } else if (token.equals(JsonToken.BOOLEAN)) {
+ output.put(fieldName, reader.nextBoolean());
} else {
reader.skipValue();
- errorMsg = "Unsupported value type.";
+ errorMsg = "Unsupported value type: " + token;
}
}
reader.endObject();
@@ -72,17 +72,36 @@
}
/**
- * Parses one string array from the {@link JsonReader}.
+ * Parses one JSON array from the {@link JsonReader}.
*/
- public static List<String> parseArray(JsonReader reader) throws IOException {
- ArrayList<String> output = new ArrayList<>();
+ public static JSONArray parseArray(JsonReader reader) throws IOException, JSONException {
+ JSONArray output = new JSONArray();
+ String errorMsg = null;
reader.beginArray();
while (reader.hasNext()) {
- output.add(reader.nextString());
+ JsonToken token = reader.peek();
+ if (token.equals(JsonToken.BEGIN_ARRAY)) {
+ output.put(parseArray(reader));
+ } else if (token.equals(JsonToken.STRING)) {
+ output.put(reader.nextString());
+ } else if (token.equals(JsonToken.BEGIN_OBJECT)) {
+ try {
+ output.put(parse(reader));
+ } catch (JSONException e) {
+ errorMsg = e.getMessage();
+ }
+ } else {
+ reader.skipValue();
+ errorMsg = "Unsupported value type: " + token;
+ }
}
reader.endArray();
+ if (errorMsg != null) {
+ throw new JSONException(errorMsg);
+ }
+
return output;
}
}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
index c71ef83..129dd9b 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
@@ -36,7 +36,6 @@
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
-import com.android.systemui.accessibility.accessibilitymenu.Flags;
import com.android.systemui.accessibility.accessibilitymenu.R;
/**
@@ -62,10 +61,8 @@
((TextView) findViewById(R.id.action_bar_title)).setText(
getResources().getString(R.string.accessibility_menu_settings_name)
);
- if (Flags.actionBarWrapContent()) {
- setHeightWrapContent(findViewById(com.android.internal.R.id.action_bar));
- setHeightWrapContent(findViewById(com.android.internal.R.id.action_bar_container));
- }
+ setHeightWrapContent(findViewById(com.android.internal.R.id.action_bar));
+ setHeightWrapContent(findViewById(com.android.internal.R.id.action_bar_container));
}
private void setHeightWrapContent(View view) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
index 82bcece..55b87db 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
@@ -27,6 +27,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.EnableSceneContainer
@@ -72,8 +73,10 @@
@Test
@DisableFlags(DualShade.FLAG_NAME)
- fun actions_singleShade() =
+ fun actions_communalNotAvailable_singleShade() =
testScope.runTest {
+ kosmos.setCommunalAvailable(false)
+
val actions by collectLastValue(underTest.actions)
setUpState(
@@ -85,6 +88,8 @@
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))
+ assertThat(actions?.get(Swipe.Start)).isNull()
+ assertThat(actions?.get(Swipe.End)).isNull()
setUpState(
isShadeTouchable = false,
@@ -102,12 +107,16 @@
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))
+ assertThat(actions?.get(Swipe.Start)).isNull()
+ assertThat(actions?.get(Swipe.End)).isNull()
}
@Test
@DisableFlags(DualShade.FLAG_NAME)
- fun actions_splitShade() =
+ fun actions_communalNotAvailable_splitShade() =
testScope.runTest {
+ kosmos.setCommunalAvailable(false)
+
val actions by collectLastValue(underTest.actions)
setUpState(
@@ -119,6 +128,8 @@
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))
+ assertThat(actions?.get(Swipe.Start)).isNull()
+ assertThat(actions?.get(Swipe.End)).isNull()
setUpState(
isShadeTouchable = false,
@@ -136,12 +147,16 @@
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))
+ assertThat(actions?.get(Swipe.Start)).isNull()
+ assertThat(actions?.get(Swipe.End)).isNull()
}
@Test
@EnableFlags(DualShade.FLAG_NAME)
- fun actions_dualShade() =
+ fun actions_communalNotAvailable_dualShade() =
testScope.runTest {
+ kosmos.setCommunalAvailable(false)
+
val actions by collectLastValue(underTest.actions)
setUpState(
@@ -155,6 +170,8 @@
.isEqualTo(
UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true)
)
+ assertThat(actions?.get(Swipe.Start)).isNull()
+ assertThat(actions?.get(Swipe.End)).isNull()
setUpState(
isShadeTouchable = false,
@@ -170,6 +187,128 @@
.isEqualTo(
UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true)
)
+ assertThat(actions?.get(Swipe.Start)).isNull()
+ assertThat(actions?.get(Swipe.End)).isNull()
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun actions_communalAvailable_singleShade() =
+ testScope.runTest {
+ kosmos.setCommunalAvailable(true)
+
+ val actions by collectLastValue(underTest.actions)
+
+ setUpState(
+ isShadeTouchable = true,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Single,
+ )
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+ assertThat(actions?.get(Swipe.Down))
+ .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))
+ assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+ assertThat(actions?.get(Swipe.End)).isNull()
+
+ setUpState(
+ isShadeTouchable = false,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Single,
+ )
+ assertThat(actions).isEmpty()
+
+ setUpState(
+ isShadeTouchable = true,
+ isDeviceUnlocked = true,
+ shadeMode = ShadeMode.Single,
+ )
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+ assertThat(actions?.get(Swipe.Down))
+ .isEqualTo(UserActionResult(Scenes.Shade, isIrreversible = true))
+ assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+ assertThat(actions?.get(Swipe.End)).isNull()
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun actions_communalAvailable_splitShade() =
+ testScope.runTest {
+ kosmos.setCommunalAvailable(true)
+
+ val actions by collectLastValue(underTest.actions)
+
+ setUpState(
+ isShadeTouchable = true,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Split,
+ )
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+ assertThat(actions?.get(Swipe.Down))
+ .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))
+ assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+ assertThat(actions?.get(Swipe.End)).isNull()
+
+ setUpState(
+ isShadeTouchable = false,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Split,
+ )
+ assertThat(actions).isEmpty()
+
+ setUpState(
+ isShadeTouchable = true,
+ isDeviceUnlocked = true,
+ shadeMode = ShadeMode.Split,
+ )
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+ assertThat(actions?.get(Swipe.Down))
+ .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade, isIrreversible = true))
+ assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+ assertThat(actions?.get(Swipe.End)).isNull()
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun actions_communalAvailable_dualShade() =
+ testScope.runTest {
+ kosmos.setCommunalAvailable(true)
+
+ val actions by collectLastValue(underTest.actions)
+
+ setUpState(
+ isShadeTouchable = true,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Dual,
+ )
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
+ assertThat(actions?.get(Swipe.Down))
+ .isEqualTo(
+ UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true)
+ )
+ assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+ assertThat(actions?.get(Swipe.End)).isNull()
+
+ setUpState(
+ isShadeTouchable = false,
+ isDeviceUnlocked = false,
+ shadeMode = ShadeMode.Dual,
+ )
+ assertThat(actions).isEmpty()
+
+ setUpState(isShadeTouchable = true, isDeviceUnlocked = true, shadeMode = ShadeMode.Dual)
+ assertThat(actions).isNotEmpty()
+ assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
+ assertThat(actions?.get(Swipe.Down))
+ .isEqualTo(
+ UserActionResult.ShowOverlay(Overlays.NotificationsShade, isIrreversible = true)
+ )
+ assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
+ assertThat(actions?.get(Swipe.End)).isNull()
}
private fun TestScope.setUpState(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
index f045404..0d32b7f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt
@@ -18,36 +18,31 @@
import android.content.Context
import android.content.Context.INPUT_SERVICE
-import android.hardware.input.InputGestureData
-import android.hardware.input.InputGestureData.createKeyTrigger
-import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS
+import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
import android.hardware.input.fakeInputManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
-import android.view.KeyEvent.KEYCODE_A
import android.view.KeyEvent.KEYCODE_SLASH
-import android.view.KeyEvent.META_ALT_ON
import android.view.KeyEvent.META_CAPS_LOCK_ON
-import android.view.KeyEvent.META_CTRL_ON
-import android.view.KeyEvent.META_FUNCTION_ON
-import android.view.KeyEvent.META_META_LEFT_ON
import android.view.KeyEvent.META_META_ON
-import android.view.KeyEvent.META_SHIFT_ON
-import android.view.KeyEvent.META_SHIFT_RIGHT_ON
-import android.view.KeyEvent.META_SYM_ON
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.hardware.input.Flags.FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES
import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult
import com.android.systemui.keyboard.shortcut.customShortcutCategoriesRepository
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.ALL_SUPPORTED_MODIFIERS
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsInputGestureData
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allCustomizableInputGesturesWithSimpleShortcutCombinations
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.customizableInputGestureWithUnknownKeyGestureType
import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.expectedShortcutCategoriesWithSimpleShortcutCombination
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardAddCustomShortcutRequestInfo
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardDeleteCustomShortcutRequestInfo
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardKeyCombination
import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
import com.android.systemui.kosmos.testScope
@@ -74,24 +69,21 @@
it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext })
}
- private val fakeInputManager = kosmos.fakeInputManager
+ private val inputManager = kosmos.fakeInputManager.inputManager
private val testScope = kosmos.testScope
private val helper = kosmos.shortcutHelperTestHelper
private val repo = kosmos.customShortcutCategoriesRepository
@Before
fun setup() {
- whenever(mockUserContext.getSystemService(INPUT_SERVICE))
- .thenReturn(fakeInputManager.inputManager)
+ whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager)
}
@Test
@EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
fun categories_emitsCorrectlyConvertedShortcutCategories() {
testScope.runTest {
- whenever(
- fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
- )
+ whenever(inputManager.getCustomInputGestures(/* filter= */ anyOrNull()))
.thenReturn(allCustomizableInputGesturesWithSimpleShortcutCombinations)
helper.toggle(deviceId = 123)
@@ -106,9 +98,7 @@
@DisableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
fun categories_emitsEmptyListWhenFlagIsDisabled() {
testScope.runTest {
- whenever(
- fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
- )
+ whenever(inputManager.getCustomInputGestures(/* filter= */ anyOrNull()))
.thenReturn(allCustomizableInputGesturesWithSimpleShortcutCombinations)
helper.toggle(deviceId = 123)
@@ -122,9 +112,7 @@
@EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
fun categories_ignoresUnknownKeyGestureTypes() {
testScope.runTest {
- whenever(
- fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())
- )
+ whenever(inputManager.getCustomInputGestures(/* filter= */ anyOrNull()))
.thenReturn(customizableInputGestureWithUnknownKeyGestureType)
helper.toggle(deviceId = 123)
@@ -151,7 +139,7 @@
helper.toggle(deviceId = 123)
val pressedKeys by collectLastValue(repo.pressedKeys)
repo.updateUserKeyCombination(
- KeyCombination(modifiers = allSupportedModifiers, keyCode = null)
+ KeyCombination(modifiers = ALL_SUPPORTED_MODIFIERS, keyCode = null)
)
assertThat(pressedKeys)
@@ -199,11 +187,11 @@
@Test
fun shortcutBeingCustomized_updatedOnCustomizationRequested() {
testScope.runTest {
- repo.onCustomizationRequested(standardCustomizationRequestInfo)
+ repo.onCustomizationRequested(standardAddCustomShortcutRequestInfo)
val shortcutBeingCustomized = repo.getShortcutBeingCustomized()
- assertThat(shortcutBeingCustomized).isEqualTo(standardCustomizationRequestInfo)
+ assertThat(shortcutBeingCustomized).isEqualTo(standardAddCustomShortcutRequestInfo)
}
}
@@ -223,7 +211,7 @@
fun buildInputGestureDataForShortcutBeingCustomized_noKeyCombinationSelected_returnsNull() {
testScope.runTest {
helper.toggle(deviceId = 123)
- repo.onCustomizationRequested(standardCustomizationRequestInfo)
+ repo.onCustomizationRequested(standardAddCustomShortcutRequestInfo)
val inputGestureData = repo.buildInputGestureDataForShortcutBeingCustomized()
@@ -235,46 +223,32 @@
fun buildInputGestureDataForShortcutBeingCustomized_successfullyBuildInputGestureData() {
testScope.runTest {
helper.toggle(deviceId = 123)
- repo.onCustomizationRequested(standardCustomizationRequestInfo)
+ repo.onCustomizationRequested(standardAddCustomShortcutRequestInfo)
repo.updateUserKeyCombination(standardKeyCombination)
val inputGestureData = repo.buildInputGestureDataForShortcutBeingCustomized()
// using toString as we're testing for only structural equality not referential.
// inputGestureData is a java class and isEqual Tests for referential equality
// as well which would cause this assert to fail
- assertThat(inputGestureData.toString()).isEqualTo(standardInputGestureData.toString())
+ assertThat(inputGestureData.toString()).isEqualTo(allAppsInputGestureData.toString())
}
}
- private val standardCustomizationRequestInfo =
- ShortcutCustomizationRequestInfo.Add(
- label = "Open apps list",
- categoryType = ShortcutCategoryType.System,
- subCategoryLabel = "System controls",
- )
+ @Test
+ @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+ fun deleteShortcut_successfullyRetrievesGestureDataAndDeletesShortcut() {
+ testScope.runTest {
+ whenever(inputManager.getCustomInputGestures(anyOrNull()))
+ .thenReturn(listOf(allAppsInputGestureData, goHomeInputGestureData))
+ whenever(inputManager.removeCustomInputGesture(allAppsInputGestureData))
+ .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_SUCCESS)
- private val standardKeyCombination =
- KeyCombination(
- modifiers = META_META_ON or META_SHIFT_ON or META_META_LEFT_ON or META_SHIFT_RIGHT_ON,
- keyCode = KEYCODE_A,
- )
+ helper.toggle(deviceId = 123)
+ repo.onCustomizationRequested(standardDeleteCustomShortcutRequestInfo)
- private val allSupportedModifiers =
- META_META_ON or
- META_CTRL_ON or
- META_FUNCTION_ON or
- META_SHIFT_ON or
- META_ALT_ON or
- META_SYM_ON
+ val result = repo.deleteShortcutCurrentlyBeingCustomized()
- private val standardInputGestureData =
- InputGestureData.Builder()
- .setKeyGestureType(KEY_GESTURE_TYPE_ALL_APPS)
- .setTrigger(
- createKeyTrigger(
- /* keycode = */ standardKeyCombination.keyCode!!,
- /* modifierState = */ standardKeyCombination.modifiers and allSupportedModifiers,
- )
- )
- .build()
+ assertThat(result).isEqualTo(ShortcutCustomizationRequestResult.SUCCESS)
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
index a1e7ef4..8466eab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt
@@ -19,9 +19,23 @@
import android.hardware.input.InputGestureData
import android.hardware.input.InputGestureData.createKeyTrigger
import android.hardware.input.KeyGestureEvent
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS
+import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_HOME
+import android.os.SystemClock
import android.view.KeyEvent
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.KEYCODE_A
+import android.view.KeyEvent.META_ALT_ON
+import android.view.KeyEvent.META_CTRL_ON
+import android.view.KeyEvent.META_FUNCTION_ON
+import android.view.KeyEvent.META_META_LEFT_ON
+import android.view.KeyEvent.META_META_ON
+import android.view.KeyEvent.META_SHIFT_ON
+import android.view.KeyEvent.META_SHIFT_RIGHT_ON
+import android.view.KeyEvent.META_SYM_ON
import android.view.KeyboardShortcutGroup
import android.view.KeyboardShortcutInfo
+import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination
import com.android.systemui.keyboard.shortcut.shared.model.Shortcut
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
@@ -29,9 +43,11 @@
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand
+import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
import com.android.systemui.keyboard.shortcut.shared.model.shortcut
+import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState
import com.android.systemui.res.R
object TestShortcuts {
@@ -525,4 +541,110 @@
keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER
),
)
+
+ val standardAddCustomShortcutRequestInfo =
+ ShortcutCustomizationRequestInfo.Add(
+ label = "Open apps list",
+ categoryType = System,
+ subCategoryLabel = "System controls",
+ )
+
+ val standardDeleteCustomShortcutRequestInfo =
+ ShortcutCustomizationRequestInfo.Delete(
+ label = "Open apps list",
+ categoryType = System,
+ subCategoryLabel = "System controls",
+ )
+
+ val standardKeyCombination =
+ KeyCombination(
+ modifiers = META_META_ON or META_SHIFT_ON or META_META_LEFT_ON or META_SHIFT_RIGHT_ON,
+ keyCode = KEYCODE_A,
+ )
+
+ const val ALL_SUPPORTED_MODIFIERS =
+ META_META_ON or
+ META_CTRL_ON or
+ META_FUNCTION_ON or
+ META_SHIFT_ON or
+ META_ALT_ON or
+ META_SYM_ON
+
+ val allAppsInputGestureData: InputGestureData =
+ InputGestureData.Builder()
+ .setKeyGestureType(KEY_GESTURE_TYPE_ALL_APPS)
+ .setTrigger(
+ createKeyTrigger(
+ /* keycode = */ standardKeyCombination.keyCode!!,
+ /* modifierState = */ standardKeyCombination.modifiers and
+ ALL_SUPPORTED_MODIFIERS,
+ )
+ )
+ .build()
+
+ val goHomeInputGestureData: InputGestureData =
+ InputGestureData.Builder()
+ .setKeyGestureType(KEY_GESTURE_TYPE_HOME)
+ .setTrigger(
+ createKeyTrigger(
+ /* keycode = */ standardKeyCombination.keyCode!!,
+ /* modifierState = */ standardKeyCombination.modifiers and
+ ALL_SUPPORTED_MODIFIERS,
+ )
+ )
+ .build()
+
+ val expectedStandardDeleteShortcutUiState =
+ ShortcutCustomizationUiState.DeleteShortcutDialog(isDialogShowing = false)
+
+ val keyDownEventWithoutActionKeyPressed =
+ androidx.compose.ui.input.key.KeyEvent(
+ android.view.KeyEvent(
+ /* downTime = */ SystemClock.uptimeMillis(),
+ /* eventTime = */ SystemClock.uptimeMillis(),
+ /* action = */ ACTION_DOWN,
+ /* code = */ KEYCODE_A,
+ /* repeat = */ 0,
+ /* metaState = */ META_CTRL_ON,
+ )
+ )
+
+ val keyDownEventWithActionKeyPressed =
+ androidx.compose.ui.input.key.KeyEvent(
+ android.view.KeyEvent(
+ /* downTime = */ SystemClock.uptimeMillis(),
+ /* eventTime = */ SystemClock.uptimeMillis(),
+ /* action = */ ACTION_DOWN,
+ /* code = */ KEYCODE_A,
+ /* repeat = */ 0,
+ /* metaState = */ META_CTRL_ON or META_META_ON,
+ )
+ )
+
+ val keyUpEventWithActionKeyPressed =
+ androidx.compose.ui.input.key.KeyEvent(
+ android.view.KeyEvent(
+ /* downTime = */ SystemClock.uptimeMillis(),
+ /* eventTime = */ SystemClock.uptimeMillis(),
+ /* action = */ ACTION_DOWN,
+ /* code = */ KEYCODE_A,
+ /* repeat = */ 0,
+ /* metaState = */ 0,
+ )
+ )
+
+ val standardAddShortcutRequest =
+ ShortcutCustomizationRequestInfo.Add(
+ label = "Standard shortcut",
+ categoryType = ShortcutCategoryType.System,
+ subCategoryLabel = "Standard subcategory",
+ )
+
+ val expectedStandardAddShortcutUiState =
+ ShortcutCustomizationUiState.AddShortcutDialog(
+ shortcutLabel = "Standard shortcut",
+ defaultCustomShortcutModifierKey =
+ ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta),
+ isDialogShowing = false,
+ )
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockAnimations.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockAnimations.kt
new file mode 100644
index 0000000..2df14a8
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockAnimations.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT 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.plugins.clocks
+
+import android.view.View
+import com.android.systemui.plugins.annotations.ProtectedInterface
+
+/** Methods which trigger various clock animations */
+@ProtectedInterface
+interface ClockAnimations {
+ /** Runs an enter animation (if any) */
+ fun enter()
+
+ /** Sets how far into AOD the device currently is. */
+ fun doze(fraction: Float)
+
+ /** Sets how far into the folding animation the device is. */
+ fun fold(fraction: Float)
+
+ /** Runs the battery animation (if any). */
+ fun charge()
+
+ /**
+ * Runs when the clock's position changed during the move animation.
+ *
+ * @param fromLeft the [View.getLeft] position of the clock, before it started moving.
+ * @param direction the direction in which it is moving. A positive number means right, and
+ * negative means left.
+ * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
+ * it finished moving.
+ * @deprecated use {@link #onPositionUpdated(float, float)} instead.
+ */
+ fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float)
+
+ /**
+ * Runs when the clock's position changed during the move animation.
+ *
+ * @param distance is the total distance in pixels to offset the glyphs when animation
+ * completes. Negative distance means we are animating the position towards the center.
+ * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
+ * it finished moving.
+ */
+ fun onPositionUpdated(distance: Float, fraction: Float)
+
+ /**
+ * Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview,
+ * 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize
+ */
+ fun onPickerCarouselSwiping(swipingFraction: Float)
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt
new file mode 100644
index 0000000..d84d890
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockConfig.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT 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.plugins.clocks
+
+/**
+ * Exposes the rendering capabilities of this clock to SystemUI so that it can be hosted and render
+ * correctly in SystemUI's process. Ideally all clocks could be rendered identically, but in
+ * practice we different clocks require different behavior from SystemUI.
+ */
+data class ClockConfig(
+ val id: ClockId,
+
+ /** Localized name of the clock */
+ val name: String,
+
+ /** Localized accessibility description for the clock */
+ val description: String,
+
+ /** Transition to AOD should move smartspace like large clock instead of small clock */
+ val useAlternateSmartspaceAODTransition: Boolean = false,
+
+ /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */
+ @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone")
+ val isReactiveToTone: Boolean = true,
+
+ /** True if the clock is large frame clock, which will use weather in compose. */
+ val useCustomClockScene: Boolean = false,
+)
+
+/** Render configuration options for a specific clock face. */
+data class ClockFaceConfig(
+ /** Expected interval between calls to onTimeTick. Can always reduce to PER_MINUTE in AOD. */
+ val tickRate: ClockTickRate = ClockTickRate.PER_MINUTE,
+
+ /** Call to check whether the clock consumes weather data */
+ val hasCustomWeatherDataDisplay: Boolean = false,
+
+ /**
+ * Whether this clock has a custom position update animation. If true, the keyguard will call
+ * `onPositionUpdated` to notify the clock of a position update animation. If false, a default
+ * animation will be used (e.g. a simple translation).
+ */
+ val hasCustomPositionUpdatedAnimation: Boolean = false,
+
+ /** True if the clock is large frame clock, which will use weatherBlueprint in compose. */
+ val useCustomClockScene: Boolean = false,
+)
+
+/** Tick rates for clocks */
+enum class ClockTickRate(val value: Int) {
+ PER_MINUTE(2), // Update the clock once per minute.
+ PER_SECOND(1), // Update the clock once per second.
+ PER_FRAME(0), // Update the clock every second.
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
new file mode 100644
index 0000000..32fec32
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT 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.plugins.clocks
+
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.SimpleProperty
+import java.io.PrintWriter
+
+/** Interface for controlling an active clock */
+@ProtectedInterface
+interface ClockController {
+ @get:SimpleProperty
+ /** A small version of the clock, appropriate for smaller viewports */
+ val smallClock: ClockFaceController
+
+ @get:SimpleProperty
+ /** A large version of the clock, appropriate when a bigger viewport is available */
+ val largeClock: ClockFaceController
+
+ @get:SimpleProperty
+ /** Determines the way the hosting app should behave when rendering either clock face */
+ val config: ClockConfig
+
+ @get:SimpleProperty
+ /** Events that clocks may need to respond to */
+ val events: ClockEvents
+
+ /** Initializes various rendering parameters. If never called, provides reasonable defaults. */
+ fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float)
+
+ /** Optional method for dumping debug information */
+ fun dump(pw: PrintWriter)
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockEvents.kt
new file mode 100644
index 0000000..235475f
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockEvents.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT 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.plugins.clocks
+
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.ProtectedReturn
+import java.util.Locale
+import java.util.TimeZone
+
+/** Events that should call when various rendering parameters change */
+@ProtectedInterface
+interface ClockEvents {
+ @get:ProtectedReturn("return false;")
+ /** Set to enable or disable swipe interaction */
+ var isReactiveTouchInteractionEnabled: Boolean // TODO(b/364664388): Remove/Rename
+
+ /** Call whenever timezone changes */
+ fun onTimeZoneChanged(timeZone: TimeZone)
+
+ /** Call whenever the text time format changes (12hr vs 24hr) */
+ fun onTimeFormatChanged(is24Hr: Boolean)
+
+ /** Call whenever the locale changes */
+ fun onLocaleChanged(locale: Locale)
+
+ /** Call whenever the weather data should update */
+ fun onWeatherDataChanged(data: WeatherData)
+
+ /** Call with alarm information */
+ fun onAlarmDataChanged(data: AlarmData)
+
+ /** Call with zen/dnd information */
+ fun onZenDataChanged(data: ZenData)
+
+ /** Update reactive axes for this clock */
+ fun onFontAxesChanged(axes: List<ClockFontAxisSetting>)
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceController.kt
new file mode 100644
index 0000000..8a023f1
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceController.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT 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.plugins.clocks
+
+import android.view.View
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.SimpleProperty
+
+/** Interface for a specific clock face version rendered by the clock */
+@ProtectedInterface
+interface ClockFaceController {
+ @get:SimpleProperty
+ @Deprecated("Prefer use of layout")
+ /** View that renders the clock face */
+ val view: View
+
+ @get:SimpleProperty
+ /** Layout specification for this clock */
+ val layout: ClockFaceLayout
+
+ @get:SimpleProperty
+ /** Determines the way the hosting app should behave when rendering this clock face */
+ val config: ClockFaceConfig
+
+ @get:SimpleProperty
+ /** Current theme information the clock is using */
+ val theme: ThemeConfig
+
+ @get:SimpleProperty
+ /** Events specific to this clock face */
+ val events: ClockFaceEvents
+
+ @get:SimpleProperty
+ /** Triggers for various animations */
+ val animations: ClockAnimations
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
new file mode 100644
index 0000000..029e546
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT 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.plugins.clocks
+
+import android.graphics.Rect
+import com.android.systemui.plugins.annotations.ProtectedInterface
+
+/** Events that have specific data about the related face */
+@ProtectedInterface
+interface ClockFaceEvents {
+ /** Call every tick to update the rendered time */
+ fun onTimeTick()
+
+ /**
+ * Call whenever the theme or seedColor is updated
+ *
+ * Theme can be specific to the clock face.
+ * - isDarkTheme -> clock should be light
+ * - !isDarkTheme -> clock should be dark
+ */
+ fun onThemeChanged(theme: ThemeConfig)
+
+ /**
+ * Call whenever font settings change. Pass in a target font size in pixels. The specific clock
+ * design is allowed to ignore this target size on a case-by-case basis.
+ */
+ fun onFontSettingChanged(fontSizePx: Float)
+
+ /**
+ * Target region information for the clock face. For small clock, this will match the bounds of
+ * the parent view mostly, but have a target height based on the height of the default clock.
+ * For large clocks, the parent view is the entire device size, but most clocks will want to
+ * render within the centered targetRect to avoid obstructing other elements. The specified
+ * targetRegion is relative to the parent view.
+ */
+ fun onTargetRegionChanged(targetRegion: Rect?)
+
+ /** Called to notify the clock about its display. */
+ fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean)
+}
+
+/** Contains Theming information for the clock face */
+data class ThemeConfig(
+ /** True if the clock should use dark theme (light text on dark background) */
+ val isDarkTheme: Boolean,
+
+ /**
+ * A clock specific seed color to use when theming, if any was specified by the user. A null
+ * value denotes that we should use the seed color for the current system theme.
+ */
+ val seedColor: Int?,
+)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceLayout.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceLayout.kt
new file mode 100644
index 0000000..fb5ef02
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceLayout.kt
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT 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.plugins.clocks
+
+import android.content.Context
+import android.util.DisplayMetrics
+import android.view.View
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
+import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
+import com.android.internal.policy.SystemBarUtils
+import com.android.systemui.plugins.annotations.GeneratedImport
+import com.android.systemui.plugins.annotations.ProtectedInterface
+import com.android.systemui.plugins.annotations.ProtectedReturn
+
+/** Specifies layout information for the clock face */
+@ProtectedInterface
+@GeneratedImport("java.util.ArrayList")
+@GeneratedImport("android.view.View")
+interface ClockFaceLayout {
+ @get:ProtectedReturn("return new ArrayList<View>();")
+ /** All clock views to add to the root constraint layout before applying constraints. */
+ val views: List<View>
+
+ @ProtectedReturn("return constraints;")
+ /** Custom constraints to apply to Lockscreen ConstraintLayout. */
+ fun applyConstraints(constraints: ConstraintSet): ConstraintSet
+
+ @ProtectedReturn("return constraints;")
+ /** Custom constraints to apply to preview ConstraintLayout. */
+ fun applyPreviewConstraints(
+ clockPreviewConfig: ClockPreviewConfig,
+ constraints: ConstraintSet,
+ ): ConstraintSet
+
+ /** Apply specified AOD BurnIn parameters to this layout */
+ fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel)
+}
+
+/** Data class to contain AOD BurnIn information for correct aod rendering */
+data class AodClockBurnInModel(
+ /** Scale that the clock should render at to mitigate burnin */
+ val scale: Float,
+
+ /** X-Translation for the clock to mitigate burnin */
+ val translationX: Float,
+
+ /** Y-Translation for the clock to mitigate burnin */
+ val translationY: Float,
+)
+
+/** A ClockFaceLayout that applies the default lockscreen layout to a single view */
+class DefaultClockFaceLayout(val view: View) : ClockFaceLayout {
+ override val views = listOf(view)
+
+ override fun applyConstraints(constraints: ConstraintSet): ConstraintSet {
+ if (views.size != 1) {
+ throw IllegalArgumentException(
+ "Should have only one container view when using DefaultClockFaceLayout"
+ )
+ }
+ return constraints
+ }
+
+ override fun applyPreviewConstraints(
+ clockPreviewConfig: ClockPreviewConfig,
+ constraints: ConstraintSet,
+ ): ConstraintSet {
+ return applyDefaultPreviewConstraints(clockPreviewConfig, constraints)
+ }
+
+ override fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel) {
+ // Default clock doesn't need detailed control of view
+ }
+
+ companion object {
+ fun applyDefaultPreviewConstraints(
+ clockPreviewConfig: ClockPreviewConfig,
+ constraints: ConstraintSet,
+ ): ConstraintSet {
+ constraints.apply {
+ val context = clockPreviewConfig.previewContext
+ val lockscreenClockViewLargeId = getId(context, "lockscreen_clock_view_large")
+ constrainWidth(lockscreenClockViewLargeId, WRAP_CONTENT)
+ constrainHeight(lockscreenClockViewLargeId, WRAP_CONTENT)
+ constrainMaxHeight(lockscreenClockViewLargeId, 0)
+
+ val largeClockTopMargin =
+ SystemBarUtils.getStatusBarHeight(context) +
+ getDimen(context, "small_clock_padding_top") +
+ getDimen(context, "keyguard_smartspace_top_offset") +
+ getDimen(context, "date_weather_view_height") +
+ getDimen(context, "enhanced_smartspace_height")
+ connect(lockscreenClockViewLargeId, TOP, PARENT_ID, TOP, largeClockTopMargin)
+ connect(lockscreenClockViewLargeId, START, PARENT_ID, START)
+ connect(lockscreenClockViewLargeId, END, PARENT_ID, END)
+
+ // In preview, we'll show UDFPS icon for UDFPS devices
+ // and nothing for non-UDFPS devices,
+ // and we're not planning to add this vide in clockHostView
+ // so we only need position of device entry icon to constrain clock
+ // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection
+ val bottomPaddingPx = getDimen(context, "lock_icon_margin_bottom")
+ val defaultDensity =
+ DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
+ DisplayMetrics.DENSITY_DEFAULT.toFloat()
+ val lockIconRadiusPx = (defaultDensity * 36).toInt()
+ val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx
+
+ connect(lockscreenClockViewLargeId, BOTTOM, PARENT_ID, BOTTOM, clockBottomMargin)
+ val smallClockViewId = getId(context, "lockscreen_clock_view")
+ constrainWidth(smallClockViewId, WRAP_CONTENT)
+ constrainHeight(smallClockViewId, getDimen(context, "small_clock_height"))
+ connect(
+ smallClockViewId,
+ START,
+ PARENT_ID,
+ START,
+ getDimen(context, "clock_padding_start") +
+ getDimen(context, "status_view_margin_horizontal"),
+ )
+ val smallClockTopMargin =
+ getSmallClockTopPadding(
+ clockPreviewConfig = clockPreviewConfig,
+ SystemBarUtils.getStatusBarHeight(context),
+ )
+ connect(smallClockViewId, TOP, PARENT_ID, TOP, smallClockTopMargin)
+ }
+ return constraints
+ }
+
+ fun getId(context: Context, name: String): Int {
+ val packageName = context.packageName
+ val res = context.packageManager.getResourcesForApplication(packageName)
+ val id = res.getIdentifier(name, "id", packageName)
+ return id
+ }
+
+ fun getDimen(context: Context, name: String): Int {
+ val packageName = context.packageName
+ val res = context.resources
+ val id = res.getIdentifier(name, "dimen", packageName)
+ return if (id == 0) 0 else res.getDimensionPixelSize(id)
+ }
+
+ fun getSmallClockTopPadding(
+ clockPreviewConfig: ClockPreviewConfig,
+ statusBarHeight: Int,
+ ): Int {
+ return if (clockPreviewConfig.isShadeLayoutWide) {
+ getDimen(clockPreviewConfig.previewContext, "keyguard_split_shade_top_margin") -
+ if (clockPreviewConfig.isSceneContainerFlagEnabled) statusBarHeight else 0
+ } else {
+ getDimen(clockPreviewConfig.previewContext, "keyguard_clock_top_margin") +
+ if (!clockPreviewConfig.isSceneContainerFlagEnabled) statusBarHeight else 0
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockMessageBuffers.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockMessageBuffers.kt
new file mode 100644
index 0000000..bec589a
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockMessageBuffers.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT 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.plugins.clocks
+
+import com.android.systemui.log.core.MessageBuffer
+
+/** MessageBuffers for clocks that want to log information to SystemUI dumps */
+data class ClockMessageBuffers(
+ /** Message buffer for general infrastructure */
+ val infraMessageBuffer: MessageBuffer,
+
+ /** Message buffer for small clock rendering */
+ val smallClockMessageBuffer: MessageBuffer,
+
+ /** Message buffer for large clock rendering */
+ val largeClockMessageBuffer: MessageBuffer,
+) {
+ constructor(buffer: MessageBuffer) : this(buffer, buffer, buffer) {}
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
new file mode 100644
index 0000000..1bc9367
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT 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.plugins.clocks
+
+import android.graphics.drawable.Drawable
+
+data class ClockPickerConfig
+@JvmOverloads
+constructor(
+ val id: String,
+
+ /** Localized name of the clock */
+ val name: String,
+
+ /** Localized accessibility description for the clock */
+ val description: String,
+
+ /* Static & lightweight thumbnail version of the clock */
+ val thumbnail: Drawable,
+
+ /** True if the clock will react to tone changes in the seed color */
+ val isReactiveToTone: Boolean = true,
+
+ /** Font axes that can be modified on this clock */
+ val axes: List<ClockFontAxis> = listOf(),
+)
+
+/** Represents an Axis that can be modified */
+data class ClockFontAxis(
+ /** Axis key, not user renderable */
+ val key: String,
+
+ /** Intended mode of user interaction */
+ val type: AxisType,
+
+ /** Maximum value the axis supports */
+ val maxValue: Float,
+
+ /** Minimum value the axis supports */
+ val minValue: Float,
+
+ /** Current value the axis is set to */
+ val currentValue: Float,
+
+ /** User-renderable name of the axis */
+ val name: String,
+
+ /** Description of the axis */
+ val description: String,
+) {
+ fun toSetting() = ClockFontAxisSetting(key, currentValue)
+
+ companion object {
+ fun merge(
+ fontAxes: List<ClockFontAxis>,
+ axisSettings: List<ClockFontAxisSetting>,
+ ): List<ClockFontAxis> {
+ val result = mutableListOf<ClockFontAxis>()
+ for (axis in fontAxes) {
+ val setting = axisSettings.firstOrNull { axis.key == it.key }
+ val output = setting?.let { axis.copy(currentValue = it.value) } ?: axis
+ result.add(output)
+ }
+ return result
+ }
+ }
+}
+
+/** Axis user interaction modes */
+enum class AxisType {
+ /** Continuous range between minValue & maxValue. */
+ Float,
+
+ /** Only minValue & maxValue are valid. No intermediate values between them are allowed. */
+ Boolean,
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index 8ea5725..7426f06 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -13,35 +13,11 @@
*/
package com.android.systemui.plugins.clocks
-import android.content.Context
-import android.graphics.Rect
-import android.graphics.drawable.Drawable
-import android.util.DisplayMetrics
-import android.view.View
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.END
-import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import androidx.constraintlayout.widget.ConstraintSet.START
-import androidx.constraintlayout.widget.ConstraintSet.TOP
-import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
-import com.android.internal.annotations.Keep
-import com.android.internal.policy.SystemBarUtils
-import com.android.systemui.log.core.MessageBuffer
import com.android.systemui.plugins.Plugin
import com.android.systemui.plugins.annotations.GeneratedImport
import com.android.systemui.plugins.annotations.ProtectedInterface
import com.android.systemui.plugins.annotations.ProtectedReturn
import com.android.systemui.plugins.annotations.ProvidesInterface
-import com.android.systemui.plugins.annotations.SimpleProperty
-import java.io.PrintWriter
-import java.util.Locale
-import java.util.TimeZone
-import org.json.JSONArray
-import org.json.JSONObject
-
-/** Identifies a clock design */
-typealias ClockId = String
/** A Plugin which exposes the ClockProvider interface */
@ProtectedInterface
@@ -74,528 +50,8 @@
fun getClockPickerConfig(settings: ClockSettings): ClockPickerConfig
}
-/** Interface for controlling an active clock */
-@ProtectedInterface
-interface ClockController {
- @get:SimpleProperty
- /** A small version of the clock, appropriate for smaller viewports */
- val smallClock: ClockFaceController
-
- @get:SimpleProperty
- /** A large version of the clock, appropriate when a bigger viewport is available */
- val largeClock: ClockFaceController
-
- @get:SimpleProperty
- /** Determines the way the hosting app should behave when rendering either clock face */
- val config: ClockConfig
-
- @get:SimpleProperty
- /** Events that clocks may need to respond to */
- val events: ClockEvents
-
- /** Initializes various rendering parameters. If never called, provides reasonable defaults. */
- fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float)
-
- /** Optional method for dumping debug information */
- fun dump(pw: PrintWriter)
-}
-
-/** Interface for a specific clock face version rendered by the clock */
-@ProtectedInterface
-interface ClockFaceController {
- @get:SimpleProperty
- @Deprecated("Prefer use of layout")
- /** View that renders the clock face */
- val view: View
-
- @get:SimpleProperty
- /** Layout specification for this clock */
- val layout: ClockFaceLayout
-
- @get:SimpleProperty
- /** Determines the way the hosting app should behave when rendering this clock face */
- val config: ClockFaceConfig
-
- @get:SimpleProperty
- /** Current theme information the clock is using */
- val theme: ThemeConfig
-
- @get:SimpleProperty
- /** Events specific to this clock face */
- val events: ClockFaceEvents
-
- @get:SimpleProperty
- /** Triggers for various animations */
- val animations: ClockAnimations
-}
-
-/** For clocks that want to report debug information */
-data class ClockMessageBuffers(
- /** Message buffer for general infra */
- val infraMessageBuffer: MessageBuffer,
-
- /** Message buffer for small clock renering */
- val smallClockMessageBuffer: MessageBuffer,
-
- /** Message buffer for large clock rendering */
- val largeClockMessageBuffer: MessageBuffer,
-) {
- constructor(buffer: MessageBuffer) : this(buffer, buffer, buffer) {}
-}
-
-data class AodClockBurnInModel(val scale: Float, val translationX: Float, val translationY: Float)
-
-/** Specifies layout information for the clock face */
-@ProtectedInterface
-@GeneratedImport("java.util.ArrayList")
-@GeneratedImport("android.view.View")
-interface ClockFaceLayout {
- @get:ProtectedReturn("return new ArrayList<View>();")
- /** All clock views to add to the root constraint layout before applying constraints. */
- val views: List<View>
-
- @ProtectedReturn("return constraints;")
- /** Custom constraints to apply to Lockscreen ConstraintLayout. */
- fun applyConstraints(constraints: ConstraintSet): ConstraintSet
-
- @ProtectedReturn("return constraints;")
- /** Custom constraints to apply to preview ConstraintLayout. */
- fun applyPreviewConstraints(
- clockPreviewConfig: ClockPreviewConfig,
- constraints: ConstraintSet,
- ): ConstraintSet
-
- fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel)
-}
-
-/** A ClockFaceLayout that applies the default lockscreen layout to a single view */
-class DefaultClockFaceLayout(val view: View) : ClockFaceLayout {
- // both small and large clock should have a container (RelativeLayout in
- // SimpleClockFaceController)
- override val views = listOf(view)
-
- override fun applyConstraints(constraints: ConstraintSet): ConstraintSet {
- if (views.size != 1) {
- throw IllegalArgumentException(
- "Should have only one container view when using DefaultClockFaceLayout"
- )
- }
- return constraints
- }
-
- override fun applyPreviewConstraints(
- clockPreviewConfig: ClockPreviewConfig,
- constraints: ConstraintSet,
- ): ConstraintSet {
- return applyDefaultPreviewConstraints(clockPreviewConfig, constraints)
- }
-
- override fun applyAodBurnIn(aodBurnInModel: AodClockBurnInModel) {
- // Default clock doesn't need detailed control of view
- }
-
- companion object {
- fun applyDefaultPreviewConstraints(
- clockPreviewConfig: ClockPreviewConfig,
- constraints: ConstraintSet,
- ): ConstraintSet {
- constraints.apply {
- val context = clockPreviewConfig.previewContext
- val lockscreenClockViewLargeId = getId(context, "lockscreen_clock_view_large")
- constrainWidth(lockscreenClockViewLargeId, WRAP_CONTENT)
- constrainHeight(lockscreenClockViewLargeId, WRAP_CONTENT)
- constrainMaxHeight(lockscreenClockViewLargeId, 0)
-
- val largeClockTopMargin =
- SystemBarUtils.getStatusBarHeight(context) +
- getDimen(context, "small_clock_padding_top") +
- getDimen(context, "keyguard_smartspace_top_offset") +
- getDimen(context, "date_weather_view_height") +
- getDimen(context, "enhanced_smartspace_height")
- connect(lockscreenClockViewLargeId, TOP, PARENT_ID, TOP, largeClockTopMargin)
- connect(lockscreenClockViewLargeId, START, PARENT_ID, START)
- connect(lockscreenClockViewLargeId, END, PARENT_ID, END)
-
- // In preview, we'll show UDFPS icon for UDFPS devices
- // and nothing for non-UDFPS devices,
- // and we're not planning to add this vide in clockHostView
- // so we only need position of device entry icon to constrain clock
- // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection
- val bottomPaddingPx = getDimen(context, "lock_icon_margin_bottom")
- val defaultDensity =
- DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
- DisplayMetrics.DENSITY_DEFAULT.toFloat()
- val lockIconRadiusPx = (defaultDensity * 36).toInt()
- val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx
-
- connect(lockscreenClockViewLargeId, BOTTOM, PARENT_ID, BOTTOM, clockBottomMargin)
- val smallClockViewId = getId(context, "lockscreen_clock_view")
- constrainWidth(smallClockViewId, WRAP_CONTENT)
- constrainHeight(smallClockViewId, getDimen(context, "small_clock_height"))
- connect(
- smallClockViewId,
- START,
- PARENT_ID,
- START,
- getDimen(context, "clock_padding_start") +
- getDimen(context, "status_view_margin_horizontal"),
- )
- val smallClockTopMargin =
- getSmallClockTopPadding(
- clockPreviewConfig = clockPreviewConfig,
- SystemBarUtils.getStatusBarHeight(context),
- )
- connect(smallClockViewId, TOP, PARENT_ID, TOP, smallClockTopMargin)
- }
- return constraints
- }
-
- fun getId(context: Context, name: String): Int {
- val packageName = context.packageName
- val res = context.packageManager.getResourcesForApplication(packageName)
- val id = res.getIdentifier(name, "id", packageName)
- return id
- }
-
- fun getDimen(context: Context, name: String): Int {
- val packageName = context.packageName
- val res = context.resources
- val id = res.getIdentifier(name, "dimen", packageName)
- return if (id == 0) 0 else res.getDimensionPixelSize(id)
- }
-
- fun getSmallClockTopPadding(
- clockPreviewConfig: ClockPreviewConfig,
- statusBarHeight: Int,
- ): Int {
- return if (clockPreviewConfig.isShadeLayoutWide) {
- getDimen(clockPreviewConfig.previewContext, "keyguard_split_shade_top_margin") -
- if (clockPreviewConfig.isSceneContainerFlagEnabled) statusBarHeight else 0
- } else {
- getDimen(clockPreviewConfig.previewContext, "keyguard_clock_top_margin") +
- if (!clockPreviewConfig.isSceneContainerFlagEnabled) statusBarHeight else 0
- }
- }
- }
-}
-
-/** Events that should call when various rendering parameters change */
-@ProtectedInterface
-interface ClockEvents {
- @get:ProtectedReturn("return false;")
- /** Set to enable or disable swipe interaction */
- var isReactiveTouchInteractionEnabled: Boolean // TODO(b/364664388): Remove/Rename
-
- /** Call whenever timezone changes */
- fun onTimeZoneChanged(timeZone: TimeZone)
-
- /** Call whenever the text time format changes (12hr vs 24hr) */
- fun onTimeFormatChanged(is24Hr: Boolean)
-
- /** Call whenever the locale changes */
- fun onLocaleChanged(locale: Locale)
-
- /** Call whenever the weather data should update */
- fun onWeatherDataChanged(data: WeatherData)
-
- /** Call with alarm information */
- fun onAlarmDataChanged(data: AlarmData)
-
- /** Call with zen/dnd information */
- fun onZenDataChanged(data: ZenData)
-
- /** Update reactive axes for this clock */
- fun onFontAxesChanged(axes: List<ClockFontAxisSetting>)
-}
-
-/** Axis setting value for a clock */
-data class ClockFontAxisSetting(
- /** Axis key; matches ClockFontAxis.key */
- val key: String,
-
- /** Value to set this axis to */
- val value: Float,
-) {
- companion object {
- private val KEY_AXIS_KEY = "key"
- private val KEY_AXIS_VALUE = "value"
-
- fun toJson(setting: ClockFontAxisSetting): JSONObject {
- return JSONObject().apply {
- put(KEY_AXIS_KEY, setting.key)
- put(KEY_AXIS_VALUE, setting.value)
- }
- }
-
- fun toJson(settings: List<ClockFontAxisSetting>): JSONArray {
- return JSONArray().apply {
- for (axis in settings) {
- put(toJson(axis))
- }
- }
- }
-
- fun fromJson(jsonObj: JSONObject): ClockFontAxisSetting {
- return ClockFontAxisSetting(
- key = jsonObj.getString(KEY_AXIS_KEY),
- value = jsonObj.getDouble(KEY_AXIS_VALUE).toFloat(),
- )
- }
-
- fun fromJson(jsonArray: JSONArray): List<ClockFontAxisSetting> {
- val result = mutableListOf<ClockFontAxisSetting>()
- for (i in 0..jsonArray.length() - 1) {
- val obj = jsonArray.getJSONObject(i)
- if (obj == null) continue
- result.add(fromJson(obj))
- }
- return result
- }
-
- fun toFVar(settings: List<ClockFontAxisSetting>): String {
- val sb = StringBuilder()
- for (axis in settings) {
- if (sb.length > 0) sb.append(", ")
- sb.append("'${axis.key}' ${axis.value.toInt()}")
- }
- return sb.toString()
- }
- }
-}
-
-/** Methods which trigger various clock animations */
-@ProtectedInterface
-interface ClockAnimations {
- /** Runs an enter animation (if any) */
- fun enter()
-
- /** Sets how far into AOD the device currently is. */
- fun doze(fraction: Float)
-
- /** Sets how far into the folding animation the device is. */
- fun fold(fraction: Float)
-
- /** Runs the battery animation (if any). */
- fun charge()
-
- /**
- * Runs when the clock's position changed during the move animation.
- *
- * @param fromLeft the [View.getLeft] position of the clock, before it started moving.
- * @param direction the direction in which it is moving. A positive number means right, and
- * negative means left.
- * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
- * it finished moving.
- * @deprecated use {@link #onPositionUpdated(float, float)} instead.
- */
- fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float)
-
- /**
- * Runs when the clock's position changed during the move animation.
- *
- * @param distance is the total distance in pixels to offset the glyphs when animation
- * completes. Negative distance means we are animating the position towards the center.
- * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
- * it finished moving.
- */
- fun onPositionUpdated(distance: Float, fraction: Float)
-
- /**
- * Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview,
- * 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize
- */
- fun onPickerCarouselSwiping(swipingFraction: Float)
-}
-
-/** Events that have specific data about the related face */
-@ProtectedInterface
-interface ClockFaceEvents {
- /** Call every time tick */
- fun onTimeTick()
-
- /**
- * Call whenever the theme or seedColor is updated
- *
- * Theme can be specific to the clock face.
- * - isDarkTheme -> clock should be light
- * - !isDarkTheme -> clock should be dark
- */
- fun onThemeChanged(theme: ThemeConfig)
-
- /**
- * Call whenever font settings change. Pass in a target font size in pixels. The specific clock
- * design is allowed to ignore this target size on a case-by-case basis.
- */
- fun onFontSettingChanged(fontSizePx: Float)
-
- /**
- * Target region information for the clock face. For small clock, this will match the bounds of
- * the parent view mostly, but have a target height based on the height of the default clock.
- * For large clocks, the parent view is the entire device size, but most clocks will want to
- * render within the centered targetRect to avoid obstructing other elements. The specified
- * targetRegion is relative to the parent view.
- */
- fun onTargetRegionChanged(targetRegion: Rect?)
-
- /** Called to notify the clock about its display. */
- fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean)
-}
-
-data class ThemeConfig(val isDarkTheme: Boolean, val seedColor: Int?)
-
-/** Tick rates for clocks */
-enum class ClockTickRate(val value: Int) {
- PER_MINUTE(2), // Update the clock once per minute.
- PER_SECOND(1), // Update the clock once per second.
- PER_FRAME(0), // Update the clock every second.
-}
+/** Identifies a clock design */
+typealias ClockId = String
/** Some data about a clock design */
data class ClockMetadata(val clockId: ClockId)
-
-data class ClockPickerConfig
-@JvmOverloads
-constructor(
- val id: String,
-
- /** Localized name of the clock */
- val name: String,
-
- /** Localized accessibility description for the clock */
- val description: String,
-
- /* Static & lightweight thumbnail version of the clock */
- val thumbnail: Drawable,
-
- /** True if the clock will react to tone changes in the seed color */
- val isReactiveToTone: Boolean = true,
-
- /** Font axes that can be modified on this clock */
- val axes: List<ClockFontAxis> = listOf(),
-)
-
-/** Represents an Axis that can be modified */
-data class ClockFontAxis(
- /** Axis key, not user renderable */
- val key: String,
-
- /** Intended mode of user interaction */
- val type: AxisType,
-
- /** Maximum value the axis supports */
- val maxValue: Float,
-
- /** Minimum value the axis supports */
- val minValue: Float,
-
- /** Current value the axis is set to */
- val currentValue: Float,
-
- /** User-renderable name of the axis */
- val name: String,
-
- /** Description of the axis */
- val description: String,
-) {
- fun toSetting() = ClockFontAxisSetting(key, currentValue)
-
- companion object {
- fun merge(
- fontAxes: List<ClockFontAxis>,
- axisSettings: List<ClockFontAxisSetting>,
- ): List<ClockFontAxis> {
- val result = mutableListOf<ClockFontAxis>()
- for (axis in fontAxes) {
- val setting = axisSettings.firstOrNull { axis.key == it.key }
- val output = setting?.let { axis.copy(currentValue = it.value) } ?: axis
- result.add(output)
- }
- return result
- }
- }
-}
-
-/** Axis user interaction modes */
-enum class AxisType {
- /** Continuous range between minValue & maxValue. */
- Float,
-
- /** Only minValue & maxValue are valid. No intermediate values between them are allowed. */
- Boolean,
-}
-
-/** Render configuration for the full clock. Modifies the way systemUI behaves with this clock. */
-data class ClockConfig(
- val id: String,
-
- /** Localized name of the clock */
- val name: String,
-
- /** Localized accessibility description for the clock */
- val description: String,
-
- /** Transition to AOD should move smartspace like large clock instead of small clock */
- val useAlternateSmartspaceAODTransition: Boolean = false,
-
- /** Deprecated version of isReactiveToTone; moved to ClockPickerConfig */
- @Deprecated("TODO(b/352049256): Remove in favor of ClockPickerConfig.isReactiveToTone")
- val isReactiveToTone: Boolean = true,
-
- /** True if the clock is large frame clock, which will use weather in compose. */
- val useCustomClockScene: Boolean = false,
-)
-
-/** Render configuration options for a clock face. Modifies the way SystemUI behaves. */
-data class ClockFaceConfig(
- /** Expected interval between calls to onTimeTick. Can always reduce to PER_MINUTE in AOD. */
- val tickRate: ClockTickRate = ClockTickRate.PER_MINUTE,
-
- /** Call to check whether the clock consumes weather data */
- val hasCustomWeatherDataDisplay: Boolean = false,
-
- /**
- * Whether this clock has a custom position update animation. If true, the keyguard will call
- * `onPositionUpdated` to notify the clock of a position update animation. If false, a default
- * animation will be used (e.g. a simple translation).
- */
- val hasCustomPositionUpdatedAnimation: Boolean = false,
-
- /** True if the clock is large frame clock, which will use weatherBlueprint in compose. */
- val useCustomClockScene: Boolean = false,
-)
-
-/** Structure for keeping clock-specific settings */
-@Keep
-data class ClockSettings(
- val clockId: ClockId? = null,
- val seedColor: Int? = null,
- val axes: List<ClockFontAxisSetting> = listOf(),
-) {
- // Exclude metadata from equality checks
- var metadata: JSONObject = JSONObject()
-
- companion object {
- private val KEY_CLOCK_ID = "clockId"
- private val KEY_SEED_COLOR = "seedColor"
- private val KEY_METADATA = "metadata"
- private val KEY_AXIS_LIST = "axes"
-
- fun toJson(setting: ClockSettings): JSONObject {
- return JSONObject().apply {
- put(KEY_CLOCK_ID, setting.clockId)
- put(KEY_SEED_COLOR, setting.seedColor)
- put(KEY_METADATA, setting.metadata)
- put(KEY_AXIS_LIST, ClockFontAxisSetting.toJson(setting.axes))
- }
- }
-
- fun fromJson(json: JSONObject): ClockSettings {
- val clockId = if (!json.isNull(KEY_CLOCK_ID)) json.getString(KEY_CLOCK_ID) else null
- val seedColor = if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null
- val axisList = json.optJSONArray(KEY_AXIS_LIST)?.let(ClockFontAxisSetting::fromJson)
- return ClockSettings(clockId, seedColor, axisList ?: listOf()).apply {
- metadata = json.optJSONObject(KEY_METADATA) ?: JSONObject()
- }
- }
- }
-}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt
new file mode 100644
index 0000000..6128c00
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockSettings.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT 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.plugins.clocks
+
+import com.android.internal.annotations.Keep
+import org.json.JSONArray
+import org.json.JSONObject
+
+@Keep
+/** Structure for keeping clock-specific settings */
+data class ClockSettings(
+ val clockId: ClockId? = null,
+ val seedColor: Int? = null,
+ val axes: List<ClockFontAxisSetting> = listOf(),
+) {
+ // Exclude metadata from equality checks
+ var metadata: JSONObject = JSONObject()
+
+ companion object {
+ private val KEY_CLOCK_ID = "clockId"
+ private val KEY_SEED_COLOR = "seedColor"
+ private val KEY_METADATA = "metadata"
+ private val KEY_AXIS_LIST = "axes"
+
+ fun toJson(setting: ClockSettings): JSONObject {
+ return JSONObject().apply {
+ put(KEY_CLOCK_ID, setting.clockId)
+ put(KEY_SEED_COLOR, setting.seedColor)
+ put(KEY_METADATA, setting.metadata)
+ put(KEY_AXIS_LIST, ClockFontAxisSetting.toJson(setting.axes))
+ }
+ }
+
+ fun fromJson(json: JSONObject): ClockSettings {
+ val clockId = if (!json.isNull(KEY_CLOCK_ID)) json.getString(KEY_CLOCK_ID) else null
+ val seedColor = if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null
+ val axisList = json.optJSONArray(KEY_AXIS_LIST)?.let(ClockFontAxisSetting::fromJson)
+ return ClockSettings(clockId, seedColor, axisList ?: listOf()).apply {
+ metadata = json.optJSONObject(KEY_METADATA) ?: JSONObject()
+ }
+ }
+ }
+}
+
+@Keep
+/** Axis setting value for a clock */
+data class ClockFontAxisSetting(
+ /** Axis key; matches ClockFontAxis.key */
+ val key: String,
+
+ /** Value to set this axis to */
+ val value: Float,
+) {
+ companion object {
+ private val KEY_AXIS_KEY = "key"
+ private val KEY_AXIS_VALUE = "value"
+
+ fun toJson(setting: ClockFontAxisSetting): JSONObject {
+ return JSONObject().apply {
+ put(KEY_AXIS_KEY, setting.key)
+ put(KEY_AXIS_VALUE, setting.value)
+ }
+ }
+
+ fun toJson(settings: List<ClockFontAxisSetting>): JSONArray {
+ return JSONArray().apply {
+ for (axis in settings) {
+ put(toJson(axis))
+ }
+ }
+ }
+
+ fun fromJson(jsonObj: JSONObject): ClockFontAxisSetting {
+ return ClockFontAxisSetting(
+ key = jsonObj.getString(KEY_AXIS_KEY),
+ value = jsonObj.getDouble(KEY_AXIS_VALUE).toFloat(),
+ )
+ }
+
+ fun fromJson(jsonArray: JSONArray): List<ClockFontAxisSetting> {
+ val result = mutableListOf<ClockFontAxisSetting>()
+ for (i in 0..jsonArray.length() - 1) {
+ val obj = jsonArray.getJSONObject(i)
+ if (obj == null) continue
+ result.add(fromJson(obj))
+ }
+ return result
+ }
+
+ fun toFVar(settings: List<ClockFontAxisSetting>): String {
+ val sb = StringBuilder()
+ for (axis in settings) {
+ if (sb.length > 0) sb.append(", ")
+ sb.append("'${axis.key}' ${axis.value.toInt()}")
+ }
+ return sb.toString()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
index b37206a..160574fa 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
@@ -19,6 +19,7 @@
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
@@ -38,6 +39,7 @@
class DreamUserActionsViewModel
@AssistedInject
constructor(
+ private val communalInteractor: CommunalInteractor,
private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
private val shadeInteractor: ShadeInteractor,
) : UserActionsViewModel() {
@@ -50,10 +52,13 @@
} else {
combine(
deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked },
+ communalInteractor.isCommunalAvailable,
shadeInteractor.shadeMode,
- ) { isDeviceUnlocked, shadeMode ->
+ ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
buildList {
- add(Swipe.Start to Scenes.Communal)
+ if (isCommunalAvailable) {
+ add(Swipe.Start to Scenes.Communal)
+ }
val bouncerOrGone =
if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
index 99cafd3..321fd57 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt
@@ -28,6 +28,7 @@
import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
import android.hardware.input.InputSettings
import android.hardware.input.KeyGestureEvent
+import android.hardware.input.KeyGestureEvent.KeyGestureType
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.mutableStateOf
@@ -44,6 +45,8 @@
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -52,8 +55,6 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
-import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
@SysUISingleton
class CustomShortcutCategoriesRepository
@@ -65,7 +66,7 @@
@Background private val bgCoroutineContext: CoroutineContext,
private val shortcutCategoriesUtils: ShortcutCategoriesUtils,
private val context: Context,
- private val inputGestureMaps: InputGestureMaps
+ private val inputGestureMaps: InputGestureMaps,
) : ShortcutCategoriesRepository {
private val userContext: Context
@@ -130,9 +131,7 @@
emptyList()
} else {
val customInputGesturesForUser: List<InputGestureData> =
- if (InputSettings.isCustomizableInputGesturesFeatureFlagEnabled()) {
- inputManager.getCustomInputGestures(/* filter= */ null)
- } else emptyList()
+ getCustomInputGestures()
val sources = toInternalGroupSources(customInputGesturesForUser)
val supportedKeyCodes =
shortcutCategoriesUtils.fetchSupportedKeyCodes(
@@ -173,16 +172,20 @@
.addKeyGestureTypeFromShortcutLabel()
.addTriggerFromSelectedKeyCombination()
.build()
- // TODO(b/379648200) add app launch data for application categories shortcut after
- // dynamic
- // label/icon mapping implementation
+ // TODO(b/379648200) add app launch data after dynamic label/icon mapping implementation
} catch (e: IllegalArgumentException) {
Log.w(TAG, "could not add custom shortcut: $e")
return null
}
}
- suspend fun confirmAndSetShortcutCurrentlyBeingCustomized(): ShortcutCustomizationRequestResult {
+ private fun retrieveInputGestureDataForShortcutBeingDeleted(): InputGestureData? {
+ val keyGestureType = getKeyGestureTypeFromShortcutBeingDeletedLabel()
+ return getCustomInputGestures().firstOrNull { it.action.keyGestureType() == keyGestureType }
+ }
+
+ suspend fun confirmAndSetShortcutCurrentlyBeingCustomized():
+ ShortcutCustomizationRequestResult {
return withContext(bgCoroutineContext) {
val inputGestureData =
buildInputGestureDataForShortcutBeingCustomized()
@@ -201,33 +204,88 @@
}
}
- private fun Builder.addKeyGestureTypeFromShortcutLabel(): Builder {
- val shortcutBeingCustomized =
- getShortcutBeingCustomized() as? ShortcutCustomizationRequestInfo.Add
-
- if (shortcutBeingCustomized == null) {
- Log.w(TAG, "User requested to set shortcut but shortcut being customized is null")
- return this
+ suspend fun deleteShortcutCurrentlyBeingCustomized():
+ ShortcutCustomizationRequestResult {
+ return withContext(bgCoroutineContext) {
+ val inputGestureData =
+ retrieveInputGestureDataForShortcutBeingDeleted()
+ ?: return@withContext ShortcutCustomizationRequestResult.ERROR_OTHER
+ return@withContext when (
+ val result = inputManager.removeCustomInputGesture(inputGestureData)
+ ) {
+ CUSTOM_INPUT_GESTURE_RESULT_SUCCESS -> ShortcutCustomizationRequestResult.SUCCESS
+ else -> {
+ Log.w(
+ TAG,
+ "Attempted to delete shortcut being customized " +
+ "${_shortcutBeingCustomized.value} but ran into an error. InputGestureData" +
+ " = $inputGestureData, error code: $result",
+ )
+ ShortcutCustomizationRequestResult.ERROR_OTHER
+ }
+ }
}
+ }
- val keyGestureType =
- inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutBeingCustomized.label]
+ private fun getCustomInputGestures(): List<InputGestureData> {
+ return if (InputSettings.isCustomizableInputGesturesFeatureFlagEnabled()) {
+ inputManager.getCustomInputGestures(/* filter= */ InputGestureData.Filter.KEY)
+ } else emptyList()
+ }
+
+ private fun Builder.addKeyGestureTypeFromShortcutLabel(): Builder {
+ val keyGestureType = getKeyGestureTypeFromShortcutBeingCustomizedLabel()
if (keyGestureType == null) {
- Log.w(TAG, "Could not find KeyGestureType for shortcut $shortcutBeingCustomized")
+ Log.w(
+ TAG,
+ "Could not find KeyGestureType for shortcut ${_shortcutBeingCustomized.value}",
+ )
return this
}
return setKeyGestureType(keyGestureType)
}
+ @KeyGestureType
+ private fun getKeyGestureTypeFromShortcutBeingCustomizedLabel(): Int? {
+ val shortcutBeingCustomized =
+ getShortcutBeingCustomized() as? ShortcutCustomizationRequestInfo.Add
+
+ if (shortcutBeingCustomized == null) {
+ Log.w(
+ TAG,
+ "Requested key gesture type from label but shortcut being customized is null",
+ )
+ return null
+ }
+
+ return inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutBeingCustomized.label]
+ }
+
+ @KeyGestureType
+ private fun getKeyGestureTypeFromShortcutBeingDeletedLabel(): Int? {
+ val shortcutBeingCustomized =
+ getShortcutBeingCustomized() as? ShortcutCustomizationRequestInfo.Delete
+
+ if (shortcutBeingCustomized == null) {
+ Log.w(
+ TAG,
+ "Requested key gesture type from label but shortcut being customized is null",
+ )
+ return null
+ }
+
+ return inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutBeingCustomized.label]
+ }
+
private fun Builder.addTriggerFromSelectedKeyCombination(): Builder {
val selectedKeyCombination = _selectedKeyCombination.value
if (selectedKeyCombination?.keyCode == null) {
Log.w(
TAG,
"User requested to set shortcut but selected key combination is " +
- "$selectedKeyCombination",
+ "$selectedKeyCombination",
)
return this
}
@@ -235,8 +293,7 @@
return setTrigger(
createKeyTrigger(
/* keycode = */ selectedKeyCombination.keyCode,
- /* modifierState = */
- shortcutCategoriesUtils.removeUnsupportedModifiers(
+ /* modifierState = */ shortcutCategoriesUtils.removeUnsupportedModifiers(
selectedKeyCombination.modifiers
),
)
@@ -256,10 +313,8 @@
val keyTrigger = gestureData.trigger as KeyTrigger
val keyGestureType = gestureData.action.keyGestureType()
fetchGroupLabelByGestureType(keyGestureType)?.let { groupLabel ->
- toInternalKeyboardShortcutInfo(
- keyGestureType,
- keyTrigger
- )?.let { internalKeyboardShortcutInfo ->
+ toInternalKeyboardShortcutInfo(keyGestureType, keyTrigger)?.let {
+ internalKeyboardShortcutInfo ->
val group =
InternalKeyboardShortcutGroup(
label = groupLabel,
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
index d228a15..ecc0761 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureMaps.kt
@@ -48,9 +48,7 @@
import com.android.systemui.res.R
import javax.inject.Inject
-class InputGestureMaps
-@Inject
-constructor(private val context: Context) {
+class InputGestureMaps @Inject constructor(private val context: Context) {
val gestureToShortcutCategoryTypeMap =
mapOf(
// System Category
@@ -180,9 +178,10 @@
)
val shortcutLabelToKeyGestureTypeMap: Map<String, Int>
- get() = gestureToInternalKeyboardShortcutInfoLabelResIdMap.entries.associateBy({
- context.getString(it.value)
- }) {
- it.key
- }
+ get() =
+ gestureToInternalKeyboardShortcutInfoLabelResIdMap.entries.associateBy({
+ context.getString(it.value)
+ }) {
+ it.key
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
index 95bc9f6..a0897f2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt
@@ -36,9 +36,9 @@
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutIcon
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory
-import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
class ShortcutCategoriesUtils
@Inject
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt
index f4e2f05..7743c53 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt
@@ -41,7 +41,13 @@
customShortcutRepository.onCustomizationRequested(requestInfo)
}
- suspend fun confirmAndSetShortcutCurrentlyBeingCustomized(): ShortcutCustomizationRequestResult {
+ suspend fun confirmAndSetShortcutCurrentlyBeingCustomized():
+ ShortcutCustomizationRequestResult {
return customShortcutRepository.confirmAndSetShortcutCurrentlyBeingCustomized()
}
+
+ suspend fun deleteShortcutCurrentlyBeingCustomized():
+ ShortcutCustomizationRequestResult {
+ return customShortcutRepository.deleteShortcutCurrentlyBeingCustomized()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
index 813a1fca..4648053 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCategory.kt
@@ -18,25 +18,31 @@
sealed interface ShortcutCategoryType {
val isTrusted: Boolean
+ val includeInCustomization: Boolean
data object System : ShortcutCategoryType {
override val isTrusted: Boolean = true
+ override val includeInCustomization: Boolean = true
}
data object MultiTasking : ShortcutCategoryType {
override val isTrusted: Boolean = true
+ override val includeInCustomization: Boolean = true
}
data object InputMethodEditor : ShortcutCategoryType {
override val isTrusted: Boolean = false
+ override val includeInCustomization: Boolean = false
}
data object AppCategories : ShortcutCategoryType {
override val isTrusted: Boolean = true
+ override val includeInCustomization: Boolean = true
}
data class CurrentApp(val packageName: String) : ShortcutCategoryType {
override val isTrusted: Boolean = false
+ override val includeInCustomization: Boolean = false
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
index 67caadb..f28618b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/ShortcutCustomizationDialogStarter.kt
@@ -73,22 +73,17 @@
return dialogFactory.create(dialogDelegate = ShortcutCustomizationDialogDelegate()) { dialog
->
val uiState by
- viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle(
- initialValue = ShortcutCustomizationUiState.Inactive
- )
+ viewModel.shortcutCustomizationUiState.collectAsStateWithLifecycle(
+ initialValue = ShortcutCustomizationUiState.Inactive
+ )
val coroutineScope = rememberCoroutineScope()
ShortcutCustomizationDialog(
uiState = uiState,
- modifier = Modifier
- .width(364.dp)
- .wrapContentHeight()
- .padding(vertical = 24.dp),
+ modifier = Modifier.width(364.dp).wrapContentHeight().padding(vertical = 24.dp),
onKeyPress = { viewModel.onKeyPressed(it) },
onCancel = { dialog.dismiss() },
- onConfirmSetShortcut = {
- coroutineScope.launch { viewModel.onSetShortcut() }
- },
- onConfirmDeleteShortcut = { viewModel.onDeleteShortcut() },
+ onConfirmSetShortcut = { coroutineScope.launch { viewModel.onSetShortcut() } },
+ onConfirmDeleteShortcut = { coroutineScope.launch { viewModel.deleteShortcutCurrentlyBeingCustomized() } },
)
dialog.setOnDismissListener { viewModel.onDialogDismissed() }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index f41ff7c..e3675de 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -461,7 +461,7 @@
SubCategoryContainerDualPane(
searchQuery = searchQuery,
subCategory = subcategory,
- isCustomizing = isCustomizing,
+ isCustomizing = isCustomizing and category.type.includeInCustomization,
onCustomizationRequested = { requestInfo ->
when (requestInfo) {
is ShortcutCustomizationRequestInfo.Add ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
index 8696827..b467bb4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt
@@ -79,8 +79,7 @@
}
is ShortcutCustomizationRequestInfo.Delete -> {
- _shortcutCustomizationUiState.value =
- DeleteShortcutDialog(isDialogShowing = false)
+ _shortcutCustomizationUiState.value = DeleteShortcutDialog(isDialogShowing = false)
shortcutCustomizationInteractor.onCustomizationRequested(requestInfo)
}
}
@@ -134,17 +133,23 @@
}
}
- fun onDeleteShortcut() {
- // TODO(b/373631984) not yet implemented
+ suspend fun deleteShortcutCurrentlyBeingCustomized() {
+ val result =
+ shortcutCustomizationInteractor.deleteShortcutCurrentlyBeingCustomized()
+
+ _shortcutCustomizationUiState.update { uiState ->
+ when (result) {
+ ShortcutCustomizationRequestResult.SUCCESS -> ShortcutCustomizationUiState.Inactive
+ else -> uiState
+ }
+ }
}
private fun getUiStateWithErrorMessage(
uiState: ShortcutCustomizationUiState,
errorMessage: String,
): ShortcutCustomizationUiState {
- return (uiState as? AddShortcutDialog)?.copy(
- errorMessage = errorMessage
- ) ?: uiState
+ return (uiState as? AddShortcutDialog)?.copy(errorMessage = errorMessage) ?: uiState
}
private fun updatePressedKeys(keyEvent: KeyEvent) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 80ac2fc..0a4e8c6 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -18,6 +18,7 @@
import static android.content.pm.ActivityInfo.CONFIG_FONT_SCALE;
import static android.view.InputDevice.SOURCE_MOUSE;
import static android.view.InputDevice.SOURCE_TOUCHPAD;
+import static android.view.MotionEvent.TOOL_TYPE_FINGER;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
import static com.android.systemui.Flags.edgebackGestureHandlerGetRunningTasksBackground;
@@ -216,6 +217,7 @@
private final int mDisplayId;
private final UiThreadContext mUiThreadContext;
+ private final Handler mBgHandler;
private final Executor mBackgroundExecutor;
private final Rect mPipExcludedBounds = new Rect();
@@ -378,11 +380,14 @@
@Override
public void onInputDeviceAdded(int deviceId) {
if (isTrackpadDevice(deviceId)) {
- boolean wasEmpty = mTrackpadsConnected.isEmpty();
- mTrackpadsConnected.add(deviceId);
- if (wasEmpty) {
- update();
- }
+ // This updates the gesture handler state and should be running on the main thread.
+ mUiThreadContext.getHandler().post(() -> {
+ boolean wasEmpty = mTrackpadsConnected.isEmpty();
+ mTrackpadsConnected.add(deviceId);
+ if (wasEmpty) {
+ update();
+ }
+ });
}
}
@@ -391,10 +396,13 @@
@Override
public void onInputDeviceRemoved(int deviceId) {
- mTrackpadsConnected.remove(deviceId);
- if (mTrackpadsConnected.isEmpty()) {
- update();
- }
+ // This updates the gesture handler state and should be running on the main thread.
+ mUiThreadContext.getHandler().post(() -> {
+ mTrackpadsConnected.remove(deviceId);
+ if (mTrackpadsConnected.isEmpty()) {
+ update();
+ }
+ });
}
private void update() {
@@ -408,12 +416,12 @@
}
private boolean isTrackpadDevice(int deviceId) {
+ // This is a blocking binder call that should run on a bg thread.
InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
if (inputDevice == null) {
return false;
}
- return inputDevice.getSources() == (InputDevice.SOURCE_MOUSE
- | InputDevice.SOURCE_TOUCHPAD);
+ return inputDevice.getSources() == (SOURCE_MOUSE | SOURCE_TOUCHPAD);
}
};
@@ -457,6 +465,7 @@
mDisplayId = context.getDisplayId();
mUiThreadContext = uiThreadContext;
mBackgroundExecutor = backgroundExecutor;
+ mBgHandler = bgHandler;
mUserTracker = userTracker;
mOverviewProxyService = overviewProxyService;
mSysUiState = sysUiState;
@@ -611,9 +620,7 @@
mIsAttached = true;
mOverviewProxyService.addCallback(mQuickSwitchListener);
mSysUiState.addCallback(mSysUiStateCallback);
- mInputManager.registerInputDeviceListener(
- mInputDeviceListener,
- mUiThreadContext.getHandler());
+ mInputManager.registerInputDeviceListener(mInputDeviceListener, mBgHandler);
int[] inputDevices = mInputManager.getInputDeviceIds();
for (int inputDeviceId : inputDevices) {
mInputDeviceListener.onInputDeviceAdded(inputDeviceId);
@@ -1089,8 +1096,8 @@
&& isValidTrackpadBackGesture(true /* isTrackpadEvent */);
} else {
mAllowGesture = isBackAllowedCommon && !mUsingThreeButtonNav && isWithinInsets
- && isWithinTouchRegion((int) ev.getX(), (int) ev.getY())
- && !isButtonPressFromTrackpad(ev);
+ && isWithinTouchRegion((int) ev.getX(), (int) ev.getY())
+ && !isButtonPressFromTrackpad(ev);
}
if (mAllowGesture) {
mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge);
@@ -1202,10 +1209,8 @@
}
private boolean isButtonPressFromTrackpad(MotionEvent ev) {
- // We don't allow back for button press from the trackpad, and yet we do with a mouse.
- int sources = InputManager.getInstance().getInputDevice(ev.getDeviceId()).getSources();
- int sourceTrackpad = (SOURCE_MOUSE | SOURCE_TOUCHPAD);
- return (sources & sourceTrackpad) == sourceTrackpad && ev.getButtonState() != 0;
+ return ev.getSource() == (SOURCE_MOUSE | SOURCE_TOUCHPAD)
+ && ev.getToolType(ev.getActionIndex()) == TOOL_TYPE_FINGER;
}
private void dispatchToBackAnimation(MotionEvent event) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
index f706cf6..d0ce34c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt
@@ -23,18 +23,20 @@
import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE
import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS
import android.hardware.input.fakeInputManager
-import android.os.SystemClock
-import android.view.KeyEvent.ACTION_DOWN
-import android.view.KeyEvent.KEYCODE_A
-import android.view.KeyEvent.META_CTRL_ON
-import android.view.KeyEvent.META_META_ON
-import androidx.compose.ui.input.key.KeyEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
-import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allAppsInputGestureData
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.expectedStandardAddShortcutUiState
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.expectedStandardDeleteShortcutUiState
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyDownEventWithActionKeyPressed
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyDownEventWithoutActionKeyPressed
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.keyUpEventWithActionKeyPressed
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardAddCustomShortcutRequestInfo
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardAddShortcutRequest
+import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardDeleteCustomShortcutRequestInfo
import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey
import com.android.systemui.keyboard.shortcut.shortcutCustomizationViewModelFactory
import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper
@@ -97,7 +99,7 @@
@Test
fun uiState_correctlyUpdatedWhenDeleteShortcutCustomizationIsRequested() {
testScope.runTest {
- viewModel.onShortcutCustomizationRequested(standardDeleteShortcutRequest)
+ viewModel.onShortcutCustomizationRequested(standardDeleteCustomShortcutRequestInfo)
val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
assertThat(uiState).isEqualTo(expectedStandardDeleteShortcutUiState)
@@ -120,7 +122,7 @@
fun uiState_consumedOnDeleteDialogShown() {
testScope.runTest {
val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
- viewModel.onShortcutCustomizationRequested(standardDeleteShortcutRequest)
+ viewModel.onShortcutCustomizationRequested(standardDeleteCustomShortcutRequestInfo)
viewModel.onDialogShown()
assertThat(
@@ -168,7 +170,7 @@
fun uiState_errorMessage_isEmptyByDefault() {
testScope.runTest {
val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
- viewModel.onShortcutCustomizationRequested(allAppsShortcutCustomizationRequest)
+ viewModel.onShortcutCustomizationRequested(standardAddCustomShortcutRequestInfo)
viewModel.onDialogShown()
assertThat((uiState as ShortcutCustomizationUiState.AddShortcutDialog).errorMessage)
@@ -227,6 +229,21 @@
}
@Test
+ fun uiState_becomesInactiveAfterSuccessfullyDeletingShortcut() {
+ testScope.runTest {
+ val uiState by collectLastValue(viewModel.shortcutCustomizationUiState)
+ whenever(inputManager.getCustomInputGestures(any()))
+ .thenReturn(listOf(goHomeInputGestureData, allAppsInputGestureData))
+ whenever(inputManager.removeCustomInputGesture(any()))
+ .thenReturn(CUSTOM_INPUT_GESTURE_RESULT_SUCCESS)
+
+ openDeleteShortcutDialogAndDeleteShortcut()
+
+ assertThat(uiState).isEqualTo(ShortcutCustomizationUiState.Inactive)
+ }
+ }
+
+ @Test
fun onKeyPressed_handlesKeyEvents_whereActionKeyIsAlsoPressed() {
testScope.runTest {
viewModel.onShortcutCustomizationRequested(standardAddShortcutRequest)
@@ -281,7 +298,7 @@
}
private suspend fun openAddShortcutDialogAndSetShortcut() {
- viewModel.onShortcutCustomizationRequested(allAppsShortcutCustomizationRequest)
+ viewModel.onShortcutCustomizationRequested(standardAddCustomShortcutRequestInfo)
viewModel.onDialogShown()
viewModel.onKeyPressed(keyDownEventWithActionKeyPressed)
@@ -290,71 +307,10 @@
viewModel.onSetShortcut()
}
- private val keyDownEventWithoutActionKeyPressed =
- KeyEvent(
- android.view.KeyEvent(
- /* downTime = */ SystemClock.uptimeMillis(),
- /* eventTime = */ SystemClock.uptimeMillis(),
- /* action = */ ACTION_DOWN,
- /* code = */ KEYCODE_A,
- /* repeat = */ 0,
- /* metaState = */ META_CTRL_ON,
- )
- )
+ private suspend fun openDeleteShortcutDialogAndDeleteShortcut() {
+ viewModel.onShortcutCustomizationRequested(standardDeleteCustomShortcutRequestInfo)
+ viewModel.onDialogShown()
- private val keyDownEventWithActionKeyPressed =
- KeyEvent(
- android.view.KeyEvent(
- /* downTime = */ SystemClock.uptimeMillis(),
- /* eventTime = */ SystemClock.uptimeMillis(),
- /* action = */ ACTION_DOWN,
- /* code = */ KEYCODE_A,
- /* repeat = */ 0,
- /* metaState = */ META_CTRL_ON or META_META_ON,
- )
- )
-
- private val keyUpEventWithActionKeyPressed =
- KeyEvent(
- android.view.KeyEvent(
- /* downTime = */ SystemClock.uptimeMillis(),
- /* eventTime = */ SystemClock.uptimeMillis(),
- /* action = */ ACTION_DOWN,
- /* code = */ KEYCODE_A,
- /* repeat = */ 0,
- /* metaState = */ 0,
- )
- )
-
- private val standardAddShortcutRequest =
- ShortcutCustomizationRequestInfo.Add(
- label = "Standard shortcut",
- categoryType = ShortcutCategoryType.System,
- subCategoryLabel = "Standard subcategory",
- )
-
- private val standardDeleteShortcutRequest =
- ShortcutCustomizationRequestInfo.Delete(
- label = "Standard shortcut",
- categoryType = ShortcutCategoryType.System,
- subCategoryLabel = "Standard subcategory",
- )
-
- private val allAppsShortcutCustomizationRequest =
- ShortcutCustomizationRequestInfo.Add(
- label = "Open apps list",
- categoryType = ShortcutCategoryType.System,
- subCategoryLabel = "System controls",
- )
-
- private val expectedStandardAddShortcutUiState =
- ShortcutCustomizationUiState.AddShortcutDialog(
- shortcutLabel = "Standard shortcut",
- defaultCustomShortcutModifierKey =
- ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta),
- isDialogShowing = false,
- )
-
- private val expectedStandardDeleteShortcutUiState =
- ShortcutCustomizationUiState.DeleteShortcutDialog(isDialogShowing = false)
+ viewModel.deleteShortcutCurrentlyBeingCustomized()
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
index b24b3ad..71746b5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.dreams.ui.viewmodel
+import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -23,6 +24,7 @@
val Kosmos.dreamUserActionsViewModel by
Kosmos.Fixture {
DreamUserActionsViewModel(
+ communalInteractor = communalInteractor,
deviceUnlockedInteractor = deviceUnlockedInteractor,
shadeInteractor = shadeInteractor,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
index 721c0b8..2c85816 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt
@@ -102,10 +102,7 @@
)
}
-val Kosmos.inputGestureMaps by
- Kosmos.Fixture {
- InputGestureMaps(applicationContext)
- }
+val Kosmos.inputGestureMaps by Kosmos.Fixture { InputGestureMaps(applicationContext) }
val Kosmos.customShortcutCategoriesRepository by
Kosmos.Fixture {
@@ -116,7 +113,7 @@
testDispatcher,
shortcutCategoriesUtils,
applicationContext,
- inputGestureMaps
+ inputGestureMaps,
)
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 053ec82..d79e66d 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -131,9 +131,6 @@
import static android.provider.Settings.Global.DEBUG_APP;
import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER;
import static android.security.Flags.preventIntentRedirect;
-import static android.security.Flags.preventIntentRedirectCollectNestedKeysOnServerIfNotCollected;
-import static android.security.Flags.preventIntentRedirectShowToast;
-import static android.security.Flags.preventIntentRedirectThrowExceptionIfNestedKeysNotCollected;
import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS;
import static android.view.Display.INVALID_DISPLAY;
@@ -390,7 +387,6 @@
import android.view.View;
import android.view.WindowManager;
import android.view.autofill.AutofillManagerInternal;
-import android.widget.Toast;
import com.android.internal.annotations.CompositeRWLock;
import com.android.internal.annotations.GuardedBy;
@@ -441,7 +437,6 @@
import com.android.server.SystemService;
import com.android.server.SystemServiceManager;
import com.android.server.ThreadPriorityBooster;
-import com.android.server.UiThread;
import com.android.server.Watchdog;
import com.android.server.am.LowMemDetector.MemFactor;
import com.android.server.appop.AppOpsService;
@@ -483,7 +478,6 @@
import dalvik.annotation.optimization.NeverCompile;
import dalvik.system.VMRuntime;
-
import libcore.util.EmptyArray;
import java.io.File;
@@ -19319,31 +19313,8 @@
*/
public void addCreatorToken(@Nullable Intent intent, String creatorPackage) {
if (!preventIntentRedirect()) return;
- if (intent == null) return;
- if ((intent.getExtendedFlags() & Intent.EXTENDED_FLAG_NESTED_INTENT_KEYS_COLLECTED) == 0) {
- Slog.wtf(TAG,
- "[IntentRedirect] The intent does not have its nested keys collected as a "
- + "preparation for creating intent creator tokens. Intent: "
- + intent + "; creatorPackage: " + creatorPackage);
- if (preventIntentRedirectShowToast()) {
- UiThread.getHandler().post(
- () -> Toast.makeText(mContext,
- "Nested keys not collected. go/report-bug-intentRedir to report a"
- + " bug", Toast.LENGTH_LONG).show());
- }
- if (preventIntentRedirectThrowExceptionIfNestedKeysNotCollected()) {
- // this flag will be internal only, not ramped to public.
- throw new SecurityException(
- "The intent does not have its nested keys collected as a preparation for "
- + "creating intent creator tokens. Intent: "
- + intent + "; creatorPackage: " + creatorPackage);
- }
- if (preventIntentRedirectCollectNestedKeysOnServerIfNotCollected()) {
- // this flag will be ramped to public.
- intent.collectExtraIntentKeys();
- }
- }
+ if (intent == null) return;
String targetPackage = intent.getComponent() != null
? intent.getComponent().getPackageName()
diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS
index ab7cd5f..1a6051b 100644
--- a/services/core/java/com/android/server/am/OWNERS
+++ b/services/core/java/com/android/server/am/OWNERS
@@ -66,6 +66,9 @@
# Activity Security
per-file ActivityManager* = file:/ACTIVITY_SECURITY_OWNERS
+# Aconfig Flags
+per-file flags.aconfig = yamasani@google.com, bills@google.com, nalini@google.com
+
# Londoners
michaelwr@google.com #{LAST_RESORT_SUGGESTION}
narayan@google.com #{LAST_RESORT_SUGGESTION}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 78c4f74..f28f3e1 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -140,6 +140,7 @@
// The list is sorted.
@VisibleForTesting
static final String[] sDeviceConfigAconfigScopes = new String[] {
+ "aaos_power_triage",
"aaos_sdv",
"accessibility",
"android_core_networking",
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index 0e7d2b6..a06f9ef 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -524,6 +524,13 @@
static final String PROPERTY_DISABLE_CEC_ON_STANDBY_IN_LOW_ENERGY_MODE =
"persist.sys.hdmi.property_disable_cec_on_standby_in_low_energy_mode";
+ /**
+ * Property that checks if CEC was manually enabled by the user in offline mode. With the help
+ * of this property we avoid turning off CEC when the device goes to sleep and if the device
+ * is in low energy mode.
+ */
+ static final String PROPERTY_USER_ACTION_KEEP_CEC_ENABLED_IN_OFFLINE_MODE =
+ "persist.sys.hdmi.property_user_action_keep_cec_enabled_in_offline_mode";
static final int RECORDING_TYPE_DIGITAL_RF = 1;
static final int RECORDING_TYPE_ANALOGUE_RF = 2;
static final int RECORDING_TYPE_EXTERNAL_PHYSICAL_ADDRESS = 3;
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 0c5069f..6e98bff 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -4027,7 +4027,8 @@
return;
}
if (isTvDevice() && getDisableCecOnStandbyByLowEnergyMode()
- && mPowerManager.isLowPowerStandbyEnabled()) {
+ && mPowerManager.isLowPowerStandbyEnabled()
+ && !userEnabledCecInOfflineMode()) {
Slog.w(TAG, "Disable CEC on standby due to low power energy mode.");
setWasCecDisabledOnStandbyByLowEnergyMode(true);
getHdmiCecConfig().setIntValue(
@@ -5225,4 +5226,14 @@
Constants.PROPERTY_WAS_CEC_DISABLED_ON_STANDBY_BY_LOW_ENERGY_MODE,
String.valueOf(value));
}
+
+ /**
+ * Reads the property that checks if CEC was enabled by the user while in offline mode such that
+ * it won't be disabled when going to sleep by low energy mode.
+ */
+ @VisibleForTesting
+ protected boolean userEnabledCecInOfflineMode() {
+ return SystemProperties.getBoolean(
+ Constants.PROPERTY_USER_ACTION_KEEP_CEC_ENABLED_IN_OFFLINE_MODE, false);
+ }
}
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 76049ca..d177d0e 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -34,6 +34,7 @@
import android.hardware.contexthub.HubEndpointInfo;
import android.hardware.contexthub.IContextHubEndpoint;
import android.hardware.contexthub.IContextHubEndpointCallback;
+import android.hardware.contexthub.IContextHubEndpointDiscoveryCallback;
import android.hardware.contexthub.MessageDeliveryStatus;
import android.hardware.location.ContextHubInfo;
import android.hardware.location.ContextHubMessage;
@@ -793,6 +794,31 @@
return null;
}
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+ @Override
+ public void registerEndpointDiscoveryCallbackId(
+ long endpointId, IContextHubEndpointDiscoveryCallback callback) throws RemoteException {
+ super.registerEndpointDiscoveryCallbackId_enforcePermission();
+ // TODO(b/375487784): Implement this
+ }
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+ @Override
+ public void registerEndpointDiscoveryCallbackDescriptor(
+ String serviceDescriptor, IContextHubEndpointDiscoveryCallback callback)
+ throws RemoteException {
+ super.registerEndpointDiscoveryCallbackDescriptor_enforcePermission();
+ // TODO(b/375487784): Implement this
+ }
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+ @Override
+ public void unregisterEndpointDiscoveryCallback(IContextHubEndpointDiscoveryCallback callback)
+ throws RemoteException {
+ super.unregisterEndpointDiscoveryCallback_enforcePermission();
+ // TODO(b/375487784): Implement this
+ }
+
/**
* Creates an internal load transaction callback to be used for old API clients
*
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index 887e186..708bca7 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -2528,22 +2528,24 @@
}
private void notifyDeviceLockedListenersForUser(int userId, boolean locked) {
- int numListeners = mDeviceLockedStateListeners.beginBroadcast();
- try {
- IntStream.range(0, numListeners).forEach(i -> {
- try {
- Integer uid = (Integer) mDeviceLockedStateListeners.getBroadcastCookie(i);
- if (userId == uid.intValue()) {
- mDeviceLockedStateListeners.getBroadcastItem(i)
- .onDeviceLockedStateChanged(locked);
+ synchronized (mDeviceLockedStateListeners) {
+ int numListeners = mDeviceLockedStateListeners.beginBroadcast();
+ try {
+ IntStream.range(0, numListeners).forEach(i -> {
+ try {
+ Integer uid = (Integer) mDeviceLockedStateListeners.getBroadcastCookie(i);
+ if (userId == uid.intValue()) {
+ mDeviceLockedStateListeners.getBroadcastItem(i)
+ .onDeviceLockedStateChanged(locked);
+ }
+ } catch (RemoteException re) {
+ Log.i(TAG, "Service died", re);
}
- } catch (RemoteException re) {
- Log.i(TAG, "Service died", re);
- }
- });
+ });
- } finally {
- mDeviceLockedStateListeners.finishBroadcast();
+ } finally {
+ mDeviceLockedStateListeners.finishBroadcast();
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 0337f5f..bbef578 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -3181,7 +3181,7 @@
throw new IllegalArgumentException("Invalid crop rect supplied: " + crop);
}
int orientation = screenOrientations[i];
- if (orientation == ORIENTATION_UNKNOWN && cropMap.size() > 1) {
+ if (orientation == ORIENTATION_UNKNOWN && crops.size() > 1) {
throw new IllegalArgumentException("Invalid crops supplied: the UNKNOWN"
+ "screen orientation should only be used in a singleton map");
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 0816e7b..5be4490 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -118,6 +118,7 @@
private boolean mDisableCecOnStandbyByLowEnergyMode;
private boolean mWasCecDisabledOnStandbyByLowEnergyMode;
private boolean mUseHdmiCecPowerStatusController;
+ private boolean mUserEnabledCecInOfflineMode;
private class DeviceEventListener {
private HdmiDeviceInfo mDevice;
@@ -250,6 +251,11 @@
protected void setWasCecDisabledOnStandbyByLowEnergyMode(boolean value) {
mWasCecDisabledOnStandbyByLowEnergyMode = value;
}
+
+ @Override
+ protected boolean userEnabledCecInOfflineMode() {
+ return mUserEnabledCecInOfflineMode;
+ }
};
mHdmiControlService.setIoLooper(mMyLooper);
@@ -298,6 +304,7 @@
mWasCecDisabledOnStandbyByLowEnergyMode = false;
mDisableCecOnStandbyByLowEnergyMode = false;
mUseHdmiCecPowerStatusController = false;
+ mUserEnabledCecInOfflineMode = false;
mNativeWrapper.clearResultMessages();
}
@@ -2400,6 +2407,32 @@
assertTrue(mVendorCommandListeners.contains(vendorCommandListenerInvocationSettingChange));
}
+ @Test
+ public void lowEnergyMode_userEnabledCecInOfflineMode_onStandby_cecStaysEnabled() {
+ mDisableCecOnStandbyByLowEnergyMode = true;
+ mUseHdmiCecPowerStatusController = true;
+ mUserEnabledCecInOfflineMode = true;
+ mPowerManager.setIsLowPowerStandbyEnabled(true);
+
+ assertEquals(mHdmiCecLocalDeviceTv.mService.getHdmiCecConfig().getIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED),
+ HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
+ mHdmiControlService.onStandby(STANDBY_SCREEN_OFF);
+ mTestLooper.dispatchAll();
+
+ assertEquals(mHdmiCecLocalDeviceTv.mService.getHdmiCecConfig().getIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED),
+ HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
+ assertFalse(mWasCecDisabledOnStandbyByLowEnergyMode);
+ mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON);
+ mTestLooper.dispatchAll();
+
+ assertEquals(mHdmiCecLocalDeviceTv.mService.getHdmiCecConfig().getIntValue(
+ HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_ENABLED),
+ HdmiControlManager.HDMI_CEC_CONTROL_ENABLED);
+ assertFalse(mWasCecDisabledOnStandbyByLowEnergyMode);
+ }
+
protected static class MockTvDevice extends HdmiCecLocalDeviceTv {
MockTvDevice(HdmiControlService service) {
super(service);
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 23203ed..bf103d5 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -746,7 +746,7 @@
* @hide
*/
public static final String ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED =
- "android.telephony.action.ACTION_SATELLITE_SUBSCRIBER_ID_LIST_CHANGED";
+ "android.telephony.satellite.action.SATELLITE_SUBSCRIBER_ID_LIST_CHANGED";
/**
@@ -757,7 +757,7 @@
* @hide
*/
public static final String ACTION_SATELLITE_START_NON_EMERGENCY_SESSION =
- "android.telephony.action.ACTION_SATELLITE_START_NON_EMERGENCY_SESSION";
+ "android.telephony.satellite.action.SATELLITE_START_NON_EMERGENCY_SESSION";
/**
* Meta-data represents whether the application supports P2P SMS over carrier roaming satellite
* which needs manual trigger to connect to satellite. The messaging applications that supports
diff --git a/tools/systemfeatures/Android.bp b/tools/systemfeatures/Android.bp
index e6d0a3d..2ebede3 100644
--- a/tools/systemfeatures/Android.bp
+++ b/tools/systemfeatures/Android.bp
@@ -13,6 +13,7 @@
srcs: [
"src/**/*.java",
"src/**/*.kt",
+ ":framework-metalava-annotations",
],
static_libs: [
"guava",
@@ -26,6 +27,12 @@
static_libs: ["systemfeatures-gen-lib"],
}
+java_plugin {
+ name: "systemfeatures-metadata-processor",
+ processor_class: "com.android.systemfeatures.SystemFeaturesMetadataProcessor",
+ static_libs: ["systemfeatures-gen-lib"],
+}
+
genrule {
name: "systemfeatures-gen-tests-srcs",
cmd: "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwNoFeatures --readonly=false > $(location RwNoFeatures.java) && " +
@@ -61,6 +68,7 @@
"systemfeatures-gen-lib",
"truth",
],
+ plugins: ["systemfeatures-metadata-processor"],
}
// Rename the goldens as they may be copied into the source tree, and we don't
diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesMetadataProcessor.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesMetadataProcessor.kt
new file mode 100644
index 0000000..100d869
--- /dev/null
+++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesMetadataProcessor.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.systemfeatures
+
+import android.annotation.SdkConstant
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.TypeSpec
+import java.io.IOException
+import javax.annotation.processing.AbstractProcessor
+import javax.annotation.processing.ProcessingEnvironment
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.SourceVersion
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.tools.Diagnostic
+
+/*
+ * Simple Java code generator for computing metadata for system features.
+ *
+ * <p>The output is a single class file, `com.android.internal.pm.SystemFeaturesMetadata`, with
+ * properties computed from feature constant definitions in the PackageManager class. This
+ * class is only produced if the processed environment includes PackageManager; all other
+ * invocations are ignored.
+ */
+class SystemFeaturesMetadataProcessor : AbstractProcessor() {
+
+ private lateinit var packageManagerType: TypeElement
+
+ override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
+
+ override fun getSupportedAnnotationTypes() = setOf(SDK_CONSTANT_ANNOTATION_NAME)
+
+ override fun init(processingEnv: ProcessingEnvironment) {
+ super.init(processingEnv)
+ packageManagerType =
+ processingEnv.elementUtils.getTypeElement("android.content.pm.PackageManager")!!
+ }
+
+ override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
+ if (roundEnv.processingOver()) {
+ return false
+ }
+
+ // We're only interested in feature constants defined in PackageManager.
+ var featureCount = 0
+ roundEnv.getElementsAnnotatedWith(SdkConstant::class.java).forEach {
+ if (
+ it.enclosingElement == packageManagerType &&
+ it.getAnnotation(SdkConstant::class.java).value ==
+ SdkConstant.SdkConstantType.FEATURE
+ ) {
+ featureCount++
+ }
+ }
+
+ if (featureCount == 0) {
+ // This is fine, and happens for any environment that doesn't include PackageManager.
+ return false
+ }
+
+ val systemFeatureMetadata =
+ TypeSpec.classBuilder("SystemFeaturesMetadata")
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addJavadoc("@hide")
+ .addField(
+ FieldSpec.builder(Int::class.java, "SDK_FEATURE_COUNT")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
+ .addJavadoc(
+ "The number of `@SdkConstant` features defined in PackageManager."
+ )
+ .addJavadoc("@hide")
+ .initializer("\$L", featureCount)
+ .build()
+ )
+ .build()
+
+ try {
+ JavaFile.builder("com.android.internal.pm", systemFeatureMetadata)
+ .skipJavaLangImports(true)
+ .build()
+ .writeTo(processingEnv.filer)
+ } catch (e: IOException) {
+ processingEnv.messager.printMessage(
+ Diagnostic.Kind.ERROR,
+ "Failed to write file: ${e.message}",
+ )
+ }
+
+ return true
+ }
+
+ companion object {
+ private val SDK_CONSTANT_ANNOTATION_NAME = SdkConstant::class.qualifiedName
+ }
+}
diff --git a/tools/systemfeatures/tests/src/PackageManager.java b/tools/systemfeatures/tests/src/PackageManager.java
index db67048..839a937 100644
--- a/tools/systemfeatures/tests/src/PackageManager.java
+++ b/tools/systemfeatures/tests/src/PackageManager.java
@@ -16,14 +16,33 @@
package android.content.pm;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+
/** Stub for testing */
public class PackageManager {
+ @SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_AUTO = "automotive";
+
+ @SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_PC = "pc";
+
+ @SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_VULKAN = "vulkan";
+
+ @SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_WATCH = "watch";
+
+ @SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_WIFI = "wifi";
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String FEATURE_INTENT_CATEGORY = "intent_category_with_feature_name_prefix";
+
+ public static final String FEATURE_NOT_ANNOTATED = "not_annotated";
+
+ public static final String NOT_FEATURE = "not_feature";
+
/** @hide */
public boolean hasSystemFeature(String featureName, int version) {
return false;
diff --git a/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java
new file mode 100644
index 0000000..4ffb5b9
--- /dev/null
+++ b/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.systemfeatures;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.internal.pm.SystemFeaturesMetadata;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SystemFeaturesMetadataProcessorTest {
+
+ @Test
+ public void testSdkFeatureCount() {
+ // See the fake PackageManager definition in this directory.
+ // It defines 5 annotated features, and any/all other constants should be ignored.
+ assertThat(SystemFeaturesMetadata.SDK_FEATURE_COUNT).isEqualTo(5);
+ }
+}