[BR14] Tracker Data Saver status in ConnectivityService
BYPASS_INCLUSIVE_LANGUAGE_REASON=Using public API
Test: atest ConnectivityCoverageTests:android.net.connectivity.com.android.server.CSBpfNetMapsTest
(on U and V devices respectively)
Bug: 297836825
Fix: 314858283
Change-Id: Ibeac8cd3a33468c6539234255328affc6b3f8ffe
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index a934ddb..fa27d0e 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -16,8 +16,6 @@
package android.net;
import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
-import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
-import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1;
import static android.net.NetworkRequest.Type.BACKGROUND_REQUEST;
import static android.net.NetworkRequest.Type.LISTEN;
@@ -27,8 +25,6 @@
import static android.net.NetworkRequest.Type.TRACK_SYSTEM_DEFAULT;
import static android.net.QosCallback.QosCallbackRegistrationException;
-import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
-
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
@@ -41,16 +37,12 @@
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
-import android.annotation.TargetApi;
-import android.app.Application;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
import android.compat.annotation.UnsupportedAppUsage;
-import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.net.ConnectivityDiagnosticsManager.DataStallReport.DetectionMethod;
import android.net.IpSecManager.UdpEncapsulationSocket;
import android.net.SocketKeepalive.Callback;
@@ -82,8 +74,6 @@
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.utils.build.SdkLevel;
import libcore.net.event.NetworkEventDispatcher;
@@ -105,7 +95,6 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class that answers queries about the state of network connectivity. It also
@@ -6262,90 +6251,6 @@
}
/**
- * Helper class to track data saver status.
- *
- * The class will fetch current data saver status from {@link NetworkPolicyManager} when
- * initialized, and listening for status changed intent to cache the latest status.
- *
- * @hide
- */
- @TargetApi(Build.VERSION_CODES.TIRAMISU) // RECEIVER_NOT_EXPORTED requires T.
- @VisibleForTesting(visibility = PRIVATE)
- public static class DataSaverStatusTracker extends BroadcastReceiver {
- private static final Object sDataSaverStatusTrackerLock = new Object();
-
- private static volatile DataSaverStatusTracker sInstance;
-
- /**
- * Gets a static instance of the class.
- *
- * @param context A {@link Context} for initialization. Note that since the data saver
- * status is global on a device, passing any context is equivalent.
- * @return The static instance of a {@link DataSaverStatusTracker}.
- */
- public static DataSaverStatusTracker getInstance(@NonNull Context context) {
- if (sInstance == null) {
- synchronized (sDataSaverStatusTrackerLock) {
- if (sInstance == null) {
- sInstance = new DataSaverStatusTracker(context);
- }
- }
- }
- return sInstance;
- }
-
- private final NetworkPolicyManager mNpm;
- // The value updates on the caller's binder thread or UI thread.
- private final AtomicBoolean mIsDataSaverEnabled;
-
- @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
- public DataSaverStatusTracker(final Context context) {
- // To avoid leaks, take the application context.
- final Context appContext;
- if (context instanceof Application) {
- appContext = context;
- } else {
- appContext = context.getApplicationContext();
- }
-
- if ((appContext.getApplicationInfo().flags & FLAG_PERSISTENT) == 0
- && (appContext.getApplicationInfo().flags & FLAG_SYSTEM) == 0) {
- throw new IllegalStateException("Unexpected caller: "
- + appContext.getApplicationInfo().packageName);
- }
-
- mNpm = appContext.getSystemService(NetworkPolicyManager.class);
- final IntentFilter filter = new IntentFilter(
- ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED);
- // The receiver should not receive broadcasts from other Apps.
- appContext.registerReceiver(this, filter, Context.RECEIVER_NOT_EXPORTED);
- mIsDataSaverEnabled = new AtomicBoolean();
- updateDataSaverEnabled();
- }
-
- // Runs on caller's UI thread.
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED)) {
- updateDataSaverEnabled();
- } else {
- throw new IllegalStateException("Unexpected intent " + intent);
- }
- }
-
- public boolean getDataSaverEnabled() {
- return mIsDataSaverEnabled.get();
- }
-
- private void updateDataSaverEnabled() {
- // Uid doesn't really matter, but use a fixed UID to make things clearer.
- final int dataSaverForCallerUid = mNpm.getRestrictBackgroundStatus(Process.SYSTEM_UID);
- mIsDataSaverEnabled.set(dataSaverForCallerUid
- != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
- }
- }
-
- /**
* Return whether the network is blocked for the given uid and metered condition.
*
* Similar to {@link NetworkPolicyManager#isUidNetworkingBlocked}, but directly reads the BPF
@@ -6370,17 +6275,12 @@
@RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
public boolean isUidNetworkingBlocked(int uid, boolean isNetworkMetered) {
final BpfNetMapsReader reader = BpfNetMapsReader.getInstance();
-
- final boolean isDataSaverEnabled;
- if (SdkLevel.isAtLeastV()) {
- isDataSaverEnabled = reader.getDataSaverEnabled();
- } else {
- final DataSaverStatusTracker dataSaverStatusTracker =
- DataSaverStatusTracker.getInstance(mContext);
- isDataSaverEnabled = dataSaverStatusTracker.getDataSaverEnabled();
- }
-
- return reader.isUidNetworkingBlocked(uid, isNetworkMetered, isDataSaverEnabled);
+ // Note that before V, the data saver status in bpf is written by ConnectivityService
+ // when receiving {@link #ACTION_RESTRICT_BACKGROUND_CHANGED}. Thus,
+ // the status is not synchronized.
+ // On V+, the data saver status is set by platform code when enabling/disabling
+ // data saver, which is synchronized.
+ return reader.isUidNetworkingBlocked(uid, isNetworkMetered, reader.getDataSaverEnabled());
}
/** @hide */
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 5f67246..cc71153 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -32,6 +32,7 @@
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
+import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
import static android.net.ConnectivityManager.BLOCKED_REASON_LOCKDOWN_VPN;
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
@@ -1770,6 +1771,19 @@
mUserAllContext.registerReceiver(mPackageIntentReceiver, packageIntentFilter,
null /* broadcastPermission */, mHandler);
+ // This is needed for pre-V devices to propagate the data saver status
+ // to the BPF map. This isn't supported before Android T because BPF maps are
+ // unsupported, and it's also unnecessary on Android V and later versions,
+ // as the platform code handles data saver bit updates. Additionally, checking
+ // the initial data saver status here is superfluous because the intent won't
+ // be sent until the system is ready.
+ if (mDeps.isAtLeastT() && !mDeps.isAtLeastV()) {
+ final IntentFilter dataSaverIntentFilter =
+ new IntentFilter(ACTION_RESTRICT_BACKGROUND_CHANGED);
+ mUserAllContext.registerReceiver(mDataSaverReceiver, dataSaverIntentFilter,
+ null /* broadcastPermission */, mHandler);
+ }
+
// TrackMultiNetworkActivities feature should be enabled by trunk stable flag.
// But reading the trunk stable flags from mainline modules is not supported yet.
// So enabling this feature on V+ release.
@@ -7051,6 +7065,32 @@
}
};
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private final BroadcastReceiver mDataSaverReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mDeps.isAtLeastV()) {
+ throw new IllegalStateException(
+ "data saver status should be updated from platform");
+ }
+ ensureRunningOnConnectivityServiceThread();
+ switch (intent.getAction()) {
+ case ACTION_RESTRICT_BACKGROUND_CHANGED:
+ // If the uid is present in the deny list, the API will consistently
+ // return ENABLED. To retrieve the global switch status, the system
+ // uid is chosen because it will never be included in the deny list.
+ final int dataSaverForSystemUid =
+ mPolicyManager.getRestrictBackgroundStatus(Process.SYSTEM_UID);
+ final boolean isDataSaverEnabled = (dataSaverForSystemUid
+ != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED);
+ mBpfNetMaps.setDataSaverEnabled(isDataSaverEnabled);
+ break;
+ default:
+ Log.wtf(TAG, "received unexpected intent: " + intent.getAction());
+ }
+ }
+ };
+
private final HashMap<Messenger, NetworkProviderInfo> mNetworkProviderInfos = new HashMap<>();
private final HashMap<NetworkRequest, NetworkRequestInfo> mNetworkRequests = new HashMap<>();
@@ -12985,7 +13025,7 @@
}
}
- @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Override
public void setDataSaverEnabled(final boolean enable) {
enforceNetworkStackOrSettingsPermission();
diff --git a/tests/unit/java/android/net/ConnectivityManagerTest.java b/tests/unit/java/android/net/ConnectivityManagerTest.java
index 0082af2..b71a46f 100644
--- a/tests/unit/java/android/net/ConnectivityManagerTest.java
+++ b/tests/unit/java/android/net/ConnectivityManagerTest.java
@@ -16,13 +16,6 @@
package android.net;
-import static android.content.Context.RECEIVER_NOT_EXPORTED;
-import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
-import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
-import static android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
-import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
import static android.net.ConnectivityManager.TYPE_NONE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS;
import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN;
@@ -46,7 +39,6 @@
import static com.android.testutils.MiscAsserts.assertThrows;
-import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -70,10 +62,7 @@
import android.app.PendingIntent;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
-import android.net.ConnectivityManager.DataSaverStatusTracker;
import android.net.ConnectivityManager.NetworkCallback;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
@@ -527,44 +516,6 @@
+ " attempts", ref.get());
}
- @DevSdkIgnoreRule.IgnoreAfter(VERSION_CODES.UPSIDE_DOWN_CAKE)
- @Test
- public void testDataSaverStatusTracker() {
- mockService(NetworkPolicyManager.class, Context.NETWORK_POLICY_SERVICE, mNpm);
- // Mock proper application info.
- doReturn(mCtx).when(mCtx).getApplicationContext();
- final ApplicationInfo mockAppInfo = new ApplicationInfo();
- mockAppInfo.flags = FLAG_PERSISTENT | FLAG_SYSTEM;
- doReturn(mockAppInfo).when(mCtx).getApplicationInfo();
- // Enable data saver.
- doReturn(RESTRICT_BACKGROUND_STATUS_ENABLED).when(mNpm)
- .getRestrictBackgroundStatus(anyInt());
-
- final DataSaverStatusTracker tracker = new DataSaverStatusTracker(mCtx);
- // Verify the data saver status is correct right after initialization.
- assertTrue(tracker.getDataSaverEnabled());
-
- // Verify the tracker register receiver with expected intent filter.
- final ArgumentCaptor<IntentFilter> intentFilterCaptor =
- ArgumentCaptor.forClass(IntentFilter.class);
- verify(mCtx).registerReceiver(
- any(), intentFilterCaptor.capture(), eq(RECEIVER_NOT_EXPORTED));
- assertEquals(ACTION_RESTRICT_BACKGROUND_CHANGED,
- intentFilterCaptor.getValue().getAction(0));
-
- // Mock data saver status changed event and verify the tracker tracks the
- // status accordingly.
- doReturn(RESTRICT_BACKGROUND_STATUS_DISABLED).when(mNpm)
- .getRestrictBackgroundStatus(anyInt());
- tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
- assertFalse(tracker.getDataSaverEnabled());
-
- doReturn(RESTRICT_BACKGROUND_STATUS_WHITELISTED).when(mNpm)
- .getRestrictBackgroundStatus(anyInt());
- tracker.onReceive(mCtx, new Intent(ACTION_RESTRICT_BACKGROUND_CHANGED));
- assertTrue(tracker.getDataSaverEnabled());
- }
-
private <T> void mockService(Class<T> clazz, String name, T service) {
doReturn(service).when(mCtx).getSystemService(name);
doReturn(name).when(mCtx).getSystemServiceName(clazz);
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
new file mode 100644
index 0000000..c26ec53
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBpfNetMapsTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2023 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
+
+import android.content.Intent
+import android.net.ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
+import android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.S_V2) // Bpf only supports in T+.
+class CSBpfNetMapsTest : CSTest() {
+ @get:Rule
+ val ignoreRule = DevSdkIgnoreRule()
+
+ @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testCSTrackDataSaverBeforeV() {
+ val inOrder = inOrder(bpfNetMaps)
+ mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_WHITELISTED)
+ inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+ mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_DISABLED)
+ inOrder.verify(bpfNetMaps).setDataSaverEnabled(false)
+ mockDataSaverStatus(RESTRICT_BACKGROUND_STATUS_ENABLED)
+ inOrder.verify(bpfNetMaps).setDataSaverEnabled(true)
+ }
+
+ // Data Saver Status is updated from platform code in V+.
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Test
+ fun testCSTrackDataSaverAboveU() {
+ listOf(RESTRICT_BACKGROUND_STATUS_WHITELISTED, RESTRICT_BACKGROUND_STATUS_ENABLED,
+ RESTRICT_BACKGROUND_STATUS_DISABLED).forEach {
+ mockDataSaverStatus(it)
+ verify(bpfNetMaps, never()).setDataSaverEnabled(anyBoolean())
+ }
+ }
+
+ private fun mockDataSaverStatus(status: Int) {
+ doReturn(status).`when`(context.networkPolicyManager).getRestrictBackgroundStatus(anyInt())
+ // While the production code dispatches the intent on the handler thread,
+ // The test would dispatch the intent in the caller thread. Make it dispatch
+ // on the handler thread to match production behavior.
+ visibleOnHandlerThread(csHandler) {
+ context.sendBroadcast(Intent(ACTION_RESTRICT_BACKGROUND_CHANGED))
+ }
+ waitForIdle()
+ }
+}