diff options
3 files changed, 296 insertions, 46 deletions
diff --git a/services/autofill/java/com/android/server/autofill/RequestId.java b/services/autofill/java/com/android/server/autofill/RequestId.java new file mode 100644 index 000000000000..29ad786dbd4b --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/RequestId.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.autofill; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +// Helper class containing various methods to deal with FillRequest Ids. +// For authentication flows, there needs to be a way to know whether to retrieve the Fill +// Response from the primary provider or the secondary provider from the requestId. A simple +// way to achieve this is by assigning odd number request ids to secondary provider and +// even numbers to primary provider. +public class RequestId { + + private AtomicInteger sIdCounter; + + // Mainly used for tests + RequestId(int start) { + sIdCounter = new AtomicInteger(start); + } + + public RequestId() { + this((int) (Math.floor(Math.random() * 0xFFFF))); + } + + public static int getLastRequestIdIndex(List<Integer> requestIds) { + int lastId = -1; + int indexOfBiggest = -1; + // Biggest number is usually the latest request, since IDs only increase + // The only exception is when the request ID wraps around back to 0 + for (int i = requestIds.size() - 1; i >= 0; i--) { + if (requestIds.get(i) > lastId) { + lastId = requestIds.get(i); + indexOfBiggest = i; + } + } + + // 0xFFFE + 2 == 0x1 (for secondary) + // 0xFFFD + 2 == 0x0 (for primary) + // Wrap has occurred + if (lastId >= 0xFFFD) { + // Calculate the biggest size possible + // If list only has one kind of request ids - we need to multiple by 2 + // (since they skip odd ints) + // Also subtract one from size because at least one integer exists pre-wrap + int calcSize = (requestIds.size()) * 2; + //Biggest possible id after wrapping + int biggestPossible = (lastId + calcSize) % 0xFFFF; + lastId = -1; + indexOfBiggest = -1; + for (int i = 0; i < requestIds.size(); i++) { + int currentId = requestIds.get(i); + if (currentId <= biggestPossible && currentId > lastId) { + lastId = currentId; + indexOfBiggest = i; + } + } + } + + return indexOfBiggest; + } + + public int nextId(boolean isSecondary) { + // For authentication flows, there needs to be a way to know whether to retrieve the Fill + // Response from the primary provider or the secondary provider from the requestId. A simple + // way to achieve this is by assigning odd number request ids to secondary provider and + // even numbers to primary provider. + int requestId; + + do { + requestId = sIdCounter.incrementAndGet() % 0xFFFF; + sIdCounter.set(requestId); + } while (isSecondaryProvider(requestId) != isSecondary); + return requestId; + } + + public static boolean isSecondaryProvider(int requestId) { + return requestId % 2 == 1; + } +} diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 6c76aebc0288..1c544c8e2567 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -263,7 +263,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState static final int AUGMENTED_AUTOFILL_REQUEST_ID = 1; - private static AtomicInteger sIdCounter = new AtomicInteger(2); + private static RequestId mRequestId = new RequestId(); private static AtomicInteger sIdCounterForPcc = new AtomicInteger(2); @@ -1333,7 +1333,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } viewState.setState(newState); - int requestId = getRequestId(isSecondary); + int requestId = mRequestId.nextId(isSecondary); // Create a metrics log for the request final int ordinal = mRequestLogs.size() + 1; @@ -1415,25 +1415,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState requestAssistStructureLocked(requestId, flags); } - private static int getRequestId(boolean isSecondary) { - // For authentication flows, there needs to be a way to know whether to retrieve the Fill - // Response from the primary provider or the secondary provider from the requestId. A simple - // way to achieve this is by assigning odd number request ids to secondary provider and - // even numbers to primary provider. - int requestId; - // TODO(b/158623971): Update this to prevent possible overflow - if (isSecondary) { - do { - requestId = sIdCounter.getAndIncrement(); - } while (!isSecondaryProviderRequestId(requestId)); - } else { - do { - requestId = sIdCounter.getAndIncrement(); - } while (requestId == INVALID_REQUEST_ID || isSecondaryProviderRequestId(requestId)); - } - return requestId; - } - private boolean isRequestSupportFillDialog(int flags) { return (flags & FLAG_SUPPORTS_FILL_DIALOG) != 0; } @@ -1441,7 +1422,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") private void requestAssistStructureForPccLocked(int flags) { if (!mClassificationState.shouldTriggerRequest()) return; - mFillRequestIdSnapshot = sIdCounter.get(); + mFillRequestIdSnapshot = sIdCounterForPcc.get(); mClassificationState.updatePendingRequest(); // Get request id int requestId; @@ -2879,18 +2860,18 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // the auth UI. Slog.w(TAG, "setAuthenticationResultLocked(" + authenticationId + "): no responses"); mPresentationStatsEventLogger.maybeSetAuthenticationResult( - AUTHENTICATION_RESULT_FAILURE); + AUTHENTICATION_RESULT_FAILURE); mPresentationStatsEventLogger.logAndEndEvent(); removeFromService(); return; } - final FillResponse authenticatedResponse = isSecondaryProviderRequestId(requestId) + final FillResponse authenticatedResponse = mRequestId.isSecondaryProvider(requestId) ? mSecondaryResponses.get(requestId) : mResponses.get(requestId); if (authenticatedResponse == null || data == null) { Slog.w(TAG, "no authenticated response"); mPresentationStatsEventLogger.maybeSetAuthenticationResult( - AUTHENTICATION_RESULT_FAILURE); + AUTHENTICATION_RESULT_FAILURE); mPresentationStatsEventLogger.logAndEndEvent(); removeFromService(); return; @@ -2905,7 +2886,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (dataset == null) { Slog.w(TAG, "no dataset with index " + datasetIdx + " on fill response"); mPresentationStatsEventLogger.maybeSetAuthenticationResult( - AUTHENTICATION_RESULT_FAILURE); + AUTHENTICATION_RESULT_FAILURE); mPresentationStatsEventLogger.logAndEndEvent(); removeFromService(); return; @@ -2946,7 +2927,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } logAuthenticationStatusLocked(requestId, MetricsEvent.AUTOFILL_AUTHENTICATED); mPresentationStatsEventLogger.maybeSetAuthenticationResult( - AUTHENTICATION_RESULT_SUCCESS); + AUTHENTICATION_RESULT_SUCCESS); replaceResponseLocked(authenticatedResponse, (FillResponse) result, newClientState); } else if (result instanceof GetCredentialResponse) { if (sDebug) { @@ -2980,9 +2961,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState logAuthenticationStatusLocked(requestId, MetricsEvent.AUTOFILL_DATASET_AUTHENTICATED); mPresentationStatsEventLogger.maybeSetAuthenticationResult( - AUTHENTICATION_RESULT_SUCCESS); + AUTHENTICATION_RESULT_SUCCESS); if (newClientState != null) { - if (sDebug) Slog.d(TAG, "Updating client state from auth dataset"); + if (sDebug) + Slog.d(TAG, "Updating client state from auth dataset"); mClientState = newClientState; } Dataset datasetFromResult = getEffectiveDatasetForAuthentication((Dataset) result); @@ -2997,7 +2979,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState logAuthenticationStatusLocked(requestId, MetricsEvent.AUTOFILL_INVALID_DATASET_AUTHENTICATION); mPresentationStatsEventLogger.maybeSetAuthenticationResult( - AUTHENTICATION_RESULT_FAILURE); + AUTHENTICATION_RESULT_FAILURE); } } else { if (result != null) { @@ -3006,15 +2988,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState logAuthenticationStatusLocked(requestId, MetricsEvent.AUTOFILL_INVALID_AUTHENTICATION); mPresentationStatsEventLogger.maybeSetAuthenticationResult( - AUTHENTICATION_RESULT_FAILURE); + AUTHENTICATION_RESULT_FAILURE); processNullResponseLocked(requestId, 0); } } - private static boolean isSecondaryProviderRequestId(int requestId) { - return requestId % 2 == 1; - } - private Dataset getDatasetFromCredentialResponse(GetCredentialResponse result) { if (result == null) { return null; @@ -6923,22 +6901,15 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @GuardedBy("mLock") private int getLastResponseIndexLocked() { - // The response ids are monotonically increasing so - // we just find the largest id which is the last. We - // do not rely on the internal ordering in sparse - // array to avoid - wow this stopped working!? - int lastResponseIdx = -1; - int lastResponseId = -1; if (mResponses != null) { + List<Integer> requestIdList = new ArrayList<>(); final int responseCount = mResponses.size(); for (int i = 0; i < responseCount; i++) { - if (mResponses.keyAt(i) > lastResponseId) { - lastResponseIdx = i; - lastResponseId = mResponses.keyAt(i); - } + requestIdList.add(mResponses.keyAt(i)); } + return mRequestId.getLastRequestIdIndex(requestIdList); } - return lastResponseIdx; + return -1; } private LogMaker newLogMaker(int category) { diff --git a/services/tests/servicestests/src/com/android/server/autofill/RequestIdTest.java b/services/tests/servicestests/src/com/android/server/autofill/RequestIdTest.java new file mode 100644 index 000000000000..6d56c417f789 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/autofill/RequestIdTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.autofill; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(JUnit4.class) +public class RequestIdTest { + + List<Integer> datasetPrimaryNoWrap = new ArrayList<>(); + List<Integer> datasetPrimaryWrap = new ArrayList<>(); + List<Integer> datasetSecondaryNoWrap = new ArrayList<>(); + List<Integer> datasetSecondaryWrap = new ArrayList<>(); + List<Integer> datasetMixedNoWrap = new ArrayList<>(); + List<Integer> datasetMixedWrap = new ArrayList<>(); + + @Before + public void setup() throws Exception { + int datasetSize = 300; + + { // Generate primary only ids that do not wrap + RequestId requestId = new RequestId(0); + for (int i = 0; i < datasetSize; i++) { + datasetPrimaryNoWrap.add(requestId.nextId(false)); + } + } + + { // Generate primary only ids that wrap + RequestId requestId = new RequestId(0xff00); + for (int i = 0; i < datasetSize; i++) { + datasetPrimaryWrap.add(requestId.nextId(false)); + } + } + + { // Generate SECONDARY only ids that do not wrap + RequestId requestId = new RequestId(0); + for (int i = 0; i < datasetSize; i++) { + datasetSecondaryNoWrap.add(requestId.nextId(true)); + } + } + + { // Generate SECONDARY only ids that wrap + RequestId requestId = new RequestId(0xff00); + for (int i = 0; i < datasetSize; i++) { + datasetSecondaryWrap.add(requestId.nextId(true)); + } + } + + { // Generate MIXED only ids that do not wrap + RequestId requestId = new RequestId(0); + for (int i = 0; i < datasetSize; i++) { + datasetMixedNoWrap.add(requestId.nextId(i % 2 != 0)); + } + } + + { // Generate MIXED only ids that wrap + RequestId requestId = new RequestId(0xff00); + for (int i = 0; i < datasetSize; i++) { + datasetMixedWrap.add(requestId.nextId(i % 2 != 0)); + } + } + } + + @Test + public void testRequestIdLists() { + for (int id : datasetPrimaryNoWrap) { + assertThat(RequestId.isSecondaryProvider(id)).isFalse(); + assertThat(id >= 0).isTrue(); + assertThat(id < 0xffff).isTrue(); + } + + for (int id : datasetPrimaryWrap) { + assertThat(RequestId.isSecondaryProvider(id)).isFalse(); + assertThat(id >= 0).isTrue(); + assertThat(id < 0xffff).isTrue(); + } + + for (int id : datasetSecondaryNoWrap) { + assertThat(RequestId.isSecondaryProvider(id)).isTrue(); + assertThat(id >= 0).isTrue(); + assertThat(id < 0xffff).isTrue(); + } + + for (int id : datasetSecondaryWrap) { + assertThat(RequestId.isSecondaryProvider(id)).isTrue(); + assertThat(id >= 0).isTrue(); + assertThat(id < 0xffff).isTrue(); + } + } + + @Test + public void testRequestIdGeneration() { + RequestId requestId = new RequestId(0); + + // Large Primary + for (int i = 0; i < 100000; i++) { + int y = requestId.nextId(false); + assertThat(RequestId.isSecondaryProvider(y)).isFalse(); + assertThat(y >= 0).isTrue(); + assertThat(y < 0xffff).isTrue(); + } + + // Large Secondary + requestId = new RequestId(0); + for (int i = 0; i < 100000; i++) { + int y = requestId.nextId(true); + assertThat(RequestId.isSecondaryProvider(y)).isTrue(); + assertThat(y >= 0).isTrue(); + assertThat(y < 0xffff).isTrue(); + } + + // Large Mixed + requestId = new RequestId(0); + for (int i = 0; i < 50000; i++) { + int y = requestId.nextId(i % 2 != 0); + assertThat(RequestId.isSecondaryProvider(y)).isEqualTo(i % 2 == 0); + assertThat(y >= 0).isTrue(); + assertThat(y < 0xffff).isTrue(); + } + } + + @Test + public void testGetLastRequestId() { + // In this test, request ids are generated FIFO, so the last entry is also the last + // request + + { // Primary no wrap + int lastIdIndex = datasetPrimaryNoWrap.size() - 1; + int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetPrimaryNoWrap); + assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex); + } + + { // Primary wrap + int lastIdIndex = datasetPrimaryWrap.size() - 1; + int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetPrimaryWrap); + assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex); + } + + { // Secondary no wrap + int lastIdIndex = datasetSecondaryNoWrap.size() - 1; + int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetSecondaryNoWrap); + assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex); + } + + { // Secondary wrap + int lastIdIndex = datasetSecondaryWrap.size() - 1; + int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetSecondaryWrap); + assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex); + } + + { // Mixed no wrap + int lastIdIndex = datasetMixedNoWrap.size() - 1; + int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetMixedNoWrap); + assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex); + } + + { // Mixed wrap + int lastIdIndex = datasetMixedWrap.size() - 1; + int lastComputedIdIndex = RequestId.getLastRequestIdIndex(datasetMixedWrap); + assertThat(lastIdIndex).isEqualTo(lastComputedIdIndex); + } + + } +} |