Merge "Reload StackStateAnimator resources on configuration changes" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index ddc7e02..bfa486b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -1375,6 +1375,7 @@
field public static final int reqTouchScreen = 16843303; // 0x1010227
field public static final int requestLegacyExternalStorage = 16844291; // 0x1010603
field public static final int requestRawExternalStorageAccess = 16844357; // 0x1010645
+ field @FlaggedApi("android.security.content_uri_permission_apis") public static final int requireContentUriPermissionFromCaller;
field public static final int requireDeviceScreenOn = 16844317; // 0x101061d
field public static final int requireDeviceUnlock = 16843756; // 0x10103ec
field public static final int required = 16843406; // 0x101028e
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 03b8141..a942e0d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3151,16 +3151,38 @@
package android.app.wearable {
+ @FlaggedApi("android.app.wearable.enable_data_request_observer_api") public final class WearableSensingDataRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getDataSize();
+ method public int getDataType();
+ method public static int getMaxRequestSize();
+ method public static int getRateLimit();
+ method @NonNull public static java.time.Duration getRateLimitWindowSize();
+ method @NonNull public android.os.PersistableBundle getRequestDetails();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.app.wearable.WearableSensingDataRequest> CREATOR;
+ }
+
+ @FlaggedApi("android.app.wearable.enable_data_request_observer_api") public static final class WearableSensingDataRequest.Builder {
+ ctor public WearableSensingDataRequest.Builder(int);
+ method @NonNull public android.app.wearable.WearableSensingDataRequest build();
+ method @NonNull public android.app.wearable.WearableSensingDataRequest.Builder setRequestDetails(@NonNull android.os.PersistableBundle);
+ }
+
public class WearableSensingManager {
+ method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @Nullable public static android.app.wearable.WearableSensingDataRequest getDataRequestFromIntent(@NonNull android.content.Intent);
method @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideData(@NonNull android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideDataStream(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void provideWearableConnection(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void registerDataRequestObserver(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE) public void unregisterDataRequestObserver(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
field public static final int STATUS_ACCESS_DENIED = 5; // 0x5
field @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") public static final int STATUS_CHANNEL_ERROR = 7; // 0x7
field public static final int STATUS_SERVICE_UNAVAILABLE = 3; // 0x3
field public static final int STATUS_SUCCESS = 1; // 0x1
field public static final int STATUS_UNKNOWN = 0; // 0x0
field public static final int STATUS_UNSUPPORTED = 2; // 0x2
+ field @FlaggedApi("android.app.wearable.enable_data_request_observer_api") public static final int STATUS_UNSUPPORTED_DATA_TYPE = 8; // 0x8
field @FlaggedApi("android.app.wearable.enable_unsupported_operation_status_code") public static final int STATUS_UNSUPPORTED_OPERATION = 6; // 0x6
field public static final int STATUS_WEARABLE_UNAVAILABLE = 4; // 0x4
}
@@ -13429,10 +13451,21 @@
package android.service.wearable {
+ @FlaggedApi("android.app.wearable.enable_data_request_observer_api") public interface WearableSensingDataRequester {
+ method public void requestData(@NonNull android.app.wearable.WearableSensingDataRequest, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ field public static final int STATUS_OBSERVER_CANCELLED = 2; // 0x2
+ field public static final int STATUS_SUCCESS = 1; // 0x1
+ field public static final int STATUS_TOO_FREQUENT = 4; // 0x4
+ field public static final int STATUS_TOO_LARGE = 3; // 0x3
+ field public static final int STATUS_UNKNOWN = 0; // 0x0
+ }
+
public abstract class WearableSensingService extends android.app.Service {
ctor public WearableSensingService();
method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
method @BinderThread public abstract void onDataProvided(@NonNull android.os.PersistableBundle, @Nullable android.os.SharedMemory, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @BinderThread public void onDataRequestObserverRegistered(int, @NonNull String, @NonNull android.service.wearable.WearableSensingDataRequester, @NonNull java.util.function.Consumer<java.lang.Integer>);
+ method @FlaggedApi("android.app.wearable.enable_data_request_observer_api") @BinderThread public void onDataRequestObserverUnregistered(int, @NonNull String, @NonNull android.service.wearable.WearableSensingDataRequester, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @BinderThread public abstract void onDataStreamProvided(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @BinderThread public abstract void onQueryServiceStatus(@NonNull java.util.Set<java.lang.Integer>, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>);
method @FlaggedApi("android.app.wearable.enable_provide_wearable_connection_api") @BinderThread public void onSecureWearableConnectionProvided(@NonNull android.os.ParcelFileDescriptor, @NonNull java.util.function.Consumer<java.lang.Integer>);
diff --git a/core/java/android/app/wearable/IWearableSensingManager.aidl b/core/java/android/app/wearable/IWearableSensingManager.aidl
index 9d55ce28..3cbc8a2 100644
--- a/core/java/android/app/wearable/IWearableSensingManager.aidl
+++ b/core/java/android/app/wearable/IWearableSensingManager.aidl
@@ -16,6 +16,7 @@
package android.app.wearable;
+import android.app.PendingIntent;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
@@ -33,4 +34,8 @@
void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
+ void registerDataRequestObserver(int dataType, in PendingIntent dataRequestPendingIntent, in RemoteCallback statusCallback);
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)")
+ void unregisterDataRequestObserver(int dataType, in PendingIntent dataRequestPendingIntent, in RemoteCallback statusCallback);
}
\ No newline at end of file
diff --git a/core/java/android/app/wearable/WearableSensingDataRequest.java b/core/java/android/app/wearable/WearableSensingDataRequest.java
new file mode 100644
index 0000000..9329b37
--- /dev/null
+++ b/core/java/android/app/wearable/WearableSensingDataRequest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.app.wearable;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+
+import java.time.Duration;
+
+/**
+ * Data class for a data request for wearable sensing.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+@SystemApi
+public final class WearableSensingDataRequest implements Parcelable {
+ private static final int MAX_REQUEST_SIZE = 200;
+ private static final Duration RATE_LIMIT_WINDOW_SIZE = Duration.ofMinutes(1);
+ private static final int RATE_LIMIT = 30;
+
+ private final int mDataType;
+ @NonNull private final PersistableBundle mRequestDetails;
+
+ private WearableSensingDataRequest(int dataType, @NonNull PersistableBundle requestDetails) {
+ mDataType = dataType;
+ mRequestDetails = requestDetails;
+ }
+
+ /** Returns the data type this request is for. */
+ public int getDataType() {
+ return mDataType;
+ }
+
+ /** Returns the details for this request. */
+ @NonNull
+ public PersistableBundle getRequestDetails() {
+ return mRequestDetails;
+ }
+
+ /** Returns the data size of this object when it is parcelled. */
+ public int getDataSize() {
+ Parcel parcel = Parcel.obtain();
+ try {
+ writeToParcel(parcel, describeContents());
+ return parcel.dataSize();
+ } finally {
+ parcel.recycle();
+ }
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mDataType);
+ dest.writeTypedObject(mRequestDetails, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "WearableSensingDataRequest { "
+ + "dataType = "
+ + mDataType
+ + ", "
+ + "requestDetails = "
+ + mRequestDetails
+ + " }";
+ }
+
+ /**
+ * Returns a String representation of this data request that shows its contents.
+ *
+ * @hide
+ */
+ public String toExpandedString() {
+ if (mRequestDetails != null) {
+ // Trigger unparcelling so that its individual fields will be listed in toString
+ boolean unused =
+ mRequestDetails.getBoolean(
+ "PlaceholderForWearableSensingDataRequest#toExpandedString()");
+ }
+ return toString();
+ }
+
+ /**
+ * The bundle key for this class of object, used in {@code RemoteCallback#sendResult}.
+ *
+ * @hide
+ */
+ public static final String REQUEST_BUNDLE_KEY =
+ "android.app.wearable.WearableSensingDataRequestBundleKey";
+
+ /**
+ * The bundle key for the status callback for a data request, used in {@code
+ * RemoteCallback#sendResult}.
+ *
+ * @hide
+ */
+ public static final String REQUEST_STATUS_CALLBACK_BUNDLE_KEY =
+ "android.app.wearable.WearableSensingDataRequestStatusCallbackBundleKey";
+
+ public static final @NonNull Parcelable.Creator<WearableSensingDataRequest> CREATOR =
+ new Parcelable.Creator<WearableSensingDataRequest>() {
+ @Override
+ public WearableSensingDataRequest[] newArray(int size) {
+ return new WearableSensingDataRequest[size];
+ }
+
+ @Override
+ public WearableSensingDataRequest createFromParcel(@NonNull Parcel in) {
+ int dataType = in.readInt();
+ PersistableBundle requestDetails =
+ in.readTypedObject(PersistableBundle.CREATOR);
+ return new WearableSensingDataRequest(dataType, requestDetails);
+ }
+ };
+
+ /**
+ * Returns the maximum allowed size of a WearableSensingDataRequest when it is parcelled.
+ * Instances that exceed this size can be constructed, but will be rejected by the system when
+ * they leave the isolated WearableSensingService.
+ */
+ public static int getMaxRequestSize() {
+ return MAX_REQUEST_SIZE;
+ }
+
+ /**
+ * Returns the rolling time window used to perform rate limiting on data requests leaving the
+ * WearableSensingService.
+ */
+ @NonNull
+ public static Duration getRateLimitWindowSize() {
+ return RATE_LIMIT_WINDOW_SIZE;
+ }
+
+ /**
+ * Returns the number of data requests allowed to leave the WearableSensingService in each
+ * {@link #getRateLimitWindowSize()}.
+ */
+ public static int getRateLimit() {
+ return RATE_LIMIT;
+ }
+
+ /** A builder for WearableSensingDataRequest. */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ public static final class Builder {
+ private int mDataType;
+ private PersistableBundle mRequestDetails;
+
+ public Builder(int dataType) {
+ mDataType = dataType;
+ }
+
+ /** Sets the request details. */
+ public @NonNull Builder setRequestDetails(@NonNull PersistableBundle requestDetails) {
+ mRequestDetails = requestDetails;
+ return this;
+ }
+
+ /** Builds the WearableSensingDataRequest. */
+ public @NonNull WearableSensingDataRequest build() {
+ if (mRequestDetails == null) {
+ mRequestDetails = PersistableBundle.EMPTY;
+ }
+ return new WearableSensingDataRequest(mDataType, mRequestDetails);
+ }
+ }
+}
diff --git a/core/java/android/app/wearable/WearableSensingManager.java b/core/java/android/app/wearable/WearableSensingManager.java
index 401d0b7..077f7b5 100644
--- a/core/java/android/app/wearable/WearableSensingManager.java
+++ b/core/java/android/app/wearable/WearableSensingManager.java
@@ -25,9 +25,11 @@
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.app.PendingIntent;
import android.app.ambientcontext.AmbientContextEvent;
import android.companion.CompanionDeviceManager;
import android.content.Context;
+import android.content.Intent;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
@@ -58,7 +60,6 @@
*
* @hide
*/
-
@SystemApi
@SystemService(Context.WEARABLE_SENSING_SERVICE)
public class WearableSensingManager {
@@ -71,6 +72,14 @@
public static final String STATUS_RESPONSE_BUNDLE_KEY =
"android.app.wearable.WearableSensingStatusBundleKey";
+ /**
+ * The Intent extra key for the data request in the Intent sent to the PendingIntent registered
+ * with {@link #registerDataRequestObserver(int, PendingIntent, Executor, Consumer)}.
+ *
+ * @hide
+ */
+ public static final String EXTRA_WEARABLE_SENSING_DATA_REQUEST =
+ "android.app.wearable.extra.WEARABLE_SENSING_DATA_REQUEST";
/**
* An unknown status.
@@ -107,6 +116,7 @@
* The value of the status code that indicates the method called is not supported by the
* implementation of {@link WearableSensingService}.
*/
+
@FlaggedApi(Flags.FLAG_ENABLE_UNSUPPORTED_OPERATION_STATUS_CODE)
public static final int STATUS_UNSUPPORTED_OPERATION = 6;
@@ -118,20 +128,42 @@
@FlaggedApi(Flags.FLAG_ENABLE_PROVIDE_WEARABLE_CONNECTION_API)
public static final int STATUS_CHANNEL_ERROR = 7;
+ /** The value of the status code that indicates the provided data type is not supported. */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ public static final int STATUS_UNSUPPORTED_DATA_TYPE = 8;
+
/** @hide */
- @IntDef(prefix = { "STATUS_" }, value = {
- STATUS_UNKNOWN,
- STATUS_SUCCESS,
- STATUS_UNSUPPORTED,
- STATUS_SERVICE_UNAVAILABLE,
- STATUS_WEARABLE_UNAVAILABLE,
- STATUS_ACCESS_DENIED,
- STATUS_UNSUPPORTED_OPERATION,
- STATUS_CHANNEL_ERROR
- })
+ @IntDef(
+ prefix = {"STATUS_"},
+ value = {
+ STATUS_UNKNOWN,
+ STATUS_SUCCESS,
+ STATUS_UNSUPPORTED,
+ STATUS_SERVICE_UNAVAILABLE,
+ STATUS_WEARABLE_UNAVAILABLE,
+ STATUS_ACCESS_DENIED,
+ STATUS_UNSUPPORTED_OPERATION,
+ STATUS_CHANNEL_ERROR,
+ STATUS_UNSUPPORTED_DATA_TYPE
+ })
@Retention(RetentionPolicy.SOURCE)
public @interface StatusCode {}
+ /**
+ * Retrieves a {@link WearableSensingDataRequest} from the Intent sent to the PendingIntent
+ * provided to {@link #registerDataRequestObserver(int, PendingIntent, Executor, Consumer)}.
+ *
+ * @param intent The Intent received from the PendingIntent.
+ * @return The WearableSensingDataRequest in the provided Intent, or null if the Intent does not
+ * contain a WearableSensingDataRequest.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ @Nullable
+ public static WearableSensingDataRequest getDataRequestFromIntent(@NonNull Intent intent) {
+ return intent.getParcelableExtra(
+ EXTRA_WEARABLE_SENSING_DATA_REQUEST, WearableSensingDataRequest.class);
+ }
+
private final Context mContext;
private final IWearableSensingManager mService;
@@ -256,6 +288,99 @@
}
}
+ /**
+ * Registers a data request observer for the provided data type.
+ *
+ * <p>When data is requested, the provided {@code dataRequestPendingIntent} will be invoked. A
+ * {@link WearableSensingDataRequest} can be extracted from the Intent sent to {@code
+ * dataRequestPendingIntent} by calling {@link #getDataRequestFromIntent(Intent)}. The observer
+ * can then provide the requested data via {@link #provideData(PersistableBundle, SharedMemory,
+ * Executor, Consumer)}.
+ *
+ * <p>There is no limit to the number of observers registered for a data type. How they are
+ * handled depends on the implementation of WearableSensingService.
+ *
+ * <p>When the observer is no longer needed, {@link #unregisterDataRequestObserver(int,
+ * PendingIntent, Executor, Consumer)} should be called with the same {@code
+ * dataRequestPendingIntent}. It should be done regardless of the status code returned from
+ * {@code statusConsumer} in order to clean up housekeeping data for the {@code
+ * dataRequestPendingIntent} maintained by the system.
+ *
+ * <p>Example:
+ *
+ * <pre>{@code
+ * // Create a PendingIntent for MyDataRequestBroadcastReceiver
+ * Intent intent =
+ * new Intent(actionString).setClass(context, MyDataRequestBroadcastReceiver.class);
+ * PendingIntent pendingIntent = PendingIntent.getBroadcast(
+ * context, 0, intent, PendingIntent.FLAG_MUTABLE);
+ *
+ * // Register the PendingIntent as a data request observer
+ * wearableSensingManager.registerDataRequestObserver(
+ * dataType, pendingIntent, executor, statusConsumer);
+ *
+ * // Within MyDataRequestBroadcastReceiver, receive the broadcast Intent and extract the
+ * // WearableSensingDataRequest
+ * {@literal @}Override
+ * public void onReceive(Context context, Intent intent) {
+ * WearableSensingDataRequest dataRequest =
+ * WearableSensingManager.getDataRequestFromIntent(intent);
+ * // After parsing the dataRequest, provide the data
+ * wearableSensingManager.provideData(data, sharedMemory, executor, statusConsumer);
+ * }
+ * }</pre>
+ *
+ * @param dataType The data type to listen to. Values are defined by the application that
+ * implements {@link WearableSensingService}.
+ * @param dataRequestPendingIntent A mutable {@link PendingIntent} that will be invoked when
+ * data is requested. See {@link #getDataRequestFromIntent(Intent)}. Activities are not
+ * allowed to be launched using this PendingIntent.
+ * @param executor Executor on which to run the consumer callback.
+ * @param statusConsumer A consumer that handles the status code for the observer registration.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ @RequiresPermission(Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)
+ public void registerDataRequestObserver(
+ int dataType,
+ @NonNull PendingIntent dataRequestPendingIntent,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull @StatusCode Consumer<Integer> statusConsumer) {
+ try {
+ RemoteCallback statusCallback = createStatusCallback(executor, statusConsumer);
+ mService.registerDataRequestObserver(
+ dataType, dataRequestPendingIntent, statusCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unregisters a previously registered data request observer. If the provided {@link
+ * PendingIntent} was not registered, or is already unregistered, the {@link
+ * WearableSensingService} will not be notified.
+ *
+ * @param dataType The data type the observer is for.
+ * @param dataRequestPendingIntent The observer to unregister.
+ * @param executor Executor on which to run the consumer callback.
+ * @param statusConsumer A consumer that handles the status code for the observer
+ * unregistration.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ @RequiresPermission(Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE)
+ public void unregisterDataRequestObserver(
+ int dataType,
+ @NonNull PendingIntent dataRequestPendingIntent,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull @StatusCode Consumer<Integer> statusConsumer) {
+ try {
+ RemoteCallback statusCallback = createStatusCallback(executor, statusConsumer);
+ mService.unregisterDataRequestObserver(
+ dataType, dataRequestPendingIntent, statusCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private static RemoteCallback createStatusCallback(
Executor executor, Consumer<Integer> statusConsumer) {
return new RemoteCallback(
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 9fe8af5..a64ee5b 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -239,6 +239,110 @@
public String requiredDisplayCategory;
/**
+ * Constant corresponding to {@code none} in the
+ * {@link android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ * @hide
+ */
+ public static final int CONTENT_URI_PERMISSION_NONE = 0;
+
+ /**
+ * Constant corresponding to {@code read} in the
+ * {@link android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ * @hide
+ */
+ public static final int CONTENT_URI_PERMISSION_READ = 1;
+
+ /**
+ * Constant corresponding to {@code write} in the
+ * {@link android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ * @hide
+ */
+ public static final int CONTENT_URI_PERMISSION_WRITE = 2;
+
+ /**
+ * Constant corresponding to {@code readOrWrite} in the
+ * {@link android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ * @hide
+ */
+ public static final int CONTENT_URI_PERMISSION_READ_OR_WRITE = 3;
+
+ /**
+ * Constant corresponding to {@code readAndWrite} in the
+ * {@link android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ * @hide
+ */
+ public static final int CONTENT_URI_PERMISSION_READ_AND_WRITE = 4;
+
+ /** @hide */
+ @SuppressWarnings("SwitchIntDef")
+ public static boolean isRequiredContentUriPermissionRead(
+ @RequiredContentUriPermission int permission) {
+ return switch (permission) {
+ case CONTENT_URI_PERMISSION_READ_AND_WRITE, CONTENT_URI_PERMISSION_READ_OR_WRITE,
+ CONTENT_URI_PERMISSION_READ -> true;
+ default -> false;
+ };
+ }
+
+ /** @hide */
+ @SuppressWarnings("SwitchIntDef")
+ public static boolean isRequiredContentUriPermissionWrite(
+ @RequiredContentUriPermission int permission) {
+ return switch (permission) {
+ case CONTENT_URI_PERMISSION_READ_AND_WRITE, CONTENT_URI_PERMISSION_READ_OR_WRITE,
+ CONTENT_URI_PERMISSION_WRITE -> true;
+ default -> false;
+ };
+ }
+
+ /** @hide */
+ @IntDef(prefix = "CONTENT_URI_PERMISSION_", value = {
+ CONTENT_URI_PERMISSION_NONE,
+ CONTENT_URI_PERMISSION_READ,
+ CONTENT_URI_PERMISSION_WRITE,
+ CONTENT_URI_PERMISSION_READ_OR_WRITE,
+ CONTENT_URI_PERMISSION_READ_AND_WRITE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RequiredContentUriPermission {
+ }
+
+ private String requiredContentUriPermissionToFullString(
+ @RequiredContentUriPermission int permission) {
+ return switch (permission) {
+ case CONTENT_URI_PERMISSION_NONE -> "CONTENT_URI_PERMISSION_NONE";
+ case CONTENT_URI_PERMISSION_READ -> "CONTENT_URI_PERMISSION_READ";
+ case CONTENT_URI_PERMISSION_WRITE -> "CONTENT_URI_PERMISSION_WRITE";
+ case CONTENT_URI_PERMISSION_READ_OR_WRITE -> "CONTENT_URI_PERMISSION_READ_OR_WRITE";
+ case CONTENT_URI_PERMISSION_READ_AND_WRITE -> "CONTENT_URI_PERMISSION_READ_AND_WRITE";
+ default -> "unknown=" + permission;
+ };
+ }
+
+ /** @hide */
+ public static String requiredContentUriPermissionToShortString(
+ @RequiredContentUriPermission int permission) {
+ return switch (permission) {
+ case CONTENT_URI_PERMISSION_NONE -> "none";
+ case CONTENT_URI_PERMISSION_READ -> "read";
+ case CONTENT_URI_PERMISSION_WRITE -> "write";
+ case CONTENT_URI_PERMISSION_READ_OR_WRITE -> "read or write";
+ case CONTENT_URI_PERMISSION_READ_AND_WRITE -> "read and write";
+ default -> "unknown=" + permission;
+ };
+ }
+
+ /**
+ * Specifies permissions necessary to launch this activity via
+ * {@link android.content.Context#startActivity} when passing content URIs. The default value is
+ * {@code none}, meaning no specific permissions are required. Setting this attribute restricts
+ * activity invocation based on the invoker's permissions.
+ * @hide
+ */
+ @RequiredContentUriPermission
+ public int requireContentUriPermissionFromCaller;
+
+ /**
* Activity can not be resized and always occupies the fullscreen area with all windows fully
* visible.
* @hide
@@ -1590,6 +1694,7 @@
mMinAspectRatio = orig.mMinAspectRatio;
supportsSizeChanges = orig.supportsSizeChanges;
requiredDisplayCategory = orig.requiredDisplayCategory;
+ requireContentUriPermissionFromCaller = orig.requireContentUriPermissionFromCaller;
}
/**
@@ -1946,6 +2051,11 @@
if (requiredDisplayCategory != null) {
pw.println(prefix + "requiredDisplayCategory=" + requiredDisplayCategory);
}
+ if ((dumpFlags & DUMP_FLAG_DETAILS) != 0) {
+ pw.println(prefix + "requireContentUriPermissionFromCaller="
+ + requiredContentUriPermissionToFullString(
+ requireContentUriPermissionFromCaller));
+ }
super.dumpBack(pw, prefix, dumpFlags);
}
@@ -1993,6 +2103,7 @@
dest.writeBoolean(supportsSizeChanges);
sForStringSet.parcel(mKnownActivityEmbeddingCerts, dest, flags);
dest.writeString8(requiredDisplayCategory);
+ dest.writeInt(requireContentUriPermissionFromCaller);
}
/**
@@ -2119,6 +2230,7 @@
mKnownActivityEmbeddingCerts = null;
}
requiredDisplayCategory = source.readString8();
+ requireContentUriPermissionFromCaller = source.readInt();
}
/**
diff --git a/core/java/android/service/wearable/IWearableSensingService.aidl b/core/java/android/service/wearable/IWearableSensingService.aidl
index 8fdb2c2..0556188 100644
--- a/core/java/android/service/wearable/IWearableSensingService.aidl
+++ b/core/java/android/service/wearable/IWearableSensingService.aidl
@@ -31,6 +31,8 @@
void provideSecureWearableConnection(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
void provideDataStream(in ParcelFileDescriptor parcelFileDescriptor, in RemoteCallback callback);
void provideData(in PersistableBundle data, in SharedMemory sharedMemory, in RemoteCallback callback);
+ void registerDataRequestObserver(int dataType, in RemoteCallback dataRequestCallback, int dataRequestObserverId, in String packageName, in RemoteCallback statusCallback);
+ void unregisterDataRequestObserver(int dataType, int dataRequestObserverId, in String packageName, in RemoteCallback statusCallback);
void startDetection(in AmbientContextEventRequest request, in String packageName,
in RemoteCallback detectionResultCallback, in RemoteCallback statusCallback);
void stopDetection(in String packageName);
diff --git a/core/java/android/service/wearable/WearableSensingDataRequester.java b/core/java/android/service/wearable/WearableSensingDataRequester.java
new file mode 100644
index 0000000..5a8104f
--- /dev/null
+++ b/core/java/android/service/wearable/WearableSensingDataRequester.java
@@ -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 android.service.wearable;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.app.wearable.Flags;
+import android.app.wearable.WearableSensingDataRequest;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.function.Consumer;
+
+/**
+ * An interface to request wearable sensing data.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+@SystemApi
+public interface WearableSensingDataRequester {
+
+ /** An unknown status. */
+ int STATUS_UNKNOWN = 0;
+
+ /** The value of the status code that indicates success. */
+ int STATUS_SUCCESS = 1;
+
+ /**
+ * The value of the status code that indicates the request is rejected because the data request
+ * observer PendingIntent has been cancelled.
+ */
+ int STATUS_OBSERVER_CANCELLED = 2;
+
+ /**
+ * The value of the status code that indicates the request is rejected because it is larger than
+ * {@link WearableSensingDataRequest#getMaxRequestSize()}.
+ */
+ int STATUS_TOO_LARGE = 3;
+
+ /**
+ * The value of the status code that indicates the request is rejected because it exceeds the
+ * rate limit. See {@link WearableSensingDataRequest#getRateLimit()}.
+ */
+ int STATUS_TOO_FREQUENT = 4;
+
+ /** @hide */
+ @IntDef(
+ prefix = {"STATUS_"},
+ value = {
+ STATUS_UNKNOWN,
+ STATUS_SUCCESS,
+ STATUS_OBSERVER_CANCELLED,
+ STATUS_TOO_LARGE,
+ STATUS_TOO_FREQUENT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface StatusCode {}
+
+ /**
+ * Sends a data request. See {@link WearableSensingService#onDataRequestObserverRegistered(int,
+ * String, WearableSensingDataRequester, Consumer)} for size and rate restrictions on data
+ * requests.
+ *
+ * @param dataRequest The data request to send.
+ * @param statusConsumer A consumer that handles the status code for the data request.
+ */
+ void requestData(
+ @NonNull WearableSensingDataRequest dataRequest,
+ @NonNull @StatusCode Consumer<Integer> statusConsumer);
+}
diff --git a/core/java/android/service/wearable/WearableSensingService.java b/core/java/android/service/wearable/WearableSensingService.java
index d21115b..d25cff7 100644
--- a/core/java/android/service/wearable/WearableSensingService.java
+++ b/core/java/android/service/wearable/WearableSensingService.java
@@ -25,6 +25,7 @@
import android.app.ambientcontext.AmbientContextEvent;
import android.app.ambientcontext.AmbientContextEventRequest;
import android.app.wearable.Flags;
+import android.app.wearable.WearableSensingDataRequest;
import android.app.wearable.WearableSensingManager;
import android.content.Intent;
import android.os.Bundle;
@@ -36,6 +37,7 @@
import android.service.ambientcontext.AmbientContextDetectionResult;
import android.service.ambientcontext.AmbientContextDetectionServiceStatus;
import android.util.Slog;
+import android.util.SparseArray;
import java.util.Arrays;
import java.util.HashSet;
@@ -90,6 +92,9 @@
public static final String SERVICE_INTERFACE =
"android.service.wearable.WearableSensingService";
+ private final SparseArray<WearableSensingDataRequester> mDataRequestObserverIdToRequesterMap =
+ new SparseArray<>();
+
@Nullable
@Override
public final IBinder onBind(@NonNull Intent intent) {
@@ -128,6 +133,55 @@
/** {@inheritDoc} */
@Override
+ public void registerDataRequestObserver(
+ int dataType,
+ RemoteCallback dataRequestCallback,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ Objects.requireNonNull(dataRequestCallback);
+ Objects.requireNonNull(statusCallback);
+ WearableSensingDataRequester dataRequester;
+ synchronized (mDataRequestObserverIdToRequesterMap) {
+ dataRequester =
+ mDataRequestObserverIdToRequesterMap.get(dataRequestObserverId);
+ if (dataRequester == null) {
+ dataRequester = createDataRequester(dataRequestCallback);
+ mDataRequestObserverIdToRequesterMap.put(
+ dataRequestObserverId, dataRequester);
+ }
+ }
+ Consumer<Integer> statusConsumer = createWearableStatusConsumer(statusCallback);
+ WearableSensingService.this.onDataRequestObserverRegistered(
+ dataType, packageName, dataRequester, statusConsumer);
+ }
+
+ @Override
+ public void unregisterDataRequestObserver(
+ int dataType,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ WearableSensingDataRequester dataRequester;
+ synchronized (mDataRequestObserverIdToRequesterMap) {
+ dataRequester =
+ mDataRequestObserverIdToRequesterMap.get(dataRequestObserverId);
+ if (dataRequester == null) {
+ Slog.w(
+ TAG,
+ "dataRequestObserverId not found, cannot unregister data"
+ + " request observer.");
+ return;
+ }
+ mDataRequestObserverIdToRequesterMap.remove(dataRequestObserverId);
+ }
+ Consumer<Integer> statusConsumer = createWearableStatusConsumer(statusCallback);
+ WearableSensingService.this.onDataRequestObserverUnregistered(
+ dataType, packageName, dataRequester, statusConsumer);
+ }
+
+ /** {@inheritDoc} */
+ @Override
public void startDetection(
@NonNull AmbientContextEventRequest request,
String packageName,
@@ -231,19 +285,19 @@
@NonNull Consumer<Integer> statusConsumer);
/**
- * Called when configurations and read-only data in a {@link PersistableBundle}
- * can be used by the WearableSensingService and sends the result to the {@link Consumer}
- * right after the call. It is dependent on the application to define the type of data to
- * provide. This is used by applications that will also provide an implementation of an isolated
- * WearableSensingService. If the data was provided successfully
- * {@link WearableSensingManager#STATUS_SUCCESS} will be provided.
+ * Called when configurations and read-only data in a {@link PersistableBundle} can be used by
+ * the WearableSensingService and sends the result to the {@link Consumer} right after the call.
+ * It is dependent on the application to define the type of data to provide. This is used by
+ * applications that will also provide an implementation of an isolated WearableSensingService.
+ * If the data was provided successfully {@link WearableSensingManager#STATUS_SUCCESS} will be
+ * provided.
*
* @param data Application configuration data to provide to the {@link WearableSensingService}.
- * PersistableBundle does not allow any remotable objects or other contents
- * that can be used to communicate with other processes.
- * @param sharedMemory The unrestricted data blob to
- * provide to the {@link WearableSensingService}. Use this to provide the
- * sensing models data or other such data to the trusted process.
+ * PersistableBundle does not allow any remotable objects or other contents that can be used
+ * to communicate with other processes.
+ * @param sharedMemory The unrestricted data blob to provide to the {@link
+ * WearableSensingService}. Use this to provide the sensing models data or other such data
+ * to the trusted process.
* @param statusConsumer the consumer for the service status.
*/
@BinderThread
@@ -253,6 +307,68 @@
@NonNull Consumer<Integer> statusConsumer);
/**
+ * Called when a data request observer is registered. Each request must not be larger than
+ * {@link WearableSensingDataRequest#getMaxRequestSize()}. In addition, at most {@link
+ * WearableSensingDataRequester#getRateLimit()} requests can be sent every rolling {@link
+ * WearableSensingDataRequester#getRateLimitWindowSize()}. Requests that are too large or too
+ * frequent will be dropped by the system. See {@link
+ * WearableSensingDataRequester#requestData(WearableSensingDataRequest, Consumer)} for details
+ * about the status code returned for each request.
+ *
+ * <p>The implementing class should override this method. After the data requester is received,
+ * it should send a {@link WearableSensingManager#STATUS_SUCCESS} status code to the {@code
+ * statusConsumer} unless it encounters an error condition described by a status code listed in
+ * {@link WearableSensingManager}, such as {@link
+ * WearableSensingManager#STATUS_WEARABLE_UNAVAILABLE}, in which case it should return the
+ * corresponding status code.
+ *
+ * @param dataType The data type the observer is registered for. Values are defined by the
+ * application that implements this class.
+ * @param packageName The package name of the app that will receive the requests.
+ * @param dataRequester A handle to the observer registered. It can be used to request data of
+ * the specified data type.
+ * @param statusConsumer the consumer for the status of the data request observer registration.
+ * This is different from the status for each data request.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ @BinderThread
+ public void onDataRequestObserverRegistered(
+ int dataType,
+ @NonNull String packageName,
+ @NonNull WearableSensingDataRequester dataRequester,
+ @NonNull Consumer<Integer> statusConsumer) {
+ statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION);
+ }
+
+ /**
+ * Called when a data request observer is unregistered.
+ *
+ * <p>The implementing class should override this method. It should send a {@link
+ * WearableSensingManager#STATUS_SUCCESS} status code to the {@code statusConsumer} unless it
+ * encounters an error condition described by a status code listed in {@link
+ * WearableSensingManager}, such as {@link WearableSensingManager#STATUS_WEARABLE_UNAVAILABLE},
+ * in which case it should return the corresponding status code.
+ *
+ * @param dataType The data type the observer is for.
+ * @param packageName The package name of the app that will receive the requests sent to the
+ * dataRequester.
+ * @param dataRequester A handle to the observer to be unregistered. It is the exact same
+ * instance provided in a previous {@link #onDataRequestConsumerRegistered(int, String,
+ * WearableSensingDataRequester, Consumer)} invocation.
+ * @param statusConsumer the consumer for the status of the data request observer
+ * unregistration. This is different from the status for each data request.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API)
+ @BinderThread
+ public void onDataRequestObserverUnregistered(
+ int dataType,
+ @NonNull String packageName,
+ @NonNull WearableSensingDataRequester dataRequester,
+ @NonNull Consumer<Integer> statusConsumer) {
+ statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION);
+ }
+
+ /**
* Called when a client app requests starting detection of the events in the request. The
* implementation should keep track of whether the user has explicitly consented to detecting
* the events using on-going ambient sensor (e.g. microphone), and agreed to share the
@@ -309,6 +425,25 @@
return intArray;
}
+ private static WearableSensingDataRequester createDataRequester(
+ RemoteCallback dataRequestCallback) {
+ return (request, requestStatusConsumer) -> {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(WearableSensingDataRequest.REQUEST_BUNDLE_KEY, request);
+ RemoteCallback requestStatusCallback =
+ new RemoteCallback(
+ requestStatusBundle -> {
+ requestStatusConsumer.accept(
+ requestStatusBundle.getInt(
+ WearableSensingManager.STATUS_RESPONSE_BUNDLE_KEY));
+ });
+ bundle.putParcelable(
+ WearableSensingDataRequest.REQUEST_STATUS_CALLBACK_BUNDLE_KEY,
+ requestStatusCallback);
+ dataRequestCallback.sendResult(bundle);
+ };
+ }
+
@NonNull
private static Consumer<Integer> createWearableStatusConsumer(RemoteCallback statusCallback) {
return response -> {
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedActivity.java b/core/java/com/android/internal/pm/pkg/component/ParsedActivity.java
index b0f3578..a051c1b 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedActivity.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedActivity.java
@@ -89,4 +89,7 @@
*/
@Nullable
String getRequiredDisplayCategory();
+
+ /** Gets the permissions necessary for launching the activity when using content URIs. */
+ int getRequireContentUriPermissionFromCaller();
}
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedActivityImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedActivityImpl.java
index 2f977ee..1218793 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedActivityImpl.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedActivityImpl.java
@@ -36,9 +36,9 @@
import android.text.TextUtils;
import android.util.ArraySet;
+import com.android.internal.pm.pkg.parsing.ParsingUtils;
import com.android.internal.util.DataClass;
import com.android.internal.util.Parcelling.BuiltIn.ForInternedString;
-import com.android.internal.pm.pkg.parsing.ParsingUtils;
import java.util.Collections;
import java.util.Locale;
@@ -97,6 +97,8 @@
@Nullable
private String mRequiredDisplayCategory;
+ private int mRequireContentUriPermissionFromCaller;
+
public ParsedActivityImpl(ParsedActivityImpl other) {
super(other);
this.theme = other.theme;
@@ -124,6 +126,7 @@
this.windowLayout = other.windowLayout;
this.mKnownActivityEmbeddingCerts = other.mKnownActivityEmbeddingCerts;
this.mRequiredDisplayCategory = other.mRequiredDisplayCategory;
+ this.mRequireContentUriPermissionFromCaller = other.mRequireContentUriPermissionFromCaller;
}
/**
@@ -192,6 +195,8 @@
alias.setDirectBootAware(target.isDirectBootAware());
alias.setProcessName(target.getProcessName());
alias.setRequiredDisplayCategory(target.getRequiredDisplayCategory());
+ alias.setRequireContentUriPermissionFromCaller(
+ target.getRequireContentUriPermissionFromCaller());
return alias;
// Not all attributes from the target ParsedActivity are copied to the alias.
@@ -320,6 +325,7 @@
}
sForStringSet.parcel(this.mKnownActivityEmbeddingCerts, dest, flags);
dest.writeString8(this.mRequiredDisplayCategory);
+ dest.writeInt(this.mRequireContentUriPermissionFromCaller);
}
public ParsedActivityImpl() {
@@ -355,6 +361,7 @@
}
this.mKnownActivityEmbeddingCerts = sForStringSet.unparcel(in);
this.mRequiredDisplayCategory = in.readString8();
+ this.mRequireContentUriPermissionFromCaller = in.readInt();
}
@NonNull
@@ -412,7 +419,8 @@
int rotationAnimation,
int colorMode,
@Nullable ActivityInfo.WindowLayout windowLayout,
- @Nullable String requiredDisplayCategory) {
+ @Nullable String requiredDisplayCategory,
+ int requireContentUriPermissionFromCaller) {
this.theme = theme;
this.uiOptions = uiOptions;
this.targetActivity = targetActivity;
@@ -438,6 +446,7 @@
this.colorMode = colorMode;
this.windowLayout = windowLayout;
this.mRequiredDisplayCategory = requiredDisplayCategory;
+ this.mRequireContentUriPermissionFromCaller = requireContentUriPermissionFromCaller;
// onConstructed(); // You can define this method to get a callback
}
@@ -563,6 +572,11 @@
}
@DataClass.Generated.Member
+ public int getRequireContentUriPermissionFromCaller() {
+ return mRequireContentUriPermissionFromCaller;
+ }
+
+ @DataClass.Generated.Member
public @NonNull ParsedActivityImpl setTheme( int value) {
theme = value;
return this;
@@ -694,11 +708,17 @@
return this;
}
+ @DataClass.Generated.Member
+ public @NonNull ParsedActivityImpl setRequireContentUriPermissionFromCaller( int value) {
+ mRequireContentUriPermissionFromCaller = value;
+ return this;
+ }
+
@DataClass.Generated(
- time = 1701338377709L,
+ time = 1706180262165L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/com/android/internal/pm/pkg/component/ParsedActivityImpl.java",
- inputSignatures = "private int theme\nprivate int uiOptions\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String targetActivity\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String parentActivityName\nprivate @android.annotation.Nullable java.lang.String taskAffinity\nprivate int privateFlags\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String permission\nprivate @android.annotation.Nullable java.util.Set<java.lang.String> mKnownActivityEmbeddingCerts\nprivate int launchMode\nprivate int documentLaunchMode\nprivate int maxRecents\nprivate int configChanges\nprivate int softInputMode\nprivate int persistableMode\nprivate int lockTaskLaunchMode\nprivate int screenOrientation\nprivate int resizeMode\nprivate float maxAspectRatio\nprivate float minAspectRatio\nprivate boolean supportsSizeChanges\nprivate @android.annotation.Nullable java.lang.String requestedVrComponent\nprivate int rotationAnimation\nprivate int colorMode\nprivate @android.annotation.Nullable android.content.pm.ActivityInfo.WindowLayout windowLayout\nprivate @android.annotation.Nullable java.lang.String mRequiredDisplayCategory\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<com.android.internal.pm.pkg.component.ParsedActivityImpl> CREATOR\npublic static @android.annotation.NonNull com.android.internal.pm.pkg.component.ParsedActivityImpl makeAppDetailsActivity(java.lang.String,java.lang.String,int,java.lang.String,boolean)\nstatic @android.annotation.NonNull com.android.internal.pm.pkg.component.ParsedActivityImpl makeAlias(java.lang.String,com.android.internal.pm.pkg.component.ParsedActivity)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setMaxAspectRatio(int,float)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setMinAspectRatio(int,float)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setTargetActivity(java.lang.String)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setPermission(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override java.util.Set<java.lang.String> getKnownActivityEmbeddingCerts()\npublic void setKnownActivityEmbeddingCerts(java.util.Set<java.lang.String>)\npublic java.lang.String toString()\npublic @java.lang.Override int describeContents()\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\nclass ParsedActivityImpl extends com.android.internal.pm.pkg.component.ParsedMainComponentImpl implements [com.android.internal.pm.pkg.component.ParsedActivity, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genBuilder=false, genParcelable=false)")
+ inputSignatures = "private int theme\nprivate int uiOptions\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String targetActivity\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String parentActivityName\nprivate @android.annotation.Nullable java.lang.String taskAffinity\nprivate int privateFlags\nprivate @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String permission\nprivate @android.annotation.Nullable java.util.Set<java.lang.String> mKnownActivityEmbeddingCerts\nprivate int launchMode\nprivate int documentLaunchMode\nprivate int maxRecents\nprivate int configChanges\nprivate int softInputMode\nprivate int persistableMode\nprivate int lockTaskLaunchMode\nprivate int screenOrientation\nprivate int resizeMode\nprivate float maxAspectRatio\nprivate float minAspectRatio\nprivate boolean supportsSizeChanges\nprivate @android.annotation.Nullable java.lang.String requestedVrComponent\nprivate int rotationAnimation\nprivate int colorMode\nprivate @android.annotation.Nullable android.content.pm.ActivityInfo.WindowLayout windowLayout\nprivate @android.annotation.Nullable java.lang.String mRequiredDisplayCategory\nprivate int mRequireContentUriPermissionFromCaller\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<com.android.internal.pm.pkg.component.ParsedActivityImpl> CREATOR\npublic static @android.annotation.NonNull com.android.internal.pm.pkg.component.ParsedActivityImpl makeAppDetailsActivity(java.lang.String,java.lang.String,int,java.lang.String,boolean)\nstatic @android.annotation.NonNull com.android.internal.pm.pkg.component.ParsedActivityImpl makeAlias(java.lang.String,com.android.internal.pm.pkg.component.ParsedActivity)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setMaxAspectRatio(int,float)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setMinAspectRatio(int,float)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setTargetActivity(java.lang.String)\npublic com.android.internal.pm.pkg.component.ParsedActivityImpl setPermission(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override java.util.Set<java.lang.String> getKnownActivityEmbeddingCerts()\npublic void setKnownActivityEmbeddingCerts(java.util.Set<java.lang.String>)\npublic java.lang.String toString()\npublic @java.lang.Override int describeContents()\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\nclass ParsedActivityImpl extends com.android.internal.pm.pkg.component.ParsedMainComponentImpl implements [com.android.internal.pm.pkg.component.ParsedActivity, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genBuilder=false, genParcelable=false)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java
index c3f7dab..9f71d88 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java
@@ -241,6 +241,10 @@
activity.setRequiredDisplayCategory(requiredDisplayCategory);
+ activity.setRequireContentUriPermissionFromCaller(sa.getInt(
+ R.styleable.AndroidManifestActivity_requireContentUriPermissionFromCaller,
+ ActivityInfo.CONTENT_URI_PERMISSION_NONE));
+
return parseActivityOrAlias(activity, pkg, tag, parser, res, sa, receiver,
false /*isAlias*/, visibleToEphemeral, input,
R.styleable.AndroidManifestActivity_parentActivityName,
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index d910940..4741012 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -3278,6 +3278,31 @@
<p> By default, the behavior is configured by the same attribute in application.
-->
<attr name="enableOnBackInvokedCallback" format="boolean"/>
+
+ <!-- Specifies permissions necessary to launch this activity via
+ {@link android.content.Context#startActivity} when passing content URIs. The default
+ value is {@code none}, meaning no specific permissions are required. Setting this
+ attribute restricts activity invocation based on the invoker's permissions. If the
+ invoker doesn't have the required permissions, the activity start will be denied via a
+ {@link java.lang.SecurityException}.
+
+ <p> Note that the enforcement works for content URIs inside
+ {@link android.content.Intent#getData} and {@link android.content.Intent#getClipData}.
+ @FlaggedApi("android.security.content_uri_permission_apis") -->
+ <attr name="requireContentUriPermissionFromCaller" format="string">
+ <!-- Default, no specific permissions are required. -->
+ <enum name="none" value="0" />
+ <!-- Enforces the invoker to have read access to the passed content URIs. -->
+ <enum name="read" value="1" />
+ <!-- Enforces the invoker to have write access to the passed content URIs. -->
+ <enum name="write" value="2" />
+ <!-- Enforces the invoker to have either read or write access to the passed content
+ URIs. -->
+ <enum name="readOrWrite" value="3" />
+ <!-- Enforces the invoker to have both read and write access to the passed content
+ URIs. -->
+ <enum name="readAndWrite" value="4" />
+ </attr>
</declare-styleable>
<!-- The <code>activity-alias</code> tag declares a new
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 4799c37..f9cf28c 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -149,6 +149,8 @@
<public name="autoTransact"/>
<!-- @FlaggedApi("com.android.window.flags.enforce_edge_to_edge") -->
<public name="windowOptOutEdgeToEdgeEnforcement"/>
+ <!-- @FlaggedApi("android.security.content_uri_permission_apis") -->
+ <public name="requireContentUriPermissionFromCaller" />
</staging-public-group>
<staging-public-group type="id" first-id="0x01bc0000">
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index b3d2bc9..c8fbad4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -46,7 +46,7 @@
val draggable: DraggableHandler = SceneDraggableHandler(this)
private var _swipeTransition: SwipeTransition? = null
- internal var swipeTransition: SwipeTransition
+ private var swipeTransition: SwipeTransition
get() = _swipeTransition ?: error("SwipeTransition needs to be initialized")
set(value) {
_swipeTransition = value
@@ -92,10 +92,6 @@
/** The [Swipes] associated to the current gesture. */
private var swipes: Swipes? = null
- /** The [UserActionResult] associated to up and down swipes. */
- private var upOrLeftResult: UserActionResult? = null
- private var downOrRightResult: UserActionResult? = null
-
/**
* Whether we should immediately intercept a gesture.
*
@@ -128,7 +124,7 @@
// This [transition] was already driving the animation: simply take over it.
// Stop animating and start from where the current offset.
swipeTransition.cancelOffsetAnimation()
- updateSwipesResults(swipeTransition._fromScene)
+ swipes!!.updateSwipesResults(swipeTransition._fromScene)
return
}
@@ -144,16 +140,24 @@
}
val fromScene = layoutImpl.scene(transitionState.currentScene)
- updateSwipes(fromScene, startedPosition, pointersDown)
+ val newSwipes = computeSwipes(fromScene, startedPosition, pointersDown)
+ swipes = newSwipes
+ val result = newSwipes.findUserActionResult(fromScene, overSlop, true)
- val result =
- findUserActionResult(fromScene, directionOffset = overSlop, updateSwipesResults = true)
- ?: return
- updateTransition(SwipeTransition(fromScene, result), force = true)
- }
+ // As we were unable to locate a valid target scene, the initial SwipeTransition cannot be
+ // defined.
+ if (result == null) return
- private fun updateSwipes(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) {
- this.swipes = computeSwipes(fromScene, startedPosition, pointersDown)
+ val newSwipeTransition =
+ SwipeTransition(
+ fromScene = fromScene,
+ result = result,
+ swipes = newSwipes,
+ layoutImpl = layoutImpl,
+ orientation = orientation
+ )
+
+ updateTransition(newSwipeTransition, force = true)
}
private fun computeSwipes(
@@ -210,13 +214,6 @@
}
}
- private fun Scene.getAbsoluteDistance(distance: UserActionDistance?): Float {
- val targetSize = this.targetSize
- return with(distance ?: DefaultSwipeDistance) {
- layoutImpl.density.absoluteDistance(targetSize, orientation)
- }
- }
-
internal fun onDrag(delta: Float) {
if (delta == 0f || !isDrivingTransition) return
swipeTransition.dragOffset += delta
@@ -226,15 +223,17 @@
val isNewFromScene = fromScene.key != swipeTransition.fromScene
val result =
- findUserActionResult(
- fromScene,
- swipeTransition.dragOffset,
- updateSwipesResults = isNewFromScene,
+ swipes!!.findUserActionResult(
+ fromScene = fromScene,
+ directionOffset = swipeTransition.dragOffset,
+ updateSwipesResults = isNewFromScene
)
- ?: run {
- onDragStopped(delta, true)
- return
- }
+
+ if (result == null) {
+ onDragStopped(velocity = delta, canChangeScene = true)
+ return
+ }
+
swipeTransition.dragOffset += acceleratedOffset
if (
@@ -242,25 +241,20 @@
result.toScene != swipeTransition.toScene ||
result.transitionKey != swipeTransition.key
) {
- updateTransition(
- SwipeTransition(fromScene, result).apply {
- this.dragOffset = swipeTransition.dragOffset
- }
- )
+ val newSwipeTransition =
+ SwipeTransition(
+ fromScene = fromScene,
+ result = result,
+ swipes = swipes!!,
+ layoutImpl = layoutImpl,
+ orientation = orientation
+ )
+ .apply { dragOffset = swipeTransition.dragOffset }
+
+ updateTransition(newSwipeTransition)
}
}
- private fun updateSwipesResults(fromScene: Scene) {
- val (upOrLeftResult, downOrRightResult) =
- computeSwipesResults(
- fromScene,
- this.swipes ?: error("updateSwipes() should be called before updateSwipesResults()")
- )
-
- this.upOrLeftResult = upOrLeftResult
- this.downOrRightResult = downOrRightResult
- }
-
private fun computeSwipesResults(
fromScene: Scene,
swipes: Swipes
@@ -295,74 +289,20 @@
// If the swipe was not committed, don't do anything.
if (swipeTransition._currentScene != toScene) {
- return Pair(fromScene, 0f)
+ return fromScene to 0f
}
// If the offset is past the distance then let's change fromScene so that the user can swipe
// to the next screen or go back to the previous one.
val offset = swipeTransition.dragOffset
- return if (offset <= -absoluteDistance && upOrLeftResult?.toScene == toScene.key) {
- Pair(toScene, absoluteDistance)
- } else if (offset >= absoluteDistance && downOrRightResult?.toScene == toScene.key) {
- Pair(toScene, -absoluteDistance)
+ return if (offset <= -absoluteDistance && swipes!!.upOrLeftResult?.toScene == toScene.key) {
+ toScene to absoluteDistance
+ } else if (
+ offset >= absoluteDistance && swipes!!.downOrRightResult?.toScene == toScene.key
+ ) {
+ toScene to -absoluteDistance
} else {
- Pair(fromScene, 0f)
- }
- }
-
- /**
- * Returns the [UserActionResult] from [fromScene] in the direction of [directionOffset].
- *
- * @param fromScene the scene from which we look for the target
- * @param directionOffset signed float that indicates the direction. Positive is down or right
- * negative is up or left.
- * @param updateSwipesResults whether the target scenes should be updated to the current values
- * held in the Scenes map. Usually we don't want to update them while doing a drag, because
- * this could change the target scene (jump cutting) to a different scene, when some system
- * state changed the targets the background. However, an update is needed any time we
- * calculate the targets for a new fromScene.
- * @return null when there are no targets in either direction. If one direction is null and you
- * drag into the null direction this function will return the opposite direction, assuming
- * that the users intention is to start the drag into the other direction eventually. If
- * [directionOffset] is 0f and both direction are available, it will default to
- * [upOrLeftResult].
- */
- private fun findUserActionResult(
- fromScene: Scene,
- directionOffset: Float,
- updateSwipesResults: Boolean,
- ): UserActionResult? {
- if (updateSwipesResults) updateSwipesResults(fromScene)
-
- return when {
- upOrLeftResult == null && downOrRightResult == null -> null
- (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
- upOrLeftResult
- else -> downOrRightResult
- }
- }
-
- /**
- * A strict version of [findUserActionResult] that will return null when there is no Scene in
- * [directionOffset] direction
- */
- private fun findUserActionResultStrict(directionOffset: Float): UserActionResult? {
- return when {
- directionOffset > 0f -> upOrLeftResult
- directionOffset < 0f -> downOrRightResult
- else -> null
- }
- }
-
- private fun computeAbsoluteDistance(
- fromScene: Scene,
- result: UserActionResult,
- ): Float {
- return if (result == upOrLeftResult) {
- -fromScene.getAbsoluteDistance(result.distance)
- } else {
- check(result == downOrRightResult)
- fromScene.getAbsoluteDistance(result.distance)
+ fromScene to 0f
}
}
@@ -430,19 +370,24 @@
if (startFromIdlePosition) {
// If there is a target scene, we start the overscroll animation.
- val result =
- findUserActionResultStrict(velocity)
- ?: run {
- // We will not animate
- layoutState.finishTransition(swipeTransition, idleScene = fromScene.key)
- return
- }
+ val result = swipes!!.findUserActionResultStrict(velocity)
+ if (result == null) {
+ // We will not animate
+ layoutState.finishTransition(swipeTransition, idleScene = fromScene.key)
+ return
+ }
- updateTransition(
- SwipeTransition(fromScene, result).apply {
- _currentScene = swipeTransition._currentScene
- }
- )
+ val newSwipeTransition =
+ SwipeTransition(
+ fromScene = fromScene,
+ result = result,
+ swipes = swipes!!,
+ layoutImpl = layoutImpl,
+ orientation = orientation
+ )
+ .apply { _currentScene = swipeTransition._currentScene }
+
+ updateTransition(newSwipeTransition)
animateTo(targetScene = fromScene, targetOffset = 0f)
} else {
// We were between two scenes: animate to the initial scene.
@@ -486,134 +431,220 @@
}
}
- private fun SwipeTransition(fromScene: Scene, result: UserActionResult): SwipeTransition {
- return SwipeTransition(
- result.transitionKey,
- fromScene,
- layoutImpl.scene(result.toScene),
- computeAbsoluteDistance(fromScene, result),
- )
- }
-
- internal class SwipeTransition(
- val key: TransitionKey?,
- val _fromScene: Scene,
- val _toScene: Scene,
- /**
- * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is
- * above or to the left of [toScene].
- */
- val distance: Float,
- ) : TransitionState.Transition(_fromScene.key, _toScene.key) {
- var _currentScene by mutableStateOf(_fromScene)
- override val currentScene: SceneKey
- get() = _currentScene.key
-
- override val progress: Float
- get() {
- val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
- return offset / distance
- }
-
- override val isInitiatedByUserInput = true
-
- /** The current offset caused by the drag gesture. */
- var dragOffset by mutableFloatStateOf(0f)
-
- /**
- * Whether the offset is animated (the user lifted their finger) or if it is driven by
- * gesture.
- */
- var isAnimatingOffset by mutableStateOf(false)
-
- // If we are not animating offset, it means the offset is being driven by the user's finger.
- override val isUserInputOngoing: Boolean
- get() = !isAnimatingOffset
-
- /** The animatable used to animate the offset once the user lifted its finger. */
- val offsetAnimatable = Animatable(0f, OffsetVisibilityThreshold)
-
- /** Job to check that there is at most one offset animation in progress. */
- private var offsetAnimationJob: Job? = null
-
- /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
- lateinit var swipeSpec: SpringSpec<Float>
-
- /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
- private fun startOffsetAnimation(job: () -> Job) {
- cancelOffsetAnimation()
- offsetAnimationJob = job()
- }
-
- /** Cancel any ongoing offset animation. */
- // TODO(b/317063114) This should be a suspended function to avoid multiple jobs running at
- // the same time.
- fun cancelOffsetAnimation() {
- offsetAnimationJob?.cancel()
- finishOffsetAnimation()
- }
-
- fun finishOffsetAnimation() {
- if (isAnimatingOffset) {
- isAnimatingOffset = false
- dragOffset = offsetAnimatable.value
- }
- }
-
- fun animateOffset(
- // TODO(b/317063114) The CoroutineScope should be removed.
- coroutineScope: CoroutineScope,
- initialVelocity: Float,
- targetOffset: Float,
- onAnimationCompleted: () -> Unit,
- ) {
- startOffsetAnimation {
- coroutineScope.launch {
- animateOffset(targetOffset, initialVelocity)
- onAnimationCompleted()
- }
- }
- }
-
- private suspend fun animateOffset(targetOffset: Float, initialVelocity: Float) {
- if (!isAnimatingOffset) {
- offsetAnimatable.snapTo(dragOffset)
- }
- isAnimatingOffset = true
-
- offsetAnimatable.animateTo(
- targetValue = targetOffset,
- animationSpec = swipeSpec,
- initialVelocity = initialVelocity,
- )
-
- finishOffsetAnimation()
- }
- }
-
companion object {
private const val TAG = "SceneGestureHandler"
}
+}
- private object DefaultSwipeDistance : UserActionDistance {
- override fun Density.absoluteDistance(
- fromSceneSize: IntSize,
- orientation: Orientation,
- ): Float {
- return when (orientation) {
- Orientation.Horizontal -> fromSceneSize.width
- Orientation.Vertical -> fromSceneSize.height
- }.toFloat()
+private fun SwipeTransition(
+ fromScene: Scene,
+ result: UserActionResult,
+ swipes: Swipes,
+ layoutImpl: SceneTransitionLayoutImpl,
+ orientation: Orientation,
+): SwipeTransition {
+ val upOrLeftResult = swipes.upOrLeftResult
+ val downOrRightResult = swipes.downOrRightResult
+ val userActionDistance = result.distance ?: DefaultSwipeDistance
+ val absoluteDistance =
+ with(userActionDistance) {
+ layoutImpl.density.absoluteDistance(fromScene.targetSize, orientation)
+ }
+
+ return SwipeTransition(
+ key = result.transitionKey,
+ _fromScene = fromScene,
+ _toScene = layoutImpl.scene(result.toScene),
+ distance =
+ when (result) {
+ upOrLeftResult -> -absoluteDistance
+ downOrRightResult -> absoluteDistance
+ else -> error("Unknown result $result ($upOrLeftResult $downOrRightResult)")
+ },
+ )
+}
+
+private class SwipeTransition(
+ val key: TransitionKey?,
+ val _fromScene: Scene,
+ val _toScene: Scene,
+ /**
+ * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is above
+ * or to the left of [toScene]
+ */
+ val distance: Float,
+) : TransitionState.Transition(_fromScene.key, _toScene.key) {
+ var _currentScene by mutableStateOf(_fromScene)
+ override val currentScene: SceneKey
+ get() = _currentScene.key
+
+ override val progress: Float
+ get() {
+ val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
+ return offset / distance
+ }
+
+ override val isInitiatedByUserInput = true
+
+ /** The current offset caused by the drag gesture. */
+ var dragOffset by mutableFloatStateOf(0f)
+
+ /**
+ * Whether the offset is animated (the user lifted their finger) or if it is driven by gesture.
+ */
+ var isAnimatingOffset by mutableStateOf(false)
+
+ // If we are not animating offset, it means the offset is being driven by the user's finger.
+ override val isUserInputOngoing: Boolean
+ get() = !isAnimatingOffset
+
+ /** The animatable used to animate the offset once the user lifted its finger. */
+ val offsetAnimatable = Animatable(0f, OffsetVisibilityThreshold)
+
+ /** Job to check that there is at most one offset animation in progress. */
+ private var offsetAnimationJob: Job? = null
+
+ /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
+ lateinit var swipeSpec: SpringSpec<Float>
+
+ /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
+ private fun startOffsetAnimation(job: () -> Job) {
+ cancelOffsetAnimation()
+ offsetAnimationJob = job()
+ }
+
+ /** Cancel any ongoing offset animation. */
+ // TODO(b/317063114) This should be a suspended function to avoid multiple jobs running at
+ // the same time.
+ fun cancelOffsetAnimation() {
+ offsetAnimationJob?.cancel()
+ finishOffsetAnimation()
+ }
+
+ fun finishOffsetAnimation() {
+ if (isAnimatingOffset) {
+ isAnimatingOffset = false
+ dragOffset = offsetAnimatable.value
}
}
- /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */
- private class Swipes(
- val upOrLeft: Swipe?,
- val downOrRight: Swipe?,
- val upOrLeftNoSource: Swipe?,
- val downOrRightNoSource: Swipe?,
- )
+ fun animateOffset(
+ // TODO(b/317063114) The CoroutineScope should be removed.
+ coroutineScope: CoroutineScope,
+ initialVelocity: Float,
+ targetOffset: Float,
+ onAnimationCompleted: () -> Unit,
+ ) {
+ startOffsetAnimation {
+ coroutineScope.launch {
+ animateOffset(targetOffset, initialVelocity)
+ onAnimationCompleted()
+ }
+ }
+ }
+
+ private suspend fun animateOffset(targetOffset: Float, initialVelocity: Float) {
+ if (!isAnimatingOffset) {
+ offsetAnimatable.snapTo(dragOffset)
+ }
+ isAnimatingOffset = true
+
+ offsetAnimatable.animateTo(
+ targetValue = targetOffset,
+ animationSpec = swipeSpec,
+ initialVelocity = initialVelocity,
+ )
+
+ finishOffsetAnimation()
+ }
+}
+
+private object DefaultSwipeDistance : UserActionDistance {
+ override fun Density.absoluteDistance(
+ fromSceneSize: IntSize,
+ orientation: Orientation,
+ ): Float {
+ return when (orientation) {
+ Orientation.Horizontal -> fromSceneSize.width
+ Orientation.Vertical -> fromSceneSize.height
+ }.toFloat()
+ }
+}
+
+/** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */
+private class Swipes(
+ val upOrLeft: Swipe?,
+ val downOrRight: Swipe?,
+ val upOrLeftNoSource: Swipe?,
+ val downOrRightNoSource: Swipe?,
+) {
+ /** The [UserActionResult] associated to up and down swipes. */
+ var upOrLeftResult: UserActionResult? = null
+ var downOrRightResult: UserActionResult? = null
+
+ fun computeSwipesResults(fromScene: Scene): Pair<UserActionResult?, UserActionResult?> {
+ val userActions = fromScene.userActions
+ fun result(swipe: Swipe?): UserActionResult? {
+ return userActions[swipe ?: return null]
+ }
+
+ val upOrLeftResult = result(upOrLeft) ?: result(upOrLeftNoSource)
+ val downOrRightResult = result(downOrRight) ?: result(downOrRightNoSource)
+ return upOrLeftResult to downOrRightResult
+ }
+
+ fun updateSwipesResults(fromScene: Scene) {
+ val (upOrLeftResult, downOrRightResult) = computeSwipesResults(fromScene)
+
+ this.upOrLeftResult = upOrLeftResult
+ this.downOrRightResult = downOrRightResult
+ }
+
+ /**
+ * Returns the [UserActionResult] from [fromScene] in the direction of [directionOffset].
+ *
+ * @param fromScene the scene from which we look for the target
+ * @param directionOffset signed float that indicates the direction. Positive is down or right
+ * negative is up or left.
+ * @param updateSwipesResults whether the target scenes should be updated to the current values
+ * held in the Scenes map. Usually we don't want to update them while doing a drag, because
+ * this could change the target scene (jump cutting) to a different scene, when some system
+ * state changed the targets the background. However, an update is needed any time we
+ * calculate the targets for a new fromScene.
+ * @return null when there are no targets in either direction. If one direction is null and you
+ * drag into the null direction this function will return the opposite direction, assuming
+ * that the users intention is to start the drag into the other direction eventually. If
+ * [directionOffset] is 0f and both direction are available, it will default to
+ * [upOrLeftResult].
+ */
+ fun findUserActionResult(
+ fromScene: Scene,
+ directionOffset: Float,
+ updateSwipesResults: Boolean,
+ ): UserActionResult? {
+ if (updateSwipesResults) {
+ updateSwipesResults(fromScene)
+ }
+
+ return when {
+ upOrLeftResult == null && downOrRightResult == null -> null
+ (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
+ upOrLeftResult
+ else -> downOrRightResult
+ }
+ }
+
+ /**
+ * A strict version of [findUserActionResult] that will return null when there is no Scene in
+ * [directionOffset] direction
+ */
+ fun findUserActionResultStrict(directionOffset: Float): UserActionResult? {
+ return when {
+ directionOffset > 0f -> upOrLeftResult
+ directionOffset < 0f -> downOrRightResult
+ else -> null
+ }
+ }
}
private class SceneDraggableHandler(
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index dacbdb4..c91d298 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -127,6 +127,9 @@
val progress: Float
get() = (transitionState as Transition).progress
+ val isUserInputOngoing: Boolean
+ get() = (transitionState as Transition).isUserInputOngoing
+
fun advanceUntilIdle() {
testScope.testScheduler.advanceUntilIdle()
}
@@ -538,12 +541,11 @@
onDragStopped(velocity = velocityThreshold)
assertTransition(currentScene = SceneC)
- assertThat(sceneGestureHandler.isDrivingTransition).isTrue()
- assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isTrue()
+ assertThat(isUserInputOngoing).isFalse()
// Start a new gesture while the offset is animating
onDragStartedImmediately()
- assertThat(sceneGestureHandler.swipeTransition.isAnimatingOffset).isFalse()
+ assertThat(isUserInputOngoing).isTrue()
}
@Test
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt
index aab0b1e..e88aaf01 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt
@@ -22,6 +22,7 @@
import android.content.ContentProvider
import android.content.Context
import android.content.Intent
+import android.util.Log
import androidx.core.app.AppComponentFactory
import com.android.systemui.dagger.ContextComponentHelper
import com.android.systemui.dagger.SysUIComponent
@@ -90,8 +91,7 @@
return app
}
- @UsesReflection(
- KeepTarget(instanceOfClassConstant = SysUIComponent::class, methodName = "inject"))
+ @UsesReflection(KeepTarget(instanceOfClassConstant = SysUIComponent::class, methodName = "inject"))
override fun instantiateProviderCompat(cl: ClassLoader, className: String): ContentProvider {
val contentProvider = super.instantiateProviderCompat(cl, className)
if (contentProvider is ContextInitializer) {
@@ -103,12 +103,11 @@
.getMethod("inject", contentProvider.javaClass)
injectMethod.invoke(rootComponent, contentProvider)
} catch (e: NoSuchMethodException) {
- throw RuntimeException("No injector for class: " + contentProvider.javaClass, e)
+ Log.w(TAG, "No injector for class: " + contentProvider.javaClass, e)
} catch (e: IllegalAccessException) {
- throw RuntimeException("Injector inaccessible for class: " +
- contentProvider.javaClass, e)
+ Log.w(TAG, "No injector for class: " + contentProvider.javaClass, e)
} catch (e: InvocationTargetException) {
- throw RuntimeException("Error while injecting: " + contentProvider.javaClass, e)
+ Log.w(TAG, "No injector for class: " + contentProvider.javaClass, e)
}
initializer
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/OWNERS b/packages/SystemUI/src/com/android/systemui/media/OWNERS
index b2d00df..69ea57b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/media/OWNERS
@@ -1,5 +1 @@
per-file MediaProjectionPermissionActivity.java = michaelwr@google.com
-
-# Haptics team also works on Ringtone
-per-file NotificationPlayer.java = file:/services/core/java/com/android/server/vibrator/OWNERS
-per-file RingtonePlayer.java = file:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/OWNERS b/packages/SystemUI/tests/src/com/android/systemui/media/OWNERS
deleted file mode 100644
index 142862d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/media/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-# Haptics team also works on Ringtones (RingtonePlayer/NotificationPlayer)
-file:/services/core/java/com/android/server/vibrator/OWNERS
diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
index a9e5a54..1c70af0 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
@@ -594,6 +594,7 @@
}
ai.applicationInfo = applicationInfo;
ai.requiredDisplayCategory = a.getRequiredDisplayCategory();
+ ai.requireContentUriPermissionFromCaller = a.getRequireContentUriPermissionFromCaller();
ai.setKnownActivityEmbeddingCerts(a.getKnownActivityEmbeddingCerts());
assignFieldsComponentInfoParsedMainComponent(ai, a, pkgSetting, userId);
return ai;
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
index 03c75e0..195e91c 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Intent;
+import android.content.pm.ActivityInfo.RequiredContentUriPermission;
import android.content.pm.ProviderInfo;
import android.net.Uri;
import android.os.IBinder;
@@ -63,6 +64,15 @@
String targetPkg, int targetUserId);
/**
+ * Same as {@link #checkGrantUriPermissionFromIntent(Intent, int, String, int)}, but with an
+ * extra parameter {@code requireContentUriPermissionFromCaller}, which is the value from {@link
+ * android.R.attr#requireContentUriPermissionFromCaller} attribute.
+ */
+ NeededUriGrants checkGrantUriPermissionFromIntent(Intent intent, int callingUid,
+ String targetPkg, int targetUserId,
+ @RequiredContentUriPermission int requireContentUriPermissionFromCaller);
+
+ /**
* Extend a previously calculated set of permissions grants to the given
* owner. All security checks will have already been performed as part of
* calculating {@link NeededUriGrants}.
diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
index ce2cbed..d2f6701 100644
--- a/services/core/java/com/android/server/uri/UriGrantsManagerService.java
+++ b/services/core/java/com/android/server/uri/UriGrantsManagerService.java
@@ -23,6 +23,10 @@
import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
import static android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION;
import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+import static android.content.pm.ActivityInfo.CONTENT_URI_PERMISSION_NONE;
+import static android.content.pm.ActivityInfo.CONTENT_URI_PERMISSION_READ_OR_WRITE;
+import static android.content.pm.ActivityInfo.isRequiredContentUriPermissionRead;
+import static android.content.pm.ActivityInfo.isRequiredContentUriPermissionWrite;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO;
@@ -53,6 +57,8 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ActivityInfo.RequiredContentUriPermission;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
@@ -609,7 +615,8 @@
/** Like checkGrantUriPermission, but takes an Intent. */
private NeededUriGrants checkGrantUriPermissionFromIntentUnlocked(int callingUid,
- String targetPkg, Intent intent, int mode, NeededUriGrants needed, int targetUserId) {
+ String targetPkg, Intent intent, int mode, NeededUriGrants needed, int targetUserId,
+ @RequiredContentUriPermission Integer requireContentUriPermissionFromCaller) {
if (DEBUG) Slog.v(TAG,
"Checking URI perm to data=" + (intent != null ? intent.getData() : null)
+ " clip=" + (intent != null ? intent.getClipData() : null)
@@ -647,6 +654,10 @@
}
if (data != null) {
GrantUri grantUri = GrantUri.resolve(contentUserHint, data, mode);
+ if (android.security.Flags.contentUriPermissionApis()) {
+ enforceRequireContentUriPermissionFromCaller(requireContentUriPermissionFromCaller,
+ grantUri, callingUid);
+ }
targetUid = checkGrantUriPermissionUnlocked(callingUid, targetPkg, grantUri, mode,
targetUid);
if (targetUid > 0) {
@@ -661,6 +672,10 @@
Uri uri = clip.getItemAt(i).getUri();
if (uri != null) {
GrantUri grantUri = GrantUri.resolve(contentUserHint, uri, mode);
+ if (android.security.Flags.contentUriPermissionApis()) {
+ enforceRequireContentUriPermissionFromCaller(
+ requireContentUriPermissionFromCaller, grantUri, callingUid);
+ }
targetUid = checkGrantUriPermissionUnlocked(callingUid, targetPkg,
grantUri, mode, targetUid);
if (targetUid > 0) {
@@ -673,7 +688,8 @@
Intent clipIntent = clip.getItemAt(i).getIntent();
if (clipIntent != null) {
NeededUriGrants newNeeded = checkGrantUriPermissionFromIntentUnlocked(
- callingUid, targetPkg, clipIntent, mode, needed, targetUserId);
+ callingUid, targetPkg, clipIntent, mode, needed, targetUserId,
+ requireContentUriPermissionFromCaller);
if (newNeeded != null) {
needed = newNeeded;
}
@@ -685,6 +701,38 @@
return needed;
}
+ private void enforceRequireContentUriPermissionFromCaller(
+ @RequiredContentUriPermission Integer requireContentUriPermissionFromCaller,
+ GrantUri grantUri, int uid) {
+ // Ignore if requireContentUriPermissionFromCaller hasn't been set or the URI is a
+ // non-content URI.
+ if (requireContentUriPermissionFromCaller == null
+ || requireContentUriPermissionFromCaller == CONTENT_URI_PERMISSION_NONE
+ || !ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) {
+ return;
+ }
+
+ final boolean readMet = !isRequiredContentUriPermissionRead(
+ requireContentUriPermissionFromCaller)
+ || checkContentUriPermissionFullUnlocked(grantUri, uid,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ final boolean writeMet = !isRequiredContentUriPermissionWrite(
+ requireContentUriPermissionFromCaller)
+ || checkContentUriPermissionFullUnlocked(grantUri, uid,
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
+ boolean hasPermission =
+ requireContentUriPermissionFromCaller == CONTENT_URI_PERMISSION_READ_OR_WRITE
+ ? (readMet || writeMet) : (readMet && writeMet);
+
+ if (!hasPermission) {
+ throw new SecurityException("You can't launch this activity because you don't have the"
+ + " required " + ActivityInfo.requiredContentUriPermissionToShortString(
+ requireContentUriPermissionFromCaller) + " access to " + grantUri.uri);
+ }
+ }
+
@GuardedBy("mLock")
private void readGrantedUriPermissionsLocked() {
if (DEBUG) Slog.v(TAG, "readGrantedUriPermissions()");
@@ -1560,9 +1608,24 @@
@Override
public NeededUriGrants checkGrantUriPermissionFromIntent(Intent intent, int callingUid,
String targetPkg, int targetUserId) {
+ return internalCheckGrantUriPermissionFromIntent(intent, callingUid, targetPkg,
+ targetUserId, /* requireContentUriPermissionFromCaller */ null);
+ }
+
+ @Override
+ public NeededUriGrants checkGrantUriPermissionFromIntent(Intent intent, int callingUid,
+ String targetPkg, int targetUserId, int requireContentUriPermissionFromCaller) {
+ return internalCheckGrantUriPermissionFromIntent(intent, callingUid, targetPkg,
+ targetUserId, requireContentUriPermissionFromCaller);
+ }
+
+ private NeededUriGrants internalCheckGrantUriPermissionFromIntent(Intent intent,
+ int callingUid, String targetPkg, int targetUserId,
+ @Nullable Integer requireContentUriPermissionFromCaller) {
final int mode = (intent != null) ? intent.getFlags() : 0;
return UriGrantsManagerService.this.checkGrantUriPermissionFromIntentUnlocked(
- callingUid, targetPkg, intent, mode, null, targetUserId);
+ callingUid, targetPkg, intent, mode, null, targetUserId,
+ requireContentUriPermissionFromCaller);
}
@Override
diff --git a/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java b/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
index e1abae8..88d3daf 100644
--- a/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
+++ b/services/core/java/com/android/server/wearable/RemoteWearableSensingService.java
@@ -118,4 +118,62 @@
}
post(service -> service.provideData(data, sharedMemory, callback));
}
+
+ /**
+ * Registers a data request observer with WearableSensingService.
+ *
+ * @param dataType The data type to listen to. Values are defined by the application that
+ * implements WearableSensingService.
+ * @param dataRequestCallback The observer to send data requests to.
+ * @param dataRequestObserverId The unique ID for the data request observer. It will be used for
+ * unregistering the observer.
+ * @param packageName The package name of the app that will receive the data requests.
+ * @param statusCallback The callback for status of the method call.
+ */
+ public void registerDataRequestObserver(
+ int dataType,
+ RemoteCallback dataRequestCallback,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ if (DEBUG) {
+ Slog.i(TAG, "Registering data request observer.");
+ }
+ var unused =
+ post(
+ service ->
+ service.registerDataRequestObserver(
+ dataType,
+ dataRequestCallback,
+ dataRequestObserverId,
+ packageName,
+ statusCallback));
+ }
+
+ /**
+ * Unregisters a previously registered data request observer.
+ *
+ * @param dataType The data type the observer was registered against.
+ * @param dataRequestObserverId The unique ID of the observer to unregister.
+ * @param packageName The package name of the app that will receive requests sent to the
+ * observer.
+ * @param statusCallback The callback for status of the method call.
+ */
+ public void unregisterDataRequestObserver(
+ int dataType,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ if (DEBUG) {
+ Slog.i(TAG, "Unregistering data request observer.");
+ }
+ var unused =
+ post(
+ service ->
+ service.unregisterDataRequestObserver(
+ dataType,
+ dataRequestObserverId,
+ packageName,
+ statusCallback));
+ }
}
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
index a8d6322..0e8b82f 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java
@@ -262,4 +262,65 @@
mRemoteService.provideData(data, sharedMemory, callback);
}
}
+
+ /**
+ * Handles registering a data request observer.
+ *
+ * @param dataType The data type to listen to. Values are defined by the application that
+ * implements WearableSensingService.
+ * @param dataRequestObserver The observer to register.
+ * @param dataRequestObserverId The unique ID for the data request observer. It will be used for
+ * unregistering the observer.
+ * @param packageName The package name of the app that will receive the data requests.
+ * @param statusCallback The callback for status of the method call.
+ */
+ public void onRegisterDataRequestObserver(
+ int dataType,
+ RemoteCallback dataRequestObserver,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ synchronized (mLock) {
+ if (!setUpServiceIfNeeded()) {
+ Slog.w(TAG, "Detection service is not available at this moment.");
+ notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ ensureRemoteServiceInitiated();
+ mRemoteService.registerDataRequestObserver(
+ dataType,
+ dataRequestObserver,
+ dataRequestObserverId,
+ packageName,
+ statusCallback);
+ }
+ }
+
+ /**
+ * Handles unregistering a previously registered data request observer.
+ *
+ * @param dataType The data type the observer was registered against.
+ * @param dataRequestObserverId The unique ID of the observer to unregister.
+ * @param packageName The package name of the app that will receive requests sent to the
+ * observer.
+ * @param statusCallback The callback for status of the method call.
+ */
+ public void onUnregisterDataRequestObserver(
+ int dataType,
+ int dataRequestObserverId,
+ String packageName,
+ RemoteCallback statusCallback) {
+ synchronized (mLock) {
+ if (!setUpServiceIfNeeded()) {
+ Slog.w(TAG, "Detection service is not available at this moment.");
+ notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ ensureRemoteServiceInitiated();
+ mRemoteService.unregisterDataRequestObserver(
+ dataType, dataRequestObserverId, packageName, statusCallback);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
index 28c8f87..78952fa 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
@@ -21,12 +21,18 @@
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
+import android.app.BroadcastOptions;
+import android.app.ComponentOptions;
+import android.app.PendingIntent;
import android.app.ambientcontext.AmbientContextEvent;
import android.app.wearable.IWearableSensingManager;
+import android.app.wearable.WearableSensingDataRequest;
import android.app.wearable.WearableSensingManager;
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManagerInternal;
+import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.RemoteCallback;
@@ -35,6 +41,8 @@
import android.os.ShellCallback;
import android.os.UserHandle;
import android.provider.DeviceConfig;
+import android.service.wearable.WearableSensingDataRequester;
+import android.text.TextUtils;
import android.util.Slog;
import com.android.internal.R;
@@ -44,10 +52,13 @@
import com.android.server.infra.AbstractMasterSystemService;
import com.android.server.infra.FrameworkResourcesServiceNameResolver;
import com.android.server.pm.KnownPackages;
+import com.android.server.utils.quota.MultiRateLimiter;
import java.io.FileDescriptor;
+import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
/**
@@ -64,9 +75,38 @@
/** Default value in absence of {@link DeviceConfig} override. */
private static final boolean DEFAULT_SERVICE_ENABLED = true;
+
public static final int MAX_TEMPORARY_SERVICE_DURATION_MS = 30000;
+ private static final String RATE_LIMITER_PACKAGE_NAME = "android";
+ private static final String RATE_LIMITER_TAG =
+ WearableSensingManagerService.class.getSimpleName();
+
+ private static final class DataRequestObserverContext {
+ final int mDataType;
+ final int mUserId;
+ final int mDataRequestObserverId;
+ @NonNull final PendingIntent mDataRequestPendingIntent;
+ @NonNull final RemoteCallback mDataRequestRemoteCallback;
+
+ DataRequestObserverContext(
+ int dataType,
+ int userId,
+ int dataRequestObserverId,
+ PendingIntent dataRequestPendingIntent,
+ RemoteCallback dataRequestRemoteCallback) {
+ mDataType = dataType;
+ mUserId = userId;
+ mDataRequestObserverId = dataRequestObserverId;
+ mDataRequestPendingIntent = dataRequestPendingIntent;
+ mDataRequestRemoteCallback = dataRequestRemoteCallback;
+ }
+ }
+
private final Context mContext;
+ private final AtomicInteger mNextDataRequestObserverId = new AtomicInteger(1);
+ private final Set<DataRequestObserverContext> mDataRequestObserverContexts = new HashSet<>();
+ private final MultiRateLimiter mDataRequestRateLimiter;
volatile boolean mIsServiceEnabled;
public WearableSensingManagerService(Context context) {
@@ -78,6 +118,12 @@
PACKAGE_UPDATE_POLICY_REFRESH_EAGER
| /*To avoid high latency*/ PACKAGE_RESTART_POLICY_REFRESH_EAGER);
mContext = context;
+ mDataRequestRateLimiter =
+ new MultiRateLimiter.Builder(context)
+ .addRateLimit(
+ WearableSensingDataRequest.getRateLimit(),
+ WearableSensingDataRequest.getRateLimitWindowSize())
+ .build();
}
@Override
@@ -192,6 +238,96 @@
}
}
+ private DataRequestObserverContext getDataRequestObserverContext(
+ int dataType, int userId, PendingIntent dataRequestPendingIntent) {
+ synchronized (mDataRequestObserverContexts) {
+ for (DataRequestObserverContext observerContext : mDataRequestObserverContexts) {
+ if (observerContext.mDataType == dataType
+ && observerContext.mUserId == userId
+ && observerContext.mDataRequestPendingIntent.equals(
+ dataRequestPendingIntent)) {
+ return observerContext;
+ }
+ }
+ }
+ return null;
+ }
+
+ @NonNull
+ private RemoteCallback createDataRequestRemoteCallback(
+ PendingIntent dataRequestPendingIntent, int userId) {
+ return new RemoteCallback(
+ bundle -> {
+ WearableSensingDataRequest dataRequest =
+ bundle.getParcelable(
+ WearableSensingDataRequest.REQUEST_BUNDLE_KEY,
+ WearableSensingDataRequest.class);
+ if (dataRequest == null) {
+ Slog.e(TAG, "Received data request callback without a request.");
+ return;
+ }
+ RemoteCallback dataRequestStatusCallback =
+ bundle.getParcelable(
+ WearableSensingDataRequest.REQUEST_STATUS_CALLBACK_BUNDLE_KEY,
+ RemoteCallback.class);
+ if (dataRequestStatusCallback == null) {
+ Slog.e(TAG, "Received data request callback without a status callback.");
+ return;
+ }
+ if (dataRequest.getDataSize()
+ > WearableSensingDataRequest.getMaxRequestSize()) {
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "WearableSensingDataRequest size exceeds the maximum"
+ + " allowed size of %s bytes. Dropping the request.",
+ WearableSensingDataRequest.getMaxRequestSize()));
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ dataRequestStatusCallback,
+ WearableSensingDataRequester.STATUS_TOO_LARGE);
+ return;
+ }
+ if (!mDataRequestRateLimiter.isWithinQuota(
+ userId, RATE_LIMITER_PACKAGE_NAME, RATE_LIMITER_TAG)) {
+ Slog.w(TAG, "Data request exceeded rate limit. Dropping the request.");
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ dataRequestStatusCallback,
+ WearableSensingDataRequester.STATUS_TOO_FREQUENT);
+ return;
+ }
+ Intent intent = new Intent();
+ intent.putExtra(
+ WearableSensingManager.EXTRA_WEARABLE_SENSING_DATA_REQUEST,
+ dataRequest);
+ BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setPendingIntentBackgroundActivityStartMode(
+ ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED);
+ mDataRequestRateLimiter.noteEvent(
+ userId, RATE_LIMITER_PACKAGE_NAME, RATE_LIMITER_TAG);
+ final long previousCallingIdentity = Binder.clearCallingIdentity();
+ try {
+ dataRequestPendingIntent.send(
+ getContext(), 0, intent, null, null, null, options.toBundle());
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ dataRequestStatusCallback,
+ WearableSensingDataRequester.STATUS_SUCCESS);
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "Sending data request to %s: %s",
+ dataRequestPendingIntent.getCreatorPackage(),
+ dataRequest.toExpandedString()));
+ } catch (PendingIntent.CanceledException e) {
+ Slog.w(TAG, "Could not deliver pendingIntent: " + dataRequestPendingIntent);
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ dataRequestStatusCallback,
+ WearableSensingDataRequester.STATUS_OBSERVER_CANCELLED);
+ } finally {
+ Binder.restoreCallingIdentity(previousCallingIdentity);
+ }
+ });
+ }
+
private void callPerUserServiceIfExist(
Consumer<WearableSensingManagerPerUserService> serviceConsumer,
RemoteCallback statusCallback) {
@@ -260,8 +396,8 @@
Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE, TAG);
if (!mIsServiceEnabled) {
Slog.w(TAG, "Service not available.");
- WearableSensingManagerPerUserService.notifyStatusCallback(callback,
- WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ callback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
return;
}
callPerUserServiceIfExist(
@@ -270,6 +406,96 @@
}
@Override
+ public void registerDataRequestObserver(
+ int dataType,
+ PendingIntent dataRequestPendingIntent,
+ RemoteCallback statusCallback) {
+ Slog.i(TAG, "WearableSensingManagerInternal registerDataRequestObserver.");
+ Objects.requireNonNull(dataRequestPendingIntent);
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE, TAG);
+ if (!mIsServiceEnabled) {
+ Slog.w(TAG, "Service not available.");
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ int userId = UserHandle.getCallingUserId();
+ RemoteCallback dataRequestCallback;
+ int dataRequestObserverId;
+ synchronized (mDataRequestObserverContexts) {
+ DataRequestObserverContext previousObserverContext =
+ getDataRequestObserverContext(dataType, userId, dataRequestPendingIntent);
+ if (previousObserverContext != null) {
+ Slog.i(TAG, "Received duplicate data request observer.");
+ dataRequestCallback = previousObserverContext.mDataRequestRemoteCallback;
+ dataRequestObserverId = previousObserverContext.mDataRequestObserverId;
+ } else {
+ dataRequestCallback =
+ createDataRequestRemoteCallback(dataRequestPendingIntent, userId);
+ dataRequestObserverId = mNextDataRequestObserverId.getAndIncrement();
+ mDataRequestObserverContexts.add(
+ new DataRequestObserverContext(
+ dataType,
+ userId,
+ dataRequestObserverId,
+ dataRequestPendingIntent,
+ dataRequestCallback));
+ }
+ }
+ callPerUserServiceIfExist(
+ service ->
+ service.onRegisterDataRequestObserver(
+ dataType,
+ dataRequestCallback,
+ dataRequestObserverId,
+ dataRequestPendingIntent.getCreatorPackage(),
+ statusCallback),
+ statusCallback);
+ }
+
+ @Override
+ public void unregisterDataRequestObserver(
+ int dataType,
+ PendingIntent dataRequestPendingIntent,
+ RemoteCallback statusCallback) {
+ Slog.i(TAG, "WearableSensingManagerInternal unregisterDataRequestObserver.");
+ Objects.requireNonNull(dataRequestPendingIntent);
+ Objects.requireNonNull(statusCallback);
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.MANAGE_WEARABLE_SENSING_SERVICE, TAG);
+ if (!mIsServiceEnabled) {
+ Slog.w(TAG, "Service not available.");
+ WearableSensingManagerPerUserService.notifyStatusCallback(
+ statusCallback, WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ int userId = UserHandle.getCallingUserId();
+ int previousDataRequestObserverId;
+ String pendingIntentCreatorPackage;
+ synchronized (mDataRequestObserverContexts) {
+ DataRequestObserverContext previousObserverContext =
+ getDataRequestObserverContext(dataType, userId, dataRequestPendingIntent);
+ if (previousObserverContext == null) {
+ Slog.w(TAG, "Previous observer not found, cannot unregister.");
+ return;
+ }
+ mDataRequestObserverContexts.remove(previousObserverContext);
+ previousDataRequestObserverId = previousObserverContext.mDataRequestObserverId;
+ pendingIntentCreatorPackage =
+ previousObserverContext.mDataRequestPendingIntent.getCreatorPackage();
+ }
+ callPerUserServiceIfExist(
+ service ->
+ service.onUnregisterDataRequestObserver(
+ dataType,
+ previousDataRequestObserverId,
+ pendingIntentCreatorPackage,
+ statusCallback),
+ statusCallback);
+ }
+
+ @Override
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
new WearableSensingShellCommand(WearableSensingManagerService.this).exec(
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 85580ac..d99000e 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -591,9 +591,18 @@
// Carefully collect grants without holding lock
if (activityInfo != null) {
- intentGrants = supervisor.mService.mUgmInternal.checkGrantUriPermissionFromIntent(
- intent, resolvedCallingUid, activityInfo.applicationInfo.packageName,
- UserHandle.getUserId(activityInfo.applicationInfo.uid));
+ if (android.security.Flags.contentUriPermissionApis()) {
+ intentGrants = supervisor.mService.mUgmInternal
+ .checkGrantUriPermissionFromIntent(intent, resolvedCallingUid,
+ activityInfo.applicationInfo.packageName,
+ UserHandle.getUserId(activityInfo.applicationInfo.uid),
+ activityInfo.requireContentUriPermissionFromCaller);
+ } else {
+ intentGrants = supervisor.mService.mUgmInternal
+ .checkGrantUriPermissionFromIntent(intent, resolvedCallingUid,
+ activityInfo.applicationInfo.packageName,
+ UserHandle.getUserId(activityInfo.applicationInfo.uid));
+ }
}
}
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
index 2c8b1cd..349b831 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt
@@ -54,7 +54,8 @@
ParsedActivity::getTheme,
ParsedActivity::getUiOptions,
ParsedActivity::isSupportsSizeChanges,
- ParsedActivity::getRequiredDisplayCategory
+ ParsedActivity::getRequiredDisplayCategory,
+ ParsedActivity::getRequireContentUriPermissionFromCaller
)
override fun mainComponentSubclassExtraParams() = listOf(
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index ad6e2c6..3743483 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -154,6 +154,7 @@
"androidx.annotation_annotation",
"androidx.test.rules",
"services.core",
+ "flag-junit",
],
srcs: [
"src/com/android/server/uri/**/*.java",
diff --git a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
index 3218586..24abc18 100644
--- a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java
@@ -16,6 +16,8 @@
package com.android.server.uri;
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.NULL_DEFAULT;
+
import static com.android.server.uri.UriGrantsMockContext.FLAG_PERSISTABLE;
import static com.android.server.uri.UriGrantsMockContext.FLAG_PREFIX;
import static com.android.server.uri.UriGrantsMockContext.FLAG_READ;
@@ -57,22 +59,49 @@
import android.net.Uri;
import android.os.Process;
import android.os.UserHandle;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.platform.test.ravenwood.RavenwoodRule;
import android.util.ArraySet;
-import androidx.test.InstrumentationRegistry;
-
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
import java.util.Arrays;
+import java.util.List;
import java.util.Set;
+@RunWith(Parameterized.class)
public class UriGrantsManagerServiceTest {
@Rule
public final RavenwoodRule mRavenwood = new RavenwoodRule();
+ /**
+ * Why this class needs to test all combinations of
+ * {@link android.security.Flags#FLAG_CONTENT_URI_PERMISSION_APIS}:
+ *
+ * <p>Although tests in this class don't directly query the flag, its value
+ * is needed for {@link UriGrantsManagerInternal#checkGrantUriPermissionFromIntent}. This is
+ * particularly important for host side tests (Ravenwood), which cannot read flag values from
+ * the device and must have them set explicitly.
+ */
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getFlags() {
+ return FlagsParameterization.allCombinationsOf(
+ android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS);
+ }
+
+ public UriGrantsManagerServiceTest(FlagsParameterization flags) {
+ mSetFlagsRule = new SetFlagsRule(NULL_DEFAULT, flags);
+ }
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule;
+
private UriGrantsMockContext mContext;
private UriGrantsManagerInternal mService;