diff --git a/Android.bp b/Android.bp index e70c707a494d3..8ed86d3d62ecf 100644 --- a/Android.bp +++ b/Android.bp @@ -421,6 +421,7 @@ java_defaults { "core/java/com/android/internal/appwidget/IAppWidgetHost.aidl", "core/java/com/android/internal/backup/IBackupTransport.aidl", "core/java/com/android/internal/backup/IObbBackupService.aidl", + "core/java/com/android/internal/infra/IAndroidFuture.aidl", "core/java/com/android/internal/inputmethod/IInputContentUriToken.aidl", "core/java/com/android/internal/inputmethod/IInputMethodPrivilegedOperations.aidl", "core/java/com/android/internal/inputmethod/IMultiClientInputMethod.aidl", diff --git a/core/java/android/permission/IPermissionController.aidl b/core/java/android/permission/IPermissionController.aidl index 45c01bcf255b6..ec0fe926b599a 100644 --- a/core/java/android/permission/IPermissionController.aidl +++ b/core/java/android/permission/IPermissionController.aidl @@ -20,6 +20,7 @@ import android.os.RemoteCallback; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.UserHandle; +import com.android.internal.infra.AndroidFuture; /** * Interface for system apps to communication with the permission controller. @@ -28,17 +29,17 @@ import android.os.UserHandle; */ oneway interface IPermissionController { void revokeRuntimePermissions(in Bundle request, boolean doDryRun, int reason, - String callerPackageName, in RemoteCallback callback); + String callerPackageName, in AndroidFuture callback); void getRuntimePermissionBackup(in UserHandle user, in ParcelFileDescriptor pipe); void restoreRuntimePermissionBackup(in UserHandle user, in ParcelFileDescriptor pipe); void restoreDelayedRuntimePermissionBackup(String packageName, in UserHandle user, - in RemoteCallback callback); - void getAppPermissions(String packageName, in RemoteCallback callback); + in AndroidFuture callback); + void getAppPermissions(String packageName, in AndroidFuture callback); void revokeRuntimePermission(String packageName, String permissionName); void countPermissionApps(in List permissionNames, int flags, - in RemoteCallback callback); - void getPermissionUsages(boolean countSystem, long numMillis, in RemoteCallback callback); + in AndroidFuture callback); + void getPermissionUsages(boolean countSystem, long numMillis, in AndroidFuture callback); void setRuntimePermissionGrantStateByDeviceAdmin(String callerPackageName, String packageName, - String permission, int grantState, in RemoteCallback callback); - void grantOrUpgradeDefaultRuntimePermissions(in RemoteCallback callback); + String permission, int grantState, in AndroidFuture callback); + void grantOrUpgradeDefaultRuntimePermissions(in AndroidFuture callback); } diff --git a/core/java/android/permission/PermissionControllerManager.java b/core/java/android/permission/PermissionControllerManager.java index de0bcb628f5ec..566ba8a50fff8 100644 --- a/core/java/android/permission/PermissionControllerManager.java +++ b/core/java/android/permission/PermissionControllerManager.java @@ -46,7 +46,6 @@ import android.content.pm.ResolveInfo; import android.os.Binder; import android.os.Bundle; import android.os.Handler; -import android.os.RemoteCallback; import android.os.UserHandle; import android.util.ArrayMap; import android.util.Log; @@ -57,7 +56,6 @@ import com.android.internal.infra.AndroidFuture; import com.android.internal.infra.RemoteStream; import com.android.internal.infra.ServiceConnector; import com.android.internal.util.CollectionUtils; -import com.android.internal.util.Preconditions; import libcore.util.EmptyArray; @@ -67,7 +65,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -94,14 +91,6 @@ public final class PermissionControllerManager { private static ArrayMap, ServiceConnector> sRemoteServices = new ArrayMap<>(1); - /** - * The key for retrieving the result from the returned bundle. - * - * @hide - */ - public static final String KEY_RESULT = - "android.permission.PermissionControllerManager.key.result"; - /** @hide */ @IntDef(prefix = { "REASON_" }, value = { REASON_MALWARE, @@ -291,31 +280,17 @@ public final class PermissionControllerManager { new ArrayList<>(appRequest.getValue())); } - AndroidFuture revokeRuntimePermissionsResult = new AndroidFuture<>(); + AndroidFuture>> revokeRuntimePermissionsResult = + new AndroidFuture<>(); service.revokeRuntimePermissions(bundledizedRequest, doDryRun, reason, mContext.getPackageName(), - new RemoteCallback(revokeRuntimePermissionsResult::complete)); + revokeRuntimePermissionsResult); return revokeRuntimePermissionsResult; - }).thenApply(revokeRuntimePermissionsResult -> { - Map> revoked = new ArrayMap<>(); - Bundle bundleizedRevoked = revokeRuntimePermissionsResult.getBundle(KEY_RESULT); - - for (String packageName : bundleizedRevoked.keySet()) { - Preconditions.checkNotNull(packageName); - - ArrayList permissions = - bundleizedRevoked.getStringArrayList(packageName); - Preconditions.checkCollectionElementsNotNull(permissions, - "permissions"); - - revoked.put(packageName, permissions); - } - return revoked; }).whenCompleteAsync((revoked, err) -> { long token = Binder.clearCallingIdentity(); try { if (err != null) { - Log.e(TAG, "Failure when revoking runtime permissions", err); + Log.e(TAG, "Failure when revoking runtime permissions " + revoked, err); callback.onRevokeRuntimePermissions(Collections.emptyMap()); } else { callback.onRevokeRuntimePermissions(revoked); @@ -356,11 +331,10 @@ public final class PermissionControllerManager { checkNotNull(callback); mRemoteService.postAsync(service -> { - CompletableFuture setRuntimePermissionGrantStateResult = - new CompletableFuture<>(); + AndroidFuture setRuntimePermissionGrantStateResult = new AndroidFuture<>(); service.setRuntimePermissionGrantStateByDeviceAdmin( callerPackageName, packageName, permission, grantState, - new RemoteCallback(setRuntimePermissionGrantStateResult::complete)); + setRuntimePermissionGrantStateResult); return setRuntimePermissionGrantStateResult; }).whenCompleteAsync((setRuntimePermissionGrantStateResult, err) -> { long token = Binder.clearCallingIdentity(); @@ -370,8 +344,7 @@ public final class PermissionControllerManager { err); callback.accept(false); } else { - callback.accept( - setRuntimePermissionGrantStateResult.getBoolean(KEY_RESULT, false)); + callback.accept(setRuntimePermissionGrantStateResult); } } finally { Binder.restoreCallingIdentity(token); @@ -453,10 +426,10 @@ public final class PermissionControllerManager { checkNotNull(callback); mRemoteService.postAsync(service -> { - CompletableFuture restoreDelayedRuntimePermissionBackupResult = - new CompletableFuture<>(); + AndroidFuture restoreDelayedRuntimePermissionBackupResult = + new AndroidFuture<>(); service.restoreDelayedRuntimePermissionBackup(packageName, user, - new RemoteCallback(restoreDelayedRuntimePermissionBackupResult::complete)); + restoreDelayedRuntimePermissionBackupResult); return restoreDelayedRuntimePermissionBackupResult; }).whenCompleteAsync((restoreDelayedRuntimePermissionBackupResult, err) -> { long token = Binder.clearCallingIdentity(); @@ -466,8 +439,7 @@ public final class PermissionControllerManager { callback.accept(true); } else { callback.accept( - restoreDelayedRuntimePermissionBackupResult - .getBoolean(KEY_RESULT, false)); + Boolean.TRUE.equals(restoreDelayedRuntimePermissionBackupResult)); } } finally { Binder.restoreCallingIdentity(token); @@ -492,20 +464,16 @@ public final class PermissionControllerManager { Handler finalHandler = handler != null ? handler : mHandler; mRemoteService.postAsync(service -> { - CompletableFuture getAppPermissionsResult = new CompletableFuture<>(); - service.getAppPermissions(packageName, - new RemoteCallback(getAppPermissionsResult::complete)); + AndroidFuture> getAppPermissionsResult = + new AndroidFuture<>(); + service.getAppPermissions(packageName, getAppPermissionsResult); return getAppPermissionsResult; }).whenComplete((getAppPermissionsResult, err) -> finalHandler.post(() -> { if (err != null) { Log.e(TAG, "Error getting app permission", err); callback.onGetAppPermissions(Collections.emptyList()); } else { - List permissions = null; - if (getAppPermissionsResult != null) { - permissions = getAppPermissionsResult.getParcelableArrayList(KEY_RESULT); - } - callback.onGetAppPermissions(CollectionUtils.emptyIfNull(permissions)); + callback.onGetAppPermissions(CollectionUtils.emptyIfNull(getAppPermissionsResult)); } })); } @@ -548,18 +516,15 @@ public final class PermissionControllerManager { Handler finalHandler = handler != null ? handler : mHandler; mRemoteService.postAsync(service -> { - CompletableFuture countPermissionAppsResult = new CompletableFuture<>(); - service.countPermissionApps(permissionNames, flags, - new RemoteCallback(countPermissionAppsResult::complete)); + AndroidFuture countPermissionAppsResult = new AndroidFuture<>(); + service.countPermissionApps(permissionNames, flags, countPermissionAppsResult); return countPermissionAppsResult; }).whenComplete((countPermissionAppsResult, err) -> finalHandler.post(() -> { if (err != null) { Log.e(TAG, "Error counting permission apps", err); callback.onCountPermissionApps(0); } else { - callback.onCountPermissionApps(countPermissionAppsResult != null - ? countPermissionAppsResult.getInt(KEY_RESULT) - : 0); + callback.onCountPermissionApps(countPermissionAppsResult); } })); } @@ -584,9 +549,9 @@ public final class PermissionControllerManager { mRemoteService.postAsync(service -> { - CompletableFuture getPermissionUsagesResult = new CompletableFuture<>(); - service.getPermissionUsages(countSystem, numMillis, - new RemoteCallback(getPermissionUsagesResult::complete)); + AndroidFuture> getPermissionUsagesResult = + new AndroidFuture<>(); + service.getPermissionUsages(countSystem, numMillis, getPermissionUsagesResult); return getPermissionUsagesResult; }).whenCompleteAsync((getPermissionUsagesResult, err) -> { if (err != null) { @@ -595,9 +560,8 @@ public final class PermissionControllerManager { } else { long token = Binder.clearCallingIdentity(); try { - callback.onPermissionUsageResult(getPermissionUsagesResult != null - ? getPermissionUsagesResult.getParcelableArrayList(KEY_RESULT) - : Collections.emptyList()); + callback.onPermissionUsageResult( + CollectionUtils.emptyIfNull(getPermissionUsagesResult)); } finally { Binder.restoreCallingIdentity(token); } @@ -619,17 +583,17 @@ public final class PermissionControllerManager { public void grantOrUpgradeDefaultRuntimePermissions( @NonNull @CallbackExecutor Executor executor, @NonNull Consumer callback) { mRemoteService.postAsync(service -> { - CompletableFuture grantOrUpgradeDefaultRuntimePermissionsResult = - new CompletableFuture<>(); + AndroidFuture grantOrUpgradeDefaultRuntimePermissionsResult = + new AndroidFuture<>(); service.grantOrUpgradeDefaultRuntimePermissions( - new RemoteCallback(grantOrUpgradeDefaultRuntimePermissionsResult::complete)); + grantOrUpgradeDefaultRuntimePermissionsResult); return grantOrUpgradeDefaultRuntimePermissionsResult; }).whenCompleteAsync((grantOrUpgradeDefaultRuntimePermissionsResult, err) -> { if (err != null) { Log.e(TAG, "Error granting or upgrading runtime permissions", err); callback.accept(false); } else { - callback.accept(grantOrUpgradeDefaultRuntimePermissionsResult != null); + callback.accept(grantOrUpgradeDefaultRuntimePermissionsResult); } }, executor); } diff --git a/core/java/android/permission/PermissionControllerService.java b/core/java/android/permission/PermissionControllerService.java index 8ae93a7f4cea6..c9a1c38448c60 100644 --- a/core/java/android/permission/PermissionControllerService.java +++ b/core/java/android/permission/PermissionControllerService.java @@ -41,12 +41,13 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.os.IBinder; import android.os.ParcelFileDescriptor; -import android.os.RemoteCallback; import android.os.UserHandle; import android.permission.PermissionControllerManager.CountPermissionAppsFlag; import android.util.ArrayMap; import android.util.Log; +import com.android.internal.infra.AndroidFuture; +import com.android.internal.util.CollectionUtils; import com.android.internal.util.Preconditions; import java.io.IOException; @@ -209,7 +210,7 @@ public abstract class PermissionControllerService extends Service { @Override public void revokeRuntimePermissions( Bundle bundleizedRequest, boolean doDryRun, int reason, - String callerPackageName, RemoteCallback callback) { + String callerPackageName, AndroidFuture callback) { checkNotNull(bundleizedRequest, "bundleizedRequest"); checkNotNull(callerPackageName); checkNotNull(callback); @@ -237,22 +238,11 @@ public abstract class PermissionControllerService extends Service { onRevokeRuntimePermissions(request, doDryRun, reason, callerPackageName, revoked -> { - checkNotNull(revoked); - Bundle bundledizedRevoked = new Bundle(); - for (Map.Entry> appRevocation : - revoked.entrySet()) { - checkNotNull(appRevocation.getKey()); - checkCollectionElementsNotNull(appRevocation.getValue(), - "permissions"); - - bundledizedRevoked.putStringArrayList(appRevocation.getKey(), - new ArrayList<>(appRevocation.getValue())); - } - - Bundle result = new Bundle(); - result.putBundle(PermissionControllerManager.KEY_RESULT, - bundledizedRevoked); - callback.sendResult(result); + CollectionUtils.forEach(revoked, (pkg, perms) -> { + Preconditions.checkNotNull(pkg); + Preconditions.checkCollectionElementsNotNull(perms, "permissions"); + }); + callback.complete(revoked); }); } @@ -294,40 +284,24 @@ public abstract class PermissionControllerService extends Service { @Override public void restoreDelayedRuntimePermissionBackup(String packageName, UserHandle user, - RemoteCallback callback) { + AndroidFuture callback) { checkNotNull(packageName); checkNotNull(user); checkNotNull(callback); enforceCallingPermission(Manifest.permission.GRANT_RUNTIME_PERMISSIONS, null); - onRestoreDelayedRuntimePermissionsBackup(packageName, user, - hasMoreBackup -> { - Bundle result = new Bundle(); - result.putBoolean(PermissionControllerManager.KEY_RESULT, - hasMoreBackup); - callback.sendResult(result); - }); + onRestoreDelayedRuntimePermissionsBackup(packageName, user, callback::complete); } @Override - public void getAppPermissions(String packageName, RemoteCallback callback) { + public void getAppPermissions(String packageName, AndroidFuture callback) { checkNotNull(packageName, "packageName"); checkNotNull(callback, "callback"); enforceCallingPermission(Manifest.permission.GET_RUNTIME_PERMISSIONS, null); - onGetAppPermissions(packageName, - permissions -> { - if (permissions != null && !permissions.isEmpty()) { - Bundle result = new Bundle(); - result.putParcelableList(PermissionControllerManager.KEY_RESULT, - permissions); - callback.sendResult(result); - } else { - callback.sendResult(null); - } - }); + onGetAppPermissions(packageName, callback::complete); } @Override @@ -349,43 +323,31 @@ public abstract class PermissionControllerService extends Service { @Override public void countPermissionApps(List permissionNames, int flags, - RemoteCallback callback) { + AndroidFuture callback) { checkCollectionElementsNotNull(permissionNames, "permissionNames"); checkFlagsArgument(flags, COUNT_WHEN_SYSTEM | COUNT_ONLY_WHEN_GRANTED); checkNotNull(callback, "callback"); enforceCallingPermission(Manifest.permission.GET_RUNTIME_PERMISSIONS, null); - onCountPermissionApps(permissionNames, flags, numApps -> { - Bundle result = new Bundle(); - result.putInt(PermissionControllerManager.KEY_RESULT, numApps); - callback.sendResult(result); - }); + onCountPermissionApps(permissionNames, flags, callback::complete); } @Override public void getPermissionUsages(boolean countSystem, long numMillis, - RemoteCallback callback) { + AndroidFuture callback) { checkArgumentNonnegative(numMillis); checkNotNull(callback, "callback"); enforceCallingPermission(Manifest.permission.GET_RUNTIME_PERMISSIONS, null); - onGetPermissionUsages(countSystem, numMillis, users -> { - if (users != null && !users.isEmpty()) { - Bundle result = new Bundle(); - result.putParcelableList(PermissionControllerManager.KEY_RESULT, users); - callback.sendResult(result); - } else { - callback.sendResult(null); - } - }); + onGetPermissionUsages(countSystem, numMillis, callback::complete); } @Override public void setRuntimePermissionGrantStateByDeviceAdmin(String callerPackageName, String packageName, String permission, int grantState, - RemoteCallback callback) { + AndroidFuture callback) { checkStringNotEmpty(callerPackageName); checkStringNotEmpty(packageName); checkStringNotEmpty(permission); @@ -406,21 +368,17 @@ public abstract class PermissionControllerService extends Service { null); onSetRuntimePermissionGrantStateByDeviceAdmin(callerPackageName, - packageName, permission, grantState, wasSet -> { - Bundle result = new Bundle(); - result.putBoolean(PermissionControllerManager.KEY_RESULT, wasSet); - callback.sendResult(result); - }); + packageName, permission, grantState, callback::complete); } @Override - public void grantOrUpgradeDefaultRuntimePermissions(@NonNull RemoteCallback callback) { + public void grantOrUpgradeDefaultRuntimePermissions(@NonNull AndroidFuture callback) { checkNotNull(callback, "callback"); enforceCallingPermission(Manifest.permission.ADJUST_RUNTIME_PERMISSIONS_POLICY, null); - onGrantOrUpgradeDefaultRuntimePermissions(() -> callback.sendResult(Bundle.EMPTY)); + onGrantOrUpgradeDefaultRuntimePermissions(() -> callback.complete(null)); } }; } diff --git a/core/java/com/android/internal/infra/AndroidFuture.aidl b/core/java/com/android/internal/infra/AndroidFuture.aidl new file mode 100644 index 0000000000000..b19aab8ab0d56 --- /dev/null +++ b/core/java/com/android/internal/infra/AndroidFuture.aidl @@ -0,0 +1,20 @@ +/* +** Copyright 2019, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package com.android.internal.infra; + +/** @hide */ +parcelable AndroidFuture; diff --git a/core/java/com/android/internal/infra/AndroidFuture.java b/core/java/com/android/internal/infra/AndroidFuture.java index c9e2d5fe8f8cf..08938a6b04bb1 100644 --- a/core/java/com/android/internal/infra/AndroidFuture.java +++ b/core/java/com/android/internal/infra/AndroidFuture.java @@ -23,6 +23,9 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Handler; import android.os.Message; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; import android.util.ExceptionUtils; import android.util.Log; @@ -45,6 +48,7 @@ import java.util.function.Supplier; * A customized {@link CompletableFuture} with focus on reducing the number of allocations involved * in a typical future usage scenario for Android. * + *

* In particular this involves allocations optimizations in: *

    *
  • {@link #thenCompose(Function)}
  • @@ -55,10 +59,20 @@ import java.util.function.Supplier; *
* As well as their *Async versions. * + *

+ * You can pass {@link AndroidFuture} across an IPC. + * When doing so, completing the future on the other side will propagate the completion back, + * effectively acting as an error-aware remote callback. + * + *

+ * {@link AndroidFuture} is {@link Parcelable} iff its wrapped type {@code T} is + * effectively parcelable, i.e. is supported by {@link Parcel#readValue}/{@link Parcel#writeValue}. + * * @param see {@link CompletableFuture} */ -public class AndroidFuture extends CompletableFuture { +public class AndroidFuture extends CompletableFuture implements Parcelable { + private static final boolean DEBUG = false; private static final String LOG_TAG = AndroidFuture.class.getSimpleName(); private final @NonNull Object mLock = new Object(); @@ -67,6 +81,38 @@ public class AndroidFuture extends CompletableFuture { @GuardedBy("mLock") private @Nullable Executor mListenerExecutor = DIRECT_EXECUTOR; private @NonNull Handler mTimeoutHandler = Handler.getMain(); + private final @Nullable IAndroidFuture mRemoteOrigin; + + public AndroidFuture() { + super(); + mRemoteOrigin = null; + } + + AndroidFuture(Parcel in) { + super(); + if (in.readBoolean()) { + // Done + if (in.readBoolean()) { + // Failed + try { + in.readException(); + } catch (Throwable e) { + completeExceptionally(e); + } + if (!isCompletedExceptionally()) { + throw new IllegalStateException( + "Error unparceling AndroidFuture: exception expected"); + } + } else { + // Success + complete((T) in.readValue(null)); + } + mRemoteOrigin = null; + } else { + // Not done + mRemoteOrigin = IAndroidFuture.Stub.asInterface(in.readStrongBinder()); + } + } @Override public boolean complete(@Nullable T value) { @@ -90,6 +136,11 @@ public class AndroidFuture extends CompletableFuture { protected void onCompleted(@Nullable T res, @Nullable Throwable err) { cancelTimeout(); + if (DEBUG) { + Log.i(LOG_TAG, this + " completed with result " + (err == null ? res : err), + new RuntimeException()); + } + BiConsumer listener; synchronized (mLock) { listener = mListener; @@ -99,6 +150,14 @@ public class AndroidFuture extends CompletableFuture { if (listener != null) { callListenerAsync(listener, res, err); } + + if (mRemoteOrigin != null) { + try { + mRemoteOrigin.complete(this /* resultContainer */); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Failed to propagate completion", e); + } + } } @Override @@ -413,4 +472,49 @@ public class AndroidFuture extends CompletableFuture { } } } + + @Override + public void writeToParcel(Parcel dest, int flags) { + boolean done = isDone(); + dest.writeBoolean(done); + if (done) { + T result; + try { + result = get(); + } catch (Exception t) { + dest.writeBoolean(true); + dest.writeException(t); + return; + } + dest.writeBoolean(false); + dest.writeValue(result); + } else { + dest.writeStrongBinder(new IAndroidFuture.Stub() { + @Override + public void complete(AndroidFuture resultContainer) { + try { + AndroidFuture.this.complete((T) resultContainer.get()); + } catch (Throwable t) { + completeExceptionally(t); + } + } + }.asBinder()); + } + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public AndroidFuture createFromParcel(Parcel parcel) { + return new AndroidFuture(parcel); + } + + public AndroidFuture[] newArray(int size) { + return new AndroidFuture[size]; + } + }; } diff --git a/core/java/com/android/internal/infra/IAndroidFuture.aidl b/core/java/com/android/internal/infra/IAndroidFuture.aidl new file mode 100644 index 0000000000000..302fddecbdaeb --- /dev/null +++ b/core/java/com/android/internal/infra/IAndroidFuture.aidl @@ -0,0 +1,23 @@ +/* +** Copyright 2019, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package com.android.internal.infra; + +import com.android.internal.infra.AndroidFuture; + +oneway interface IAndroidFuture { + void complete(in AndroidFuture resultContainer); +} \ No newline at end of file diff --git a/core/java/com/android/internal/util/CollectionUtils.java b/core/java/com/android/internal/util/CollectionUtils.java index 78fdfe4227124..f9cf23b29a696 100644 --- a/core/java/com/android/internal/util/CollectionUtils.java +++ b/core/java/com/android/internal/util/CollectionUtils.java @@ -18,6 +18,7 @@ package com.android.internal.util; import android.annotation.NonNull; import android.annotation.Nullable; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.ExceptionUtils; @@ -29,6 +30,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; @@ -327,6 +329,33 @@ public class CollectionUtils { } } + /** + * Applies {@code action} to each element in {@code cur} + * + * This avoids creating an iterator if the given map is an {@link ArrayMap} + * For non-{@link ArrayMap}s it avoids creating {@link Map.Entry} instances + */ + public static void forEach(@Nullable Map cur, @Nullable BiConsumer action) { + if (cur == null || action == null) { + return; + } + int size = cur.size(); + if (size == 0) { + return; + } + + if (cur instanceof ArrayMap) { + ArrayMap arrayMap = (ArrayMap) cur; + for (int i = 0; i < size; i++) { + action.accept(arrayMap.keyAt(i), arrayMap.valueAt(i)); + } + } else { + for (K key : cur.keySet()) { + action.accept(key, cur.get(key)); + } + } + } + /** * @return the first element if not empty/null, null otherwise */