Merge "Add a limit on how much data an app can acquire a lease on." into rvc-dev

This commit is contained in:
Sudheer Shanka
2020-03-06 22:39:06 +00:00
committed by Android (Google) Code Review
11 changed files with 385 additions and 22 deletions

View File

@@ -242,7 +242,7 @@ class BlobMetadata {
return hasOtherLeasees(null, uid); return hasOtherLeasees(null, uid);
} }
private boolean isALeasee(@Nullable String packageName, int uid) { boolean isALeasee(@Nullable String packageName, int uid) {
synchronized (mMetadataLock) { synchronized (mMetadataLock) {
// Check if the package is a leasee of the data blob. // Check if the package is a leasee of the data blob.
for (int i = 0, size = mLeasees.size(); i < size; ++i) { for (int i = 0, size = mLeasees.size(); i < size; ++i) {

View File

@@ -15,12 +15,23 @@
*/ */
package com.android.server.blob; 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.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.content.Context;
import android.os.Environment; import android.os.Environment;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.util.DataUnit;
import android.util.Log; import android.util.Log;
import android.util.Slog; import android.util.Slog;
import com.android.internal.util.IndentingPrintWriter;
import java.io.File; import java.io.File;
import java.util.concurrent.TimeUnit; 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 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 @Nullable
public static File prepareBlobFile(long sessionId) { public static File prepareBlobFile(long sessionId) {
final File blobsDir = prepareBlobsDir(); final File blobsDir = prepareBlobsDir();
@@ -123,4 +204,21 @@ class BlobStoreConfig {
public static File getBlobStoreRootDir() { public static File getBlobStoreRootDir() {
return new File(Environment.getDataSystemDirectory(), ROOT_DIR_NAME); 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();
}
} }

View File

@@ -188,7 +188,9 @@ public class BlobStoreManagerService extends SystemService {
@Override @Override
public void onBootPhase(int phase) { 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) { synchronized (mBlobsLock) {
final SparseArray<SparseArray<String>> allPackages = getAllPackages(); final SparseArray<SparseArray<String>> allPackages = getAllPackages();
readBlobSessionsLocked(allPackages); readBlobSessionsLocked(allPackages);
@@ -381,6 +383,11 @@ public class BlobStoreManagerService extends SystemService {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Lease expiry cannot be later than blobs expiry time"); "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, blobMetadata.addLeasee(callingPackage, callingUid,
descriptionResId, description, leaseExpiryTimeMillis); descriptionResId, description, leaseExpiryTimeMillis);
if (LOGV) { 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, private void releaseLeaseInternal(BlobHandle blobHandle, int callingUid,
String callingPackage) { String callingPackage) {
synchronized (mBlobsLock) { synchronized (mBlobsLock) {
@@ -1236,8 +1255,10 @@ public class BlobStoreManagerService extends SystemService {
} }
synchronized (mBlobsLock) { synchronized (mBlobsLock) {
fout.println("mCurrentMaxSessionId: " + mCurrentMaxSessionId); if (dumpArgs.shouldDumpAllSections()) {
fout.println(); fout.println("mCurrentMaxSessionId: " + mCurrentMaxSessionId);
fout.println();
}
if (dumpArgs.shouldDumpSessions()) { if (dumpArgs.shouldDumpSessions()) {
dumpSessionsLocked(fout, dumpArgs); dumpSessionsLocked(fout, dumpArgs);
@@ -1248,6 +1269,14 @@ public class BlobStoreManagerService extends SystemService {
fout.println(); fout.println();
} }
} }
if (dumpArgs.shouldDumpConfig()) {
fout.println("BlobStore config:");
fout.increaseIndent();
BlobStoreConfig.dump(fout, mContext);
fout.decreaseIndent();
fout.println();
}
} }
@Override @Override
@@ -1260,14 +1289,16 @@ public class BlobStoreManagerService extends SystemService {
} }
static final class DumpArgs { 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 boolean mDumpFull;
private final ArrayList<String> mDumpPackages = new ArrayList<>(); private final ArrayList<String> mDumpPackages = new ArrayList<>();
private final ArrayList<Integer> mDumpUids = new ArrayList<>(); private final ArrayList<Integer> mDumpUids = new ArrayList<>();
private final ArrayList<Integer> mDumpUserIds = new ArrayList<>(); private final ArrayList<Integer> mDumpUserIds = new ArrayList<>();
private final ArrayList<Long> mDumpBlobIds = new ArrayList<>(); private final ArrayList<Long> mDumpBlobIds = new ArrayList<>();
private boolean mDumpOnlySelectedSections;
private boolean mDumpSessions;
private boolean mDumpBlobs;
private boolean mDumpHelp; private boolean mDumpHelp;
public boolean shouldDumpSession(String packageName, int uid, long blobId) { public boolean shouldDumpSession(String packageName, int uid, long blobId) {
@@ -1286,18 +1317,41 @@ public class BlobStoreManagerService extends SystemService {
return true; return true;
} }
public boolean shouldDumpAllSections() {
return mSelectedSectionFlags == 0;
}
public void allowDumpSessions() {
mSelectedSectionFlags |= FLAG_DUMP_SESSIONS;
}
public boolean shouldDumpSessions() { public boolean shouldDumpSessions() {
if (!mDumpOnlySelectedSections) { if (shouldDumpAllSections()) {
return true; return true;
} }
return mDumpSessions; return (mSelectedSectionFlags & FLAG_DUMP_SESSIONS) != 0;
}
public void allowDumpBlobs() {
mSelectedSectionFlags |= FLAG_DUMP_BLOBS;
} }
public boolean shouldDumpBlobs() { public boolean shouldDumpBlobs() {
if (!mDumpOnlySelectedSections) { if (shouldDumpAllSections()) {
return true; return true;
} }
return mDumpBlobs; return (mSelectedSectionFlags & FLAG_DUMP_BLOBS) != 0;
}
public void allowDumpConfig() {
mSelectedSectionFlags |= FLAG_DUMP_CONFIG;
}
public boolean shouldDumpConfig() {
if (shouldDumpAllSections()) {
return true;
}
return (mSelectedSectionFlags & FLAG_DUMP_CONFIG) != 0;
} }
public boolean shouldDumpBlob(long blobId) { public boolean shouldDumpBlob(long blobId) {
@@ -1334,11 +1388,11 @@ public class BlobStoreManagerService extends SystemService {
dumpArgs.mDumpFull = true; dumpArgs.mDumpFull = true;
} }
} else if ("--sessions".equals(opt)) { } else if ("--sessions".equals(opt)) {
dumpArgs.mDumpOnlySelectedSections = true; dumpArgs.allowDumpSessions();
dumpArgs.mDumpSessions = true;
} else if ("--blobs".equals(opt)) { } else if ("--blobs".equals(opt)) {
dumpArgs.mDumpOnlySelectedSections = true; dumpArgs.allowDumpBlobs();
dumpArgs.mDumpBlobs = true; } else if ("--config".equals(opt)) {
dumpArgs.allowDumpConfig();
} else if ("--package".equals(opt) || "-p".equals(opt)) { } else if ("--package".equals(opt) || "-p".equals(opt)) {
dumpArgs.mDumpPackages.add(getStringArgRequired(args, ++i, "packageName")); dumpArgs.mDumpPackages.add(getStringArgRequired(args, ++i, "packageName"));
} else if ("--uid".equals(opt) || "-u".equals(opt)) { } else if ("--uid".equals(opt) || "-u".equals(opt)) {
@@ -1397,6 +1451,8 @@ public class BlobStoreManagerService extends SystemService {
printWithIndent(pw, "Dump only the sessions info"); printWithIndent(pw, "Dump only the sessions info");
pw.println("--blobs"); pw.println("--blobs");
printWithIndent(pw, "Dump only the committed blobs info"); printWithIndent(pw, "Dump only the committed blobs info");
pw.println("--config");
printWithIndent(pw, "Dump only the config values");
pw.println("--package | -p [package-name]"); pw.println("--package | -p [package-name]");
printWithIndent(pw, "Dump blobs info associated with the given package"); printWithIndent(pw, "Dump blobs info associated with the given package");
pw.println("--uid | -u [uid]"); pw.println("--uid | -u [uid]");

View File

@@ -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_ATTENTION_MANAGER_SERVICE = "attention_manager_service";
field public static final String NAMESPACE_AUTOFILL = "autofill"; field public static final String NAMESPACE_AUTOFILL = "autofill";
field public static final String NAMESPACE_BIOMETRICS = "biometrics"; 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_CONNECTIVITY = "connectivity";
field public static final String NAMESPACE_CONTENT_CAPTURE = "content_capture"; field public static final String NAMESPACE_CONTENT_CAPTURE = "content_capture";
field @Deprecated public static final String NAMESPACE_DEX_BOOT = "dex_boot"; field @Deprecated public static final String NAMESPACE_DEX_BOOT = "dex_boot";

View File

@@ -110,6 +110,14 @@ public final class DeviceConfig {
@TestApi @TestApi
public static final String NAMESPACE_AUTOFILL = "autofill"; 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. * Namespace for all networking connectivity related features.
* *

View File

@@ -47,6 +47,7 @@ message ConfigSettingsProto {
repeated SettingProto systemui_settings = 20; repeated SettingProto systemui_settings = 20;
repeated SettingProto telephony_settings = 21; repeated SettingProto telephony_settings = 21;
repeated SettingProto textclassifier_settings = 22; repeated SettingProto textclassifier_settings = 22;
repeated SettingProto blobstore_settings = 23;
message NamespaceProto { message NamespaceProto {
optional string namespace = 1; optional string namespace = 1;

View File

@@ -52,6 +52,8 @@ class SettingsProtoDumpUtil {
ConfigSettingsProto.APP_COMPAT_SETTINGS); ConfigSettingsProto.APP_COMPAT_SETTINGS);
namespaceToFieldMap.put(DeviceConfig.NAMESPACE_AUTOFILL, namespaceToFieldMap.put(DeviceConfig.NAMESPACE_AUTOFILL,
ConfigSettingsProto.AUTOFILL_SETTINGS); ConfigSettingsProto.AUTOFILL_SETTINGS);
namespaceToFieldMap.put(DeviceConfig.NAMESPACE_BLOBSTORE,
ConfigSettingsProto.BLOBSTORE_SETTINGS);
namespaceToFieldMap.put(DeviceConfig.NAMESPACE_CONNECTIVITY, namespaceToFieldMap.put(DeviceConfig.NAMESPACE_CONNECTIVITY,
ConfigSettingsProto.CONNECTIVITY_SETTINGS); ConfigSettingsProto.CONNECTIVITY_SETTINGS);
namespaceToFieldMap.put(DeviceConfig.NAMESPACE_CONTENT_CAPTURE, namespaceToFieldMap.put(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,

View File

@@ -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");
}
}
}

View File

@@ -303,6 +303,37 @@ public class BlobStoreManagerServiceTest {
assertThat(mService.getKnownIdsForTest()).containsExactly(blobId1, blobId2, blobId3); 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, private BlobStoreSession createBlobStoreSessionMock(String ownerPackageName, int ownerUid,
long sessionId, File sessionFile) { long sessionId, File sessionFile) {
return createBlobStoreSessionMock(ownerPackageName, ownerUid, sessionId, sessionFile, return createBlobStoreSessionMock(ownerPackageName, ownerUid, sessionId, sessionFile,

View File

@@ -24,15 +24,18 @@ import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.provider.DeviceConfig; import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties; import android.provider.DeviceConfig.Properties;
import android.util.ArrayMap;
import android.util.Pair; import android.util.Pair;
import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
import org.junit.rules.TestRule; import org.junit.rules.TestRule;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
@@ -109,6 +112,27 @@ public final class TestableDeviceConfig implements StaticMockFixture {
String name = invocationOnMock.getArgument(1); String name = invocationOnMock.getArgument(1);
return mKeyValueMap.get(getKey(namespace, name)); return mKeyValueMap.get(getKey(namespace, name));
}).when(() -> DeviceConfig.getProperty(anyString(), anyString())); }).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; 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) { 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); Properties properties = Mockito.mock(Properties.class);
when(properties.getNamespace()).thenReturn(namespace); when(properties.getNamespace()).thenReturn(namespace);
when(properties.getKeyset()).thenReturn(Collections.singleton(name)); when(properties.getKeyset()).thenReturn(keyValues.keySet());
when(properties.getBoolean(anyString(), anyBoolean())).thenAnswer( when(properties.getBoolean(anyString(), anyBoolean())).thenAnswer(
invocation -> { invocation -> {
String key = invocation.getArgument(0); String key = invocation.getArgument(0);
boolean defaultValue = invocation.getArgument(1); 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); return Boolean.parseBoolean(value);
} else { } else {
return defaultValue; return defaultValue;
@@ -143,7 +177,8 @@ public final class TestableDeviceConfig implements StaticMockFixture {
invocation -> { invocation -> {
String key = invocation.getArgument(0); String key = invocation.getArgument(0);
float defaultValue = invocation.getArgument(1); float defaultValue = invocation.getArgument(1);
if (name.equalsIgnoreCase(key) && value != null) { final String value = keyValues.get(key.toLowerCase());
if (value != null) {
try { try {
return Float.parseFloat(value); return Float.parseFloat(value);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
@@ -158,7 +193,8 @@ public final class TestableDeviceConfig implements StaticMockFixture {
invocation -> { invocation -> {
String key = invocation.getArgument(0); String key = invocation.getArgument(0);
int defaultValue = invocation.getArgument(1); int defaultValue = invocation.getArgument(1);
if (name.equalsIgnoreCase(key) && value != null) { final String value = keyValues.get(key.toLowerCase());
if (value != null) {
try { try {
return Integer.parseInt(value); return Integer.parseInt(value);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
@@ -173,7 +209,8 @@ public final class TestableDeviceConfig implements StaticMockFixture {
invocation -> { invocation -> {
String key = invocation.getArgument(0); String key = invocation.getArgument(0);
long defaultValue = invocation.getArgument(1); long defaultValue = invocation.getArgument(1);
if (name.equalsIgnoreCase(key) && value != null) { final String value = keyValues.get(key.toLowerCase());
if (value != null) {
try { try {
return Long.parseLong(value); return Long.parseLong(value);
} catch (NumberFormatException e) { } 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 -> { invocation -> {
String key = invocation.getArgument(0); String key = invocation.getArgument(0);
String defaultValue = invocation.getArgument(1); String defaultValue = invocation.getArgument(1);
if (name.equalsIgnoreCase(key) && value != null) { final String value = keyValues.get(key.toLowerCase());
if (value != null) {
return value; return value;
} else { } else {
return defaultValue; return defaultValue;

View File

@@ -23,6 +23,7 @@ import static com.google.common.truth.Truth.assertThat;
import android.app.ActivityThread; import android.app.ActivityThread;
import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.Presubmit;
import android.provider.DeviceConfig; import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4; import androidx.test.runner.AndroidJUnit4;
@@ -90,6 +91,39 @@ public class TestableDeviceConfigTest {
assertThat(result).isEqualTo(newValue); assertThat(result).isEqualTo(newValue);
} }
@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 @Test
public void testListener() throws InterruptedException { public void testListener() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1); CountDownLatch countDownLatch = new CountDownLatch(1);