// Copyright 2017 Google, Inc. // // 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.firebase.jobdispatcher; import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; import static junit.framework.Assert.fail; import static org.junit.Assume.assumeTrue; import android.app.UiAutomation; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkInfo; import android.net.NetworkRequest; import android.os.Build; import android.os.ParcelFileDescriptor; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.util.Log; import com.google.common.util.concurrent.SettableFuture; import java.io.FileInputStream; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; /** * Verifies jobs with network constraints don't run under airplane mode. Requires API level 21+ (for * {@link UiAutomation#executeShellCommand(String)}), as well as an installed and available Google * Play services. */ @RunWith(AndroidJUnit4.class) public final class AirplaneModeAndroidTest { /** Log tag. */ private static final String TAG = "FJD_TEST"; /** How long we're willing to wait for the network to change state. */ private static final int NETWORK_STATE_CHANGE_TIMEOUT_SECONDS = 20; /** How long we're willing to wait for a job to run after it becomes eligible. */ private static final int EXECUTE_JOB_TIMEOUT_SECONDS = 30; private Context testContext; private Context appContext; private UiAutomation uiAutomation; private ConnectivityManager connManager; private FirebaseJobDispatcher dispatcher; private final SettableFuture<Void> jobStartedFuture = SettableFuture.create(); @Before public void setUp() { assumeTrue( getClass().getSimpleName() + " requires API level 21+ to run", Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP); testContext = InstrumentationRegistry.getContext(); appContext = InstrumentationRegistry.getTargetContext(); uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); connManager = (ConnectivityManager) testContext.getSystemService(Context.CONNECTIVITY_SERVICE); dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(appContext)); TestJobService.setProxy( new TestJobService.JobServiceProxy() { @Override public boolean onStartJob(JobService jobService, JobParameters params) { jobStartedFuture.set(null); return false; } @Override public boolean onStopJob(JobService jobService, JobParameters params) { return false; } }); } @After public void tearDown() throws Exception { setAirplaneModeEnabled(false); } @Test public void immediateTrigger_withNoConstraints_shouldRunInAirplaneMode() throws Exception { verifyJobRunsInAirplaneMode( dispatcher .newJobBuilder() .setService(TestJobService.class) .setTrigger(Trigger.NOW) .setTag("basic-immediate-job--no-constraint") .build()); } @Test public void executionWindowTrigger_withNoConstraints_shouldRunInAirplaneMode() throws Exception { verifyJobRunsInAirplaneMode( dispatcher .newJobBuilder() .setService(TestJobService.class) .setTrigger(Trigger.executionWindow(0, 15)) .setTag("basic-execution-window-job--no-constraint") .build()); } @Test public void immediateTrigger_withNetworkConstraint_shouldNotRunInAirplaneMode() throws Exception { verifyJobDoesntRunInAirplaneMode( dispatcher .newJobBuilder() .setService(TestJobService.class) .setTrigger(Trigger.NOW) .addConstraint(Constraint.ON_ANY_NETWORK) .setTag("basic-immediate-job") .build()); } @Test public void executionWindowTrigger_withNetworkConstraint_shouldNotRunInAirplaneMode() throws Exception { verifyJobDoesntRunInAirplaneMode( dispatcher .newJobBuilder() .setService(TestJobService.class) .setTrigger(Trigger.executionWindow(0, 15)) .addConstraint(Constraint.ON_ANY_NETWORK) .setTag("basic-execution-window-job") .build()); } private void verifyJobRunsInAirplaneMode(Job job) throws Exception { enableAirplaneMode(); dispatcher.mustSchedule(job); try { jobStartedFuture.get(EXECUTE_JOB_TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (TimeoutException e) { throw new AssertionError( "Timed out waiting for job with no network constraints to run in airplane mode", e); } } private void verifyJobDoesntRunInAirplaneMode(Job job) throws Exception { // Verify that the job does not run while the device is in airplane mode enableAirplaneMode(); dispatcher.mustSchedule(job); try { jobStartedFuture.get(EXECUTE_JOB_TIMEOUT_SECONDS, TimeUnit.SECONDS); fail("Shouldn't have run job with network constraints while in airplane mode"); } catch (TimeoutException e) { // expected } // Verify that the job runs after airplane mode is disabled disableAirplaneMode(); try { jobStartedFuture.get(EXECUTE_JOB_TIMEOUT_SECONDS, TimeUnit.SECONDS); } catch (CancellationException | ExecutionException | InterruptedException | TimeoutException e) { throw new AssertionError( "Should have run job with network constraints once airplane mode was disabled", e); } } private void enableAirplaneMode() throws Exception { setAirplaneModeEnabled(true); waitForAllNetworksToDisconnect(); } private void disableAirplaneMode() throws Exception { setAirplaneModeEnabled(false); waitForSomeNetworkToConnect(); } private void setAirplaneModeEnabled(boolean enabled) throws Exception { Log.i(TAG, "Setting airplane mode to " + enabled); String value = String.valueOf(enabled ? 1 : 0); // Update the setting executeShellCommand("settings put global airplane_mode_on " + value); // Check the setting took String newSetting = executeShellCommand("settings get global airplane_mode_on"); assertThat(newSetting).isEqualTo(value); // Let everything know we flipped the setting executeShellCommand("am broadcast -a android.intent.action.AIRPLANE_MODE"); } private void waitForAllNetworksToDisconnect() throws Exception { final SettableFuture<Void> future = SettableFuture.create(); BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context ctx, Intent intent) { Network[] networks = connManager.getAllNetworks(); for (Network network : networks) { NetworkInfo info = connManager.getNetworkInfo(network); if (info != null && info.isAvailable()) { // not done yet return; } } future.set(null); } }; testContext.registerReceiver( receiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); try { future.get(NETWORK_STATE_CHANGE_TIMEOUT_SECONDS, TimeUnit.SECONDS); } finally { testContext.unregisterReceiver(receiver); } } private void waitForSomeNetworkToConnect() throws Exception { final SettableFuture<Void> future = SettableFuture.create(); ConnectivityManager.NetworkCallback cb = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { NetworkInfo netInfo = connManager.getNetworkInfo(network); if (netInfo != null && netInfo.isConnected()) { future.set(null); } } }; connManager.requestNetwork( new NetworkRequest.Builder().addCapability(NET_CAPABILITY_INTERNET).build(), cb); try { future.get(NETWORK_STATE_CHANGE_TIMEOUT_SECONDS, TimeUnit.SECONDS); } finally { connManager.unregisterNetworkCallback(cb); } } private String executeShellCommand(String cmd) throws Exception { ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(cmd); StringBuilder stdout = new StringBuilder(); try (FileInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) { byte[] buffer = new byte[1024]; int bytesRead = 0; while ((bytesRead = inputStream.read(buffer)) != -1) { stdout.append(new String(buffer, 0, bytesRead, UTF_8)); } } Log.i(TAG, "$ adb shell " + cmd + "\n" + stdout); return stdout.toString().trim(); // trim trailing newline } }