| /* |
| * 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); |
| } |
| } |