Suspend callbacks for frozen apps
To avoid filling the binder buffers, and sending unnecessary callbacks when apps are unfrozen (for example successive LinkProperties changes or onAvailable+onLost are not useful), queue and aggregate callbacks for apps that are suspended. Test: atest Bug: 327038794 Bug: 279392981 Change-Id: I6de357ac2834f1f7e960dfbaa63adfc3825f2a82
This commit is contained in:
parent
6ade6e2042
commit
2d78124348
6 changed files with 936 additions and 10 deletions
|
|
@ -4163,6 +4163,8 @@ public class ConnectivityManager {
|
|||
*/
|
||||
@FilteredCallback(methodId = METHOD_ONAVAILABLE_5ARGS,
|
||||
calledByCallbackId = CALLBACK_AVAILABLE,
|
||||
// If this list is modified, ConnectivityService#addAvailableStateUpdateCallbacks
|
||||
// needs to be updated too.
|
||||
mayCall = { METHOD_ONAVAILABLE_4ARGS,
|
||||
METHOD_ONLOCALNETWORKINFOCHANGED,
|
||||
METHOD_ONBLOCKEDSTATUSCHANGED_INT })
|
||||
|
|
@ -4193,6 +4195,8 @@ public class ConnectivityManager {
|
|||
*/
|
||||
@FilteredCallback(methodId = METHOD_ONAVAILABLE_4ARGS,
|
||||
calledByCallbackId = CALLBACK_TRANSITIVE_CALLS_ONLY,
|
||||
// If this list is modified, ConnectivityService#addAvailableStateUpdateCallbacks
|
||||
// needs to be updated too.
|
||||
mayCall = { METHOD_ONAVAILABLE_1ARG,
|
||||
METHOD_ONNETWORKSUSPENDED,
|
||||
METHOD_ONCAPABILITIESCHANGED,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
|
|||
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
|
||||
import static android.net.ConnectivityManager.FIREWALL_RULE_DEFAULT;
|
||||
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
|
||||
import static android.net.ConnectivityManager.NETID_UNSET;
|
||||
import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_ALL;
|
||||
import static android.net.ConnectivityManager.NetworkCallback.DECLARED_METHODS_NONE;
|
||||
import static android.net.ConnectivityManager.TYPE_BLUETOOTH;
|
||||
|
|
@ -2147,7 +2148,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void updateMobileDataPreferredUids() {
|
||||
public void updateMobileDataPreferredUids() {
|
||||
mHandler.sendEmptyMessage(EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED);
|
||||
}
|
||||
|
||||
|
|
@ -3403,7 +3404,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void handleBlockedReasonsChanged(List<Pair<Integer, Integer>> reasonsList) {
|
||||
public void handleBlockedReasonsChanged(List<Pair<Integer, Integer>> reasonsList) {
|
||||
for (Pair<Integer, Integer> reasons: reasonsList) {
|
||||
final int uid = reasons.first;
|
||||
final int blockedReasons = reasons.second;
|
||||
|
|
@ -3472,7 +3473,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
private void handleFrozenUids(int[] uids, int[] frozenStates) {
|
||||
ensureRunningOnConnectivityServiceThread();
|
||||
handleDestroyFrozenSockets(uids, frozenStates);
|
||||
// TODO: handle freezing NetworkCallbacks
|
||||
handleFreezeNetworkCallbacks(uids, frozenStates);
|
||||
}
|
||||
|
||||
private void handleDestroyFrozenSockets(int[] uids, int[] frozenStates) {
|
||||
|
|
@ -3490,6 +3491,73 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
}
|
||||
}
|
||||
|
||||
private void handleFreezeNetworkCallbacks(int[] uids, int[] frozenStates) {
|
||||
if (!mQueueCallbacksForFrozenApps) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < uids.length; i++) {
|
||||
final int uid = uids[i];
|
||||
// These counters may be modified on different threads, but using them here is fine
|
||||
// because this is only an optimization where wrong behavior would only happen if they
|
||||
// are zero even though there is a request registered. This is not possible as they are
|
||||
// always incremented before posting messages to register, and decremented on the
|
||||
// handler thread when unregistering.
|
||||
if (mSystemNetworkRequestCounter.get(uid) == 0
|
||||
&& mNetworkRequestCounter.get(uid) == 0) {
|
||||
// Avoid iterating requests if there isn't any. The counters only track app requests
|
||||
// and not internal requests (for example always-on requests which do not have a
|
||||
// mMessenger), so it does not completely match the content of mRequests. This is OK
|
||||
// as only app requests need to be frozen.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (frozenStates[i] == UID_FROZEN_STATE_FROZEN) {
|
||||
freezeNetworkCallbacksForUid(uid);
|
||||
} else {
|
||||
unfreezeNetworkCallbacksForUid(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend callbacks for a UID that was just frozen.
|
||||
*
|
||||
* <p>Note that it is not possible for a process to be frozen during a blocking binder call
|
||||
* (see CachedAppOptimizer.freezeBinder), and IConnectivityManager callback registrations are
|
||||
* blocking binder calls, so no callback can be registered while the UID is frozen. This means
|
||||
* it is not necessary to check frozen state on new callback registrations, and calling this
|
||||
* method when a UID is newly frozen is sufficient.
|
||||
*
|
||||
* <p>If it ever becomes possible for a process to be frozen during a blocking binder call,
|
||||
* ConnectivityService will need to handle freezing callbacks that reach ConnectivityService
|
||||
* after the app was frozen when being registered.
|
||||
*/
|
||||
private void freezeNetworkCallbacksForUid(int uid) {
|
||||
if (DDBG) Log.d(TAG, "Freezing callbacks for UID " + uid);
|
||||
for (NetworkRequestInfo nri : mNetworkRequests.values()) {
|
||||
if (nri.mUid != uid) continue;
|
||||
// mNetworkRequests can have duplicate values for multilayer requests, but calling
|
||||
// onFrozen multiple times is fine.
|
||||
// If freezeNetworkCallbacksForUid was called multiple times in a raw for a frozen UID
|
||||
// (which would be incorrect), this would also handle it gracefully.
|
||||
nri.onFrozen();
|
||||
}
|
||||
}
|
||||
|
||||
private void unfreezeNetworkCallbacksForUid(int uid) {
|
||||
// This sends all callbacks for one NetworkRequest at a time, which may not be the
|
||||
// same order they were queued in, but different network requests use different
|
||||
// binder objects, so the relative order of their callbacks is not guaranteed.
|
||||
// If callbacks are not queued, callbacks from different binder objects may be
|
||||
// posted on different threads when the process is unfrozen, so even if they were
|
||||
// called a long time apart while the process was frozen, they may still appear in
|
||||
// different order when unfreezing it.
|
||||
for (NetworkRequestInfo nri : mNetworkRequests.values()) {
|
||||
if (nri.mUid != uid) continue;
|
||||
nri.sendQueuedCallbacks();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUpdateFirewallDestroySocketReasons(
|
||||
List<Pair<Integer, Integer>> reasonsList) {
|
||||
if (!shouldTrackFirewallDestroySocketReasons()) {
|
||||
|
|
@ -7544,6 +7612,29 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
// single NetworkRequest in mRequests.
|
||||
final List<NetworkRequest> mRequests;
|
||||
|
||||
/**
|
||||
* List of callbacks that are queued for sending later when the requesting app is unfrozen.
|
||||
*
|
||||
* <p>There may typically be hundreds of NetworkRequestInfo, so a memory-efficient structure
|
||||
* (just an int[]) is used to keep queued callbacks. This reduces the number of object
|
||||
* references.
|
||||
*
|
||||
* <p>This is intended to be used with {@link CallbackQueue} which defines the internal
|
||||
* format.
|
||||
*/
|
||||
@NonNull
|
||||
private int[] mQueuedCallbacks = new int[0];
|
||||
|
||||
private static final int MATCHED_NETID_NOT_FROZEN = -1;
|
||||
|
||||
/**
|
||||
* If this request was already satisfied by a network when the requesting UID was frozen,
|
||||
* the netId that was matched at that time. Otherwise, NETID_UNSET if no network was
|
||||
* satisfying this request when frozen (including if this is a listen and not a request),
|
||||
* and MATCHED_NETID_NOT_FROZEN if not frozen.
|
||||
*/
|
||||
private int mMatchedNetIdWhenFrozen = MATCHED_NETID_NOT_FROZEN;
|
||||
|
||||
// mSatisfier and mActiveRequest rely on one another therefore set them together.
|
||||
void setSatisfier(
|
||||
@Nullable final NetworkAgentInfo satisfier,
|
||||
|
|
@ -7715,6 +7806,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
}
|
||||
setSatisfier(satisfier, activeRequest);
|
||||
}
|
||||
mMatchedNetIdWhenFrozen = nri.mMatchedNetIdWhenFrozen;
|
||||
mQueuedCallbacks = nri.mQueuedCallbacks;
|
||||
mMessenger = nri.mMessenger;
|
||||
mBinder = nri.mBinder;
|
||||
mPid = nri.mPid;
|
||||
|
|
@ -7779,11 +7872,190 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this NRI is being frozen.
|
||||
*
|
||||
* <p>Calling this method multiple times when the NRI is frozen is fine. This may happen
|
||||
* if iterating through the NetworkRequest -> NRI map since there are duplicates in the
|
||||
* NRI values for multilayer requests. It may also happen if an app is frozen, killed,
|
||||
* restarted and refrozen since there is no callback sent when processes are killed, but in
|
||||
* that case the callbacks to the killed app do not matter.
|
||||
*/
|
||||
void onFrozen() {
|
||||
if (mMatchedNetIdWhenFrozen != MATCHED_NETID_NOT_FROZEN) {
|
||||
// Already frozen
|
||||
return;
|
||||
}
|
||||
if (mSatisfier != null) {
|
||||
mMatchedNetIdWhenFrozen = mSatisfier.network.netId;
|
||||
} else {
|
||||
mMatchedNetIdWhenFrozen = NETID_UNSET;
|
||||
}
|
||||
}
|
||||
|
||||
boolean maybeQueueCallback(@NonNull NetworkAgentInfo nai, int callbackId) {
|
||||
if (mMatchedNetIdWhenFrozen == MATCHED_NETID_NOT_FROZEN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean ignoreThisCallback = false;
|
||||
final int netId = nai.network.netId;
|
||||
final CallbackQueue queue = new CallbackQueue(mQueuedCallbacks);
|
||||
// Based on the new callback, clear previous callbacks that are no longer necessary.
|
||||
// For example, if the network is lost, there is no need to send intermediate callbacks.
|
||||
switch (callbackId) {
|
||||
// PRECHECK is not an API and not very meaningful, do not deliver it for frozen apps
|
||||
// Networks are likely to already be lost when the app is unfrozen, also skip LOSING
|
||||
case CALLBACK_PRECHECK:
|
||||
case CALLBACK_LOSING:
|
||||
ignoreThisCallback = true;
|
||||
break;
|
||||
case CALLBACK_LOST:
|
||||
// All callbacks for this netId before onLost are unnecessary. And onLost itself
|
||||
// is also unnecessary if onAvailable was previously queued for this netId: the
|
||||
// Network just appeared and disappeared while the app was frozen.
|
||||
ignoreThisCallback = queue.hasCallback(netId, CALLBACK_AVAILABLE);
|
||||
queue.removeCallbacksForNetId(netId);
|
||||
break;
|
||||
case CALLBACK_AVAILABLE:
|
||||
if (mSatisfier != null) {
|
||||
// For requests that are satisfied by individual networks (not LISTEN), when
|
||||
// AVAILABLE is received, the request is matching a new Network, so previous
|
||||
// callbacks (for other Networks) are unnecessary.
|
||||
queue.clear();
|
||||
}
|
||||
break;
|
||||
case CALLBACK_SUSPENDED:
|
||||
case CALLBACK_RESUMED:
|
||||
if (queue.hasCallback(netId, CALLBACK_AVAILABLE)) {
|
||||
// AVAILABLE will already send the latest suspended status
|
||||
ignoreThisCallback = true;
|
||||
break;
|
||||
}
|
||||
// If SUSPENDED was queued, just remove it from the queue instead of sending
|
||||
// RESUMED; and vice-versa.
|
||||
final int otherCb = callbackId == CALLBACK_SUSPENDED
|
||||
? CALLBACK_RESUMED
|
||||
: CALLBACK_SUSPENDED;
|
||||
ignoreThisCallback = queue.removeCallbacks(netId, otherCb);
|
||||
break;
|
||||
case CALLBACK_CAP_CHANGED:
|
||||
case CALLBACK_IP_CHANGED:
|
||||
case CALLBACK_LOCAL_NETWORK_INFO_CHANGED:
|
||||
case CALLBACK_BLK_CHANGED:
|
||||
ignoreThisCallback = queue.hasCallback(netId, CALLBACK_AVAILABLE);
|
||||
break;
|
||||
default:
|
||||
Log.wtf(TAG, "Unexpected callback type: "
|
||||
+ ConnectivityManager.getCallbackName(callbackId));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ignoreThisCallback) {
|
||||
// For non-listen (matching) callbacks, AVAILABLE can appear in the queue twice in a
|
||||
// row for the same network if the new AVAILABLE suppressed intermediate AVAILABLEs
|
||||
// for other networks. Example:
|
||||
// A is matched, app is frozen, B is matched, A is matched again (removes callbacks
|
||||
// for B), app is unfrozen.
|
||||
// In that case call AVAILABLE sub-callbacks to update state, but not AVAILABLE
|
||||
// itself.
|
||||
if (callbackId == CALLBACK_AVAILABLE && netId == mMatchedNetIdWhenFrozen) {
|
||||
// The queue should have been cleared here, since this is AVAILABLE on a
|
||||
// non-listen callback (mMatchedNetIdWhenFrozen is set).
|
||||
addAvailableSubCallbacks(nai, queue);
|
||||
} else {
|
||||
// When unfreezing, no need to send a callback multiple times for the same netId
|
||||
queue.removeCallbacks(netId, callbackId);
|
||||
// TODO: this code always adds the callback for simplicity. It would save
|
||||
// some CPU/memory if the code instead only added to the queue callbacks where
|
||||
// isCallbackOverridden=true, or which need to be in the queue because they
|
||||
// affect other callbacks that are overridden.
|
||||
queue.addCallback(netId, callbackId);
|
||||
}
|
||||
}
|
||||
// Instead of shrinking the queue, possibly reallocating, the NRI could keep the array
|
||||
// and length in memory for future adds, but this saves memory by avoiding the cost
|
||||
// of an extra member and of unused array length (there are often hundreds of NRIs).
|
||||
mQueuedCallbacks = queue.getShrinkedBackingArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this NRI is being unfrozen to stop queueing, and send queued callbacks.
|
||||
*
|
||||
* <p>Calling this method multiple times when the NRI is unfrozen (for example iterating
|
||||
* through the NetworkRequest -> NRI map where there are duplicate values for multilayer
|
||||
* requests) is fine.
|
||||
*/
|
||||
void sendQueuedCallbacks() {
|
||||
mMatchedNetIdWhenFrozen = MATCHED_NETID_NOT_FROZEN;
|
||||
if (mQueuedCallbacks.length == 0) {
|
||||
return;
|
||||
}
|
||||
new CallbackQueue(mQueuedCallbacks).forEach((netId, callbackId) -> {
|
||||
// For CALLBACK_LOST only, there will not be a NAI for the netId. Build and send the
|
||||
// callback directly.
|
||||
if (callbackId == CALLBACK_LOST) {
|
||||
if (isCallbackOverridden(CALLBACK_LOST)) {
|
||||
final Bundle cbBundle = makeCommonBundleForCallback(this,
|
||||
new Network(netId));
|
||||
callCallbackForRequest(this, CALLBACK_LOST, cbBundle, 0 /* arg1 */);
|
||||
}
|
||||
return; // Next item in forEach
|
||||
}
|
||||
|
||||
// Other callbacks should always have a NAI, because if a Network disconnects
|
||||
// LOST will be called, unless the request is no longer satisfied by that Network in
|
||||
// which case AVAILABLE will have been called for another Network. In both cases
|
||||
// previous callbacks are cleared.
|
||||
final NetworkAgentInfo nai = getNetworkAgentInfoForNetId(netId);
|
||||
if (nai == null) {
|
||||
Log.wtf(TAG, "Missing NetworkAgentInfo for net " + netId
|
||||
+ " for callback " + callbackId);
|
||||
return; // Next item in forEach
|
||||
}
|
||||
|
||||
final int arg1 =
|
||||
callbackId == CALLBACK_AVAILABLE || callbackId == CALLBACK_BLK_CHANGED
|
||||
? getBlockedState(nai, mAsUid)
|
||||
: 0;
|
||||
callCallbackForRequest(this, nai, callbackId, arg1);
|
||||
});
|
||||
mQueuedCallbacks = new int[0];
|
||||
}
|
||||
|
||||
boolean isCallbackOverridden(int callbackId) {
|
||||
return !mUseDeclaredMethodsForCallbacksEnabled
|
||||
|| (mDeclaredMethodsFlags & (1 << callbackId)) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue all callbacks that are called by AVAILABLE, except onAvailable.
|
||||
*
|
||||
* <p>AVAILABLE may call SUSPENDED, CAP_CHANGED, IP_CHANGED, LOCAL_NETWORK_INFO_CHANGED,
|
||||
* and BLK_CHANGED, in this order.
|
||||
*/
|
||||
private void addAvailableSubCallbacks(
|
||||
@NonNull NetworkAgentInfo nai, @NonNull CallbackQueue queue) {
|
||||
final boolean callSuspended =
|
||||
!nai.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_SUSPENDED);
|
||||
final boolean callLocalInfoChanged = nai.isLocalNetwork();
|
||||
|
||||
final int cbCount = 3 + (callSuspended ? 1 : 0) + (callLocalInfoChanged ? 1 : 0);
|
||||
// Avoid unnecessary re-allocations by reserving enough space for all callbacks to add.
|
||||
queue.ensureHasCapacity(cbCount);
|
||||
final int netId = nai.network.netId;
|
||||
if (callSuspended) {
|
||||
queue.addCallback(netId, CALLBACK_SUSPENDED);
|
||||
}
|
||||
queue.addCallback(netId, CALLBACK_CAP_CHANGED);
|
||||
queue.addCallback(netId, CALLBACK_IP_CHANGED);
|
||||
if (callLocalInfoChanged) {
|
||||
queue.addCallback(netId, CALLBACK_LOCAL_NETWORK_INFO_CHANGED);
|
||||
}
|
||||
queue.addCallback(netId, CALLBACK_BLK_CHANGED);
|
||||
}
|
||||
|
||||
boolean hasHigherOrderThan(@NonNull final NetworkRequestInfo target) {
|
||||
// Compare two preference orders.
|
||||
return mPreferenceOrder < target.mPreferenceOrder;
|
||||
|
|
@ -10277,6 +10549,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
// are Type.LISTEN, but should not have NetworkCallbacks invoked.
|
||||
return;
|
||||
}
|
||||
// Even if a callback ends up not being sent, it may affect other callbacks in the queue, so
|
||||
// queue callbacks before checking the declared methods flags.
|
||||
if (networkAgent != null && nri.maybeQueueCallback(networkAgent, notificationType)) {
|
||||
return;
|
||||
}
|
||||
if (!nri.isCallbackOverridden(notificationType)) {
|
||||
// No need to send the notification as the recipient method is not overridden
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ public class PerUidCounter {
|
|||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/**
|
||||
* Get the current counter value for the given uid.
|
||||
*/
|
||||
public synchronized int get(int uid) {
|
||||
return mUidToCount.get(uid, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,636 @@
|
|||
/*
|
||||
* Copyright (C) 2024 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.connectivityservice
|
||||
|
||||
import android.app.ActivityManager.UidFrozenStateChangedCallback
|
||||
import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN
|
||||
import android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN
|
||||
import android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER
|
||||
import android.net.ConnectivityManager.BLOCKED_REASON_NONE
|
||||
import android.net.ConnectivitySettingsManager
|
||||
import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
|
||||
import android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS
|
||||
import android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID
|
||||
import android.net.LinkAddress
|
||||
import android.net.LinkProperties
|
||||
import android.net.LocalNetworkConfig
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_FOREGROUND
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED
|
||||
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
|
||||
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
|
||||
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
|
||||
import android.net.NetworkCapabilities.TRANSPORT_WIFI
|
||||
import android.net.NetworkPolicyManager.NetworkPolicyCallback
|
||||
import android.net.NetworkRequest
|
||||
import android.os.Build
|
||||
import android.os.Process
|
||||
import com.android.server.CALLING_UID_UNMOCKED
|
||||
import com.android.server.CSAgentWrapper
|
||||
import com.android.server.CSTest
|
||||
import com.android.server.FromS
|
||||
import com.android.server.HANDLER_TIMEOUT_MS
|
||||
import com.android.server.connectivity.ConnectivityFlags.QUEUE_CALLBACKS_FOR_FROZEN_APPS
|
||||
import com.android.server.defaultLp
|
||||
import com.android.server.defaultNc
|
||||
import com.android.testutils.DevSdkIgnoreRule
|
||||
import com.android.testutils.DevSdkIgnoreRunner
|
||||
import com.android.testutils.RecorderCallback.CallbackEntry.BlockedStatus
|
||||
import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
|
||||
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
|
||||
import com.android.testutils.RecorderCallback.CallbackEntry.LocalInfoChanged
|
||||
import com.android.testutils.RecorderCallback.CallbackEntry.Lost
|
||||
import com.android.testutils.RecorderCallback.CallbackEntry.Resumed
|
||||
import com.android.testutils.RecorderCallback.CallbackEntry.Suspended
|
||||
import com.android.testutils.TestableNetworkCallback
|
||||
import com.android.testutils.visibleOnHandlerThread
|
||||
import com.android.testutils.waitForIdleSerialExecutor
|
||||
import java.util.Collections
|
||||
import kotlin.test.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.verify
|
||||
|
||||
private const val TEST_UID = 42
|
||||
|
||||
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
|
||||
@DevSdkIgnoreRunner.MonitorThreadLeak
|
||||
@RunWith(DevSdkIgnoreRunner::class)
|
||||
class CSQueuedCallbacksTest(freezingBehavior: FreezingBehavior) : CSTest() {
|
||||
companion object {
|
||||
enum class FreezingBehavior {
|
||||
UID_FROZEN,
|
||||
UID_NOT_FROZEN,
|
||||
UID_FROZEN_FEATURE_DISABLED
|
||||
}
|
||||
|
||||
// Use a parameterized test with / without freezing to make it easy to compare and make sure
|
||||
// freezing behavior (which callbacks are sent in which order) stays close to what happens
|
||||
// without freezing.
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "freezingBehavior={0}")
|
||||
fun freezingBehavior() = listOf(
|
||||
FreezingBehavior.UID_FROZEN,
|
||||
FreezingBehavior.UID_NOT_FROZEN,
|
||||
FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
|
||||
)
|
||||
|
||||
private val TAG = CSQueuedCallbacksTest::class.simpleName
|
||||
?: fail("Could not get test class name")
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val ignoreRule = DevSdkIgnoreRule()
|
||||
|
||||
private val mockedBlockedReasonsPerUid = Collections.synchronizedMap(mutableMapOf(
|
||||
Process.myUid() to BLOCKED_REASON_NONE,
|
||||
TEST_UID to BLOCKED_REASON_NONE
|
||||
))
|
||||
|
||||
private val freezeUids = freezingBehavior != FreezingBehavior.UID_NOT_FROZEN
|
||||
private val expectAllCallbacks = freezingBehavior == FreezingBehavior.UID_NOT_FROZEN ||
|
||||
freezingBehavior == FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
|
||||
init {
|
||||
setFeatureEnabled(
|
||||
QUEUE_CALLBACKS_FOR_FROZEN_APPS,
|
||||
freezingBehavior != FreezingBehavior.UID_FROZEN_FEATURE_DISABLED
|
||||
)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun subclassSetUp() {
|
||||
// Ensure cellular stays up. CS is recreated for each test so no cleanup is necessary.
|
||||
// cm.requestNetwork(
|
||||
// NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build(),
|
||||
// TestableNetworkCallback()
|
||||
// )
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFrozenWhileNetworkConnects_UpdatesAreReceived() {
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
|
||||
}
|
||||
val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
|
||||
val lpChangeOnConnect = agent.sendLpChange { setLinkAddresses("fe80:db8::123/64") }
|
||||
val ncChangeOnConnect = agent.sendNcChange {
|
||||
addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
|
||||
}
|
||||
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
|
||||
val lpChange1WhileFrozen = agent.sendLpChange {
|
||||
setLinkAddresses("fe80:db8::126/64")
|
||||
}
|
||||
val ncChange1WhileFrozen = agent.sendNcChange {
|
||||
removeCapability(NET_CAPABILITY_NOT_ROAMING)
|
||||
}
|
||||
val ncChange2WhileFrozen = agent.sendNcChange {
|
||||
addCapability(NET_CAPABILITY_NOT_ROAMING)
|
||||
addCapability(NET_CAPABILITY_NOT_CONGESTED)
|
||||
}
|
||||
val lpChange2WhileFrozen = agent.sendLpChange {
|
||||
setLinkAddresses("fe80:db8::125/64")
|
||||
}
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
// Verify callbacks that are sent before freezing
|
||||
cb.expectAvailableCallbacks(agent.network, validated = false)
|
||||
cb.expectLpWith(agent, lpChangeOnConnect)
|
||||
cb.expectNcWith(agent, ncChangeOnConnect)
|
||||
|
||||
// Below callbacks should be skipped if the processes were frozen, since a single callback
|
||||
// will be sent with the latest state after unfreezing
|
||||
if (expectAllCallbacks) {
|
||||
cb.expectLpWith(agent, lpChange1WhileFrozen)
|
||||
cb.expectNcWith(agent, ncChange1WhileFrozen)
|
||||
}
|
||||
|
||||
cb.expectNcWith(agent, ncChange2WhileFrozen)
|
||||
cb.expectLpWith(agent, lpChange2WhileFrozen)
|
||||
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFrozenWhileNetworkConnects_SuspendedUnsuspendedWhileFrozen() {
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
|
||||
}
|
||||
|
||||
val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
val rmCap = agent.sendNcChange { removeCapability(NET_CAPABILITY_NOT_SUSPENDED) }
|
||||
val addCap = agent.sendNcChange { addCapability(NET_CAPABILITY_NOT_SUSPENDED) }
|
||||
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
cb.expectAvailableCallbacks(agent.network, validated = false)
|
||||
if (expectAllCallbacks) {
|
||||
cb.expectNcWith(agent, rmCap)
|
||||
cb.expect<Suspended>(agent)
|
||||
cb.expectNcWith(agent, addCap)
|
||||
cb.expect<Resumed>(agent)
|
||||
} else {
|
||||
// When frozen, a single NetworkCapabilitiesChange will be sent at unfreezing time,
|
||||
// with nc actually identical to the original ones. This is because NetworkCapabilities
|
||||
// callbacks were sent, but CS does not keep initial NetworkCapabilities in memory, so
|
||||
// it cannot detect A->B->A.
|
||||
cb.expect<CapabilitiesChanged>(agent) {
|
||||
it.caps.hasCapability(NET_CAPABILITY_NOT_SUSPENDED)
|
||||
}
|
||||
}
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFrozenWhileNetworkConnects_UnsuspendedWhileFrozen_GetResumedCallbackWhenUnfrozen() {
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
|
||||
}
|
||||
|
||||
val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
|
||||
val rmCap = agent.sendNcChange { removeCapability(NET_CAPABILITY_NOT_SUSPENDED) }
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
val addCap = agent.sendNcChange { addCapability(NET_CAPABILITY_NOT_SUSPENDED) }
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
cb.expectAvailableCallbacks(agent.network, validated = false)
|
||||
cb.expectNcWith(agent, rmCap)
|
||||
cb.expect<Suspended>(agent)
|
||||
cb.expectNcWith(agent, addCap)
|
||||
cb.expect<Resumed>(agent)
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFrozenWhileNetworkConnects_BlockedUnblockedWhileFrozen_SingleCallbackIfFrozen() {
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
|
||||
}
|
||||
val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
|
||||
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
setUidsBlockedForDataSaver(true, TEST_UID)
|
||||
setUidsBlockedForDataSaver(false, TEST_UID)
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
cb.expectAvailableCallbacks(agent.network, validated = false)
|
||||
if (expectAllCallbacks) {
|
||||
cb.expect<BlockedStatus>(agent) { it.blocked }
|
||||
}
|
||||
// The unblocked callback is sent in any case (with the latest blocked reason), as the
|
||||
// blocked reason may have changed, and ConnectivityService cannot know that it is the same
|
||||
// as the original reason as it does not keep pre-freeze blocked reasons in memory.
|
||||
cb.expect<BlockedStatus>(agent) { !it.blocked }
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFrozenWhileNetworkConnects_BlockedWhileFrozen_GetLastBlockedCallbackOnlyIfFrozen() {
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
|
||||
}
|
||||
val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
|
||||
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
setUidsBlockedForDataSaver(true, TEST_UID)
|
||||
setUidsBlockedForDataSaver(false, TEST_UID)
|
||||
setUidsBlockedForDataSaver(true, TEST_UID)
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
cb.expectAvailableCallbacks(agent.network, validated = false)
|
||||
if (expectAllCallbacks) {
|
||||
cb.expect<BlockedStatus>(agent) { it.blocked }
|
||||
cb.expect<BlockedStatus>(agent) { !it.blocked }
|
||||
}
|
||||
cb.expect<BlockedStatus>(agent) { it.blocked }
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNetworkCallback_NetworkToggledWhileFrozen_NotSeen() {
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
|
||||
}
|
||||
val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
|
||||
wifiAgent.disconnect()
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
cb.expectAvailableCallbacks(cellAgent.network, validated = false)
|
||||
if (expectAllCallbacks) {
|
||||
cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
|
||||
cb.expect<Lost>(wifiAgent)
|
||||
}
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNetworkCallback_NetworkAppearedWhileFrozen_ReceiveLatestInfoInOnAvailable() {
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.registerNetworkCallback(NetworkRequest.Builder().build(), cb)
|
||||
}
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
val agent = Agent(TRANSPORT_CELLULAR).apply { connect() }
|
||||
waitForIdle()
|
||||
agent.makeValidationSuccess()
|
||||
val lpChange = agent.sendLpChange {
|
||||
setLinkAddresses("fe80:db8::123/64")
|
||||
}
|
||||
val suspendedChange = agent.sendNcChange {
|
||||
removeCapability(NET_CAPABILITY_NOT_SUSPENDED)
|
||||
}
|
||||
setUidsBlockedForDataSaver(true, TEST_UID)
|
||||
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
val expectLatestStatusInOnAvailable = !expectAllCallbacks
|
||||
cb.expectAvailableCallbacks(
|
||||
agent.network,
|
||||
suspended = expectLatestStatusInOnAvailable,
|
||||
validated = expectLatestStatusInOnAvailable,
|
||||
blocked = expectLatestStatusInOnAvailable
|
||||
)
|
||||
if (expectAllCallbacks) {
|
||||
cb.expectNcWith(agent) { addCapability(NET_CAPABILITY_VALIDATED) }
|
||||
cb.expectLpWith(agent, lpChange)
|
||||
cb.expectNcWith(agent, suspendedChange)
|
||||
cb.expect<Suspended>(agent)
|
||||
cb.expect<BlockedStatus>(agent) { it.blocked }
|
||||
}
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
fun testNetworkCallback_LocalNetworkAppearedWhileFrozen_ReceiveLatestInfoInOnAvailable() {
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.registerNetworkCallback(
|
||||
NetworkRequest.Builder().addCapability(NET_CAPABILITY_LOCAL_NETWORK).build(),
|
||||
cb
|
||||
)
|
||||
}
|
||||
val upstreamAgent = Agent(
|
||||
nc = defaultNc()
|
||||
.addTransportType(TRANSPORT_WIFI)
|
||||
.addCapability(NET_CAPABILITY_INTERNET),
|
||||
lp = defaultLp().apply { interfaceName = "wlan0" }
|
||||
).apply { connect() }
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
|
||||
val lnc = LocalNetworkConfig.Builder().build()
|
||||
val localAgent = Agent(
|
||||
nc = defaultNc()
|
||||
.addCapability(NET_CAPABILITY_LOCAL_NETWORK)
|
||||
.removeCapability(NET_CAPABILITY_INTERNET),
|
||||
lp = defaultLp().apply { interfaceName = "local42" },
|
||||
lnc = FromS(lnc)
|
||||
).apply { connect() }
|
||||
localAgent.sendLocalNetworkConfig(
|
||||
LocalNetworkConfig.Builder()
|
||||
.setUpstreamSelector(
|
||||
NetworkRequest.Builder()
|
||||
.addCapability(NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
cb.expectAvailableCallbacks(
|
||||
localAgent.network,
|
||||
validated = false,
|
||||
upstream = if (expectAllCallbacks) null else upstreamAgent.network
|
||||
)
|
||||
if (expectAllCallbacks) {
|
||||
cb.expect<LocalInfoChanged>(localAgent) {
|
||||
it.info.upstreamNetwork == upstreamAgent.network
|
||||
}
|
||||
}
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNetworkRequest_NetworkSwitchesWhileFrozen_ReceiveLastNetworkUpdatesOnly() {
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.requestNetwork(NetworkRequest.Builder().build(), cb)
|
||||
}
|
||||
val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
|
||||
val ethAgent = Agent(TRANSPORT_ETHERNET).apply { connect() }
|
||||
waitForIdle()
|
||||
ethAgent.makeValidationSuccess()
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
cb.expectAvailableCallbacks(cellAgent.network, validated = false)
|
||||
if (expectAllCallbacks) {
|
||||
cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
|
||||
cb.expectAvailableCallbacks(ethAgent.network, validated = false)
|
||||
cb.expectNcWith(ethAgent) { addCapability(NET_CAPABILITY_VALIDATED) }
|
||||
} else {
|
||||
cb.expectAvailableCallbacks(ethAgent.network, validated = true)
|
||||
}
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNetworkRequest_NetworkSwitchesBackWhileFrozen_ReceiveNoAvailableCallback() {
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.requestNetwork(NetworkRequest.Builder().build(), cb)
|
||||
}
|
||||
val cellAgent = Agent(TRANSPORT_CELLULAR).apply { connect() }
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
val wifiAgent = Agent(TRANSPORT_WIFI).apply { connect() }
|
||||
waitForIdle()
|
||||
|
||||
// CS switches back to validated cell over non-validated Wi-Fi
|
||||
cellAgent.makeValidationSuccess()
|
||||
val cellLpChange = cellAgent.sendLpChange {
|
||||
setLinkAddresses("fe80:db8::123/64")
|
||||
}
|
||||
setUidsBlockedForDataSaver(true, TEST_UID)
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
cb.expectAvailableCallbacks(cellAgent.network, validated = false)
|
||||
if (expectAllCallbacks) {
|
||||
cb.expectAvailableCallbacks(wifiAgent.network, validated = false)
|
||||
// There is an extra "double validated" CapabilitiesChange callback (b/245893397), so
|
||||
// callbacks are (AVAIL, NC, LP), extra NC, then further updates (LP and BLK here).
|
||||
cb.expectAvailableDoubleValidatedCallbacks(cellAgent.network)
|
||||
cb.expectLpWith(cellAgent, cellLpChange)
|
||||
cb.expect<BlockedStatus>(cellAgent) { it.blocked }
|
||||
} else {
|
||||
cb.expectNcWith(cellAgent) {
|
||||
addCapability(NET_CAPABILITY_VALIDATED)
|
||||
}
|
||||
cb.expectLpWith(cellAgent, cellLpChange)
|
||||
cb.expect<BlockedStatus>(cellAgent) { it.blocked }
|
||||
}
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTrackDefaultRequest_AppFrozenWhilePerAppDefaultRequestFiled_ReceiveChangeCallbacks() {
|
||||
val cellAgent = Agent(TRANSPORT_CELLULAR, baseNc = makeInternetNc()).apply { connect() }
|
||||
waitForIdle()
|
||||
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.registerDefaultNetworkCallback(cb)
|
||||
}
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
|
||||
// Change LinkProperties twice before the per-app network request is applied
|
||||
val lpChange1 = cellAgent.sendLpChange {
|
||||
setLinkAddresses("fe80:db8::123/64")
|
||||
}
|
||||
val lpChange2 = cellAgent.sendLpChange {
|
||||
setLinkAddresses("fe80:db8::124/64")
|
||||
}
|
||||
setMobileDataPreferredUids(setOf(TEST_UID))
|
||||
|
||||
// Change NetworkCapabilities after the per-app network request is applied
|
||||
val ncChange = cellAgent.sendNcChange {
|
||||
addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)
|
||||
}
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
// Even if a per-app network request was filed to replace the default network request for
|
||||
// the app, all network change callbacks are received
|
||||
cb.expectAvailableCallbacks(cellAgent.network, validated = false)
|
||||
if (expectAllCallbacks) {
|
||||
cb.expectLpWith(cellAgent, lpChange1)
|
||||
}
|
||||
cb.expectLpWith(cellAgent, lpChange2)
|
||||
cb.expectNcWith(cellAgent, ncChange)
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTrackDefaultRequest_AppFrozenWhilePerAppDefaultToggled_GetStatusUpdateCallbacksOnly() {
|
||||
// Add validated Wi-Fi and non-validated cell, expect Wi-Fi is preferred by default
|
||||
val wifiAgent = Agent(TRANSPORT_WIFI, baseNc = makeInternetNc()).apply { connect() }
|
||||
wifiAgent.makeValidationSuccess()
|
||||
val cellAgent = Agent(TRANSPORT_CELLULAR, baseNc = makeInternetNc()).apply { connect() }
|
||||
waitForIdle()
|
||||
|
||||
val cb = TestableNetworkCallback(logTag = TAG)
|
||||
withCallingUid(TEST_UID) {
|
||||
cm.registerDefaultNetworkCallback(cb)
|
||||
}
|
||||
maybeSetUidsFrozen(true, TEST_UID)
|
||||
|
||||
// LP change on the original Wi-Fi network
|
||||
val lpChange = wifiAgent.sendLpChange {
|
||||
setLinkAddresses("fe80:db8::123/64")
|
||||
}
|
||||
// Set per-app default to cell, then unset it
|
||||
setMobileDataPreferredUids(setOf(TEST_UID))
|
||||
setMobileDataPreferredUids(emptySet())
|
||||
|
||||
maybeSetUidsFrozen(false, TEST_UID)
|
||||
|
||||
cb.expectAvailableCallbacks(wifiAgent.network)
|
||||
if (expectAllCallbacks) {
|
||||
cb.expectLpWith(wifiAgent, lpChange)
|
||||
cb.expectAvailableCallbacks(cellAgent.network, validated = false)
|
||||
// Cellular stops being foreground since it is now matched for this app
|
||||
cb.expect<CapabilitiesChanged> { it.caps.hasCapability(NET_CAPABILITY_FOREGROUND) }
|
||||
cb.expectAvailableCallbacks(wifiAgent.network)
|
||||
} else {
|
||||
// After switching to cell and back while frozen, only network attribute update
|
||||
// callbacks (and not AVAILABLE) for the original Wi-Fi network should be sent
|
||||
cb.expect<CapabilitiesChanged>(wifiAgent)
|
||||
cb.expectLpWith(wifiAgent, lpChange)
|
||||
cb.expect<BlockedStatus> { !it.blocked }
|
||||
}
|
||||
cb.assertNoCallback(timeoutMs = 0L)
|
||||
}
|
||||
|
||||
private fun setUidsBlockedForDataSaver(blocked: Boolean, vararg uid: Int) {
|
||||
val reason = if (blocked) {
|
||||
BLOCKED_METERED_REASON_DATA_SAVER
|
||||
} else {
|
||||
BLOCKED_REASON_NONE
|
||||
}
|
||||
if (deps.isAtLeastV) {
|
||||
visibleOnHandlerThread(csHandler) {
|
||||
service.handleBlockedReasonsChanged(uid.map { android.util.Pair(it, reason) })
|
||||
}
|
||||
} else {
|
||||
notifyLegacyBlockedReasonChanged(reason, uid)
|
||||
waitForIdle()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun notifyLegacyBlockedReasonChanged(reason: Int, uids: IntArray) {
|
||||
val cbCaptor = ArgumentCaptor.forClass(NetworkPolicyCallback::class.java)
|
||||
verify(context.networkPolicyManager).registerNetworkPolicyCallback(
|
||||
any(),
|
||||
cbCaptor.capture()
|
||||
)
|
||||
uids.forEach {
|
||||
cbCaptor.value.onUidBlockedReasonChanged(it, reason)
|
||||
}
|
||||
}
|
||||
|
||||
private fun withCallingUid(uid: Int, action: () -> Unit) {
|
||||
deps.callingUid = uid
|
||||
action()
|
||||
deps.callingUid = CALLING_UID_UNMOCKED
|
||||
}
|
||||
|
||||
private fun getUidFrozenStateChangedCallback(): UidFrozenStateChangedCallback {
|
||||
val captor = ArgumentCaptor.forClass(UidFrozenStateChangedCallback::class.java)
|
||||
verify(activityManager).registerUidFrozenStateChangedCallback(any(), captor.capture())
|
||||
return captor.value
|
||||
}
|
||||
|
||||
private fun maybeSetUidsFrozen(frozen: Boolean, vararg uids: Int) {
|
||||
if (!freezeUids) return
|
||||
val state = if (frozen) UID_FROZEN_STATE_FROZEN else UID_FROZEN_STATE_UNFROZEN
|
||||
getUidFrozenStateChangedCallback()
|
||||
.onUidFrozenStateChanged(uids, IntArray(uids.size) { state })
|
||||
waitForIdle()
|
||||
}
|
||||
|
||||
private fun CSAgentWrapper.makeValidationSuccess() {
|
||||
setValidationResult(
|
||||
NETWORK_VALIDATION_RESULT_VALID,
|
||||
probesCompleted = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTPS,
|
||||
probesSucceeded = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTPS
|
||||
)
|
||||
cm.reportNetworkConnectivity(network, true)
|
||||
// Ensure validation is scheduled
|
||||
waitForIdle()
|
||||
// Ensure validation completes on mock executor
|
||||
waitForIdleSerialExecutor(CSTestExecutor, HANDLER_TIMEOUT_MS)
|
||||
// Ensure validation results are processed
|
||||
waitForIdle()
|
||||
}
|
||||
|
||||
private fun setMobileDataPreferredUids(uids: Set<Int>) {
|
||||
ConnectivitySettingsManager.setMobileDataPreferredUids(context, uids)
|
||||
service.updateMobileDataPreferredUids()
|
||||
waitForIdle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeInternetNc() = NetworkCapabilities.Builder(defaultNc())
|
||||
.addCapability(NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
|
||||
private fun CSAgentWrapper.sendLpChange(
|
||||
mutator: LinkProperties.() -> Unit
|
||||
): LinkProperties.() -> Unit {
|
||||
lp.mutator()
|
||||
sendLinkProperties(lp)
|
||||
return mutator
|
||||
}
|
||||
|
||||
private fun CSAgentWrapper.sendNcChange(
|
||||
mutator: NetworkCapabilities.() -> Unit
|
||||
): NetworkCapabilities.() -> Unit {
|
||||
nc.mutator()
|
||||
sendNetworkCapabilities(nc)
|
||||
return mutator
|
||||
}
|
||||
|
||||
private fun TestableNetworkCallback.expectLpWith(
|
||||
agent: CSAgentWrapper,
|
||||
change: LinkProperties.() -> Unit
|
||||
) = expect<LinkPropertiesChanged>(agent) {
|
||||
// This test uses changes that are no-op when already applied (idempotent): verify that the
|
||||
// change is already applied.
|
||||
it.lp == LinkProperties(it.lp).apply(change)
|
||||
}
|
||||
|
||||
private fun TestableNetworkCallback.expectNcWith(
|
||||
agent: CSAgentWrapper,
|
||||
change: NetworkCapabilities.() -> Unit
|
||||
) = expect<CapabilitiesChanged>(agent) {
|
||||
it.caps == NetworkCapabilities(it.caps).apply(change)
|
||||
}
|
||||
|
||||
private fun LinkProperties.setLinkAddresses(vararg addrs: String) {
|
||||
setLinkAddresses(addrs.map { LinkAddress(it) })
|
||||
}
|
||||
|
|
@ -192,7 +192,8 @@ class CSAgentWrapper(
|
|||
connect()
|
||||
}
|
||||
|
||||
fun setProbesStatus(probesCompleted: Int, probesSucceeded: Int) {
|
||||
fun setValidationResult(result: Int, probesCompleted: Int, probesSucceeded: Int) {
|
||||
nmValidationResult = result
|
||||
nmProbesCompleted = probesCompleted
|
||||
nmProbesSucceeded = probesSucceeded
|
||||
}
|
||||
|
|
@ -204,8 +205,10 @@ class CSAgentWrapper(
|
|||
// in the beginning. Because NETWORK_VALIDATION_PROBE_HTTP is the decisive probe for captive
|
||||
// portal, considering the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet and set only
|
||||
// DNS and HTTP probes completed.
|
||||
setProbesStatus(
|
||||
NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP /* probesCompleted */,
|
||||
VALIDATION_RESULT_INVALID /* probesSucceeded */)
|
||||
setValidationResult(
|
||||
VALIDATION_RESULT_INVALID,
|
||||
probesCompleted = NETWORK_VALIDATION_PROBE_DNS or NETWORK_VALIDATION_PROBE_HTTP,
|
||||
probesSucceeded = NO_PROBE_RESULT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -538,8 +538,12 @@ open class CSTest {
|
|||
provider: NetworkProvider? = null
|
||||
) = CSAgentWrapper(context, deps, csHandlerThread, networkStack,
|
||||
nac, nc, lp, lnc, score, provider)
|
||||
fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper {
|
||||
val nc = NetworkCapabilities.Builder().apply {
|
||||
fun Agent(
|
||||
vararg transports: Int,
|
||||
baseNc: NetworkCapabilities = defaultNc(),
|
||||
lp: LinkProperties = defaultLp()
|
||||
): CSAgentWrapper {
|
||||
val nc = NetworkCapabilities.Builder(baseNc).apply {
|
||||
transports.forEach {
|
||||
addTransportType(it)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue