summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/am/ActiveServices.java4
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerConstants.java21
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerUtils.java124
-rw-r--r--services/tests/servicestests/src/com/android/server/am/ActivityManagerUtilsTest.java130
4 files changed, 279 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 5a1a505d7aa8..938e36e4b42c 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -5904,6 +5904,10 @@ public final class ActiveServices {
* @param durationMs Only meaningful for EXIT event, the duration from ENTER and EXIT state.
*/
private void logForegroundServiceStateChanged(ServiceRecord r, int state, int durationMs) {
+ if (!ActivityManagerUtils.shouldSamplePackageForAtom(
+ r.packageName, mAm.mConstants.mDefaultFgsAtomSampleRate)) {
+ return;
+ }
FrameworkStatsLog.write(FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED,
r.appInfo.uid,
r.shortInstanceName,
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index c8363dd59005..bf574521b895 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -97,6 +97,7 @@ final class ActivityManagerConstants extends ContentObserver {
static final String KEY_BOOT_TIME_TEMP_ALLOWLIST_DURATION = "boot_time_temp_allowlist_duration";
static final String KEY_FG_TO_BG_FGS_GRACE_DURATION = "fg_to_bg_fgs_grace_duration";
static final String KEY_FGS_START_FOREGROUND_TIMEOUT = "fgs_start_foreground_timeout";
+ static final String KEY_FGS_ATOM_SAMPLE_RATE = "fgs_atom_sample_rate";
private static final int DEFAULT_MAX_CACHED_PROCESSES = 32;
private static final long DEFAULT_BACKGROUND_SETTLE_TIME = 60*1000;
@@ -137,6 +138,7 @@ final class ActivityManagerConstants extends ContentObserver {
private static final int DEFAULT_BOOT_TIME_TEMP_ALLOWLIST_DURATION = 10 * 1000;
private static final long DEFAULT_FG_TO_BG_FGS_GRACE_DURATION = 5 * 1000;
private static final int DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS = 10 * 1000;
+ private static final float DEFAULT_FGS_ATOM_SAMPLE_RATE = 1; // 100 %
// Flag stored in the DeviceConfig API.
/**
@@ -430,6 +432,13 @@ final class ActivityManagerConstants extends ContentObserver {
*/
volatile long mFgsStartForegroundTimeoutMs = DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS;
+ /**
+ * Sample rate for the FGS westworld atom.
+ *
+ * If the value is 0.1, 10% of the installed packages would be sampled.
+ */
+ volatile float mDefaultFgsAtomSampleRate = DEFAULT_FGS_ATOM_SAMPLE_RATE;
+
private final ActivityManagerService mService;
private ContentResolver mResolver;
private final KeyValueListParser mParser = new KeyValueListParser(',');
@@ -629,6 +638,9 @@ final class ActivityManagerConstants extends ContentObserver {
case KEY_FGS_START_FOREGROUND_TIMEOUT:
updateFgsStartForegroundTimeout();
break;
+ case KEY_FGS_ATOM_SAMPLE_RATE:
+ updateFgsAtomSamplePercent();
+ break;
default:
break;
}
@@ -933,6 +945,13 @@ final class ActivityManagerConstants extends ContentObserver {
DEFAULT_FGS_START_FOREGROUND_TIMEOUT_MS);
}
+ private void updateFgsAtomSamplePercent() {
+ mDefaultFgsAtomSampleRate = DeviceConfig.getFloat(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_FGS_ATOM_SAMPLE_RATE,
+ DEFAULT_FGS_ATOM_SAMPLE_RATE);
+ }
+
private void updateImperceptibleKillExemptions() {
IMPERCEPTIBLE_KILL_EXEMPT_PACKAGES.clear();
IMPERCEPTIBLE_KILL_EXEMPT_PACKAGES.addAll(mDefaultImperceptibleKillExemptPackages);
@@ -1145,6 +1164,8 @@ final class ActivityManagerConstants extends ContentObserver {
pw.println(mFlagFgsStartRestrictionEnabled);
pw.print(" "); pw.print(KEY_DEFAULT_FGS_STARTS_RESTRICTION_CHECK_CALLER_TARGET_SDK);
pw.print("="); pw.println(mFgsStartRestrictionCheckCallerTargetSdk);
+ pw.print(" "); pw.print(KEY_FGS_ATOM_SAMPLE_RATE);
+ pw.print("="); pw.println(mDefaultFgsAtomSampleRate);
pw.println();
if (mOverrideMaxCachedProcesses >= 0) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerUtils.java b/services/core/java/com/android/server/am/ActivityManagerUtils.java
new file mode 100644
index 000000000000..dd2414866542
--- /dev/null
+++ b/services/core/java/com/android/server/am/ActivityManagerUtils.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2021 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.am;
+
+import android.app.ActivityThread;
+import android.provider.Settings;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * To store random utility methods...
+ */
+public class ActivityManagerUtils {
+ private ActivityManagerUtils() {
+ }
+
+ private static Integer sAndroidIdHash;
+
+ @GuardedBy("sHashCache")
+ private static final ArrayMap<String, Integer> sHashCache = new ArrayMap<>();
+
+ private static String sInjectedAndroidId;
+
+ /** Used by the unit tests to inject an android ID. Do not set in the prod code. */
+ @VisibleForTesting
+ static void injectAndroidIdForTest(String androidId) {
+ sInjectedAndroidId = androidId;
+ sAndroidIdHash = null;
+ }
+
+ /**
+ * Return a hash between [0, MAX_VALUE] generated from the android ID.
+ */
+ @VisibleForTesting
+ static int getAndroidIdHash() {
+ // No synchronization is required. Double-initialization is fine here.
+ if (sAndroidIdHash == null) {
+ final String androidId = Settings.Secure.getString(
+ ActivityThread.currentApplication().getContentResolver(),
+ Settings.Secure.ANDROID_ID);
+ sAndroidIdHash = getUnsignedHashUnCached(
+ sInjectedAndroidId != null ? sInjectedAndroidId : androidId);
+ }
+ return sAndroidIdHash;
+ }
+
+ /**
+ * Return a hash between [0, MAX_VALUE] generated from a package name, using a cache.
+ *
+ * Because all the results are cached, do not use it for dynamically generated strings.
+ */
+ @VisibleForTesting
+ static int getUnsignedHashCached(String s) {
+ synchronized (sHashCache) {
+ final Integer cached = sHashCache.get(s);
+ if (cached != null) {
+ return cached;
+ }
+ final int hash = getUnsignedHashUnCached(s);
+ sHashCache.put(s.intern(), hash);
+ return hash;
+ }
+ }
+
+ /**
+ * Return a hash between [0, MAX_VALUE] generated from a package name.
+ */
+ private static int getUnsignedHashUnCached(String s) {
+ try {
+ final MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ digest.update(s.getBytes());
+ return unsignedIntFromBytes(digest.digest());
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @VisibleForTesting
+ static int unsignedIntFromBytes(byte[] longEnoughBytes) {
+ return (extractByte(longEnoughBytes, 0)
+ | extractByte(longEnoughBytes, 1)
+ | extractByte(longEnoughBytes, 2)
+ | extractByte(longEnoughBytes, 3))
+ & 0x7FFF_FFFF;
+ }
+
+ private static int extractByte(byte[] bytes, int index) {
+ return (((int) bytes[index]) & 0xFF) << (index * 8);
+ }
+
+ /**
+ * @return whether a package should be logged, using a random value based on the ANDROID_ID,
+ * with a given sampling rate.
+ */
+ public static boolean shouldSamplePackageForAtom(String packageName, float rate) {
+ if (rate <= 0) {
+ return false;
+ }
+ if (rate >= 1) {
+ return true;
+ }
+ final int hash = getUnsignedHashCached(packageName) ^ getAndroidIdHash();
+
+ return (((double) hash) / Integer.MAX_VALUE) <= rate;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerUtilsTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerUtilsTest.java
new file mode 100644
index 000000000000..96103e36ae92
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerUtilsTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2021 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.am;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+public class ActivityManagerUtilsTest {
+ @Test
+ public void getAndroidIdHash() {
+ // getAndroidIdHash() essentially returns a random a value. Just make sure it's
+ // non-negative.
+ assertThat(ActivityManagerUtils.getAndroidIdHash()).isAtLeast(0);
+ }
+
+ @Test
+ public void getUnsignedHashCached() {
+ assertThat(ActivityManagerUtils.getUnsignedHashCached("x")).isEqualTo(
+ ActivityManagerUtils.getUnsignedHashCached("x"));
+
+ assertThat(ActivityManagerUtils.getUnsignedHashCached("x")).isNotEqualTo(
+ ActivityManagerUtils.getUnsignedHashCached("y"));
+ }
+
+ @Test
+ public void shouldSamplePackage_sampleNone() {
+ final int numTests = 100000;
+ for (int i = 0; i < numTests; i++) {
+ assertThat(ActivityManagerUtils.shouldSamplePackageForAtom("" + i, 0))
+ .isFalse();
+ }
+ }
+
+ @Test
+ public void shouldSamplePackage_sampleAll() {
+ final int numTests = 100000;
+
+ for (int i = 0; i < numTests; i++) {
+ assertThat(ActivityManagerUtils.shouldSamplePackageForAtom("" + i, 1))
+ .isTrue();
+ }
+ }
+
+ /**
+ * Make sure, with the same android ID, an expected rate of the packages are selected.
+ */
+ @Test
+ public void shouldSamplePackage_sampleSome_fixedAndroidId() {
+ checkShouldSamplePackage_fixedAndroidId(0.1f);
+ checkShouldSamplePackage_fixedAndroidId(0.5f);
+ checkShouldSamplePackage_fixedAndroidId(0.9f);
+ }
+
+ /**
+ * Make sure, the same package is selected on an expected rate of the devices.
+ */
+ @Test
+ public void shouldSamplePackage_sampleSome_fixedPackage() {
+ checkShouldSamplePackage_fixedPackage(0.1f);
+ checkShouldSamplePackage_fixedPackage(0.5f);
+ checkShouldSamplePackage_fixedPackage(0.9f);
+ }
+
+ private void checkShouldSamplePackage_fixedPackage(float sampleRate) {
+ checkShouldSamplePackage(sampleRate, sampleRate, true, false);
+ }
+
+ private void checkShouldSamplePackage_fixedAndroidId(float sampleRate) {
+ checkShouldSamplePackage(sampleRate, sampleRate, false, true);
+ }
+
+ @Test
+ public void testSheckShouldSamplePackage() {
+ // Just make sure checkShouldSamplePackage is actually working...
+ try {
+ checkShouldSamplePackage(0.3f, 0.6f, false, true);
+ fail();
+ } catch (AssertionError expected) {
+ }
+ try {
+ checkShouldSamplePackage(0.6f, 0.3f, true, false);
+ fail();
+ } catch (AssertionError expected) {
+ }
+ }
+
+ private void checkShouldSamplePackage(float inputSampleRate, float expectedRate,
+ boolean fixedPackage, boolean fixedAndroidId) {
+ final int numTests = 100000;
+
+ try {
+ int numSampled = 0;
+ for (int i = 0; i < numTests; i++) {
+ final String pkg = fixedPackage ? "fixed-package" : "" + i;
+ ActivityManagerUtils.injectAndroidIdForTest(
+ fixedAndroidId ? "fixed-android-id" : "" + i);
+
+ if (ActivityManagerUtils.shouldSamplePackageForAtom(pkg, inputSampleRate)) {
+ numSampled++;
+ }
+ assertThat(ActivityManagerUtils.getUnsignedHashCached(pkg)).isEqualTo(
+ ActivityManagerUtils.getUnsignedHashCached(pkg));
+ }
+ final double actualSampleRate = ((double) numSampled) / numTests;
+
+ assertThat(actualSampleRate).isWithin(0.05).of(expectedRate);
+ } finally {
+ ActivityManagerUtils.injectAndroidIdForTest(null);
+ }
+ }
+}