Skip to content
This repository has been archived by the owner on Apr 8, 2020. It is now read-only.

Commit

Permalink
Add ConstraintChecker to Firebase Job Dispatcher
Browse files Browse the repository at this point in the history
This adds a new "ConstraintChecker" class to the library. This class is
responsible for validating that a job execution constraints are
satisfied prior to starting jobs.

This is useful for checks that the Google Play services scheduling
engine is unable to definitively determine such as app network
accessibility when Data Saver Mode is enabled.

This change just adds the ConstraintChecker. A future change will update
the ExecutionDelegator to make use of the ConstraintChecker.

The initial version of the ConstraintChecker only looks for satisfaction
of network constraints. To do this, we need the ACCESS_NETWORK_STATE
permission which is a normal permission that Android can automatically
grant to apps at install time[1].

[1]: https://developer.android.com/guide/topics/permissions/overview.html#normal-dangerous

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=187928253
  • Loading branch information
ciarand committed May 23, 2018
1 parent c8a7024 commit ca610f8
Show file tree
Hide file tree
Showing 3 changed files with 286 additions and 0 deletions.
3 changes: 3 additions & 0 deletions jobdispatcher/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.firebase.jobdispatcher">

<!-- Access to network state permission is used by ConstraintChecker to determine
whether network constraints for jobs have been satisfied prior to executing them. -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application>
<!-- Receives GooglePlay execution requests and forwards them to the
appropriate internal service. -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2018 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 android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.support.v4.net.ConnectivityManagerCompat;
import android.util.Log;
import com.firebase.jobdispatcher.Constraint.JobConstraint;

/** Class responsible for verifying that job constraints are satisfied. */
class ConstraintChecker {

/** Logging tag. */
/* package */ static final String TAG = "FJD.ConstraintChecker";

private final Context context;

/**
* Constructs a new ConstraintChecker
*
* @param context Android Application Context object.
*/
/* package */ ConstraintChecker(Context context) {
this.context = context;
}

/**
* Returns true iff all the specified job constraints are satisfied. Note: At the moment this
* method only checks for network constraints and nothing more.
*
* @param job the job whose constraints are to be checked.
*/
public boolean areConstraintsSatisfied(JobInvocation job) {

int jobConstraints = Constraint.compact(job.getConstraints());
return areNetworkConstraintsSatisfied(jobConstraints);
}

/**
* Returns true if the specified jobConstraints are satisfied. We only check whether network
* constraints are satisfied. All other constraints are assumed to be satisfied.
*/
private boolean areNetworkConstraintsSatisfied(@JobConstraint int jobConstraints) {

// Network constraints are always satisfied for jobs that don't need a network
if (!wantsNetwork(jobConstraints)) {
return true;
}

// Ensure basic network connectivity is available.
ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (!isNetworkConnected(connectivityManager)) {
return false;
}

// Note: Constraint.ON_ANY_NETWORK and Constraint.ON_UNMETERED_NETWORK are mutually exclusive.
// Constraints satisfied if we don't need an unmetered network (as that implies any network is
// OK) or current network is unmetered.
return !wantsUnmeteredNetwork(jobConstraints) || isNetworkUnmetered(connectivityManager);
}

/** Returns true if any of the given {@code jobConstraints} require a network. */
private static boolean wantsNetwork(@JobConstraint int jobConstraints) {
return wantsAnyNetwork(jobConstraints) || wantsUnmeteredNetwork(jobConstraints);
}

/** Returns true if any of the given {@code jobConstraints} require an unmetered network. */
private static boolean wantsUnmeteredNetwork(@JobConstraint int jobConstraints) {
return (jobConstraints & Constraint.ON_UNMETERED_NETWORK) != 0;
}

/**
* Returns true if any of the given {@code jobConstraints} is set to {@code
* Constraint.ON_ANY_NETWORK}
*/
private static boolean wantsAnyNetwork(@JobConstraint int jobConstraints) {
return (jobConstraints & Constraint.ON_ANY_NETWORK) != 0;
}

/**
* Returns true is network is connected (i.e. can pass data) based on the available network
* information.
*/
private static boolean isNetworkConnected(ConnectivityManager connectivityManager) {
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if (networkInfo == null) {
// When network information is unavailable, we conservatively
// assume network is inaccessible.
Log.i(TAG, "NetworkInfo null, assuming network inaccessible");
return false;
} else {
return networkInfo.isConnected();
}
}

/** Returns true if the currently active network is unmetered. */
private static boolean isNetworkUnmetered(ConnectivityManager connectivityManager) {
return !ConnectivityManagerCompat.isActiveNetworkMetered(connectivityManager);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright 2018 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 com.google.common.truth.Truth.assertThat;
import static org.robolectric.Shadows.shadowOf;

import android.content.Context;
import android.net.ConnectivityManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowConnectivityManager;
import org.robolectric.shadows.ShadowNetworkInfo;

/** Tests for the {@link com.firebase.jobdispatcher.ConstraintChecker} class. */
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE)
public final class ConstraintCheckerTest {

private static final String JOB_TAG = "JobTag";
private static final String JOB_SERVICE = "JobService";

private Context context;
private JobInvocation.Builder jobBuilder;
private ConstraintChecker constraintChecker;
private ShadowConnectivityManager shadowConnectivityManager;
private ShadowNetworkInfo shadowNetworkInfo;

@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
context = RuntimeEnvironment.application;
constraintChecker = new ConstraintChecker(context);
jobBuilder =
new JobInvocation.Builder().setTag(JOB_TAG).setService(JOB_SERVICE).setTrigger(Trigger.NOW);

ConnectivityManager connectivityManager =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
shadowConnectivityManager = shadowOf(connectivityManager);
shadowNetworkInfo = shadowOf(connectivityManager.getActiveNetworkInfo());
}

private void setNetworkMetered(boolean isMetered) {
// Only mobile connections are considered to be metered.
// See {@link ShadowConnectivityManager#isActiveNetworkMetered()}
if (isMetered) {
shadowNetworkInfo.setConnectionType(ConnectivityManager.TYPE_MOBILE);
} else {
shadowNetworkInfo.setConnectionType(ConnectivityManager.TYPE_WIFI);
}
}

@Test
public void testAreConstraintsSatisfied_anyNetworkRequired_satisfied() {
JobInvocation job =
jobBuilder.setConstraints(Constraint.uncompact(Constraint.ON_ANY_NETWORK)).build();

shadowNetworkInfo.setConnectionStatus(/* isConnected= */ true);

assertThat(constraintChecker.areConstraintsSatisfied(job)).isTrue();
}

@Test
public void testAreConstraintsSatisfied_anyNetworkRequired_unsatisfied_notConnected() {
JobInvocation job =
jobBuilder.setConstraints(Constraint.uncompact(Constraint.ON_ANY_NETWORK)).build();
shadowNetworkInfo.setConnectionStatus(/* isConnected= */ false);

assertThat(constraintChecker.areConstraintsSatisfied(job)).isFalse();
}

@Test
public void testAreConstraintsSatisfied_anyNetworkRequired_unsatisfied_nullNetworkInfo() {
JobInvocation job =
jobBuilder.setConstraints(Constraint.uncompact(Constraint.ON_ANY_NETWORK)).build();
shadowConnectivityManager.setActiveNetworkInfo(null);

assertThat(constraintChecker.areConstraintsSatisfied(job)).isFalse();
}

@Test
public void testAreConstraintsSatisfied_unmeteredNetworkRequired_satisfied() {
JobInvocation job =
jobBuilder.setConstraints(Constraint.uncompact(Constraint.ON_UNMETERED_NETWORK)).build();

shadowNetworkInfo.setConnectionStatus(/* isConnected= */ true);
setNetworkMetered(false);

assertThat(constraintChecker.areConstraintsSatisfied(job)).isTrue();
}

@Test
public void
testAreConstraintsSatisfied_unmeteredNetworkRequired_unsatisfied_networkDisconnected() {
JobInvocation job =
jobBuilder.setConstraints(Constraint.uncompact(Constraint.ON_UNMETERED_NETWORK)).build();

shadowNetworkInfo.setConnectionStatus(/* isConnected= */ false);
setNetworkMetered(false);

assertThat(constraintChecker.areConstraintsSatisfied(job)).isFalse();
}

@Test
public void testAreConstraintsSatisfied_unmeteredNetworkRequired_unsatisfied_networkMetered() {
JobInvocation job =
jobBuilder.setConstraints(Constraint.uncompact(Constraint.ON_UNMETERED_NETWORK)).build();

shadowNetworkInfo.setConnectionStatus(/* isConnected= */ true);
setNetworkMetered(true);

assertThat(constraintChecker.areConstraintsSatisfied(job)).isFalse();
}

@Test
public void testAreConstraintsSatisfied_nonNetworkConstraint() {
JobInvocation job =
jobBuilder.setConstraints(Constraint.uncompact(Constraint.DEVICE_IDLE)).build();
assertThat(constraintChecker.areConstraintsSatisfied(job)).isTrue();
}

@Test
public void testAreConstraintsSatisfied_nonNetworkConstraints() {
JobInvocation job =
jobBuilder
.setConstraints(
Constraint.uncompact(Constraint.DEVICE_IDLE | Constraint.DEVICE_CHARGING))
.build();
assertThat(constraintChecker.areConstraintsSatisfied(job)).isTrue();
}

@Test
public void
testAreConstraintsSatisfied_anyNetworkRequired_satisfied_includesNonNetworkConstraints() {
JobInvocation job =
jobBuilder
.setConstraints(
Constraint.uncompact(
Constraint.DEVICE_IDLE
| Constraint.DEVICE_CHARGING
| Constraint.ON_ANY_NETWORK))
.build();
shadowNetworkInfo.setConnectionStatus(/* isConnected= */ true);

assertThat(constraintChecker.areConstraintsSatisfied(job)).isTrue();
}
}

0 comments on commit ca610f8

Please sign in to comment.