blob: 5b59e607cba7c560aa3e3073f2de7f8513c747d8 [file] [log] [blame]
/*
* Copyright (C) 2018 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.job.controllers;
import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
import android.app.job.JobInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
import android.net.NetworkPolicyManager;
import android.os.Build;
import android.os.SystemClock;
import android.util.DataUnit;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobSchedulerService.Constants;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.Clock;
import java.time.ZoneOffset;
@RunWith(MockitoJUnitRunner.class)
public class ConnectivityControllerTest {
@Mock private Context mContext;
@Mock private ConnectivityManager mConnManager;
@Mock private NetworkPolicyManager mNetPolicyManager;
@Mock private JobSchedulerService mService;
private Constants mConstants;
private static final int UID_RED = 10001;
private static final int UID_BLUE = 10002;
@Before
public void setUp() throws Exception {
// Assume all packages are current SDK
final PackageManagerInternal pm = mock(PackageManagerInternal.class);
when(pm.getPackageTargetSdkVersion(anyString()))
.thenReturn(Build.VERSION_CODES.CUR_DEVELOPMENT);
LocalServices.removeServiceForTest(PackageManagerInternal.class);
LocalServices.addService(PackageManagerInternal.class, pm);
// Freeze the clocks at this moment in time
JobSchedulerService.sSystemClock =
Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC);
JobSchedulerService.sUptimeMillisClock =
Clock.fixed(SystemClock.uptimeMillisClock().instant(), ZoneOffset.UTC);
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
// Assume default constants for now
mConstants = new Constants();
// Get our mocks ready
when(mContext.getSystemServiceName(ConnectivityManager.class))
.thenReturn(Context.CONNECTIVITY_SERVICE);
when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE))
.thenReturn(mConnManager);
when(mContext.getSystemServiceName(NetworkPolicyManager.class))
.thenReturn(Context.NETWORK_POLICY_SERVICE);
when(mContext.getSystemService(Context.NETWORK_POLICY_SERVICE))
.thenReturn(mNetPolicyManager);
when(mService.getTestableContext()).thenReturn(mContext);
when(mService.getLock()).thenReturn(mService);
when(mService.getConstants()).thenReturn(mConstants);
}
@Test
public void testInsane() throws Exception {
final Network net = new Network(101);
final JobInfo.Builder job = createJob()
.setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
// Slow network is too slow
assertFalse(ConnectivityController.isSatisfied(createJobStatus(job), net,
createCapabilities().setLinkUpstreamBandwidthKbps(1)
.setLinkDownstreamBandwidthKbps(1), mConstants));
// Fast network looks great
assertTrue(ConnectivityController.isSatisfied(createJobStatus(job), net,
createCapabilities().setLinkUpstreamBandwidthKbps(1024)
.setLinkDownstreamBandwidthKbps(1024), mConstants));
}
@Test
public void testCongestion() throws Exception {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
final JobInfo.Builder job = createJob()
.setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
final JobStatus early = createJobStatus(job, now - 1000, now + 2000);
final JobStatus late = createJobStatus(job, now - 2000, now + 1000);
// Uncongested network is whenever
{
final Network net = new Network(101);
final NetworkCapabilities caps = createCapabilities()
.addCapability(NET_CAPABILITY_NOT_CONGESTED);
assertTrue(ConnectivityController.isSatisfied(early, net, caps, mConstants));
assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants));
}
// Congested network is more selective
{
final Network net = new Network(101);
final NetworkCapabilities caps = createCapabilities();
assertFalse(ConnectivityController.isSatisfied(early, net, caps, mConstants));
assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants));
}
}
@Test
public void testRelaxed() throws Exception {
final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
final JobInfo.Builder job = createJob()
.setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
final JobStatus early = createJobStatus(job, now - 1000, now + 2000);
final JobStatus late = createJobStatus(job, now - 2000, now + 1000);
job.setIsPrefetch(true);
final JobStatus earlyPrefetch = createJobStatus(job, now - 1000, now + 2000);
final JobStatus latePrefetch = createJobStatus(job, now - 2000, now + 1000);
// Unmetered network is whenever
{
final Network net = new Network(101);
final NetworkCapabilities caps = createCapabilities()
.addCapability(NET_CAPABILITY_NOT_CONGESTED)
.addCapability(NET_CAPABILITY_NOT_METERED);
assertTrue(ConnectivityController.isSatisfied(early, net, caps, mConstants));
assertTrue(ConnectivityController.isSatisfied(late, net, caps, mConstants));
assertTrue(ConnectivityController.isSatisfied(earlyPrefetch, net, caps, mConstants));
assertTrue(ConnectivityController.isSatisfied(latePrefetch, net, caps, mConstants));
}
// Metered network is only when prefetching and late
{
final Network net = new Network(101);
final NetworkCapabilities caps = createCapabilities()
.addCapability(NET_CAPABILITY_NOT_CONGESTED);
assertFalse(ConnectivityController.isSatisfied(early, net, caps, mConstants));
assertFalse(ConnectivityController.isSatisfied(late, net, caps, mConstants));
assertFalse(ConnectivityController.isSatisfied(earlyPrefetch, net, caps, mConstants));
assertTrue(ConnectivityController.isSatisfied(latePrefetch, net, caps, mConstants));
}
}
@Test
public void testUpdates() throws Exception {
final ArgumentCaptor<NetworkCallback> callback = ArgumentCaptor
.forClass(NetworkCallback.class);
doNothing().when(mConnManager).registerNetworkCallback(any(), callback.capture());
final ConnectivityController controller = new ConnectivityController(mService);
final Network meteredNet = new Network(101);
final NetworkCapabilities meteredCaps = createCapabilities();
final Network unmeteredNet = new Network(202);
final NetworkCapabilities unmeteredCaps = createCapabilities()
.addCapability(NET_CAPABILITY_NOT_METERED);
final JobStatus red = createJobStatus(createJob()
.setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
final JobStatus blue = createJobStatus(createJob()
.setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE);
// Pretend we're offline when job is added
{
reset(mConnManager);
answerNetwork(UID_RED, null, null);
answerNetwork(UID_BLUE, null, null);
controller.maybeStartTrackingJobLocked(red, null);
controller.maybeStartTrackingJobLocked(blue, null);
assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
assertFalse(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
}
// Metered network
{
reset(mConnManager);
answerNetwork(UID_RED, meteredNet, meteredCaps);
answerNetwork(UID_BLUE, meteredNet, meteredCaps);
callback.getValue().onCapabilitiesChanged(meteredNet, meteredCaps);
assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
}
// Unmetered network background
{
reset(mConnManager);
answerNetwork(UID_RED, meteredNet, meteredCaps);
answerNetwork(UID_BLUE, meteredNet, meteredCaps);
callback.getValue().onCapabilitiesChanged(unmeteredNet, unmeteredCaps);
assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
}
// Lost metered network
{
reset(mConnManager);
answerNetwork(UID_RED, unmeteredNet, unmeteredCaps);
answerNetwork(UID_BLUE, unmeteredNet, unmeteredCaps);
callback.getValue().onLost(meteredNet);
assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
}
// Specific UID was blocked
{
reset(mConnManager);
answerNetwork(UID_RED, null, null);
answerNetwork(UID_BLUE, unmeteredNet, unmeteredCaps);
callback.getValue().onCapabilitiesChanged(unmeteredNet, unmeteredCaps);
assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
}
}
private void answerNetwork(int uid, Network net, NetworkCapabilities caps) {
when(mConnManager.getActiveNetworkForUid(eq(uid))).thenReturn(net);
when(mConnManager.getNetworkCapabilities(eq(net))).thenReturn(caps);
if (net != null) {
final NetworkInfo ni = new NetworkInfo(ConnectivityManager.TYPE_WIFI, 0, null, null);
ni.setDetailedState(DetailedState.CONNECTED, null, null);
when(mConnManager.getNetworkInfoForUid(eq(net), eq(uid), anyBoolean())).thenReturn(ni);
}
}
private static NetworkCapabilities createCapabilities() {
return new NetworkCapabilities().addCapability(NET_CAPABILITY_INTERNET)
.addCapability(NET_CAPABILITY_VALIDATED);
}
private static JobInfo.Builder createJob() {
return new JobInfo.Builder(101, new ComponentName("foo", "bar"));
}
private static JobStatus createJobStatus(JobInfo.Builder job) {
return createJobStatus(job, android.os.Process.NOBODY_UID, 0, Long.MAX_VALUE);
}
private static JobStatus createJobStatus(JobInfo.Builder job, int uid) {
return createJobStatus(job, uid, 0, Long.MAX_VALUE);
}
private static JobStatus createJobStatus(JobInfo.Builder job,
long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis) {
return createJobStatus(job, android.os.Process.NOBODY_UID,
earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis);
}
private static JobStatus createJobStatus(JobInfo.Builder job, int uid,
long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis) {
return new JobStatus(job.build(), uid, null, -1, 0, 0, null,
earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, 0, 0, null, 0);
}
}