diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index fea9972c662cf..e0a1b3e4a3d03 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11510,7 +11510,14 @@ public final class Settings { /** * The packages whitelisted to be run in autofill compatibility mode. The list - * of packages is ":" colon delimited. + * of packages is {@code ":"} colon delimited, and each entry has the name of the + * package and an optional list of url bar resource ids (the list is delimited by + * brackets&mdash{@code [} and {@code ]}&mdash and is also comma delimited). + * + *

For example, a list with 3 packages {@code p1}, {@code p2}, and {@code p3}, where + * package {@code p1} have one id ({@code url_bar}, {@code p2} has none, and {@code p3 } + * have 2 ids {@code url_foo} and {@code url_bas}) would be + * {@code p1[url_bar]:p2:p3[url_foo,url_bas]} * * @hide */ diff --git a/core/java/android/service/autofill/AutofillServiceInfo.java b/core/java/android/service/autofill/AutofillServiceInfo.java index 70c4ec07ee4eb..de234559d53e1 100644 --- a/core/java/android/service/autofill/AutofillServiceInfo.java +++ b/core/java/android/service/autofill/AutofillServiceInfo.java @@ -32,7 +32,6 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.AttributeSet; import android.util.Log; -import android.util.Pair; import android.util.Xml; import com.android.internal.R; @@ -79,7 +78,7 @@ public final class AutofillServiceInfo { private final String mSettingsActivity; @Nullable - private final ArrayMap> mCompatibilityPackages; + private final ArrayMap mCompatibilityPackages; public AutofillServiceInfo(Context context, ComponentName comp, int userHandle) throws PackageManager.NameNotFoundException { @@ -117,7 +116,7 @@ public final class AutofillServiceInfo { } String settingsActivity = null; - ArrayMap> compatibilityPackages = null; + ArrayMap compatibilityPackages = null; try { final Resources resources = context.getPackageManager().getResourcesForApplication( @@ -153,10 +152,9 @@ public final class AutofillServiceInfo { mCompatibilityPackages = compatibilityPackages; } - private ArrayMap> parseCompatibilityPackages(XmlPullParser parser, - Resources resources) - throws IOException, XmlPullParserException { - ArrayMap> compatibilityPackages = null; + private ArrayMap parseCompatibilityPackages(XmlPullParser parser, + Resources resources) throws IOException, XmlPullParserException { + ArrayMap compatibilityPackages = null; final int outerDepth = parser.getDepth(); int type; @@ -200,13 +198,18 @@ public final class AutofillServiceInfo { } else { maxVersionCode = Long.MAX_VALUE; } - final String urlBarResourceId = cpAttributes.getString( - R.styleable.AutofillService_CompatibilityPackage_urlBarResourceId); + if (true) { // TODO(b/74445943): remove block after P DP2 is branched + final String urlBarResourceId = cpAttributes.getString( + R.styleable.AutofillService_CompatibilityPackage_urlBarResourceId); + if (urlBarResourceId != null) { + Log.e(TAG, "Service is using deprecated attribute 'urlBarResourceId'"); + } + } if (compatibilityPackages == null) { compatibilityPackages = new ArrayMap<>(); } - compatibilityPackages.put(name, new Pair<>(maxVersionCode, urlBarResourceId)); + compatibilityPackages.put(name, maxVersionCode); } finally { XmlUtils.skipCurrentTag(parser); if (cpAttributes != null) { @@ -228,23 +231,10 @@ public final class AutofillServiceInfo { return mSettingsActivity; } - public ArrayMap> getCompatibilityPackages() { + public ArrayMap getCompatibilityPackages() { return mCompatibilityPackages; } - /** - * Gets the resource id of the URL bar for a package. Used in compat mode - */ - // TODO: return a list of strings instead - @Nullable - public String getUrlBarResourceId(String packageName) { - if (mCompatibilityPackages == null) { - return null; - } - final Pair pair = mCompatibilityPackages.get(packageName); - return pair == null ? null : pair.second; - } - @Override public String toString() { final StringBuilder builder = new StringBuilder(); diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 021e88ea6093d..6d23fab54ae31 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -7978,9 +7978,7 @@ android.content.pm.PackageInfo#getLongVersionCode()} for the target package. --> - + diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 7d5d1ba1366b0..2c0deedc435f8 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -2869,6 +2869,7 @@ + diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java index 266b2d791991e..8a66414358aa5 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java @@ -59,9 +59,7 @@ import android.service.autofill.UserData; import android.text.TextUtils; import android.text.TextUtils.SimpleStringSplitter; import android.util.ArrayMap; -import android.util.ArraySet; import android.util.LocalLog; -import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -73,6 +71,7 @@ import android.view.autofill.IAutoFillManager; import android.view.autofill.IAutoFillManagerClient; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; import com.android.internal.os.BackgroundThread; import com.android.internal.os.IResultReceiver; @@ -88,8 +87,8 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Objects; -import java.util.Set; /** * Entry point service for autofill management. @@ -105,6 +104,9 @@ public final class AutofillManagerService extends SystemService { static final String RECEIVER_BUNDLE_EXTRA_SESSIONS = "sessions"; private static final char COMPAT_PACKAGE_DELIMITER = ':'; + private static final char COMPAT_PACKAGE_URL_IDS_DELIMITER = ','; + private static final char COMPAT_PACKAGE_URL_IDS_BLOCK_BEGIN = '['; + private static final char COMPAT_PACKAGE_URL_IDS_BLOCK_END = ']'; private final Context mContext; private final AutoFillUI mUi; @@ -326,7 +328,7 @@ public final class AutofillManagerService extends SystemService { if (service == null) { service = new AutofillManagerServiceImpl(mContext, mLock, mRequestsHistory, mUiLatencyHistory, mWtfHistory, resolvedUserId, mUi, - mDisabledUsers.get(resolvedUserId)); + mAutofillCompatState, mDisabledUsers.get(resolvedUserId)); mServicesCache.put(userId, service); addCompatibilityModeRequestsLocked(service, userId); } @@ -544,23 +546,24 @@ public final class AutofillManagerService extends SystemService { private void addCompatibilityModeRequestsLocked(@NonNull AutofillManagerServiceImpl service , int userId) { mAutofillCompatState.reset(); - final ArrayMap> compatPackages = + final ArrayMap compatPackages = service.getCompatibilityPackagesLocked(); if (compatPackages == null || compatPackages.isEmpty()) { return; } - final Set whiteListedPackages = getWhitelistedCompatModePackages(); + + final Map whiteListedPackages = getWhitelistedCompatModePackages(); final int compatPackageCount = compatPackages.size(); for (int i = 0; i < compatPackageCount; i++) { final String packageName = compatPackages.keyAt(i); - if (whiteListedPackages == null || !whiteListedPackages.contains(packageName)) { + if (whiteListedPackages == null || !whiteListedPackages.containsKey(packageName)) { Slog.w(TAG, "Ignoring not whitelisted compat package " + packageName); continue; } - final Long maxVersionCode = compatPackages.valueAt(i).first; + final Long maxVersionCode = compatPackages.valueAt(i); if (maxVersionCode != null) { mAutofillCompatState.addCompatibilityModeRequest(packageName, - maxVersionCode, userId); + maxVersionCode, whiteListedPackages.get(packageName), userId); } } } @@ -571,16 +574,60 @@ public final class AutofillManagerService extends SystemService { Settings.Global.AUTOFILL_COMPAT_ALLOWED_PACKAGES); } - private @Nullable Set getWhitelistedCompatModePackages() { - final String compatPackagesSetting = getWhitelistedCompatModePackagesFromSettings(); - if (TextUtils.isEmpty(compatPackagesSetting)) { + @Nullable + private Map getWhitelistedCompatModePackages() { + return getWhitelistedCompatModePackages(getWhitelistedCompatModePackagesFromSettings()); + } + + @Nullable + @VisibleForTesting + static Map getWhitelistedCompatModePackages(String setting) { + if (TextUtils.isEmpty(setting)) { return null; } - final Set compatPackages = new ArraySet<>(); + + final ArrayMap compatPackages = new ArrayMap<>(); final SimpleStringSplitter splitter = new SimpleStringSplitter(COMPAT_PACKAGE_DELIMITER); - splitter.setString(compatPackagesSetting); + splitter.setString(setting); while (splitter.hasNext()) { - compatPackages.add(splitter.next()); + final String packageBlock = splitter.next(); + final int urlBlockIndex = packageBlock.indexOf(COMPAT_PACKAGE_URL_IDS_BLOCK_BEGIN); + final String packageName; + final List urlBarIds; + if (urlBlockIndex == -1) { + packageName = packageBlock; + urlBarIds = null; + } else { + if (packageBlock.charAt(packageBlock.length() - 1) + != COMPAT_PACKAGE_URL_IDS_BLOCK_END) { + Slog.w(TAG, "Ignoring entry '" + packageBlock + "' on '" + setting + + "'because it does not end on '" + COMPAT_PACKAGE_URL_IDS_BLOCK_END + + "'"); + continue; + } + packageName = packageBlock.substring(0, urlBlockIndex); + urlBarIds = new ArrayList<>(); + final String urlBarIdsBlock = + packageBlock.substring(urlBlockIndex + 1, packageBlock.length() - 1); + if (sVerbose) { + Slog.v(TAG, "pkg:" + packageName + ": block:" + packageBlock + ": urls:" + + urlBarIds + ": block:" + urlBarIdsBlock + ":"); + } + final SimpleStringSplitter splitter2 = + new SimpleStringSplitter(COMPAT_PACKAGE_URL_IDS_DELIMITER); + splitter2.setString(urlBarIdsBlock); + while (splitter2.hasNext()) { + final String urlBarId = splitter2.next(); + urlBarIds.add(urlBarId); + } + } + if (urlBarIds == null) { + compatPackages.put(packageName, null); + } else { + final String[] urlBarIdsArray = new String[urlBarIds.size()]; + urlBarIds.toArray(urlBarIdsArray); + compatPackages.put(packageName, urlBarIdsArray); + } } return compatPackages; } @@ -598,13 +645,41 @@ public final class AutofillManagerService extends SystemService { return mAutofillCompatState.isCompatibilityModeRequested( packageName, versionCode, userId); } + } - private static final class AutofillCompatState { + /** + * Compatibility mode metadata per package. + */ + private static final class PackageCompatState { + private final long maxVersionCode; + private final String[] urlBarResourceIds; + + PackageCompatState(long maxVersionCode, String[] urlBarResourceIds) { + this.maxVersionCode = maxVersionCode; + this.urlBarResourceIds = urlBarResourceIds; + } + + @Override + public String toString() { + return "PackageCompatState: [maxVersionCode=" + maxVersionCode + + ", urlBarResourceIds=" + Arrays.toString(urlBarResourceIds) + "]"; + } + } + + /** + * Compatibility mode metadata associated with all services. + * + *

This object is defined here instead of on each {@link AutofillManagerServiceImpl} because + * it cannot hold a lock on the main lock when + * {@link AutofillCompatState#isCompatibilityModeRequested(String, long, int)} is called by + * external services. + */ + static final class AutofillCompatState { private final Object mLock = new Object(); @GuardedBy("mLock") - private SparseArray> mUserSpecs; + private SparseArray> mUserSpecs; boolean isCompatibilityModeRequested(@NonNull String packageName, long versionCode, @UserIdInt int userId) { @@ -612,30 +687,49 @@ public final class AutofillManagerService extends SystemService { if (mUserSpecs == null) { return false; } - final ArrayMap userSpec = mUserSpecs.get(userId); + final ArrayMap userSpec = mUserSpecs.get(userId); if (userSpec == null) { return false; } - final Long maxVersionCode = userSpec.get(packageName); - if (maxVersionCode == null) { + final PackageCompatState metadata = userSpec.get(packageName); + if (metadata == null) { return false; } - return versionCode <= maxVersionCode; + return versionCode <= metadata.maxVersionCode; + } + } + + @Nullable + String[] getUrlBarResourceIds(@NonNull String packageName, @UserIdInt int userId) { + synchronized (mLock) { + if (mUserSpecs == null) { + return null; + } + final ArrayMap userSpec = mUserSpecs.get(userId); + if (userSpec == null) { + return null; + } + final PackageCompatState metadata = userSpec.get(packageName); + if (metadata == null) { + return null; + } + return metadata.urlBarResourceIds; } } void addCompatibilityModeRequest(@NonNull String packageName, - long versionCode, @UserIdInt int userId) { + long versionCode, @Nullable String[] urlBarResourceIds, @UserIdInt int userId) { synchronized (mLock) { if (mUserSpecs == null) { mUserSpecs = new SparseArray<>(); } - ArrayMap userSpec = mUserSpecs.get(userId); + ArrayMap userSpec = mUserSpecs.get(userId); if (userSpec == null) { userSpec = new ArrayMap<>(); mUserSpecs.put(userId, userSpec); } - userSpec.put(packageName, versionCode); + userSpec.put(packageName, + new PackageCompatState(versionCode, urlBarResourceIds)); } } diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 8622dbe820a68..54ea3ba4546b5 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -64,7 +64,6 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.DebugUtils; import android.util.LocalLog; -import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; @@ -78,12 +77,12 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.server.LocalServices; +import com.android.server.autofill.AutofillManagerService.AutofillCompatState; import com.android.server.autofill.ui.AutoFillUI; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Random; /** @@ -164,12 +163,15 @@ final class AutofillManagerServiceImpl { @GuardedBy("mLock") private FillEventHistory mEventHistory; + /** Shared instance, doesn't need to be logged */ + private final AutofillCompatState mAutofillCompatState; + /** When was {@link PruneTask} last executed? */ private long mLastPrune = 0; AutofillManagerServiceImpl(Context context, Object lock, LocalLog requestsHistory, LocalLog uiLatencyHistory, LocalLog wtfHistory, int userId, AutoFillUI ui, - boolean disabled) { + AutofillCompatState autofillCompatState, boolean disabled) { mContext = context; mLock = lock; mRequestsHistory = requestsHistory; @@ -178,6 +180,7 @@ final class AutofillManagerServiceImpl { mUserId = userId; mUi = ui; mFieldClassificationStrategy = new FieldClassificationStrategy(context, userId); + mAutofillCompatState = autofillCompatState; updateLocked(disabled); } @@ -208,14 +211,9 @@ final class AutofillManagerServiceImpl { } - @GuardedBy("mLock") @Nullable - String getUrlBarResourceIdForCompatModeLocked(@NonNull String packageName) { - if (mInfo == null) { - Slog.w(TAG, "getUrlBarResourceIdForCompatModeLocked(): no mInfo"); - return null; - } - return mInfo.getUrlBarResourceId(packageName); + String[] getUrlBarResourceIdsForCompatMode(@NonNull String packageName) { + return mAutofillCompatState.getUrlBarResourceIds(packageName, mUserId); } @Nullable @@ -893,7 +891,7 @@ final class AutofillManagerServiceImpl { pw.print(prefix); pw.print("Field classification enabled: "); pw.println(isFieldClassificationEnabledLocked()); pw.print(prefix); pw.print("Compat pkgs: "); - final ArrayMap> compatPkgs = getCompatibilityPackagesLocked(); + final ArrayMap compatPkgs = getCompatibilityPackagesLocked(); if (compatPkgs == null) { pw.println("N/A"); } else { @@ -1022,7 +1020,7 @@ final class AutofillManagerServiceImpl { } @GuardedBy("mLock") - @Nullable ArrayMap> getCompatibilityPackagesLocked() { + @Nullable ArrayMap getCompatibilityPackagesLocked() { if (mInfo != null) { return mInfo.getCompatibilityPackages(); } diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java index 232dfdcf045bd..5c41f3f869cef 100644 --- a/services/autofill/java/com/android/server/autofill/Helper.java +++ b/services/autofill/java/com/android/server/autofill/Helper.java @@ -30,6 +30,7 @@ import android.view.autofill.AutofillId; import android.view.autofill.AutofillValue; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.util.ArrayUtils; import java.io.PrintWriter; import java.util.ArrayList; @@ -168,21 +169,24 @@ public final class Helper { /** * Sanitize the {@code webDomain} property of the URL bar node on compat mode. + * + * @param structure Assist structure + * @param urlBarIds list of ids; only the first id found will be sanitized. */ public static void sanitizeUrlBar(@NonNull AssistStructure structure, - @NonNull String urlBarId) { + @NonNull String[] urlBarIds) { final ViewNode urlBarNode = findViewNode(structure, (node) -> { - return urlBarId.equals(node.getIdEntry()); + return ArrayUtils.contains(urlBarIds, node.getIdEntry()); }); if (urlBarNode != null) { final String domain = urlBarNode.getText().toString(); if (domain.isEmpty()) { - if (sDebug) Slog.d(TAG, "sanitizeUrlBar(): empty on " + urlBarId); + if (sDebug) Slog.d(TAG, "sanitizeUrlBar(): empty on " + urlBarNode.getIdEntry()); return; } urlBarNode.setWebDomain(domain); if (sDebug) { - Slog.d(TAG, "sanitizeUrlBar(): id=" + urlBarId + ", domain=" + Slog.d(TAG, "sanitizeUrlBar(): id=" + urlBarNode.getIdEntry() + ", domain=" + urlBarNode.getWebDomain()); } } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 4abf0f8e8f1ab..26598a2e24bc9 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -274,11 +274,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } if (mCompatMode) { // Sanitize URL bar, if needed - final String urlBarId = mService.getUrlBarResourceIdForCompatModeLocked( + final String[] urlBarIds = mService.getUrlBarResourceIdsForCompatMode( mComponentName.getPackageName()); - if (sDebug) Slog.d(TAG, "url_bar in compat mode: " + urlBarId); - if (urlBarId != null) { - Helper.sanitizeUrlBar(structure, urlBarId); + if (sDebug) { + Slog.d(TAG, "url_bars in compat mode: " + Arrays.toString(urlBarIds)); + } + if (urlBarIds != null) { + Helper.sanitizeUrlBar(structure, urlBarIds); } } structure.sanitizeForParceling(true); diff --git a/services/tests/servicestests/Android.mk b/services/tests/servicestests/Android.mk index 356e64b5dd7bd..0ca0a1a104c8f 100644 --- a/services/tests/servicestests/Android.mk +++ b/services/tests/servicestests/Android.mk @@ -15,6 +15,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ frameworks-base-testutils \ services.accessibility \ services.appwidget \ + services.autofill \ services.backup \ services.core \ services.devicepolicy \ diff --git a/services/tests/servicestests/src/com/android/server/autofill/AutofillManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/autofill/AutofillManagerServiceTest.java new file mode 100644 index 0000000000000..d5a28f6b1d76f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/autofill/AutofillManagerServiceTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.autofill; + +import static com.android.server.autofill.AutofillManagerService.getWhitelistedCompatModePackages; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.Map; + +@RunWith(JUnit4.class) +public class AutofillManagerServiceTest { + + @Test + public void testGetWhitelistedCompatModePackages_null() { + assertThat(getWhitelistedCompatModePackages(null)).isNull(); + } + + @Test + public void testGetWhitelistedCompatModePackages_empty() { + assertThat(getWhitelistedCompatModePackages("")).isNull(); + } + + @Test + public void testGetWhitelistedCompatModePackages_onePackageNoUrls() { + assertThat(getWhitelistedCompatModePackages("one_is_the_loniest_package")) + .containsExactly("one_is_the_loniest_package", null); + } + + @Test + public void testGetWhitelistedCompatModePackages_onePackageMissingEndDelimiter() { + assertThat(getWhitelistedCompatModePackages("one_is_the_loniest_package[")).isEmpty(); + } + + @Test + public void testGetWhitelistedCompatModePackages_onePackageOneUrl() { + final Map result = + getWhitelistedCompatModePackages("one_is_the_loniest_package[url]"); + assertThat(result).hasSize(1); + assertThat(result.get("one_is_the_loniest_package")).asList().containsExactly("url"); + } + + @Test + public void testGetWhitelistedCompatModePackages_onePackageMultipleUrls() { + final Map result = + getWhitelistedCompatModePackages("one_is_the_loniest_package[4,5,8,15,16,23,42]"); + assertThat(result).hasSize(1); + assertThat(result.get("one_is_the_loniest_package")).asList() + .containsExactly("4", "5", "8", "15", "16", "23", "42"); + } + + @Test + public void testGetWhitelistedCompatModePackages_multiplePackagesOneInvalid() { + final Map result = getWhitelistedCompatModePackages("one:two["); + assertThat(result).hasSize(1); + assertThat(result.get("one")).isNull(); + } + + @Test + public void testGetWhitelistedCompatModePackages_multiplePackagesMultipleUrls() { + final Map result = + getWhitelistedCompatModePackages("p1[p1u1]:p2:p3[p3u1,p3u2]"); + assertThat(result).hasSize(3); + assertThat(result.get("p1")).asList().containsExactly("p1u1"); + assertThat(result.get("p2")).isNull(); + assertThat(result.get("p3")).asList().containsExactly("p3u1", "p3u2"); + } + + @Test + public void testGetWhitelistedCompatModePackages_threePackagesOneInvalid() { + final Map result = + getWhitelistedCompatModePackages("p1[p1u1]:p2[:p3[p3u1,p3u2]"); + assertThat(result).hasSize(2); + assertThat(result.get("p1")).asList().containsExactly("p1u1"); + assertThat(result.get("p3")).asList().containsExactly("p3u1", "p3u2"); + } +}