diff options
author | Sudheer Shanka <sudheersai@google.com> | 2020-03-06 22:39:06 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-03-06 22:39:06 +0000 |
commit | cd629dd39af8fd9fa10b590ce43bd7ff9dba52d3 (patch) | |
tree | 072a90f14b6f0feddb89dcb64354211c24ccbbe0 | |
parent | 6c725002b7c98334ece241d7c33620057c21a1ab (diff) | |
parent | 364364ba411892217764212c4dc242ba1e6a95f0 (diff) |
Merge "Add a limit on how much data an app can acquire a lease on." into rvc-dev
11 files changed, 385 insertions, 22 deletions
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java index 888c3af8bbf7..dab4797b313f 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobMetadata.java @@ -242,7 +242,7 @@ class BlobMetadata { return hasOtherLeasees(null, uid); } - private boolean isALeasee(@Nullable String packageName, int uid) { + boolean isALeasee(@Nullable String packageName, int uid) { synchronized (mMetadataLock) { // Check if the package is a leasee of the data blob. for (int i = 0, size = mLeasees.size(); i < size; ++i) { diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java index 6d31b2c30067..5e8ea7afaec7 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreConfig.java @@ -15,12 +15,23 @@ */ package com.android.server.blob; +import static android.provider.DeviceConfig.NAMESPACE_BLOBSTORE; +import static android.text.format.Formatter.FLAG_IEC_UNITS; +import static android.text.format.Formatter.formatFileSize; +import static android.util.TimeUtils.formatDuration; + import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Context; import android.os.Environment; +import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; +import android.util.DataUnit; import android.util.Log; import android.util.Slog; +import com.android.internal.util.IndentingPrintWriter; + import java.io.File; import java.util.concurrent.TimeUnit; @@ -55,6 +66,76 @@ class BlobStoreConfig { */ public static final long SESSION_EXPIRY_TIMEOUT_MILLIS = TimeUnit.DAYS.toMillis(7); + public static class DeviceConfigProperties { + /** + * Denotes how low the limit for the amount of data, that an app will be allowed to acquire + * a lease on, can be. + */ + public static final String KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR = + "total_bytes_per_app_limit_floor"; + public static final long DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR = + DataUnit.MEBIBYTES.toBytes(300); // 300 MiB + public static long TOTAL_BYTES_PER_APP_LIMIT_FLOOR = + DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR; + + /** + * Denotes the maximum amount of data an app can acquire a lease on, in terms of fraction + * of total disk space. + */ + public static final String KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION = + "total_bytes_per_app_limit_fraction"; + public static final float DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION = 0.01f; + public static float TOTAL_BYTES_PER_APP_LIMIT_FRACTION = + DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION; + + static void refresh(Properties properties) { + if (!NAMESPACE_BLOBSTORE.equals(properties.getNamespace())) { + return; + } + properties.getKeyset().forEach(key -> { + switch (key) { + case KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR: + TOTAL_BYTES_PER_APP_LIMIT_FLOOR = properties.getLong(key, + DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR); + break; + case KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION: + TOTAL_BYTES_PER_APP_LIMIT_FRACTION = properties.getFloat(key, + DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION); + break; + default: + Slog.wtf(TAG, "Unknown key in device config properties: " + key); + } + }); + } + + static void dump(IndentingPrintWriter fout, Context context) { + final String dumpFormat = "%s: [cur: %s, def: %s]"; + fout.println(String.format(dumpFormat, KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR, + formatFileSize(context, TOTAL_BYTES_PER_APP_LIMIT_FLOOR, FLAG_IEC_UNITS), + formatFileSize(context, DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FLOOR, + FLAG_IEC_UNITS))); + fout.println(String.format(dumpFormat, KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION, + TOTAL_BYTES_PER_APP_LIMIT_FRACTION, + DEFAULT_TOTAL_BYTES_PER_APP_LIMIT_FRACTION)); + } + } + + public static void initialize(Context context) { + DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_BLOBSTORE, + context.getMainExecutor(), + properties -> DeviceConfigProperties.refresh(properties)); + DeviceConfigProperties.refresh(DeviceConfig.getProperties(NAMESPACE_BLOBSTORE)); + } + + /** + * Returns the maximum amount of data that an app can acquire a lease on. + */ + public static long getAppDataBytesLimit() { + final long totalBytesLimit = (long) (Environment.getDataSystemDirectory().getTotalSpace() + * DeviceConfigProperties.TOTAL_BYTES_PER_APP_LIMIT_FRACTION); + return Math.max(DeviceConfigProperties.TOTAL_BYTES_PER_APP_LIMIT_FLOOR, totalBytesLimit); + } + @Nullable public static File prepareBlobFile(long sessionId) { final File blobsDir = prepareBlobsDir(); @@ -123,4 +204,21 @@ class BlobStoreConfig { public static File getBlobStoreRootDir() { return new File(Environment.getDataSystemDirectory(), ROOT_DIR_NAME); } + + public static void dump(IndentingPrintWriter fout, Context context) { + fout.println("XML current version: " + XML_VERSION_CURRENT); + + fout.println("Idle job ID: " + IDLE_JOB_ID); + fout.println("Idle job period: " + formatDuration(IDLE_JOB_PERIOD_MILLIS)); + + fout.println("Session expiry timeout: " + formatDuration(SESSION_EXPIRY_TIMEOUT_MILLIS)); + + fout.println("Total bytes per app limit: " + formatFileSize(context, + getAppDataBytesLimit(), FLAG_IEC_UNITS)); + + fout.println("Device config properties:"); + fout.increaseIndent(); + DeviceConfigProperties.dump(fout, context); + fout.decreaseIndent(); + } } diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java index 3116eb4c2407..7006781e4e4e 100644 --- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java +++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerService.java @@ -188,7 +188,9 @@ public class BlobStoreManagerService extends SystemService { @Override public void onBootPhase(int phase) { - if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { + if (phase == PHASE_ACTIVITY_MANAGER_READY) { + BlobStoreConfig.initialize(mContext); + } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) { synchronized (mBlobsLock) { final SparseArray<SparseArray<String>> allPackages = getAllPackages(); readBlobSessionsLocked(allPackages); @@ -381,6 +383,11 @@ public class BlobStoreManagerService extends SystemService { throw new IllegalArgumentException( "Lease expiry cannot be later than blobs expiry time"); } + if (getTotalUsageBytesLocked(callingUid, callingPackage) + + blobMetadata.getSize() > BlobStoreConfig.getAppDataBytesLimit()) { + throw new IllegalStateException("Total amount of data with an active lease" + + " is exceeding the max limit"); + } blobMetadata.addLeasee(callingPackage, callingUid, descriptionResId, description, leaseExpiryTimeMillis); if (LOGV) { @@ -391,6 +398,18 @@ public class BlobStoreManagerService extends SystemService { } } + @VisibleForTesting + @GuardedBy("mBlobsLock") + long getTotalUsageBytesLocked(int callingUid, String callingPackage) { + final AtomicLong totalBytes = new AtomicLong(0); + forEachBlobInUser((blobMetadata) -> { + if (blobMetadata.isALeasee(callingPackage, callingUid)) { + totalBytes.getAndAdd(blobMetadata.getSize()); + } + }, UserHandle.getUserId(callingUid)); + return totalBytes.get(); + } + private void releaseLeaseInternal(BlobHandle blobHandle, int callingUid, String callingPackage) { synchronized (mBlobsLock) { @@ -1236,8 +1255,10 @@ public class BlobStoreManagerService extends SystemService { } synchronized (mBlobsLock) { - fout.println("mCurrentMaxSessionId: " + mCurrentMaxSessionId); - fout.println(); + if (dumpArgs.shouldDumpAllSections()) { + fout.println("mCurrentMaxSessionId: " + mCurrentMaxSessionId); + fout.println(); + } if (dumpArgs.shouldDumpSessions()) { dumpSessionsLocked(fout, dumpArgs); @@ -1248,6 +1269,14 @@ public class BlobStoreManagerService extends SystemService { fout.println(); } } + + if (dumpArgs.shouldDumpConfig()) { + fout.println("BlobStore config:"); + fout.increaseIndent(); + BlobStoreConfig.dump(fout, mContext); + fout.decreaseIndent(); + fout.println(); + } } @Override @@ -1260,14 +1289,16 @@ public class BlobStoreManagerService extends SystemService { } static final class DumpArgs { + private static final int FLAG_DUMP_SESSIONS = 1 << 0; + private static final int FLAG_DUMP_BLOBS = 1 << 1; + private static final int FLAG_DUMP_CONFIG = 1 << 2; + + private int mSelectedSectionFlags; private boolean mDumpFull; private final ArrayList<String> mDumpPackages = new ArrayList<>(); private final ArrayList<Integer> mDumpUids = new ArrayList<>(); private final ArrayList<Integer> mDumpUserIds = new ArrayList<>(); private final ArrayList<Long> mDumpBlobIds = new ArrayList<>(); - private boolean mDumpOnlySelectedSections; - private boolean mDumpSessions; - private boolean mDumpBlobs; private boolean mDumpHelp; public boolean shouldDumpSession(String packageName, int uid, long blobId) { @@ -1286,18 +1317,41 @@ public class BlobStoreManagerService extends SystemService { return true; } + public boolean shouldDumpAllSections() { + return mSelectedSectionFlags == 0; + } + + public void allowDumpSessions() { + mSelectedSectionFlags |= FLAG_DUMP_SESSIONS; + } + public boolean shouldDumpSessions() { - if (!mDumpOnlySelectedSections) { + if (shouldDumpAllSections()) { return true; } - return mDumpSessions; + return (mSelectedSectionFlags & FLAG_DUMP_SESSIONS) != 0; + } + + public void allowDumpBlobs() { + mSelectedSectionFlags |= FLAG_DUMP_BLOBS; } public boolean shouldDumpBlobs() { - if (!mDumpOnlySelectedSections) { + if (shouldDumpAllSections()) { + return true; + } + return (mSelectedSectionFlags & FLAG_DUMP_BLOBS) != 0; + } + + public void allowDumpConfig() { + mSelectedSectionFlags |= FLAG_DUMP_CONFIG; + } + + public boolean shouldDumpConfig() { + if (shouldDumpAllSections()) { return true; } - return mDumpBlobs; + return (mSelectedSectionFlags & FLAG_DUMP_CONFIG) != 0; } public boolean shouldDumpBlob(long blobId) { @@ -1334,11 +1388,11 @@ public class BlobStoreManagerService extends SystemService { dumpArgs.mDumpFull = true; } } else if ("--sessions".equals(opt)) { - dumpArgs.mDumpOnlySelectedSections = true; - dumpArgs.mDumpSessions = true; + dumpArgs.allowDumpSessions(); } else if ("--blobs".equals(opt)) { - dumpArgs.mDumpOnlySelectedSections = true; - dumpArgs.mDumpBlobs = true; + dumpArgs.allowDumpBlobs(); + } else if ("--config".equals(opt)) { + dumpArgs.allowDumpConfig(); } else if ("--package".equals(opt) || "-p".equals(opt)) { dumpArgs.mDumpPackages.add(getStringArgRequired(args, ++i, "packageName")); } else if ("--uid".equals(opt) || "-u".equals(opt)) { @@ -1397,6 +1451,8 @@ public class BlobStoreManagerService extends SystemService { printWithIndent(pw, "Dump only the sessions info"); pw.println("--blobs"); printWithIndent(pw, "Dump only the committed blobs info"); + pw.println("--config"); + printWithIndent(pw, "Dump only the config values"); pw.println("--package | -p [package-name]"); printWithIndent(pw, "Dump blobs info associated with the given package"); pw.println("--uid | -u [uid]"); diff --git a/api/system-current.txt b/api/system-current.txt index f0621124eeae..2ce6fd0ff301 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -9131,6 +9131,7 @@ package android.provider { field public static final String NAMESPACE_ATTENTION_MANAGER_SERVICE = "attention_manager_service"; field public static final String NAMESPACE_AUTOFILL = "autofill"; field public static final String NAMESPACE_BIOMETRICS = "biometrics"; + field public static final String NAMESPACE_BLOBSTORE = "blobstore"; field public static final String NAMESPACE_CONNECTIVITY = "connectivity"; field public static final String NAMESPACE_CONTENT_CAPTURE = "content_capture"; field @Deprecated public static final String NAMESPACE_DEX_BOOT = "dex_boot"; diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java index 505d4ca4b399..aa511cc46de9 100644 --- a/core/java/android/provider/DeviceConfig.java +++ b/core/java/android/provider/DeviceConfig.java @@ -111,6 +111,14 @@ public final class DeviceConfig { public static final String NAMESPACE_AUTOFILL = "autofill"; /** + * Namespace for blobstore feature that allows apps to share data blobs. + * + * @hide + */ + @SystemApi + public static final String NAMESPACE_BLOBSTORE = "blobstore"; + + /** * Namespace for all networking connectivity related features. * * @hide diff --git a/core/proto/android/providers/settings/config.proto b/core/proto/android/providers/settings/config.proto index cc2419614d93..b0a70ef56dbc 100644 --- a/core/proto/android/providers/settings/config.proto +++ b/core/proto/android/providers/settings/config.proto @@ -47,6 +47,7 @@ message ConfigSettingsProto { repeated SettingProto systemui_settings = 20; repeated SettingProto telephony_settings = 21; repeated SettingProto textclassifier_settings = 22; + repeated SettingProto blobstore_settings = 23; message NamespaceProto { optional string namespace = 1; diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 8789a5c62c6c..af74121a11c9 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -52,6 +52,8 @@ class SettingsProtoDumpUtil { ConfigSettingsProto.APP_COMPAT_SETTINGS); namespaceToFieldMap.put(DeviceConfig.NAMESPACE_AUTOFILL, ConfigSettingsProto.AUTOFILL_SETTINGS); + namespaceToFieldMap.put(DeviceConfig.NAMESPACE_BLOBSTORE, + ConfigSettingsProto.BLOBSTORE_SETTINGS); namespaceToFieldMap.put(DeviceConfig.NAMESPACE_CONNECTIVITY, ConfigSettingsProto.CONNECTIVITY_SETTINGS); namespaceToFieldMap.put(DeviceConfig.NAMESPACE_CONTENT_CAPTURE, diff --git a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreConfigTest.java b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreConfigTest.java new file mode 100644 index 000000000000..ad19a4893153 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreConfigTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 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.blob; + +import static com.google.common.truth.Truth.assertThat; + +import static junit.framework.Assert.fail; + +import android.content.Context; +import android.os.Environment; +import android.platform.test.annotations.Presubmit; +import android.provider.DeviceConfig; +import android.util.DataUnit; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.testables.TestableDeviceConfig; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public class BlobStoreConfigTest { + private static final long TIMEOUT_UPDATE_PROPERTIES_MS = 1_000; + + @Rule + public TestableDeviceConfig.TestableDeviceConfigRule + mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule(); + + private Context mContext; + + @Before + public void setUp() { + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + BlobStoreConfig.initialize(mContext); + } + + @Test + public void testGetAppDataBytesLimit() throws Exception { + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_BLOBSTORE, + BlobStoreConfig.DeviceConfigProperties.KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR, + String.valueOf(DataUnit.MEBIBYTES.toBytes(1000)), + false /* makeDefault */); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_BLOBSTORE, + BlobStoreConfig.DeviceConfigProperties.KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION, + String.valueOf(0.002f), + false /* makeDefault */); + waitForListenerToHandle(); + assertThat(BlobStoreConfig.getAppDataBytesLimit()).isEqualTo( + DataUnit.MEBIBYTES.toBytes(1000)); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_BLOBSTORE, + BlobStoreConfig.DeviceConfigProperties.KEY_TOTAL_BYTES_PER_APP_LIMIT_FLOOR, + String.valueOf(DataUnit.MEBIBYTES.toBytes(100)), + false /* makeDefault */); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_BLOBSTORE, + BlobStoreConfig.DeviceConfigProperties.KEY_TOTAL_BYTES_PER_APP_LIMIT_FRACTION, + String.valueOf(0.1f), + false /* makeDefault */); + waitForListenerToHandle(); + final long expectedLimit = (long) (Environment.getDataDirectory().getTotalSpace() * 0.1f); + assertThat(BlobStoreConfig.getAppDataBytesLimit()).isEqualTo(expectedLimit); + } + + private void waitForListenerToHandle() throws Exception { + final CountDownLatch latch = new CountDownLatch(1); + mContext.getMainExecutor().execute(latch::countDown); + if (!latch.await(TIMEOUT_UPDATE_PROPERTIES_MS, TimeUnit.MILLISECONDS)) { + fail("Timed out waiting for properties to get updated"); + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java index 6e0df3ecfabf..cd39144d5c6b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/blob/BlobStoreManagerServiceTest.java @@ -303,6 +303,37 @@ public class BlobStoreManagerServiceTest { assertThat(mService.getKnownIdsForTest()).containsExactly(blobId1, blobId2, blobId3); } + @Test + public void testGetTotalUsageBytes() throws Exception { + // Setup blobs + final BlobMetadata blobMetadata1 = mock(BlobMetadata.class); + final long size1 = 4567; + doReturn(size1).when(blobMetadata1).getSize(); + doReturn(true).when(blobMetadata1).isALeasee(TEST_PKG1, TEST_UID1); + doReturn(true).when(blobMetadata1).isALeasee(TEST_PKG2, TEST_UID2); + mUserBlobs.put(mock(BlobHandle.class), blobMetadata1); + + final BlobMetadata blobMetadata2 = mock(BlobMetadata.class); + final long size2 = 89475; + doReturn(size2).when(blobMetadata2).getSize(); + doReturn(false).when(blobMetadata2).isALeasee(TEST_PKG1, TEST_UID1); + doReturn(true).when(blobMetadata2).isALeasee(TEST_PKG2, TEST_UID2); + mUserBlobs.put(mock(BlobHandle.class), blobMetadata2); + + final BlobMetadata blobMetadata3 = mock(BlobMetadata.class); + final long size3 = 328732; + doReturn(size3).when(blobMetadata3).getSize(); + doReturn(true).when(blobMetadata3).isALeasee(TEST_PKG1, TEST_UID1); + doReturn(false).when(blobMetadata3).isALeasee(TEST_PKG2, TEST_UID2); + mUserBlobs.put(mock(BlobHandle.class), blobMetadata3); + + // Verify usage is calculated correctly + assertThat(mService.getTotalUsageBytesLocked(TEST_UID1, TEST_PKG1)) + .isEqualTo(size1 + size3); + assertThat(mService.getTotalUsageBytesLocked(TEST_UID2, TEST_PKG2)) + .isEqualTo(size1 + size2); + } + private BlobStoreSession createBlobStoreSessionMock(String ownerPackageName, int ownerUid, long sessionId, File sessionFile) { return createBlobStoreSessionMock(ownerPackageName, ownerUid, sessionId, sessionFile, diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java index 32631be3ec24..64b24c12f046 100644 --- a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java +++ b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfig.java @@ -24,15 +24,18 @@ import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.when; import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; +import android.util.ArrayMap; import android.util.Pair; import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; import org.junit.rules.TestRule; +import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import org.mockito.stubbing.Answer; @@ -109,6 +112,27 @@ public final class TestableDeviceConfig implements StaticMockFixture { String name = invocationOnMock.getArgument(1); return mKeyValueMap.get(getKey(namespace, name)); }).when(() -> DeviceConfig.getProperty(anyString(), anyString())); + + doAnswer((Answer<Properties>) invocationOnMock -> { + String namespace = invocationOnMock.getArgument(0); + final int varargStartIdx = 1; + Map<String, String> keyValues = new ArrayMap<>(); + if (invocationOnMock.getArguments().length == varargStartIdx) { + mKeyValueMap.entrySet().forEach(entry -> { + Pair<String, String> nameSpaceAndName = getNameSpaceAndName(entry.getKey()); + if (!nameSpaceAndName.first.equals(namespace)) { + return; + } + keyValues.put(nameSpaceAndName.second.toLowerCase(), entry.getValue()); + }); + } else { + for (int i = varargStartIdx; i < invocationOnMock.getArguments().length; ++i) { + String name = invocationOnMock.getArgument(i); + keyValues.put(name.toLowerCase(), mKeyValueMap.get(getKey(namespace, name))); + } + } + return getProperties(namespace, keyValues); + }).when(() -> DeviceConfig.getProperties(anyString(), ArgumentMatchers.<String>any())); } /** @@ -124,15 +148,25 @@ public final class TestableDeviceConfig implements StaticMockFixture { return namespace + "/" + name; } + private Pair<String, String> getNameSpaceAndName(String key) { + final String[] values = key.split("/"); + return Pair.create(values[0], values[1]); + } + private Properties getProperties(String namespace, String name, String value) { + return getProperties(namespace, Collections.singletonMap(name.toLowerCase(), value)); + } + + private Properties getProperties(String namespace, Map<String, String> keyValues) { Properties properties = Mockito.mock(Properties.class); when(properties.getNamespace()).thenReturn(namespace); - when(properties.getKeyset()).thenReturn(Collections.singleton(name)); + when(properties.getKeyset()).thenReturn(keyValues.keySet()); when(properties.getBoolean(anyString(), anyBoolean())).thenAnswer( invocation -> { String key = invocation.getArgument(0); boolean defaultValue = invocation.getArgument(1); - if (name.equalsIgnoreCase(key) && value != null) { + final String value = keyValues.get(key.toLowerCase()); + if (value != null) { return Boolean.parseBoolean(value); } else { return defaultValue; @@ -143,7 +177,8 @@ public final class TestableDeviceConfig implements StaticMockFixture { invocation -> { String key = invocation.getArgument(0); float defaultValue = invocation.getArgument(1); - if (name.equalsIgnoreCase(key) && value != null) { + final String value = keyValues.get(key.toLowerCase()); + if (value != null) { try { return Float.parseFloat(value); } catch (NumberFormatException e) { @@ -158,7 +193,8 @@ public final class TestableDeviceConfig implements StaticMockFixture { invocation -> { String key = invocation.getArgument(0); int defaultValue = invocation.getArgument(1); - if (name.equalsIgnoreCase(key) && value != null) { + final String value = keyValues.get(key.toLowerCase()); + if (value != null) { try { return Integer.parseInt(value); } catch (NumberFormatException e) { @@ -173,7 +209,8 @@ public final class TestableDeviceConfig implements StaticMockFixture { invocation -> { String key = invocation.getArgument(0); long defaultValue = invocation.getArgument(1); - if (name.equalsIgnoreCase(key) && value != null) { + final String value = keyValues.get(key.toLowerCase()); + if (value != null) { try { return Long.parseLong(value); } catch (NumberFormatException e) { @@ -184,11 +221,12 @@ public final class TestableDeviceConfig implements StaticMockFixture { } } ); - when(properties.getString(anyString(), anyString())).thenAnswer( + when(properties.getString(anyString(), nullable(String.class))).thenAnswer( invocation -> { String key = invocation.getArgument(0); String defaultValue = invocation.getArgument(1); - if (name.equalsIgnoreCase(key) && value != null) { + final String value = keyValues.get(key.toLowerCase()); + if (value != null) { return value; } else { return defaultValue; diff --git a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java index d76c9388cacf..ba995e446f29 100644 --- a/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/testables/TestableDeviceConfigTest.java @@ -23,6 +23,7 @@ import static com.google.common.truth.Truth.assertThat; import android.app.ActivityThread; import android.platform.test.annotations.Presubmit; import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -91,6 +92,39 @@ public class TestableDeviceConfigTest { } @Test + public void getProperties_empty() { + String newKey = "key2"; + String newValue = "value2"; + DeviceConfig.setProperty(sNamespace, sKey, sValue, false); + Properties properties = DeviceConfig.getProperties(sNamespace); + assertThat(properties.getString(sKey, null)).isEqualTo(sValue); + assertThat(properties.getString(newKey, null)).isNull(); + + DeviceConfig.setProperty(sNamespace, newKey, newValue, false); + properties = DeviceConfig.getProperties(sNamespace); + assertThat(properties.getString(sKey, null)).isEqualTo(sValue); + assertThat(properties.getString(newKey, null)).isEqualTo(newValue); + + } + + @Test + public void getProperties() { + Properties properties = DeviceConfig.getProperties(sNamespace, sKey); + assertThat(properties.getString(sKey, null)).isNull(); + + DeviceConfig.setProperty(sNamespace, sKey, sValue, false); + properties = DeviceConfig.getProperties(sNamespace, sKey); + assertThat(properties.getString(sKey, null)).isEqualTo(sValue); + + String newKey = "key2"; + String newValue = "value2"; + DeviceConfig.setProperty(sNamespace, newKey, newValue, false); + properties = DeviceConfig.getProperties(sNamespace, sKey, newKey); + assertThat(properties.getString(sKey, null)).isEqualTo(sValue); + assertThat(properties.getString(newKey, null)).isEqualTo(newValue); + } + + @Test public void testListener() throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(1); |