releasing 4.4.0

This commit is contained in:
k0shk0sh 2017-10-01 10:30:21 +02:00
parent e095e990f7
commit e6230fe91f
68 changed files with 42 additions and 7774 deletions

3
.gitignore vendored
View File

@ -8,5 +8,4 @@
/app/google-services.json
/app/build/
/app/src/main/res/values/secrets.xml
/app/fastaccess-key
/jobdispatcher/build/
/app/fastaccess-key

View File

@ -29,8 +29,8 @@ android {
applicationId "com.fastaccess.github"
minSdkVersion 21
targetSdkVersion 26
versionCode 430
versionName "4.3.0"
versionCode 440
versionName "4.4.0"
buildConfigString "GITHUB_CLIENT_ID", (buildProperties.secrets['github_client_id'] | buildProperties.notThere['github_client_id']).string
buildConfigString "GITHUB_SECRET", (buildProperties.secrets['github_secret'] | buildProperties.notThere['github_secret']).string
buildConfigString "IMGUR_CLIENT_ID", (buildProperties.secrets['imgur_client_id'] | buildProperties.notThere['imgur_client_id']).string
@ -167,7 +167,7 @@ dependencies {
implementation 'com.jaredrummler:android-device-names:1.1.4'
implementation 'net.yslibrary.keyboardvisibilityevent:keyboardvisibilityevent:2.1.0'
implementation 'com.airbnb.android:lottie:2.2.0'
implementation project(path: ':jobdispatcher')
implementation 'com.firebase:firebase-jobdispatcher:0.8.2'
compileOnly "org.projectlombok:lombok:${lombokVersion}"
kapt "org.projectlombok:lombok:${lombokVersion}"
kapt "com.evernote:android-state-processor:${state_version}"

View File

@ -4,6 +4,7 @@ import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import com.fastaccess.helper.Logger;
import com.fastaccess.ui.widgets.SpannableBuilder;
import net.nightwhistler.htmlspanner.TagNodeHandler;
@ -42,10 +43,11 @@ import lombok.NoArgsConstructor;
return node.getParent() == null ? null : node.getParent().getName();
}
@Override public void beforeChildren(TagNode node, SpannableStringBuilder builder) {
@Override public void beforeChildren(TagNode node, SpannableStringBuilder builder) {
TodoItems todoItem = null;
if (node.getChildTags() != null && node.getChildTags().length > 0) {
for (TagNode tagNode : node.getChildTags()) {
Logger.e(tagNode.getName(), tagNode.getText());
if (tagNode.getName() != null && tagNode.getName().equals("input")) {
todoItem = new TodoItems();
todoItem.isChecked = tagNode.getAttributeByName("checked") != null;

View File

@ -20,10 +20,8 @@ public class MarginHandler extends TagNodeHandler {
}
}
public void handleTagNode(TagNode node, SpannableStringBuilder builder, int start, int end) {
builder.setSpan(new LeadingMarginSpan.Standard(30), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
this.appendNewLine(builder);
this.appendNewLine(builder);
}
}

View File

@ -16,7 +16,6 @@ import com.fastaccess.BuildConfig
import com.fastaccess.R
import com.fastaccess.helper.AppHelper
import com.fastaccess.helper.InputHelper
import com.fastaccess.helper.PrefGetter
import com.fastaccess.helper.ViewHelper
import com.fastaccess.provider.fabric.FabricProvider
import com.fastaccess.ui.base.BaseActivity
@ -76,7 +75,7 @@ class PremiumActivity : BaseActivity<PremiumMvp.View, PremiumPresenter>(), Premi
return true
}
@OnClick(R.id.close) fun onClose(): Unit = finish()
@OnClick(R.id.close) fun onClose() = finish()
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
@ -97,8 +96,6 @@ class PremiumActivity : BaseActivity<PremiumMvp.View, PremiumPresenter>(), Premi
override fun onAnimationRepeat(p0: Animator?) {}
override fun onAnimationEnd(p0: Animator?) {
FabricProvider.logPurchase(InputHelper.toString(editText))
PrefGetter.setProItems()
PrefGetter.setEnterpriseItem()
showMessage(R.string.success, R.string.success)
successResult()
}

View File

@ -1,6 +1,6 @@
package com.fastaccess.ui.modules.main.premium
import com.fastaccess.helper.Logger
import com.fastaccess.helper.PrefGetter
import com.fastaccess.helper.RxHelper
import com.fastaccess.ui.base.mvp.presenter.BasePresenter
import com.github.b3er.rxfirebase.database.data
@ -26,13 +26,20 @@ class PremiumPresenter : BasePresenter<PremiumMvp.View>(), PremiumMvp.Presenter
val map = it.getValue(gti)
exists = map?.contains(promo)
}
Logger.e(it.children, it.childrenCount, exists)
return@flatMap Observable.just(exists)
}
.doOnComplete { sendToView { it.hideProgress() } }
.subscribe({
when (it) {
true -> sendToView { it.onSuccessfullyActivated() }
true -> sendToView {
if (promo.contains("student")) {
PrefGetter.setProItems()
} else {
PrefGetter.setProItems()
PrefGetter.setEnterpriseItem()
}
it.onSuccessfullyActivated()
}
else -> sendToView { it.onNoMatch() }
}
}, ::println))

View File

@ -8,28 +8,22 @@
<body id="preview">
<h2><a id="FastHub_changelog_0"></a>FastHub changelog
</h2>
<h3><a id="Version__420_Create_Edit__Delete_files_make_Commits_2"></a>Version 4.3.0 (Project Columns and Cards)
<h3><a id="Version__420_Create_Edit__Delete_files_make_Commits_2"></a>Version 4.4.0 (Org Project Columns and Cards)
</h3>
<blockquote>
<p>Reporting Issues or Feature Requests in Google Play review section, will be ignored or might even get your account to be blocked from
FastHub. You are using an app for GitHub, which provides a proper way to report issues.
<br>
Please report the issues in FastHub repo instead, by opening the Drawer Menu and clicking on “Report an Issue”<strong>
PLEASE USE IT</st§rong>.
<p>Please report the issues in FastHub repo instead, by opening the Drawer Menu and clicking on “Report an Issue”<strong>
PLEASE USE IT</strong>.
</p>
</blockquote>
<h4><a id="Bugs__Enhancements__new_Features_320_7"></a>Bugs , Enhancements &amp; new Features (4.3.0)
<h4><a id="Bugs__Enhancements__new_Features_320_7"></a>Bugs , Enhancements &amp; new Features (4.4.0)
</h4>
<ul>
<li>Project Columns & Cards (Edit, Create & Delete)</li>
<li>(New) Repo Collaborators now can Edit, delete & create files.</li>
<li>(New) Repo Collaborators now can Edit, delete & Comments.</li>
<li>(New) Long press in Feeds to navigate directly to Repo.</li>
<li>(Enhancement) Made Search to have minimum 2 chars.</li>
<li>(Enhancement) Adding blockable progress when adding new comment.</li>
<li>(Fix) Crash in PRs & Issues comments due to Table rendering!.</li>
<li>(Fix) ReadMe scrolling when Device Animation is turned off.</li>
<li>(Fix) Closed/Opened Issue pagination where most of the issues in the page are PRs (GitHub API!!!!).</li>
<li>(New) Org Project Columns & Cards (Edit, Create & Delete)</li>
<li>(New) Displaying Labels under Issue/Pr description.</li>
<li>(Enhancement) Removal of PR review limit.</li>
<li>(Enhancement) Selected text will be taken in consideration when adding Image/Link.</li>
<li>(Enhancement) Removed Loading background.</li>
<li>(Enhancement) More markdown enhancement.</li>
<li>(Fix) Lots of bug fixes.</li>
<li>There are more stuff are not mentioned, find them out :stuck_out_tongue:</li>
</ul>

View File

@ -592,6 +592,19 @@
<h5>• I\'m having this issue or I want this &amp; that!!</h5>
<p>Head to https://github.com/k0shk0sh/FastHub/issues/new and create new issue for bugs or feature requests, I really do encourage you to
search before opening a ticket. Any duplicate request will result in it being closed immediately.</p>
<h5>• How do I get PROMO CODE?</h5>
<p>If you are a student, you\'ll have to provide me via Email that you are student, you will need below documents:</p>
<ul>
<li>Your university identity card & your identity card (that shows your name & your face to compare it!)</li>
<li>Your university start & end date</li>
<li>Rate FastHub in the Play Store</li>
</ul>
<p>If you aren\'t a student and you can\'t afford to pay for PRO, you\'ll need:</p>
<ul>
<li>Write an article about FastHub in social media such as (Medium)</li>
<li>Rate FastHub in the Play Store</li>
</ul>
]]></string>
<string name="faq">FAQ</string>
<string name="comments_added_successfully">Comments added successfully</string>

View File

@ -1,79 +0,0 @@
apply plugin: "com.android.library"
android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
minSdkVersion 16
targetSdkVersion 26
versionCode 1
versionName "0.8.0"
testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
}
defaultPublishConfig "release"
publishNonDefault true
buildTypes {
debug {
testCoverageEnabled true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
}
sourceSets {
// A set of testing helpers that are shared across test types
testLib { java.srcDir("src/main") }
test { java.srcDir("src/testLib") } // Robolectric tests
androidTest { java.srcDir("src/testLib") } // Android (e2e) tests
}
}
dependencies {
// The main library only depends on the Android support lib
compile "com.android.support:support-v4:26.0.1"
def junit = 'junit:junit:4.12'
def robolectric = 'org.robolectric:robolectric:3.3.2'
// The common test library uses JUnit
testLibCompile junit
// The unit tests are written using JUnit, Robolectric, and Mockito
testCompile junit
testCompile robolectric
testCompile 'org.mockito:mockito-core:2.2.5'
// The Android (e2e) tests are written using JUnit and the test support lib
androidTestCompile junit
androidTestCompile 'com.android.support.test:runner:0.5'
}
task javadocs(type: Javadoc) {
description "Generate Javadocs"
source = android.sourceSets.main.java.sourceFiles
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
classpath += configurations.compile
failOnError false
}
task javadocsJar(type: Jar, dependsOn: javadocs) {
description "Package Javadocs into a jar"
classifier = "javadoc"
from javadocs.destinationDir
}
task sourcesJar(type: Jar) {
description "Package sources into a jar"
classifier = "sources"
from android.sourceSets.main.java.sourceFiles
}
task aar(dependsOn: "assembleRelease") {
group "artifact"
description "Builds the library AARs"
}

View File

@ -1,40 +0,0 @@
apply plugin: "jacoco"
jacoco {
// see https://github.com/jacoco/jacoco/pull/288 and the top build.gradle
toolVersion "0.7.6.201602180812"
}
android {
testOptions {
unitTests.all {
systemProperty "robolectric.logging.enabled", true
systemProperty "robolectric.logging", "stdout"
jacoco {
includeNoLocationClasses = true
}
}
}
}
// ignore these when generating coverage
def ignoredPrefixes = ['R$', 'R.class', 'BuildConfig.class']
task coverage(type: JacocoReport, dependsOn: ["testDebugUnitTest"]) {
group = "Reports"
description = "Generate a coverage report"
classDirectories = fileTree(
dir: "${project.buildDir}/intermediates/classes/debug/com/firebase/",
exclude: { d -> ignoredPrefixes.any { p -> d.file.name.startsWith(p) } }
)
sourceDirectories = files(["src/main/java/com/firebase/"])
executionData = files("${project.buildDir}/jacoco/testDebugUnitTest.exec")
reports {
xml.enabled = true
html.enabled = true
html.destination "${buildDir}/coverage_html"
}
}

View File

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.firebase.jobdispatcher">
<uses-sdk android:minSdkVersion="14" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<instrumentation
android:targetPackage="com.firebase.jobdispatcher"
android:name="android.test.InstrumentationTestRunner" />
<application>
<service android:exported="false" android:name=".TestJobService">
<intent-filter>
<action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE"/>
</intent-filter>
</service>
</application>
</manifest>

View File

@ -1,69 +0,0 @@
// 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 org.junit.Assert.assertTrue;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Basic end to end test for the JobDispatcher. Requires Google Play services be installed and
* available.
*/
@RunWith(AndroidJUnit4.class)
public final class EndToEndTest {
private Context appContext;
private FirebaseJobDispatcher dispatcher;
@Before public void setUp() {
appContext = InstrumentationRegistry.getTargetContext();
dispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(appContext));
TestJobService.reset();
}
@Test public void basicImmediateJob() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
TestJobService.setProxy(new TestJobService.JobServiceProxy() {
@Override
public boolean onStartJob(JobParameters params) {
latch.countDown();
return false;
}
@Override
public boolean onStopJob(JobParameters params) {
return false;
}
});
dispatcher.mustSchedule(
dispatcher.newJobBuilder()
.setService(TestJobService.class)
.setTrigger(Trigger.NOW)
.setTag("basic-immediate-job")
.build());
assertTrue("Latch wasn't counted down as expected", latch.await(120, TimeUnit.SECONDS));
}
}

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.firebase.jobdispatcher">
<application>
<!-- Receives GooglePlay execution requests and forwards them to the
appropriate internal service. -->
<service
android:name=".GooglePlayReceiver"
android:exported="true"
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
<intent-filter>
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -1,49 +0,0 @@
// Copyright 2016 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;
final class BundleProtocol {
static final String PACKED_PARAM_BUNDLE_PREFIX = "com.firebase.jobdispatcher.";
// PACKED_PARAM values are only read on the client side, so as long as the
// extraction process gets the same changes then it's fine.
static final String PACKED_PARAM_CONSTRAINTS = "constraints";
static final String PACKED_PARAM_LIFETIME = "persistent";
static final String PACKED_PARAM_RECURRING = "recurring";
static final String PACKED_PARAM_SERVICE = "service";
static final String PACKED_PARAM_TAG = "tag";
static final String PACKED_PARAM_EXTRAS = "extras";
static final String PACKED_PARAM_TRIGGER_TYPE = "trigger_type";
static final String PACKED_PARAM_TRIGGER_WINDOW_END = "window_end";
static final String PACKED_PARAM_TRIGGER_WINDOW_START = "window_start";
static final int TRIGGER_TYPE_EXECUTION_WINDOW = 1;
static final int TRIGGER_TYPE_IMMEDIATE = 2;
static final int TRIGGER_TYPE_CONTENT_URI = 3;
static final String PACKED_PARAM_RETRY_STRATEGY_INITIAL_BACKOFF_SECONDS =
"initial_backoff_seconds";
static final String PACKED_PARAM_RETRY_STRATEGY_MAXIMUM_BACKOFF_SECONDS =
"maximum_backoff_seconds";
static final String PACKED_PARAM_RETRY_STRATEGY_POLICY = "retry_policy";
static final String PACKED_PARAM_REPLACE_CURRENT = "replace_current";
static final String PACKED_PARAM_CONTENT_URI_FLAGS_ARRAY = "content_uri_flags_array";
static final String PACKED_PARAM_CONTENT_URI_ARRAY = "content_uri_array";
static final String PACKED_PARAM_TRIGGERED_URIS = "triggered_uris";
static final String PACKED_PARAM_OBSERVED_URI = "observed_uris";
BundleProtocol() {
}
}

View File

@ -1,108 +0,0 @@
// Copyright 2016 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.support.annotation.IntDef;
import android.support.annotation.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A Constraint is a runtime requirement for a job. A job only becomes eligible to run once its
* trigger has been activated and all constraints are satisfied.
*/
public final class Constraint {
/**
* Only run the job when an unmetered network is available.
*/
public static final int ON_UNMETERED_NETWORK = 1;
/**
* Only run the job when a network connection is available. If both this and
* {@link #ON_UNMETERED_NETWORK} is provided, {@link #ON_UNMETERED_NETWORK} will take
* precedence.
*/
public static final int ON_ANY_NETWORK = 1 << 1;
/**
* Only run the job when the device is currently charging.
*/
public static final int DEVICE_CHARGING = 1 << 2;
/**
* Only run the job when the device is idle. This is ignored for devices that don't expose the
* concept of an idle state.
*/
public static final int DEVICE_IDLE = 1 << 3;
@VisibleForTesting
static final int[] ALL_CONSTRAINTS = {
ON_ANY_NETWORK, ON_UNMETERED_NETWORK, DEVICE_CHARGING, DEVICE_IDLE};
/** Constraint shouldn't ever be instantiated. */
private Constraint() {}
/**
* A tooling type-hint for any of the valid constraint values.
*/
@IntDef(flag = true, value = {
ON_ANY_NETWORK,
ON_UNMETERED_NETWORK,
DEVICE_CHARGING,
DEVICE_IDLE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface JobConstraint {}
/**
* Compact a provided array of constraints into a single int.
*
* @see #uncompact(int)
*/
static int compact(@JobConstraint int[] constraints) {
int result = 0;
if (constraints == null) {
return result;
}
for (int c : constraints) {
result |= c;
}
return result;
}
/**
* Unpack a single int into an array of constraints.
*
* @see #compact(int[])
*/
static int[] uncompact(int compactConstraints) {
int length = 0;
for (int c : ALL_CONSTRAINTS) {
length += (compactConstraints & c) == c ? 1 : 0;
}
int[] list = new int[length];
int i = 0;
for (int c : ALL_CONSTRAINTS) {
if ((compactConstraints & c) == c) {
list[i++] = c;
}
}
return list;
}
}

View File

@ -1,288 +0,0 @@
// Copyright 2016 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.firebase.jobdispatcher.RetryStrategy.RETRY_POLICY_EXPONENTIAL;
import static com.firebase.jobdispatcher.RetryStrategy.RETRY_POLICY_LINEAR;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Parcel;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* Validates Jobs according to some safe standards.
* <p>
* Custom JobValidators should typically extend from this.
*/
public class DefaultJobValidator implements JobValidator {
/**
* The maximum length of a tag, in characters (i.e. String.length()). Strings longer than this
* will cause validation to fail.
*/
public static final int MAX_TAG_LENGTH = 100;
/**
* The maximum size, in bytes, that the provided extras bundle can be. Corresponds to
* {@link Parcel#dataSize()}.
*/
public final static int MAX_EXTRAS_SIZE_BYTES = 10 * 1024;
/** Private ref to the Context. Necessary to check that the manifest is configured correctly. */
private final Context context;
public DefaultJobValidator(Context context) {
this.context = context;
}
/** @see {@link #MAX_EXTRAS_SIZE_BYTES}. */
private static int measureBundleSize(Bundle extras) {
Parcel p = Parcel.obtain();
extras.writeToParcel(p, 0);
int sizeInBytes = p.dataSize();
p.recycle();
return sizeInBytes;
}
/** Combines two {@literal List<String>s} together. */
@Nullable
private static List<String> mergeErrorLists(@Nullable List<String> errors,
@Nullable List<String> newErrors) {
if (errors == null) {
return newErrors;
}
if (newErrors == null) {
return errors;
}
errors.addAll(newErrors);
return errors;
}
@Nullable
private static List<String> addError(@Nullable List<String> errors, String newError) {
if (newError == null) {
return errors;
}
if (errors == null) {
return getMutableSingletonList(newError);
}
Collections.addAll(errors, newError);
return errors;
}
@Nullable
private static List<String> addErrorsIf(boolean condition, List<String> errors, String newErr) {
if (condition) {
return addError(errors, newErr);
}
return errors;
}
/**
* Attempts to validate the provided {@code JobParameters}. If the JobParameters is valid, null will be
* returned. If the JobParameters has errors, a list of those errors will be returned.
*/
@Nullable
@Override
@CallSuper
public List<String> validate(JobParameters job) {
List<String> errors = null;
errors = mergeErrorLists(errors, validate(job.getTrigger()));
errors = mergeErrorLists(errors, validate(job.getRetryStrategy()));
if (job.isRecurring() && job.getTrigger() == Trigger.NOW) {
errors = addError(errors, "ImmediateTriggers can't be used with recurring jobs");
}
errors = mergeErrorLists(errors, validateForTransport(job.getExtras()));
if (job.getLifetime() > Lifetime.UNTIL_NEXT_BOOT) {
//noinspection ConstantConditions
errors = mergeErrorLists(errors, validateForPersistence(job.getExtras()));
}
errors = mergeErrorLists(errors, validateTag(job.getTag()));
errors = mergeErrorLists(errors, validateService(job.getService()));
return errors;
}
/**
* Attempts to validate the provided Trigger. If valid, null is returned. Otherwise a list of
* errors will be returned.
* <p>
* Note that a Trigger that passes validation here is not necessarily valid in all permutations
* of a JobParameters. For example, an Immediate is never valid for a recurring job.
* @param trigger
*/
@Nullable
@Override
@CallSuper
public List<String> validate(JobTrigger trigger) {
if (trigger != Trigger.NOW
&& !(trigger instanceof JobTrigger.ExecutionWindowTrigger)
&& !(trigger instanceof JobTrigger.ContentUriTrigger)) {
return getMutableSingletonList("Unknown trigger provided");
}
return null;
}
/**
* Attempts to validate the provided RetryStrategy. If valid, null is returned. Otherwise a list
* of errors will be returned.
*/
@Nullable
@Override
@CallSuper
public List<String> validate(RetryStrategy retryStrategy) {
List<String> errors = null;
int policy = retryStrategy.getPolicy();
int initial = retryStrategy.getInitialBackoff();
int maximum = retryStrategy.getMaximumBackoff();
errors = addErrorsIf(policy != RETRY_POLICY_EXPONENTIAL && policy != RETRY_POLICY_LINEAR,
errors, "Unknown retry policy provided");
errors = addErrorsIf(maximum < initial,
errors, "Maximum backoff must be greater than or equal to initial backoff");
errors = addErrorsIf(300 > maximum,
errors, "Maximum backoff must be greater than 300s (5 minutes)");
errors = addErrorsIf(initial < 30,
errors, "Initial backoff must be at least 30s");
return errors;
}
@Nullable
private List<String> validateForPersistence(Bundle extras) {
List<String> errors = null;
if (extras != null) {
// check the types to make sure they're persistable
for (String k : extras.keySet()) {
errors = addError(errors, validateExtrasType(extras, k));
}
}
return errors;
}
@Nullable
private List<String> validateForTransport(Bundle extras) {
if (extras == null) {
return null;
}
int bundleSizeInBytes = measureBundleSize(extras);
if (bundleSizeInBytes > MAX_EXTRAS_SIZE_BYTES) {
return getMutableSingletonList(String.format(Locale.US,
"Extras too large: %d bytes is > the max (%d bytes)",
bundleSizeInBytes, MAX_EXTRAS_SIZE_BYTES));
}
return null;
}
@Nullable
private String validateExtrasType(Bundle extras, String key) {
Object o = extras.get(key);
if (o == null
|| o instanceof Integer
|| o instanceof Long
|| o instanceof Double
|| o instanceof String
|| o instanceof Boolean) {
return null;
}
return String.format(Locale.US,
"Received value of type '%s' for key '%s', but only the"
+ " following extra parameter types are supported:"
+ " Integer, Long, Double, String, and Boolean",
o == null ? null : o.getClass(), key);
}
private List<String> validateService(String service) {
if (service == null || service.isEmpty()) {
return getMutableSingletonList("Service can't be empty");
}
if (context == null) {
return getMutableSingletonList("Context is null, can't query PackageManager");
}
PackageManager pm = context.getPackageManager();
if (pm == null) {
return getMutableSingletonList("PackageManager is null, can't validate service");
}
final String msg = "Couldn't find a registered service with the name " + service
+ ". Is it declared in the manifest with the right intent-filter?";
Intent executeIntent = new Intent(JobService.ACTION_EXECUTE);
executeIntent.setClassName(context, service);
List<ResolveInfo> intentServices = pm.queryIntentServices(executeIntent, 0);
if (intentServices == null || intentServices.isEmpty()) {
return getMutableSingletonList(msg);
}
for (ResolveInfo info : intentServices) {
if (info.serviceInfo != null && info.serviceInfo.enabled) {
// found a match!
return null;
}
}
return getMutableSingletonList(msg);
}
private List<String> validateTag(String tag) {
if (tag == null) {
return getMutableSingletonList("Tag can't be null");
}
if (tag.length() > MAX_TAG_LENGTH) {
return getMutableSingletonList("Tag must be shorter than " + MAX_TAG_LENGTH);
}
return null;
}
@NonNull
private static List<String> getMutableSingletonList(String msg) {
ArrayList<String> strings = new ArrayList<>();
strings.add(msg);
return strings;
}
}

View File

@ -1,62 +0,0 @@
// Copyright 2016 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.support.annotation.NonNull;
import com.firebase.jobdispatcher.FirebaseJobDispatcher.CancelResult;
import com.firebase.jobdispatcher.FirebaseJobDispatcher.ScheduleResult;
/**
* Driver represents a component that understands how to schedule, validate, and execute jobs.
*/
public interface Driver {
/**
* Schedules the provided Job.
*
* @return one of the SCHEDULE_RESULT_ constants
*/
@ScheduleResult
int schedule(@NonNull Job job);
/**
* Cancels the job with the provided tag and class.
*
* @return one of the CANCEL_RESULT_ constants.
*/
@CancelResult
int cancel(@NonNull String tag);
/**
* Cancels all jobs registered with this Driver.
*
* @return one of the CANCEL_RESULT_ constants.
*/
@CancelResult
int cancelAll();
/**
* Returns a JobValidator configured for this backend.
*/
@NonNull
JobValidator getValidator();
/**
* Indicates whether the backend is available.
*/
boolean isAvailable();
}

View File

@ -1,160 +0,0 @@
// Copyright 2016 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.content.Context.BIND_AUTO_CREATE;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.v4.util.SimpleArrayMap;
import android.util.Log;
import com.firebase.jobdispatcher.JobService.JobResult;
import java.lang.ref.WeakReference;
/**
* ExecutionDelegator tracks local Binder connections to client JobServices and handles
* communication with those services.
*/
/* package */ class ExecutionDelegator {
@VisibleForTesting
static final int JOB_FINISHED = 1;
static final String TAG = "FJD.ExternalReceiver";
interface JobFinishedCallback {
void onJobFinished(@NonNull JobInvocation jobInvocation, @JobResult int result);
}
/**
* A mapping of {@link JobInvocation} to (local) binder connections.
* Synchronized by itself.
*/
private final SimpleArrayMap<JobInvocation, JobServiceConnection> serviceConnections =
new SimpleArrayMap<>();
private final ResponseHandler responseHandler =
new ResponseHandler(Looper.getMainLooper(), new WeakReference<>(this));
private final Context context;
private final JobFinishedCallback jobFinishedCallback;
ExecutionDelegator(Context context, JobFinishedCallback jobFinishedCallback) {
this.context = context;
this.jobFinishedCallback = jobFinishedCallback;
}
/**
* Executes the provided {@code jobInvocation} by kicking off the creation of a new Binder
* connection to the Service.
*
* @return true if the service was bound successfully.
*/
boolean executeJob(JobInvocation jobInvocation) {
if (jobInvocation == null) {
return false;
}
JobServiceConnection conn = new JobServiceConnection(jobInvocation,
responseHandler.obtainMessage(JOB_FINISHED));
synchronized (serviceConnections) {
JobServiceConnection oldConnection = serviceConnections.put(jobInvocation, conn);
if (oldConnection != null) {
Log.e(TAG, "Received execution request for already running job");
}
return context.bindService(createBindIntent(jobInvocation), conn, BIND_AUTO_CREATE);
}
}
@NonNull
private Intent createBindIntent(JobParameters jobParameters) {
Intent execReq = new Intent(JobService.ACTION_EXECUTE);
execReq.setClassName(context, jobParameters.getService());
return execReq;
}
void stopJob(JobInvocation job) {
synchronized (serviceConnections) {
JobServiceConnection jobServiceConnection = serviceConnections.remove(job);
if (jobServiceConnection != null) {
jobServiceConnection.onStop();
safeUnbindService(jobServiceConnection);
}
}
}
private void safeUnbindService(JobServiceConnection connection) {
if (connection != null && connection.isBound()) {
try {
context.unbindService(connection);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Error unbinding service: " + e.getMessage());
}
}
}
private void onJobFinishedMessage(JobInvocation jobInvocation, int result) {
synchronized (serviceConnections) {
JobServiceConnection connection = serviceConnections.remove(jobInvocation);
safeUnbindService(connection);
}
jobFinishedCallback.onJobFinished(jobInvocation, result);
}
private static class ResponseHandler extends Handler {
/**
* We hold a WeakReference to the ExecutionDelegator because it holds a reference to a
* Service Context and Handlers are often kept in memory longer than you'd expect because
* any pending Messages can maintain references to them.
*/
private final WeakReference<ExecutionDelegator> executionDelegatorReference;
ResponseHandler(Looper looper, WeakReference<ExecutionDelegator> executionDelegator) {
super(looper);
this.executionDelegatorReference = executionDelegator;
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case JOB_FINISHED:
if (msg.obj instanceof JobInvocation) {
ExecutionDelegator delegator = this.executionDelegatorReference.get();
if (delegator == null) {
Log.wtf(TAG, "handleMessage: service was unexpectedly GC'd"
+ ", can't send job result");
return;
}
delegator.onJobFinishedMessage((JobInvocation) msg.obj, msg.arg1);
return;
}
Log.wtf(TAG, "handleMessage: unknown obj returned");
return;
default:
Log.wtf(TAG, "handleMessage: unknown message type received: " + msg.what);
}
}
}
}

View File

@ -1,211 +0,0 @@
// Copyright 2016 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.support.annotation.IntDef;
import android.support.annotation.NonNull;
import com.firebase.jobdispatcher.RetryStrategy.RetryPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* The FirebaseJobDispatcher provides a driver-agnostic API for scheduling and cancelling Jobs.
*
* @see #FirebaseJobDispatcher(Driver)
* @see Driver
* @see JobParameters
*/
public final class FirebaseJobDispatcher {
/**
* Indicates the schedule request seems to have been successful.
*/
public final static int SCHEDULE_RESULT_SUCCESS = 0;
/**
* Indicates the schedule request encountered an unknown error.
*/
public final static int SCHEDULE_RESULT_UNKNOWN_ERROR = 1;
/**
* Indicates the schedule request failed because the driver was unavailable.
*/
public final static int SCHEDULE_RESULT_NO_DRIVER_AVAILABLE = 2;
/**
* Indicates the schedule request failed because the Trigger was unsupported.
*/
public final static int SCHEDULE_RESULT_UNSUPPORTED_TRIGGER = 3;
/**
* Indicates the schedule request failed because the service is not exposed or configured
* correctly.
*/
public final static int SCHEDULE_RESULT_BAD_SERVICE = 4;
/**
* Indicates the cancel request seems to have been successful.
*/
public final static int CANCEL_RESULT_SUCCESS = 0;
/**
* Indicates the cancel request encountered an unknown error.
*/
public final static int CANCEL_RESULT_UNKNOWN_ERROR = 1;
/**
* Indicates the cancel request failed because the driver was unavailable.
*/
public final static int CANCEL_RESULT_NO_DRIVER_AVAILABLE = 2;
/**
* The backing Driver for this instance.
*/
private final Driver mDriver;
/**
* The ValidationEnforcer configured for the current Driver.
*/
private final ValidationEnforcer mValidator;
/**
* Single instance of a RetryStrategy.Builder, configured with the current driver's validation
* settings. We can do this because the RetryStrategy.Builder is stateless.
*/
private RetryStrategy.Builder mRetryStrategyBuilder;
/**
* Instantiates a new FirebaseJobDispatcher using the provided Driver.
*/
public FirebaseJobDispatcher(Driver driver) {
mDriver = driver;
mValidator = new ValidationEnforcer(mDriver.getValidator());
mRetryStrategyBuilder = new RetryStrategy.Builder(mValidator);
}
/**
* Attempts to schedule the provided Job.
* <p>
* Returns one of the SCHEDULE_RESULT_ constants.
*/
@ScheduleResult
public int schedule(@NonNull Job job) {
if (!mDriver.isAvailable()) {
return SCHEDULE_RESULT_NO_DRIVER_AVAILABLE;
}
return mDriver.schedule(job);
}
/**
* Attempts to cancel the Job that matches the provided tag and endpoint.
* <p>
* Returns one of the CANCEL_RESULT_ constants.
*/
@CancelResult
public int cancel(@NonNull String tag) {
if (!mDriver.isAvailable()) {
return CANCEL_RESULT_NO_DRIVER_AVAILABLE;
}
return mDriver.cancel(tag);
}
/**
* Attempts to cancel all Jobs registered for this package.
* <p>
* Returns one of the CANCEL_RESULT_ constants.
*/
@CancelResult
public int cancelAll() {
if (!mDriver.isAvailable()) {
return CANCEL_RESULT_NO_DRIVER_AVAILABLE;
}
return mDriver.cancelAll();
}
/**
* Attempts to schedule the provided Job, throwing an exception if it fails.
*
* @throws ScheduleFailedException
*/
public void mustSchedule(Job job) {
if (schedule(job) != SCHEDULE_RESULT_SUCCESS) {
throw new ScheduleFailedException();
}
}
/**
* Returns a ValidationEnforcer configured for the current Driver.
*/
public ValidationEnforcer getValidator() {
return mValidator;
}
/**
* Creates a new Job.Builder, configured with the current driver's validation settings.
*/
@NonNull
public Job.Builder newJobBuilder() {
return new Job.Builder(mValidator);
}
/**
* Creates a new RetryStrategy from the provided parameters, validated with the current driver's
* {@link JobValidator}.
*
* @param policy the backoff policy to use. One of the {@link RetryPolicy} constants.
* @param initialBackoff the initial backoff, in seconds.
* @param maximumBackoff the maximum backoff, in seconds.
* @throws ValidationEnforcer.ValidationException
* @see RetryStrategy
*/
public RetryStrategy newRetryStrategy(@RetryPolicy int policy, int initialBackoff,
int maximumBackoff) {
return mRetryStrategyBuilder.build(policy, initialBackoff, maximumBackoff);
}
/**
* Results that can legally be returned from {@link #schedule(Job)} calls.
*/
@IntDef({
SCHEDULE_RESULT_SUCCESS,
SCHEDULE_RESULT_UNKNOWN_ERROR,
SCHEDULE_RESULT_NO_DRIVER_AVAILABLE,
SCHEDULE_RESULT_UNSUPPORTED_TRIGGER,
SCHEDULE_RESULT_BAD_SERVICE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ScheduleResult {
}
/**
* Results that can legally be returned from {@link #cancel(String)} or {@link #cancelAll()}
* calls.
*/
@IntDef({
CANCEL_RESULT_SUCCESS,
CANCEL_RESULT_UNKNOWN_ERROR,
CANCEL_RESULT_NO_DRIVER_AVAILABLE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CancelResult {
}
/**
* Thrown when a {@link FirebaseJobDispatcher#schedule(com.firebase.jobdispatcher.Job)} call
* fails.
*/
public final static class ScheduleFailedException extends RuntimeException {
}
}

View File

@ -1,247 +0,0 @@
// Copyright 2016 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.annotation.SuppressLint;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Pair;
import java.util.ArrayList;
/**
* Responsible for extracting a JobCallback from a given Bundle.
*
* <p>Google Play services will send the Binder packed inside a simple strong Binder wrapper ({@link
* #PENDING_CALLBACK_CLASS}) under the key {@link #BUNDLE_KEY_CALLBACK "callback"}.
*/
/* package */ final class GooglePlayCallbackExtractor {
private static final String TAG = GooglePlayReceiver.TAG;
private static final String ERROR_NULL_CALLBACK = "No callback received, terminating";
private static final String ERROR_INVALID_CALLBACK = "Bad callback received, terminating";
/** The Parcelable class that wraps the Binder we need to access. */
private static final String PENDING_CALLBACK_CLASS =
"com.google.android.gms.gcm.PendingCallback";
/** The key for the wrapped Binder. */
private static final String BUNDLE_KEY_CALLBACK = "callback";
/** A magic number that indicates the following bytes belong to a Bundle. */
private static final int BUNDLE_MAGIC = 0x4C444E42;
/** A magic number that indicates the following value is a Parcelable. */
private static final int VAL_PARCELABLE = 4;
// GuardedBy("GooglePlayCallbackExtractor.class")
private static Boolean shouldReadKeysAsStringsCached = null;
public Pair<JobCallback, Bundle> extractCallback(@Nullable Bundle data) {
if (data == null) {
Log.e(TAG, ERROR_NULL_CALLBACK);
return null;
}
return extractWrappedBinderFromParcel(data);
}
/**
* Bundles are written out in the following format:
* A header, which consists of:
* <ol>
* <li>length (int)</li>
* <li>magic number ({@link #BUNDLE_MAGIC}) (int)</li>
* <li>number of entries (int)</li>
* </ol>
* <p/>
* Then the map values, each of which looks like this:
* <ol>
* <li>string key</li>
* <li>int type marker</li>
* <li>(any) parceled value</li>
* </ol>
* <p/>
* We're just going to iterate over the map looking for the right key (BUNDLE_KEY_CALLBACK)
* and try and read the IBinder straight from the parcelled data. This is entirely dependent
* on the implementation of Parcel, but these specific parts of Parcel / Bundle haven't
* changed since 2008 and newer versions of Android will ship with newer versions of Google
* Play services which embed the IBinder directly into the Bundle (no need to deal with the
* Parcelable issues).
*/
@Nullable
@SuppressLint("ParcelClassLoader")
private Pair<JobCallback, Bundle> extractWrappedBinderFromParcel(Bundle data) {
Bundle cleanBundle = new Bundle();
Parcel serialized = toParcel(data);
JobCallback callback = null;
try {
int length = serialized.readInt();
if (length <= 0) {
// Empty Bundle
Log.w(TAG, ERROR_NULL_CALLBACK);
return null;
}
int magic = serialized.readInt();
if (magic != BUNDLE_MAGIC) {
// Not a Bundle
Log.w(TAG, ERROR_NULL_CALLBACK);
return null;
}
int numEntries = serialized.readInt();
for (int i = 0; i < numEntries; i++) {
String entryKey = readKey(serialized);
if (entryKey == null) {
continue;
}
if (!(callback == null && BUNDLE_KEY_CALLBACK.equals(entryKey))) {
// If it's not the 'callback' key, we can just read it using the standard
// mechanisms because we're not afraid of rogue BadParcelableExceptions.
Object value = serialized.readValue(null /* class loader */);
if (value instanceof String) {
cleanBundle.putString(entryKey, (String) value);
} else if (value instanceof Boolean) {
cleanBundle.putBoolean(entryKey, (boolean) value);
} else if (value instanceof Integer) {
cleanBundle.putInt(entryKey, (int) value);
} else if (value instanceof ArrayList) {
// The only acceptable ArrayList in a Bundle is one that consists entirely
// of Parcelables, so this cast is safe.
@SuppressWarnings("unchecked") // safe by specification
ArrayList<Parcelable> arrayList = (ArrayList<Parcelable>) value;
cleanBundle.putParcelableArrayList(entryKey, arrayList);
} else if (value instanceof Bundle) {
cleanBundle.putBundle(entryKey, (Bundle) value);
} else if (value instanceof Parcelable) {
cleanBundle.putParcelable(entryKey, (Parcelable) value);
}
// Move to the next key
continue;
}
int typeTag = serialized.readInt();
if (typeTag != VAL_PARCELABLE) {
// If the key is correct ("callback"), but it's not a Parcelable then something
// went wrong and we should bail.
Log.w(TAG, ERROR_INVALID_CALLBACK);
return null;
}
String clsname = serialized.readString();
if (!PENDING_CALLBACK_CLASS.equals(clsname)) {
// If it's a Parcelable, but not one we recognize then we should not try and
// unpack it.
Log.w(TAG, ERROR_INVALID_CALLBACK);
return null;
}
// Instead of trying to instantiate clsname, we'll just read its single member.
IBinder remote = serialized.readStrongBinder();
callback = new GooglePlayJobCallback(remote);
}
if (callback == null) {
Log.w(TAG, ERROR_NULL_CALLBACK);
return null;
}
return Pair.create(callback, cleanBundle);
} finally {
serialized.recycle();
}
}
private static Parcel toParcel(Bundle data) {
Parcel serialized = Parcel.obtain();
data.writeToParcel(serialized, 0);
serialized.setDataPosition(0);
return serialized;
}
/**
* Reads the next key (String) from the provided {@code serialized} Parcel.
*
* <p>Naively using {@link Parcel#readString()} fails on versions of Android older than L,
* whereas {@link Parcel#readValue(ClassLoader)} works on older versions but fails on anything L
* or newer.
*/
private String readKey(Parcel serialized) {
if (shouldReadKeysAsStrings()) {
return serialized.readString();
}
// Older platforms require readValue
Object entryKeyObj = serialized.readValue(null /* Use the system ClassLoader */);
if (!(entryKeyObj instanceof String)) {
// Should never happen (Bundle keys are always Strings)
Log.w(TAG, ERROR_INVALID_CALLBACK);
return null;
}
return (String) entryKeyObj;
}
/**
* Checks whether {@link Parcel#readString()} or {@link Parcel#readValue()} should be used to
* access Bundle keys from a serialized Parcel. Commit {@link
* https://android.googlesource.com/platform/frameworks/base/+/9c3e74f
* I57bda9eb79ceaaa9c1b94ad49d9e462b52102149} (which only officially landed in Lollipop) changed
* from using writeValue to writeString for Bundle keys. Some OEMs have pulled this change into
* their KitKat fork, so we can't trust the SDK version check. Instead, we'll write a dummy
* Bundle to a Parcel and figure it out using that.
*
* The check is cached because the result can't change during runtime.
*/
private static synchronized boolean shouldReadKeysAsStrings() {
// We're pretty sure that readString() should always be used on L+, but if we shortcircuit
// this check then we have no evidence that this code is functioning correctly on KitKat
// devices that have the corresponding writeString() change.
if (shouldReadKeysAsStringsCached == null) {
final String expectedKey = "key";
Bundle testBundle = new Bundle();
testBundle.putString(expectedKey, "value");
Parcel testParcel = toParcel(testBundle);
try {
// length
checkCondition(testParcel.readInt() > 0);
// magic
checkCondition(testParcel.readInt() == BUNDLE_MAGIC);
// num entries
checkCondition(testParcel.readInt() == 1);
shouldReadKeysAsStringsCached = expectedKey.equals(testParcel.readString());
} catch (RuntimeException e) {
shouldReadKeysAsStringsCached = Boolean.FALSE;
} finally {
testParcel.recycle();
}
}
return shouldReadKeysAsStringsCached;
}
/** Throws an {@code IllegalStateException} if {@code condition} is false. */
private static void checkCondition(boolean condition) {
if (!condition) {
throw new IllegalStateException();
}
}
}

View File

@ -1,162 +0,0 @@
// Copyright 2016 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.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
import com.firebase.jobdispatcher.FirebaseJobDispatcher.ScheduleResult;
/**
* GooglePlayDriver provides an implementation of Driver for devices with Google Play
* services installed. This backend does not do any availability checks and any uses should be
* guarded with a call to {@code GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)}
*
* @see
* <a href="https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)">GoogleApiAvailability</a>
*/
public final class GooglePlayDriver implements Driver {
static final String BACKEND_PACKAGE = "com.google.android.gms";
private final static String ACTION_SCHEDULE = "com.google.android.gms.gcm.ACTION_SCHEDULE";
private final static String BUNDLE_PARAM_SCHEDULER_ACTION = "scheduler_action";
private final static String BUNDLE_PARAM_TAG = "tag";
private final static String BUNDLE_PARAM_TOKEN = "app";
private final static String BUNDLE_PARAM_COMPONENT = "component";
private final static String SCHEDULER_ACTION_SCHEDULE_TASK = "SCHEDULE_TASK";
private final static String SCHEDULER_ACTION_CANCEL_TASK = "CANCEL_TASK";
private final static String SCHEDULER_ACTION_CANCEL_ALL = "CANCEL_ALL";
private static final String INTENT_PARAM_SOURCE = "source";
private static final String INTENT_PARAM_SOURCE_VERSION = "source_version";
private static final int JOB_DISPATCHER_SOURCE_CODE = 1 << 3;
private static final int JOB_DISPATCHER_SOURCE_VERSION_CODE = 1;
private final JobValidator mValidator;
/**
* The application Context. Used to send broadcasts.
*/
private final Context mContext;
/**
* A PendingIntent from this package. Passed inside the broadcast so the receiver can verify the
* sender's package.
*/
private final PendingIntent mToken;
/**
* Turns Jobs into Bundles.
*/
private final GooglePlayJobWriter mWriter;
/**
* Instantiates a new GooglePlayDriver.
*/
public GooglePlayDriver(Context context) {
mContext = context;
mToken = PendingIntent.getBroadcast(context, 0, new Intent(), 0);
mWriter = new GooglePlayJobWriter();
mValidator = new DefaultJobValidator(context);
}
@Override
public boolean isAvailable() {
ApplicationInfo applicationInfo = null;
try {
applicationInfo = mContext.getPackageManager().getApplicationInfo(BACKEND_PACKAGE, 0);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return applicationInfo != null && applicationInfo.enabled;
}
/**
* Schedules the provided Job.
*/
@Override
@ScheduleResult
public int schedule(@NonNull Job job) {
mContext.sendBroadcast(createScheduleRequest(job));
return FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS;
}
@Override
public int cancel(@NonNull String tag) {
mContext.sendBroadcast(createCancelRequest(tag));
return FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
}
@Override
public int cancelAll() {
mContext.sendBroadcast(createBatchCancelRequest());
return FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS;
}
@NonNull
protected Intent createCancelRequest(@NonNull String tag) {
Intent cancelReq = createSchedulerIntent(SCHEDULER_ACTION_CANCEL_TASK);
cancelReq.putExtra(BUNDLE_PARAM_TAG, tag);
cancelReq.putExtra(BUNDLE_PARAM_COMPONENT, new ComponentName(mContext, getReceiverClass()));
return cancelReq;
}
@NonNull
protected Intent createBatchCancelRequest() {
Intent cancelReq = createSchedulerIntent(SCHEDULER_ACTION_CANCEL_ALL);
cancelReq.putExtra(BUNDLE_PARAM_COMPONENT, new ComponentName(mContext, getReceiverClass()));
return cancelReq;
}
@NonNull
protected Class<GooglePlayReceiver> getReceiverClass() {
return GooglePlayReceiver.class;
}
@NonNull
@Override
public JobValidator getValidator() {
return mValidator;
}
@NonNull
private Intent createScheduleRequest(JobParameters job) {
Intent scheduleReq = createSchedulerIntent(SCHEDULER_ACTION_SCHEDULE_TASK);
scheduleReq.putExtras(mWriter.writeToBundle(job, scheduleReq.getExtras()));
return scheduleReq;
}
@NonNull
private Intent createSchedulerIntent(String schedulerAction) {
Intent scheduleReq = new Intent(ACTION_SCHEDULE);
scheduleReq.setPackage(BACKEND_PACKAGE);
scheduleReq.putExtra(BUNDLE_PARAM_SCHEDULER_ACTION, schedulerAction);
scheduleReq.putExtra(BUNDLE_PARAM_TOKEN, mToken);
scheduleReq.putExtra(INTENT_PARAM_SOURCE, JOB_DISPATCHER_SOURCE_CODE);
scheduleReq.putExtra(INTENT_PARAM_SOURCE_VERSION, JOB_DISPATCHER_SOURCE_VERSION_CODE);
return scheduleReq;
}
}

View File

@ -1,56 +0,0 @@
// Copyright 2016 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.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
/**
* Wraps the GooglePlay-specific callback class in a JobCallback-compatible interface.
*/
/* package */ final class GooglePlayJobCallback implements JobCallback {
private static final String DESCRIPTOR = "com.google.android.gms.gcm.INetworkTaskCallback";
/** The only supported transaction ID. */
private static final int TRANSACTION_TASK_FINISHED = IBinder.FIRST_CALL_TRANSACTION + 1;
private final IBinder mRemote;
public GooglePlayJobCallback(IBinder binder) {
mRemote = binder;
}
@Override
public void jobFinished(@JobService.JobResult int status) {
Parcel request = Parcel.obtain();
Parcel response = Parcel.obtain();
try {
request.writeInterfaceToken(DESCRIPTOR);
request.writeInt(status);
mRemote.transact(TRANSACTION_TASK_FINISHED, request, response, 0);
response.readException();
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
request.recycle();
response.recycle();
}
}
}

View File

@ -1,198 +0,0 @@
// Copyright 2016 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.net.Uri;
import android.os.Bundle;
import android.support.annotation.IntDef;
import android.support.annotation.VisibleForTesting;
import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger;
import com.firebase.jobdispatcher.RetryStrategy.RetryPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/* package */ final class GooglePlayJobWriter {
static final String REQUEST_PARAM_UPDATE_CURRENT = "update_current";
static final String REQUEST_PARAM_EXTRAS = "extras";
static final String REQUEST_PARAM_PERSISTED = "persisted";
static final String REQUEST_PARAM_REQUIRED_NETWORK = "requiredNetwork";
static final String REQUEST_PARAM_REQUIRES_CHARGING = "requiresCharging";
static final String REQUEST_PARAM_REQUIRES_IDLE = "requiresIdle";
static final String REQUEST_PARAM_RETRY_STRATEGY = "retryStrategy";
static final String REQUEST_PARAM_SERVICE = "service";
static final String REQUEST_PARAM_TAG = "tag";
static final String REQUEST_PARAM_RETRY_STRATEGY_INITIAL_BACKOFF_SECONDS =
"initial_backoff_seconds";
static final String REQUEST_PARAM_RETRY_STRATEGY_MAXIMUM_BACKOFF_SECONDS =
"maximum_backoff_seconds";
static final String REQUEST_PARAM_RETRY_STRATEGY_POLICY = "retry_policy";
static final String REQUEST_PARAM_TRIGGER_TYPE = "trigger_type";
static final String REQUEST_PARAM_TRIGGER_WINDOW_END = "window_end";
static final String REQUEST_PARAM_TRIGGER_WINDOW_FLEX = "period_flex";
static final String REQUEST_PARAM_TRIGGER_WINDOW_PERIOD = "period";
static final String REQUEST_PARAM_TRIGGER_WINDOW_START = "window_start";
@VisibleForTesting
/* package */ static final int LEGACY_RETRY_POLICY_EXPONENTIAL = 0;
@VisibleForTesting
/* package */ static final int LEGACY_RETRY_POLICY_LINEAR = 1;
@VisibleForTesting
/* package */ final static int LEGACY_NETWORK_UNMETERED = 1;
@VisibleForTesting
/* package */ final static int LEGACY_NETWORK_CONNECTED = 0;
@VisibleForTesting
/* package */ final static int LEGACY_NETWORK_ANY = 2;
private JobCoder jobCoder = new JobCoder(BundleProtocol.PACKED_PARAM_BUNDLE_PREFIX, false);
private static void writeExecutionWindowTriggerToBundle(JobParameters job, Bundle b,
JobTrigger.ExecutionWindowTrigger trigger) {
b.putInt(REQUEST_PARAM_TRIGGER_TYPE, BundleProtocol.TRIGGER_TYPE_EXECUTION_WINDOW);
if (job.isRecurring()) {
b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_PERIOD,
trigger.getWindowEnd());
b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_FLEX,
trigger.getWindowEnd() - trigger.getWindowStart());
} else {
b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_START,
trigger.getWindowStart());
b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_END,
trigger.getWindowEnd());
}
}
private static void writeImmediateTriggerToBundle(Bundle b) {
b.putInt(REQUEST_PARAM_TRIGGER_TYPE, BundleProtocol.TRIGGER_TYPE_IMMEDIATE);
b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_START, 0);
b.putLong(REQUEST_PARAM_TRIGGER_WINDOW_END, 30);
}
private void writeContentUriTriggerToBundle(Bundle data, ContentUriTrigger uriTrigger) {
data.putInt(BundleProtocol.PACKED_PARAM_TRIGGER_TYPE,
BundleProtocol.TRIGGER_TYPE_CONTENT_URI);
int size = uriTrigger.getUris().size();
int[] flagsArray = new int[size];
Uri[] uriArray = new Uri[size];
for (int i = 0; i < size; i++) {
ObservedUri uri = uriTrigger.getUris().get(i);
flagsArray[i] = uri.getFlags();
uriArray[i] = uri.getUri();
}
data.putIntArray(BundleProtocol.PACKED_PARAM_CONTENT_URI_FLAGS_ARRAY, flagsArray);
data.putParcelableArray(BundleProtocol.PACKED_PARAM_CONTENT_URI_ARRAY, uriArray);
}
public Bundle writeToBundle(JobParameters job, Bundle b) {
b.putString(REQUEST_PARAM_TAG, job.getTag());
b.putBoolean(REQUEST_PARAM_UPDATE_CURRENT, job.shouldReplaceCurrent());
boolean persisted = job.getLifetime() == Lifetime.FOREVER;
b.putBoolean(REQUEST_PARAM_PERSISTED, persisted);
b.putString(REQUEST_PARAM_SERVICE, GooglePlayReceiver.class.getName());
writeTriggerToBundle(job, b);
writeConstraintsToBundle(job, b);
writeRetryStrategyToBundle(job, b);
// Embed the job spec (minus extras) into the extras (under a prefix)
Bundle extras = job.getExtras();
if (extras == null) {
extras = new Bundle();
}
b.putBundle(REQUEST_PARAM_EXTRAS, jobCoder.encode(job, extras));
return b;
}
private void writeRetryStrategyToBundle(JobParameters job, Bundle b) {
RetryStrategy strategy = job.getRetryStrategy();
Bundle rb = new Bundle();
rb.putInt(REQUEST_PARAM_RETRY_STRATEGY_POLICY,
convertRetryPolicyToLegacyVersion(strategy.getPolicy()));
rb.putInt(REQUEST_PARAM_RETRY_STRATEGY_INITIAL_BACKOFF_SECONDS,
strategy.getInitialBackoff());
rb.putInt(REQUEST_PARAM_RETRY_STRATEGY_MAXIMUM_BACKOFF_SECONDS,
strategy.getMaximumBackoff());
b.putBundle(REQUEST_PARAM_RETRY_STRATEGY, rb);
}
private int convertRetryPolicyToLegacyVersion(@RetryPolicy int policy) {
switch (policy) {
case RetryStrategy.RETRY_POLICY_LINEAR:
return LEGACY_RETRY_POLICY_LINEAR;
case RetryStrategy.RETRY_POLICY_EXPONENTIAL:
// fallthrough
default:
return LEGACY_RETRY_POLICY_EXPONENTIAL;
}
}
private void writeTriggerToBundle(JobParameters job, Bundle b) {
final JobTrigger trigger = job.getTrigger();
if (trigger == Trigger.NOW) {
writeImmediateTriggerToBundle(b);
} else if (trigger instanceof JobTrigger.ExecutionWindowTrigger) {
writeExecutionWindowTriggerToBundle(job, b, (JobTrigger.ExecutionWindowTrigger) trigger);
} else if (trigger instanceof JobTrigger.ContentUriTrigger) {
writeContentUriTriggerToBundle(b, (JobTrigger.ContentUriTrigger) trigger);
} else {
throw new IllegalArgumentException("Unknown trigger: " + trigger.getClass());
}
}
private void writeConstraintsToBundle(JobParameters job, Bundle b) {
int c = Constraint.compact(job.getConstraints());
b.putBoolean(REQUEST_PARAM_REQUIRES_CHARGING,
(c & Constraint.DEVICE_CHARGING) == Constraint.DEVICE_CHARGING);
b.putBoolean(REQUEST_PARAM_REQUIRES_IDLE,
(c & Constraint.DEVICE_IDLE) == Constraint.DEVICE_IDLE);
b.putInt(REQUEST_PARAM_REQUIRED_NETWORK, convertConstraintsToLegacyNetConstant(c));
}
/**
* Converts a bitmap of Constraint values into a LegacyNetworkConstraint constant (int).
*/
@LegacyNetworkConstant
private int convertConstraintsToLegacyNetConstant(int constraintMap) {
int reqNet = LEGACY_NETWORK_ANY;
reqNet = (constraintMap & Constraint.ON_ANY_NETWORK) == Constraint.ON_ANY_NETWORK
? LEGACY_NETWORK_CONNECTED
: reqNet;
reqNet = (constraintMap & Constraint.ON_UNMETERED_NETWORK) == Constraint.ON_UNMETERED_NETWORK
? LEGACY_NETWORK_UNMETERED
: reqNet;
return reqNet;
}
@IntDef({LEGACY_NETWORK_ANY, LEGACY_NETWORK_CONNECTED, LEGACY_NETWORK_UNMETERED})
@Retention(RetentionPolicy.SOURCE)
private @interface LegacyNetworkConstant {}
}

View File

@ -1,114 +0,0 @@
// Copyright 2016 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.firebase.jobdispatcher.GooglePlayJobWriter.REQUEST_PARAM_TAG;
import static com.firebase.jobdispatcher.GooglePlayReceiver.TAG;
import android.annotation.TargetApi;
import android.app.AppOpsManager;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.util.Log;
import com.firebase.jobdispatcher.JobInvocation.Builder;
/**
* A messenger for communication with GCM Network Scheduler.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
class GooglePlayMessageHandler extends Handler {
static final int MSG_START_EXEC = 1;
static final int MSG_STOP_EXEC = 2;
static final int MSG_RESULT = 3;
private static final int MSG_INIT = 4;
private final GooglePlayReceiver googlePlayReceiver;
public GooglePlayMessageHandler(Looper mainLooper, GooglePlayReceiver googlePlayReceiver) {
super(mainLooper);
this.googlePlayReceiver = googlePlayReceiver;
}
@Override
public void handleMessage(Message message) {
if (message == null) {
return;
}
AppOpsManager appOpsManager = (AppOpsManager) googlePlayReceiver.getApplicationContext()
.getSystemService(Context.APP_OPS_SERVICE);
try {
appOpsManager.checkPackage(message.sendingUid, GooglePlayDriver.BACKEND_PACKAGE);
} catch (SecurityException e) {
Log.e(TAG, "Message was not sent from GCM.");
return;
}
switch (message.what) {
case MSG_START_EXEC:
handleStartMessage(message);
break;
case MSG_STOP_EXEC:
handleStopMessage(message);
break;
case MSG_INIT:
// Not implemented.
break;
default:
Log.e(TAG, "Unrecognized message received: " + message);
break;
}
}
private void handleStartMessage(Message message) {
final Bundle data = message.getData();
final Messenger replyTo = message.replyTo;
String tag = data.getString(REQUEST_PARAM_TAG);
if (replyTo == null || tag == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Invalid start execution message.");
}
return;
}
GooglePlayMessengerCallback messengerCallback =
new GooglePlayMessengerCallback(replyTo, tag);
JobInvocation jobInvocation = googlePlayReceiver.prepareJob(messengerCallback, data);
googlePlayReceiver.getExecutionDelegator().executeJob(jobInvocation);
}
private void handleStopMessage(Message message) {
Builder builder = GooglePlayReceiver.getJobCoder().decode(message.getData());
if (builder == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Invalid stop execution message.");
}
return;
}
JobInvocation job = builder.build();
googlePlayReceiver.getExecutionDelegator().stopJob(job);
}
}

View File

@ -1,61 +0,0 @@
// Copyright 2016 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.firebase.jobdispatcher.GooglePlayJobWriter.REQUEST_PARAM_TAG;
import android.os.Bundle;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import com.firebase.jobdispatcher.JobService.JobResult;
/**
* Wraps the GooglePlay messenger in a JobCallback-compatible interface.
*/
class GooglePlayMessengerCallback implements JobCallback {
private final Messenger messenger;
private final String tag;
GooglePlayMessengerCallback(Messenger messenger, String tag) {
this.messenger = messenger;
this.tag = tag;
}
@Override
public void jobFinished(@JobResult int status) {
try {
messenger.send(createResultMessage(status));
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
@NonNull
private Message createResultMessage(int result) {
final Message msg = Message.obtain();
msg.what = GooglePlayMessageHandler.MSG_RESULT;
msg.arg1 = result;
Bundle b = new Bundle();
b.putString(REQUEST_PARAM_TAG, tag);
msg.setData(b);
return msg;
}
}

View File

@ -1,274 +0,0 @@
// Copyright 2016 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.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
import android.os.Messenger;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.util.SimpleArrayMap;
import android.util.Log;
import android.util.Pair;
import com.firebase.jobdispatcher.Job.Builder;
import com.firebase.jobdispatcher.JobService.JobResult;
import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger;
/**
* Handles incoming execute requests from the GooglePlay driver and forwards them to your Service.
*/
public class GooglePlayReceiver extends Service implements ExecutionDelegator.JobFinishedCallback {
/**
* Logging tag.
*/
/* package */ static final String TAG = "FJD.GooglePlayReceiver";
/**
* The action sent by Google Play services that triggers job execution.
*/
@VisibleForTesting
static final String ACTION_EXECUTE = "com.google.android.gms.gcm.ACTION_TASK_READY";
/** Action sent by Google Play services when your app has been updated. */
@VisibleForTesting
static final String ACTION_INITIALIZE = "com.google.android.gms.gcm.SERVICE_ACTION_INITIALIZE";
private static final String ERROR_NULL_INTENT = "Null Intent passed, terminating";
private static final String ERROR_UNKNOWN_ACTION = "Unknown action received, terminating";
private static final String ERROR_NO_DATA = "No data provided, terminating";
private static final JobCoder prefixedCoder =
new JobCoder(BundleProtocol.PACKED_PARAM_BUNDLE_PREFIX, true);
private final GooglePlayCallbackExtractor callbackExtractor = new GooglePlayCallbackExtractor();
/**
* The single Messenger that's returned from valid onBind requests. Guarded by intrinsic lock.
*/
@VisibleForTesting
Messenger serviceMessenger;
/**
* Driver for rescheduling jobs. Guarded by intrinsic lock.
*/
@VisibleForTesting
Driver driver;
/**
* Guarded by intrinsic lock.
*/
@VisibleForTesting
ValidationEnforcer validationEnforcer;
/**
* The ExecutionDelegator used to communicate with client JobServices.
* Guarded by intrinsic lock.
*/
private ExecutionDelegator executionDelegator;
/**
* The most recent startId passed to onStartCommand.
* Guarded by intrinsic lock.
*/
private int latestStartId;
/**
* Endpoint (String) -> Tag (String) -> JobCallback
*/
private SimpleArrayMap<String, SimpleArrayMap<String, JobCallback>> callbacks =
new SimpleArrayMap<>(1);
private static void sendResultSafely(JobCallback callback, int result) {
try {
callback.jobFinished(result);
} catch (Throwable e) {
Log.e(TAG, "Encountered error running callback", e.getCause());
}
}
@Override
public final int onStartCommand(Intent intent, int flags, int startId) {
try {
super.onStartCommand(intent, flags, startId);
if (intent == null) {
Log.w(TAG, ERROR_NULL_INTENT);
return START_NOT_STICKY;
}
String action = intent.getAction();
if (ACTION_EXECUTE.equals(action)) {
getExecutionDelegator().executeJob(prepareJob(intent));
return START_NOT_STICKY;
} else if (ACTION_INITIALIZE.equals(action)) {
return START_NOT_STICKY;
}
Log.e(TAG, ERROR_UNKNOWN_ACTION);
return START_NOT_STICKY;
} finally {
synchronized (this) {
latestStartId = startId;
if (callbacks.isEmpty()) {
stopSelf(latestStartId);
}
}
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
// Only Lollipop+ supports UID checking messages, so we can't trust this system on older
// platforms.
if (intent == null
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
|| !ACTION_EXECUTE.equals(intent.getAction())) {
return null;
}
return getServiceMessenger().getBinder();
}
private synchronized Messenger getServiceMessenger() {
if (serviceMessenger == null) {
serviceMessenger =
new Messenger(new GooglePlayMessageHandler(Looper.getMainLooper(), this));
}
return serviceMessenger;
}
/* package */ synchronized ExecutionDelegator getExecutionDelegator() {
if (executionDelegator == null) {
executionDelegator = new ExecutionDelegator(this, this);
}
return executionDelegator;
}
@NonNull
private synchronized Driver getGooglePlayDriver() {
if (driver == null) {
driver = new GooglePlayDriver(getApplicationContext());
}
return driver;
}
@NonNull
private synchronized ValidationEnforcer getValidationEnforcer() {
if (validationEnforcer == null) {
validationEnforcer = new ValidationEnforcer(getGooglePlayDriver().getValidator());
}
return validationEnforcer;
}
@Nullable
@VisibleForTesting
JobInvocation prepareJob(Intent intent) {
Bundle intentExtras = intent.getExtras();
if (intentExtras == null) {
Log.e(TAG, ERROR_NO_DATA);
return null;
}
// get the callback first. If we don't have this we can't talk back to the backend.
Pair<JobCallback, Bundle> extraction = callbackExtractor.extractCallback(intentExtras);
if (extraction == null) {
Log.i(TAG, "no callback found");
return null;
}
return prepareJob(extraction.first, extraction.second);
}
@Nullable
synchronized JobInvocation prepareJob(JobCallback callback, Bundle bundle) {
JobInvocation job = prefixedCoder.decodeIntentBundle(bundle);
if (job == null) {
Log.e(TAG, "unable to decode job");
sendResultSafely(callback, JobService.RESULT_FAIL_NORETRY);
return null;
}
SimpleArrayMap<String, JobCallback> map = callbacks.get(job.getService());
if (map == null) {
map = new SimpleArrayMap<>(1);
callbacks.put(job.getService(), map);
}
map.put(job.getTag(), callback);
return job;
}
@Override
public synchronized void onJobFinished(@NonNull JobInvocation js, @JobResult int result) {
try {
SimpleArrayMap<String, JobCallback> map = callbacks.get(js.getService());
if (map == null) {
return;
}
JobCallback callback = map.remove(js.getTag());
if (callback == null) {
return;
}
if (map.isEmpty()) {
callbacks.remove(js.getService());
}
if (needsToBeRescheduled(js, result)) {
reschedule(js);
} else {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "sending jobFinished for " + js.getTag() + " = " + result);
}
sendResultSafely(callback, result);
}
} finally {
if (callbacks.isEmpty()) {
// Safe to call stopSelf, even if we're being bound to
stopSelf(latestStartId);
}
}
}
private void reschedule(JobInvocation jobInvocation) {
Job job = new Builder(getValidationEnforcer(), jobInvocation)
.setReplaceCurrent(true)
.build();
getGooglePlayDriver().schedule(job);
}
/**
* Recurring content URI triggered jobs need to be rescheduled when execution is finished.
*
* <p>GooglePlay does not support recurring content URI triggered jobs.
*
* <p>{@link JobService#RESULT_FAIL_RETRY} needs to be sent or current triggered URIs will be
* lost.
*/
private static boolean needsToBeRescheduled(JobParameters job, int result) {
return job.isRecurring()
&& job.getTrigger() instanceof ContentUriTrigger
&& result != JobService.RESULT_FAIL_RETRY;
}
static JobCoder getJobCoder() {
return prefixedCoder;
}
}

View File

@ -1,378 +0,0 @@
// Copyright 2016 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.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.firebase.jobdispatcher.Constraint.JobConstraint;
/**
* Job is the embodiment of a unit of work and an associated set of triggers, settings, and runtime
* constraints.
*/
public final class Job implements JobParameters {
private final String mService;
private final String mTag;
private final JobTrigger mTrigger;
private final RetryStrategy mRetryStrategy;
private final int mLifetime;
private final boolean mRecurring;
private final int[] mConstraints;
private final boolean mReplaceCurrent;
private Bundle mExtras;
private Job(Builder builder) {
mService = builder.mServiceClassName;
mExtras = builder.mExtras;
mTag = builder.mTag;
mTrigger = builder.mTrigger;
mRetryStrategy = builder.mRetryStrategy;
mLifetime = builder.mLifetime;
mRecurring = builder.mRecurring;
mConstraints = builder.mConstraints != null ? builder.mConstraints : new int[0];
mReplaceCurrent = builder.mReplaceCurrent;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public int[] getConstraints() {
return mConstraints;
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
public Bundle getExtras() {
return mExtras;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public RetryStrategy getRetryStrategy() {
return mRetryStrategy;
}
/**
* {@inheritDoc}
*/
@Override
public boolean shouldReplaceCurrent() {
return mReplaceCurrent;
}
@Nullable
@Override
public TriggerReason getTriggerReason() {
return null;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public String getTag() {
return mTag;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public JobTrigger getTrigger() {
return mTrigger;
}
/**
* {@inheritDoc}
*/
@Override
public int getLifetime() {
return mLifetime;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isRecurring() {
return mRecurring;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public String getService() {
return mService;
}
/**
* A class that understands how to build a {@link Job}. Retrieved by calling
* {@link FirebaseJobDispatcher#newJobBuilder()}.
*/
public final static class Builder implements JobParameters {
private final ValidationEnforcer mValidator;
private String mServiceClassName;
private Bundle mExtras;
private String mTag;
private JobTrigger mTrigger = Trigger.NOW;
private int mLifetime = Lifetime.UNTIL_NEXT_BOOT;
private int[] mConstraints;
private RetryStrategy mRetryStrategy = RetryStrategy.DEFAULT_EXPONENTIAL;
private boolean mReplaceCurrent = false;
private boolean mRecurring = false;
Builder(ValidationEnforcer validator) {
mValidator = validator;
}
Builder(ValidationEnforcer validator, JobParameters job) {
mValidator = validator;
mTag = job.getTag();
mServiceClassName = job.getService();
mTrigger = job.getTrigger();
mRecurring = job.isRecurring();
mLifetime = job.getLifetime();
mConstraints = job.getConstraints();
mExtras = job.getExtras();
mRetryStrategy = job.getRetryStrategy();
}
/**
* Adds the provided constraint to the current list of runtime constraints.
*/
public Builder addConstraint(@JobConstraint int constraint) {
// Create a new, longer constraints array
int[] newConstraints = new int[mConstraints == null ? 1 : mConstraints.length + 1];
if (mConstraints != null && mConstraints.length != 0) {
// Copy all the old values over
System.arraycopy(mConstraints, 0, newConstraints, 0, mConstraints.length);
}
// add the new value
newConstraints[newConstraints.length - 1] = constraint;
// update the pointer
mConstraints = newConstraints;
return this;
}
/**
* Sets whether this Job should replace pre-existing Jobs with the same tag.
*/
public Builder setReplaceCurrent(boolean replaceCurrent) {
mReplaceCurrent = replaceCurrent;
return this;
}
/**
* Builds the Job, using the settings provided so far.
*
* @throws ValidationEnforcer.ValidationException
*/
public Job build() {
mValidator.ensureValid(this);
return new Job(this);
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public String getService() {
return mServiceClassName;
}
/**
* Sets the backing JobService class for the Job. See {@link #getService()}.
*/
public Builder setService(Class<? extends JobService> serviceClass) {
mServiceClassName = serviceClass == null ? null : serviceClass.getName();
return this;
}
/**
* Sets the backing JobService class name for the Job. See {@link #getService()}.
*
* <p>Should not be exposed, for internal use only.
*/
Builder setServiceName(String serviceClassName) {
mServiceClassName = serviceClassName;
return this;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public String getTag() {
return mTag;
}
/**
* Sets the unique String tag used to identify the Job. See {@link #getTag()}.
*/
public Builder setTag(String tag) {
mTag = tag;
return this;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public JobTrigger getTrigger() {
return mTrigger;
}
/**
* Sets the Trigger used for the Job. See {@link #getTrigger()}.
*/
public Builder setTrigger(JobTrigger trigger) {
mTrigger = trigger;
return this;
}
/**
* {@inheritDoc}
*/
@Override
@Lifetime.LifetimeConstant
public int getLifetime() {
return mLifetime;
}
/**
* Sets the Job's lifetime, or how long it should persist. See {@link #getLifetime()}.
*/
public Builder setLifetime(@Lifetime.LifetimeConstant int lifetime) {
mLifetime = lifetime;
return this;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isRecurring() {
return mRecurring;
}
/**
* Sets whether the job should recur. The default is false.
*/
public Builder setRecurring(boolean recurring) {
mRecurring = recurring;
return this;
}
/**
* {@inheritDoc}
*/
@Override
@JobConstraint
public int[] getConstraints() {
return mConstraints == null ? new int[]{} : mConstraints;
}
/**
* Sets the Job's runtime constraints. See {@link #getConstraints()}.
*/
public Builder setConstraints(@JobConstraint int... constraints) {
mConstraints = constraints;
return this;
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
public Bundle getExtras() {
return mExtras;
}
/**
* Sets the user-defined extras associated with the Job. See {@link #getExtras()}.
*/
public Builder setExtras(Bundle extras) {
mExtras = extras;
return this;
}
/**
* {@inheritDoc}
*/
@NonNull
@Override
public RetryStrategy getRetryStrategy() {
return mRetryStrategy;
}
/**
* Set the RetryStrategy used for the Job. See {@link #getRetryStrategy()}.
*/
public Builder setRetryStrategy(RetryStrategy retryStrategy) {
mRetryStrategy = retryStrategy;
return this;
}
/**
* {@inheritDoc}
*/
@Override
public boolean shouldReplaceCurrent() {
return mReplaceCurrent;
}
@Nullable
@Override
public TriggerReason getTriggerReason() {
return null;
}
}
}

View File

@ -1,28 +0,0 @@
// Copyright 2016 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;
/**
* JobCallback describes an object that knows how to send a JobResult back to the underlying
* execution driver.
*/
public interface JobCallback {
/**
* @throws RuntimeException
*/
void jobFinished(@JobService.JobResult int status);
}

View File

@ -1,253 +0,0 @@
// Copyright 2016 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.firebase.jobdispatcher.Constraint.compact;
import static com.firebase.jobdispatcher.Constraint.uncompact;
import static com.firebase.jobdispatcher.ExecutionDelegator.TAG;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* JobCoder is a tool to encode and decode JobSpecs from Bundles.
*/
/* package */ final class JobCoder {
private final boolean includeExtras;
private final String prefix;
private static final String JSON_URI_FLAGS = "uri_flags";
private static final String JSON_URIS = "uris";
JobCoder(String prefix, boolean includeExtras) {
this.includeExtras = includeExtras;
this.prefix = prefix;
}
@NonNull
Bundle encode(@NonNull JobParameters jobParameters, @NonNull Bundle data) {
if (data == null) {
throw new IllegalArgumentException("Unexpected null Bundle provided");
}
data.putInt(prefix + BundleProtocol.PACKED_PARAM_LIFETIME,
jobParameters.getLifetime());
data.putBoolean(prefix + BundleProtocol.PACKED_PARAM_RECURRING,
jobParameters.isRecurring());
data.putBoolean(prefix + BundleProtocol.PACKED_PARAM_REPLACE_CURRENT,
jobParameters.shouldReplaceCurrent());
data.putString(prefix + BundleProtocol.PACKED_PARAM_TAG,
jobParameters.getTag());
data.putString(prefix + BundleProtocol.PACKED_PARAM_SERVICE,
jobParameters.getService());
data.putInt(prefix + BundleProtocol.PACKED_PARAM_CONSTRAINTS,
compact(jobParameters.getConstraints()));
if (includeExtras) {
data.putBundle(prefix + BundleProtocol.PACKED_PARAM_EXTRAS,
jobParameters.getExtras());
}
encodeTrigger(jobParameters.getTrigger(), data);
encodeRetryStrategy(jobParameters.getRetryStrategy(), data);
return data;
}
JobInvocation decodeIntentBundle(@NonNull Bundle bundle) {
if (bundle == null) {
Log.e(TAG, "Unexpected null Bundle provided");
return null;
}
Bundle taskExtras = bundle.getBundle(GooglePlayJobWriter.REQUEST_PARAM_EXTRAS);
if (taskExtras == null) {
return null;
}
JobInvocation.Builder builder = decode(taskExtras);
List<Uri> triggeredContentUris =
bundle.getParcelableArrayList(BundleProtocol.PACKED_PARAM_TRIGGERED_URIS);
if (triggeredContentUris != null) {
builder.setTriggerReason(new TriggerReason(triggeredContentUris));
}
return builder.build();
}
@Nullable
public JobInvocation.Builder decode(@NonNull Bundle data) {
if (data == null) {
throw new IllegalArgumentException("Unexpected null Bundle provided");
}
boolean recur = data.getBoolean(prefix + BundleProtocol.PACKED_PARAM_RECURRING);
boolean replaceCur = data.getBoolean(prefix + BundleProtocol.PACKED_PARAM_REPLACE_CURRENT);
int lifetime = data.getInt(prefix + BundleProtocol.PACKED_PARAM_LIFETIME);
int[] constraints = uncompact(data.getInt(prefix + BundleProtocol.PACKED_PARAM_CONSTRAINTS));
JobTrigger trigger = decodeTrigger(data);
RetryStrategy retryStrategy = decodeRetryStrategy(data);
String tag = data.getString(prefix + BundleProtocol.PACKED_PARAM_TAG);
String service = data.getString(prefix + BundleProtocol.PACKED_PARAM_SERVICE);
if (tag == null || service == null || trigger == null || retryStrategy == null) {
return null;
}
JobInvocation.Builder builder = new JobInvocation.Builder();
builder.setTag(tag);
builder.setService(service);
builder.setTrigger(trigger);
builder.setRetryStrategy(retryStrategy);
builder.setRecurring(recur);
//noinspection WrongConstant
builder.setLifetime(lifetime);
//noinspection WrongConstant
builder.setConstraints(constraints);
builder.setReplaceCurrent(replaceCur);
// repack the taskExtras
builder.addExtras(data);
return builder;
}
@NonNull
private JobTrigger decodeTrigger(Bundle data) {
switch (data.getInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_TYPE)) {
case BundleProtocol.TRIGGER_TYPE_IMMEDIATE:
return Trigger.NOW;
case BundleProtocol.TRIGGER_TYPE_EXECUTION_WINDOW:
return Trigger.executionWindow(
data.getInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_WINDOW_START),
data.getInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_WINDOW_END));
case BundleProtocol.TRIGGER_TYPE_CONTENT_URI:
String uris = data.getString(prefix + BundleProtocol.PACKED_PARAM_OBSERVED_URI);
List<ObservedUri> observedUris = convertJsonToObservedUris(uris);
return Trigger.contentUriTrigger(Collections.unmodifiableList(observedUris));
default:
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unsupported trigger.");
}
return null;
}
}
private void encodeTrigger(JobTrigger trigger, Bundle data) {
if (trigger == Trigger.NOW) {
data.putInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_TYPE,
BundleProtocol.TRIGGER_TYPE_IMMEDIATE);
} else if (trigger instanceof JobTrigger.ExecutionWindowTrigger) {
JobTrigger.ExecutionWindowTrigger t = (JobTrigger.ExecutionWindowTrigger) trigger;
data.putInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_TYPE,
BundleProtocol.TRIGGER_TYPE_EXECUTION_WINDOW);
data.putInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_WINDOW_START,
t.getWindowStart());
data.putInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_WINDOW_END,
t.getWindowEnd());
} else if (trigger instanceof JobTrigger.ContentUriTrigger) {
data.putInt(prefix + BundleProtocol.PACKED_PARAM_TRIGGER_TYPE,
BundleProtocol.TRIGGER_TYPE_CONTENT_URI);
ContentUriTrigger uriTrigger = (ContentUriTrigger) trigger;
String jsonTrigger = convertObservedUrisToJsonString(uriTrigger.getUris());
data.putString(prefix + BundleProtocol.PACKED_PARAM_OBSERVED_URI, jsonTrigger);
} else {
throw new IllegalArgumentException("Unsupported trigger.");
}
}
private RetryStrategy decodeRetryStrategy(Bundle data) {
int policy = data.getInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_POLICY);
if (policy != RetryStrategy.RETRY_POLICY_EXPONENTIAL
&& policy != RetryStrategy.RETRY_POLICY_LINEAR) {
return RetryStrategy.DEFAULT_EXPONENTIAL;
}
//noinspection WrongConstant
return new RetryStrategy(
policy,
data.getInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_INITIAL_BACKOFF_SECONDS),
data.getInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_MAXIMUM_BACKOFF_SECONDS));
}
private void encodeRetryStrategy(RetryStrategy retryStrategy, Bundle data) {
if (retryStrategy == null) {
retryStrategy = RetryStrategy.DEFAULT_EXPONENTIAL;
}
data.putInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_POLICY,
retryStrategy.getPolicy());
data.putInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_INITIAL_BACKOFF_SECONDS,
retryStrategy.getInitialBackoff());
data.putInt(prefix + BundleProtocol.PACKED_PARAM_RETRY_STRATEGY_MAXIMUM_BACKOFF_SECONDS,
retryStrategy.getMaximumBackoff());
}
@NonNull
private String convertObservedUrisToJsonString(@NonNull List<ObservedUri> uris) {
JSONObject contentUris = new JSONObject();
JSONArray jsonFlags = new JSONArray();
JSONArray jsonUris = new JSONArray();
for (ObservedUri uri : uris) {
jsonFlags.put(uri.getFlags());
jsonUris.put(uri.getUri());
}
try {
contentUris.put(JSON_URI_FLAGS, jsonFlags);
contentUris.put(JSON_URIS, jsonUris);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return contentUris.toString();
}
@NonNull
private List<ObservedUri> convertJsonToObservedUris(@NonNull String contentUrisJson) {
List<ObservedUri> uris = new ArrayList<>();
try {
JSONObject json = new JSONObject(contentUrisJson);
JSONArray jsonFlags = json.getJSONArray(JSON_URI_FLAGS);
JSONArray jsonUris = json.getJSONArray(JSON_URIS);
int length = jsonFlags.length();
for (int i = 0; i < length; i++) {
int flags = jsonFlags.getInt(i);
String uri = jsonUris.getString(i);
uris.add(new ObservedUri(Uri.parse(uri), flags));
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
return uris;
}
}

View File

@ -1,236 +0,0 @@
// Copyright 2016 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.os.Bundle;
import android.support.annotation.NonNull;
import com.firebase.jobdispatcher.Constraint.JobConstraint;
/**
* An internal non-Job implementation of JobParameters. Passed to JobService invocations.
*/
/* package */ final class JobInvocation implements JobParameters {
@NonNull
private final String mTag;
@NonNull
private final String mService;
@NonNull
private final JobTrigger mTrigger;
private final boolean mRecurring;
private final int mLifetime;
@NonNull
@JobConstraint
private final int[] mConstraints;
@NonNull
private final Bundle mExtras;
private final RetryStrategy mRetryStrategy;
private final boolean mReplaceCurrent;
private final TriggerReason mTriggerReason;
private JobInvocation(Builder builder) {
mTag = builder.mTag;
mService = builder.mService;
mTrigger = builder.mTrigger;
mRetryStrategy = builder.mRetryStrategy;
mRecurring = builder.mRecurring;
mLifetime = builder.mLifetime;
mConstraints = builder.mConstraints;
mExtras = builder.mExtras;
mReplaceCurrent = builder.mReplaceCurrent;
mTriggerReason = builder.mTriggerReason;
}
@NonNull
@Override
public String getService() {
return mService;
}
@NonNull
@Override
public String getTag() {
return mTag;
}
@NonNull
@Override
public JobTrigger getTrigger() {
return mTrigger;
}
@Override
public int getLifetime() {
return mLifetime;
}
@Override
public boolean isRecurring() {
return mRecurring;
}
@NonNull
@Override
public int[] getConstraints() {
return mConstraints;
}
@NonNull
@Override
public Bundle getExtras() {
return mExtras;
}
@NonNull
@Override
public RetryStrategy getRetryStrategy() {
return mRetryStrategy;
}
@Override
public boolean shouldReplaceCurrent() {
return mReplaceCurrent;
}
@Override
public TriggerReason getTriggerReason() {
return mTriggerReason;
}
static final class Builder {
@NonNull
private String mTag;
@NonNull
private String mService;
@NonNull
private JobTrigger mTrigger;
private boolean mRecurring;
private int mLifetime;
@NonNull
@JobConstraint
private int[] mConstraints;
@NonNull
private final Bundle mExtras = new Bundle();
private RetryStrategy mRetryStrategy;
private boolean mReplaceCurrent;
private TriggerReason mTriggerReason;
JobInvocation build() {
if (mTag == null || mService == null || mTrigger == null) {
throw new IllegalArgumentException("Required fields were not populated.");
}
return new JobInvocation(this);
}
public Builder setTag(@NonNull String mTag) {
this.mTag = mTag;
return this;
}
public Builder setService(@NonNull String mService) {
this.mService = mService;
return this;
}
public Builder setTrigger(@NonNull JobTrigger mTrigger) {
this.mTrigger = mTrigger;
return this;
}
public Builder setRecurring(boolean mRecurring) {
this.mRecurring = mRecurring;
return this;
}
public Builder setLifetime(@Lifetime.LifetimeConstant int mLifetime) {
this.mLifetime = mLifetime;
return this;
}
public Builder setConstraints(@JobConstraint @NonNull int[] mConstraints) {
this.mConstraints = mConstraints;
return this;
}
public Builder addExtras(@NonNull Bundle bundle) {
if (bundle != null) {
mExtras.putAll(bundle);
}
return this;
}
public Builder setRetryStrategy(RetryStrategy mRetryStrategy) {
this.mRetryStrategy = mRetryStrategy;
return this;
}
public Builder setReplaceCurrent(boolean mReplaceCurrent) {
this.mReplaceCurrent = mReplaceCurrent;
return this;
}
public Builder setTriggerReason(TriggerReason triggerReason) {
this.mTriggerReason = triggerReason;
return this;
}
}
/**
* @return true if the tag and the service of provided {@link JobInvocation} have the same
* values.
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || !getClass().equals(o.getClass())) {
return false;
}
JobInvocation jobInvocation = (JobInvocation) o;
return mTag.equals(jobInvocation.mTag)
&& mService.equals(jobInvocation.mService);
}
@Override
public int hashCode() {
int result = mTag.hashCode();
result = 31 * result + mService.hashCode();
return result;
}
}

View File

@ -1,86 +0,0 @@
// Copyright 2016 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.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.firebase.jobdispatcher.Constraint.JobConstraint;
/**
* JobParameters represents anything that can describe itself in terms of Job components.
*/
public interface JobParameters {
/**
* Returns the name of the backing JobService class.
*/
@NonNull
String getService();
/**
* Returns a string identifier for the Job. Used when cancelling Jobs and displaying debug
* messages.
*/
@NonNull
String getTag();
/**
* The Job's Trigger, which decides when the Job is ready to run.
*/
@NonNull
JobTrigger getTrigger();
/**
* The Job's lifetime; how long it should persist for.
*/
@Lifetime.LifetimeConstant
int getLifetime();
/**
* Whether the Job should repeat.
*/
boolean isRecurring();
/**
* The runtime constraints applied to this Job. A Job is not run until the trigger is activated
* and all the runtime constraints are satisfied.
*/
@JobConstraint
int[] getConstraints();
/**
* The optional set of user-supplied extras associated with this Job.
*/
@Nullable
Bundle getExtras();
/**
* The RetryStrategy for the Job. Used to determine how to handle failures.
*/
@NonNull
RetryStrategy getRetryStrategy();
/**
* Whether the Job should replace a pre-existing Job with the same tag.
*/
boolean shouldReplaceCurrent();
/** @return A {@link TriggerReason} that - if non null - describes why the job was triggered. */
@Nullable
TriggerReason getTriggerReason();
}

View File

@ -1,259 +0,0 @@
// Copyright 2016 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.app.Service;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Binder;
import android.os.IBinder;
import android.os.Message;
import android.support.annotation.IntDef;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.util.SimpleArrayMap;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Locale;
/**
* JobService is the fundamental unit of work used in the JobDispatcher.
* <p>
* Users will need to override {@link #onStartJob(JobParameters)}, which is where any asynchronous
* execution should start. This method, like most lifecycle methods, runs on the main thread; you
* <b>must</b> offload execution to another thread (or {@link android.os.AsyncTask}, or
* {@link android.os.Handler}, or your favorite flavor of concurrency).
* <p>
* Once any asynchronous work is complete {@link #jobFinished(JobParameters, boolean)} should be
* called to inform the backing driver of the result.
* <p>
* Implementations should also override {@link #onStopJob(JobParameters)}, which will be called if
* the scheduling engine wishes to interrupt your work (most likely because the runtime constraints
* that are associated with the job in question are no longer met).
*/
public abstract class JobService extends Service {
/**
* Returned to indicate the job was executed successfully. If the job is not recurring (i.e. a
* one-off) it will be dequeued and forgotten. If it is recurring the trigger will be reset and
* the job will be requeued.
*/
public static final int RESULT_SUCCESS = 0;
/**
* Returned to indicate the job encountered an error during execution and should be retried after
* a backoff period.
*/
public static final int RESULT_FAIL_RETRY = 1;
/**
* Returned to indicate the job encountered an error during execution but should not be retried.
* If the job is not recurring (i.e. a one-off) it will be dequeued and forgotten. If it is
* recurring the trigger will be reset and the job will be requeued.
*/
public static final int RESULT_FAIL_NORETRY = 2;
static final String TAG = "FJD.JobService";
@VisibleForTesting
static final String ACTION_EXECUTE = "com.firebase.jobdispatcher.ACTION_EXECUTE";
/**
* Correlates job tags (unique strings) with Messages, which are used to signal the completion
* of a job.
*/
private final SimpleArrayMap<String, JobCallback> runningJobs = new SimpleArrayMap<>(1);
private LocalBinder binder = new LocalBinder();
/**
* The entry point to your Job. Implementations should offload work to another thread of
* execution as soon as possible because this runs on the main thread. If work was offloaded,
* call {@link JobService#jobFinished(JobParameters, boolean)} to notify the scheduling service
* that the work is completed.
* <p>
* In order to reschedule use {@link JobService#jobFinished(JobParameters, boolean)}.
*
* @return {@code true} if there is more work remaining in the worker thread, {@code false} if the job was completed.
*/
@MainThread
public abstract boolean onStartJob(JobParameters job);
/**
* Called when the scheduling engine has decided to interrupt the execution of a running job,
* most likely because the runtime constraints associated with the job are no longer satisfied.
* The job must stop execution.
*
* @return true if the job should be retried
* @see com.firebase.jobdispatcher.JobInvocation.Builder#setRetryStrategy(RetryStrategy)
* @see RetryStrategy
*/
@MainThread
public abstract boolean onStopJob(JobParameters job);
@MainThread
void start(JobParameters job, Message msg) {
synchronized (runningJobs) {
if (runningJobs.containsKey(job.getTag())) {
Log.w(TAG, String
.format(Locale.US, "Job with tag = %s was already running.", job.getTag()));
return;
}
runningJobs.put(job.getTag(), new JobCallback(msg));
boolean moreWork = onStartJob(job);
if (!moreWork) {
JobCallback callback = runningJobs.remove(job.getTag());
if (callback != null) {
callback.sendResult(RESULT_SUCCESS);
}
}
}
}
@MainThread
void stop(JobInvocation job) {
synchronized (runningJobs) {
JobCallback jobCallback = runningJobs.remove(job.getTag());
if (jobCallback == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Provided job has already been executed.");
}
return;
}
boolean shouldRetry = onStopJob(job);
jobCallback.sendResult(shouldRetry ? RESULT_FAIL_RETRY : RESULT_SUCCESS);
}
}
/**
* Callback to inform the scheduling driver that you've finished executing. Can be called from
* any thread. When the system receives this message, it will release the wakelock being held.
*
* @param job
* @param needsReschedule
* whether the job should be rescheduled
* @see com.firebase.jobdispatcher.JobInvocation.Builder#setRetryStrategy(RetryStrategy)
*/
public final void jobFinished(@NonNull JobParameters job, boolean needsReschedule) {
if (job == null) {
Log.e(TAG, "jobFinished called with a null JobParameters");
return;
}
synchronized (runningJobs) {
JobCallback jobCallback = runningJobs.remove(job.getTag());
if (jobCallback != null) {
jobCallback.sendResult(needsReschedule ? RESULT_FAIL_RETRY : RESULT_SUCCESS);
}
}
}
@Override
public final int onStartCommand(Intent intent, int flags, int startId) {
stopSelf(startId);
return START_NOT_STICKY;
}
@Nullable
@Override
public final IBinder onBind(Intent intent) {
return binder;
}
@Override
public final boolean onUnbind(Intent intent) {
synchronized (runningJobs) {
for (int i = runningJobs.size() - 1; i >= 0; i--) {
JobCallback callback = runningJobs.get(runningJobs.keyAt(i));
if (callback != null && callback.message != null) {
if (callback.message.obj instanceof JobParameters) {
callback.sendResult(onStopJob((JobParameters) callback.message.obj)
// returned true, would like to be rescheduled
? RESULT_FAIL_RETRY
// returned false, but was interrupted so consider it a fail
: RESULT_FAIL_NORETRY);
}
}
}
}
return super.onUnbind(intent);
}
@Override
public final void onRebind(Intent intent) {
super.onRebind(intent);
}
@Override
public final void onStart(Intent intent, int startId) {
}
@Override
protected final void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
super.dump(fd, writer, args);
}
@Override
public final void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
public final void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
}
/**
* The result returned from a job execution.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({RESULT_SUCCESS, RESULT_FAIL_RETRY, RESULT_FAIL_NORETRY})
public @interface JobResult {
}
private final static class JobCallback {
public final Message message;
private JobCallback(Message message) {
this.message = message;
}
void sendResult(@JobResult int result) {
try {
if (message != null) {
message.arg1 = result;
message.sendToTarget();
}
} catch (Exception ignored) {}//catch this freaking crash!!!!
}
}
class LocalBinder extends Binder {
JobService getService() {
return JobService.this;
}
}
}

View File

@ -1,82 +0,0 @@
// Copyright 2016 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.firebase.jobdispatcher.ExecutionDelegator.TAG;
import android.content.ComponentName;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.Message;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
/**
* ServiceConnection for job execution.
*/
@VisibleForTesting
class JobServiceConnection implements ServiceConnection {
private final JobInvocation jobInvocation;
// Should be sent only once. Can't be reused.
private final Message jobFinishedMessage;
private boolean wasMessageUsed = false;
//Guarded by "this". Can be updated from main and binder threads.
private JobService.LocalBinder binder;
JobServiceConnection(JobInvocation jobInvocation, Message jobFinishedMessage) {
this.jobFinishedMessage = jobFinishedMessage;
this.jobInvocation = jobInvocation;
this.jobFinishedMessage.obj = this.jobInvocation;
}
@Override
public synchronized void onServiceConnected(ComponentName name, IBinder service) {
if (!(service instanceof JobService.LocalBinder)) {
Log.w(TAG, "Unknown service connected");
return;
}
if (wasMessageUsed) {
Log.w(TAG, "onServiceConnected Duplicate calls. Ignored.");
return;
} else {
wasMessageUsed = true;
}
binder = (JobService.LocalBinder) service;
JobService jobService = binder.getService();
jobService.start(jobInvocation, jobFinishedMessage);
}
@Override
public synchronized void onServiceDisconnected(ComponentName name) {
binder = null;
}
synchronized boolean isBound() {
return binder != null;
}
synchronized void onStop() {
if (isBound()) {
binder.getService().stop(jobInvocation);
}
}
}

View File

@ -1,70 +0,0 @@
// Copyright 2016 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 java.util.List;
/**
* Contains all supported triggers.
*/
public class JobTrigger {
/**
* ImmediateTrigger is a Trigger that's immediately available. The Job will be run as soon as
* the runtime constraints are satisfied.
*/
public static final class ImmediateTrigger extends JobTrigger {
/* package */ ImmediateTrigger() {}
}
/**
* ExecutionWindow represents a Job trigger that becomes eligible once
* the current elapsed time exceeds the scheduled time + the {@code windowStart}
* value. The scheduler backend is encouraged to use the windowEnd value as a
* signal that the job should be run, but this is not an enforced behavior.
*/
public static final class ExecutionWindowTrigger extends JobTrigger {
private final int mWindowStart;
private final int mWindowEnd;
/* package */ ExecutionWindowTrigger(int windowStart, int windowEnd) {
this.mWindowStart = windowStart;
this.mWindowEnd = windowEnd;
}
public int getWindowStart() {
return mWindowStart;
}
public int getWindowEnd() {
return mWindowEnd;
}
}
/** A trigger that will be triggered on content update for any of provided uris. */
public static final class ContentUriTrigger extends JobTrigger {
private final List<ObservedUri> uris;
/* package */ ContentUriTrigger(List<ObservedUri> uris) {
this.uris = uris;
}
public List<ObservedUri> getUris() {
return uris;
}
}
}

View File

@ -1,49 +0,0 @@
// Copyright 2016 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.support.annotation.Nullable;
import java.util.List;
/**
* A JobValidator is an object that knows how to validate Jobs and some of their composite
* components.
*/
public interface JobValidator {
/**
* Returns a List of error messages, or null if the JobParameters is
* valid.
*/
@Nullable
List<String> validate(JobParameters job);
/**
* Returns a List of error messages, or null if the Trigger is
* valid.
* @param trigger
*/
@Nullable
List<String> validate(JobTrigger trigger);
/**
* Returns a List of error messages, or null if the RetryStrategy
* is valid.
*/
@Nullable
List<String> validate(RetryStrategy retryStrategy);
}

View File

@ -1,40 +0,0 @@
// Copyright 2016 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.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Lifetime represents how long a Job should last.
*/
public final class Lifetime {
/**
* The Job should be preserved until the next boot. This is the default.
*/
public final static int UNTIL_NEXT_BOOT = 1;
/**
* The Job should be preserved "forever."
*/
public final static int FOREVER = 2;
@Retention(RetentionPolicy.SOURCE)
@IntDef({FOREVER, UNTIL_NEXT_BOOT})
@interface LifetimeConstant {}
}

View File

@ -1,83 +0,0 @@
// Copyright 2016 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.net.Uri;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Represents a single observed URI and any associated flags. */
public final class ObservedUri {
private final Uri uri;
private final int flags;
/** Flag enforcement. */
@IntDef(flag = true, value = Flags.FLAG_NOTIFY_FOR_DESCENDANTS)
@Retention(RetentionPolicy.SOURCE)
public @interface Flags {
/**
* Triggers if any descendants of the given URI change. Corresponds to the {@code
* notifyForDescendants} of {@link android.content.ContentResolver#registerContentObserver}.
*/
int FLAG_NOTIFY_FOR_DESCENDANTS = 1 << 0;
}
/**
* Create a new ObservedUri.
*
* @param uri The URI to observe.
* @param flags Any {@link Flags} associated with the URI.
*/
public ObservedUri(@NonNull Uri uri, @Flags int flags) {
if (uri == null) {
throw new IllegalArgumentException("URI must not be null.");
}
this.uri = uri;
this.flags = flags;
}
public Uri getUri() {
return uri;
}
public int getFlags() {
return flags;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ObservedUri)) {
return false;
}
ObservedUri otherUri = (ObservedUri) o;
return flags == otherUri.flags && uri.equals(otherUri.uri);
}
@Override
public int hashCode() {
return uri.hashCode() ^ flags;
}
}

View File

@ -1,106 +0,0 @@
// Copyright 2016 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.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* RetryStrategy represents an approach to handling job execution failures. Jobs will have a
* time-based backoff enforced, based on the chosen policy (one of {@code RETRY_POLICY_EXPONENTIAL}
* or {@code RETRY_POLICY_LINEAR}.
*/
public final class RetryStrategy {
/**
* Increase the backoff time exponentially.
* <p>
* Calculated using {@code initial_backoff * 2 ^ (num_failures - 1)}.
*/
public final static int RETRY_POLICY_EXPONENTIAL = 1;
/**
* Increase the backoff time linearly.
* <p>
* Calculated using {@code initial_backoff * num_failures}.
*/
public final static int RETRY_POLICY_LINEAR = 2;
/**
* Expected schedule is: [30s, 60s, 120s, 240s, ..., 3600s]
*/
public final static RetryStrategy DEFAULT_EXPONENTIAL =
new RetryStrategy(RETRY_POLICY_EXPONENTIAL, 30, 3600);
/**
* Expected schedule is: [30s, 60s, 90s, 120s, ..., 3600s]
*/
public final static RetryStrategy DEFAULT_LINEAR =
new RetryStrategy(RETRY_POLICY_LINEAR, 30, 3600);
@RetryPolicy
private final int mPolicy;
private final int mInitialBackoff;
private final int mMaximumBackoff;
/* package */ RetryStrategy(@RetryPolicy int policy, int initialBackoff, int maximumBackoff) {
mPolicy = policy;
mInitialBackoff = initialBackoff;
mMaximumBackoff = maximumBackoff;
}
/**
* Returns the backoff policy in place.
*/
@RetryPolicy
public int getPolicy() {
return mPolicy;
}
/**
* Returns the initial backoff (i.e. when # of failures == 1), in seconds.
*/
public int getInitialBackoff() {
return mInitialBackoff;
}
/**
* Returns the maximum backoff duration in seconds.
*/
public int getMaximumBackoff() {
return mMaximumBackoff;
}
@IntDef({RETRY_POLICY_LINEAR, RETRY_POLICY_EXPONENTIAL})
@Retention(RetentionPolicy.SOURCE)
public @interface RetryPolicy {
}
/* package */ final static class Builder {
private final ValidationEnforcer mValidator;
Builder(ValidationEnforcer validator) {
mValidator = validator;
}
public RetryStrategy build(@RetryPolicy int policy, int initialBackoff, int maxBackoff) {
RetryStrategy rs = new RetryStrategy(policy, initialBackoff, maxBackoff);
mValidator.ensureValid(rs);
return rs;
}
}
}

View File

@ -1,90 +0,0 @@
// Copyright 2016 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.os.AsyncTask;
import android.support.annotation.CallSuper;
import android.support.v4.util.SimpleArrayMap;
/**
* SimpleJobService provides a simple way of doing background work in a JobService.
*
* Users should override onRunJob and return one of the {@link JobResult} ints.
*/
public abstract class SimpleJobService extends JobService {
private final SimpleArrayMap<JobParameters, AsyncJobTask> runningJobs =
new SimpleArrayMap<>();
@CallSuper
@Override
public boolean onStartJob(JobParameters job) {
AsyncJobTask async = new AsyncJobTask(this, job);
synchronized (runningJobs) {
runningJobs.put(job, async);
}
async.execute();
return true; // more work to do
}
@CallSuper
@Override
public boolean onStopJob(JobParameters job) {
synchronized (runningJobs) {
AsyncJobTask async = runningJobs.remove(job);
if (async != null) {
async.cancel(true);
return true;
}
}
return false;
}
private void onJobFinished(JobParameters jobParameters, boolean b) {
synchronized (runningJobs) {
runningJobs.remove(jobParameters);
}
jobFinished(jobParameters, b);
}
@JobResult
public abstract int onRunJob(JobParameters job);
private static class AsyncJobTask extends AsyncTask<Void, Void, Integer> {
private final SimpleJobService jobService;
private final JobParameters jobParameters;
private AsyncJobTask(SimpleJobService jobService, JobParameters jobParameters) {
this.jobService = jobService;
this.jobParameters = jobParameters;
}
@Override
protected Integer doInBackground(Void... params) {
return jobService.onRunJob(jobParameters);
}
@Override
protected void onPostExecute(Integer integer) {
jobService.onJobFinished(jobParameters, integer == JobService.RESULT_FAIL_RETRY);
}
}
}

View File

@ -1,73 +0,0 @@
// Copyright 2016 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.support.annotation.NonNull;
import java.util.List;
/**
* Generally, a Trigger is an object that can answer the question, "is this job ready to run?"
* <p>
* More specifically, a Trigger is an opaque, abstract class used to root the type hierarchy.
*/
public final class Trigger {
/**
* Immediate is a Trigger that's immediately available. The Job will be run as soon as the
* runtime constraints are satisfied.
* <p>
* It is invalid to schedule an Immediate with a recurring Job.
*/
public final static JobTrigger.ImmediateTrigger NOW = new JobTrigger.ImmediateTrigger();
/**
* Creates a new ExecutionWindow based on the provided time interval.
*
* @param windowStart The earliest time (in seconds) the job should be
* considered eligible to run. Calculated from when the
* job was scheduled (for new jobs) or last run (for
* recurring jobs).
* @param windowEnd The latest time (in seconds) the job should be run in
* an ideal world. Calculated in the same way as
* {@code windowStart}.
* @throws IllegalArgumentException if the provided parameters are too
* restrictive.
*/
public static JobTrigger.ExecutionWindowTrigger executionWindow(int windowStart, int windowEnd) {
if (windowStart < 0) {
throw new IllegalArgumentException("Window start can't be less than 0");
} else if (windowEnd < windowStart) {
throw new IllegalArgumentException("Window end can't be less than window start");
}
return new JobTrigger.ExecutionWindowTrigger(windowStart, windowEnd);
}
/**
* Creates a new ContentUriTrigger based on the provided list of {@link ObservedUri}.
*
* @param uris The list of URIs to observe. The trigger will be available if a piece of content,
* corresponding to any of provided URIs, is updated.
* @throws IllegalArgumentException if provided list of URIs is null or empty.
*/
public static JobTrigger.ContentUriTrigger contentUriTrigger(@NonNull List<ObservedUri> uris) {
if (uris == null || uris.isEmpty()) {
throw new IllegalArgumentException("Uris must not be null or empty.");
}
return new JobTrigger.ContentUriTrigger(uris);
}
}

View File

@ -1,33 +0,0 @@
// Copyright 2016 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.net.Uri;
import java.util.List;
/** The class contains a summary of the events which caused the job to be executed. */
public class TriggerReason {
private final List<Uri> mTriggeredContentUris;
TriggerReason(List<Uri> mTriggeredContentUris) {
this.mTriggeredContentUris = mTriggeredContentUris;
}
public List<Uri> getTriggeredContentUris() {
return mTriggeredContentUris;
}
}

View File

@ -1,132 +0,0 @@
// Copyright 2016 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.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.util.List;
/**
* Wraps a JobValidator and provides helpful validation utilities.
*/
public class ValidationEnforcer implements JobValidator {
private final JobValidator mValidator;
public ValidationEnforcer(JobValidator validator) {
mValidator = validator;
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
public List<String> validate(JobParameters job) {
return mValidator.validate(job);
}
/**
* {@inheritDoc}
* @param trigger
*/
@Nullable
@Override
public List<String> validate(JobTrigger trigger) {
return mValidator.validate(trigger);
}
/**
* {@inheritDoc}
*/
@Nullable
@Override
public List<String> validate(RetryStrategy retryStrategy) {
return mValidator.validate(retryStrategy);
}
/**
* Indicates whether the provided JobParameters is valid.
*/
public final boolean isValid(JobParameters job) {
return validate(job) == null;
}
/**
* Indicates whether the provided JobTrigger is valid.
*/
public final boolean isValid(JobTrigger trigger) {
return validate(trigger) == null;
}
/**
* Indicates whether the provided RetryStrategy is valid.
*/
public final boolean isValid(RetryStrategy retryStrategy) {
return validate(retryStrategy) == null;
}
/**
* Throws a RuntimeException if the provided JobParameters is invalid.
*
* @throws ValidationException
*/
public final void ensureValid(JobParameters job) {
ensureNoErrors(validate(job));
}
/**
* Throws a RuntimeException if the provided JobTrigger is invalid.
*
* @throws ValidationException
*/
public final void ensureValid(JobTrigger trigger) {
ensureNoErrors(validate(trigger));
}
/**
* Throws a RuntimeException if the provided RetryStrategy is
* invalid.
*
* @throws ValidationException
*/
public final void ensureValid(RetryStrategy retryStrategy) {
ensureNoErrors(validate(retryStrategy));
}
private void ensureNoErrors(List<String> errors) {
if (errors != null) {
throw new ValidationException("JobParameters is invalid", errors);
}
}
/**
* An Exception thrown when a validation error is encountered.
*/
public final static class ValidationException extends RuntimeException {
private final List<String> mErrors;
public ValidationException(String msg, @NonNull List<String> errors) {
super(msg + ": " + TextUtils.join("\n - ", errors));
mErrors = errors;
}
public List<String> getErrors() {
return mErrors;
}
}
}

View File

@ -1,10 +0,0 @@
package android.net.http;
/**
* Robolectric requires this class be available in the classpath, otherwise {@link
* org.robolectric.Shadows#shadowOf(android.os.Looper)} fails. We don't use AndroidHttpClient, so
* include a stub to make Robolectric happy.
*
* @see https://github.com/robolectric/robolectric/issues/1862
*/
public class AndroidHttpClient {}

View File

@ -1,66 +0,0 @@
// Copyright 2016 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 org.junit.Assert.assertEquals;
import android.text.TextUtils;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class ConstraintTest {
/**
* Just to get 100% coverage.
*/
@Test
public void testPrivateConstructor() throws Exception {
TestUtil.assertHasSinglePrivateConstructor(Constraint.class);
}
@Test
public void testCompactAndUnCompact() {
for (List<Integer> combo : TestUtil.getAllConstraintCombinations()) {
int[] input = TestUtil.toIntArray(combo);
Arrays.sort(input);
int[] output = Constraint.uncompact(Constraint.compact(input));
Arrays.sort(output);
for (int i = 0; i < combo.size(); i++) {
assertEquals("Combination = " + TextUtils.join(", ", combo),
input[i],
output[i]);
}
assertEquals("Expected length of two arrays to be the same",
input.length,
output.length);
}
}
@Test
public void compactNull() {
assertEquals(0, Constraint.compact(null));
}
}

View File

@ -1,52 +0,0 @@
// Copyright 2016 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 junit.framework.Assert.assertEquals;
import android.provider.ContactsContract;
import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/** Test for {@link ContentUriTrigger}. */
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 21)
public class ContentUriTriggerTest {
@Test(expected = IllegalArgumentException.class)
public void constrains_null() throws Exception {
Trigger.contentUriTrigger(null);
}
@Test(expected = IllegalArgumentException.class)
public void constrains_emptyList() throws Exception {
Trigger.contentUriTrigger(Collections.<ObservedUri>emptyList());
}
@Test
public void constrains_valid() throws Exception {
List<ObservedUri> uris = Arrays.asList(new ObservedUri(ContactsContract.AUTHORITY_URI, 0));
ContentUriTrigger uriTrigger = Trigger.contentUriTrigger(uris);
assertEquals(uris, uriTrigger.getUris());
}
}

View File

@ -1,119 +0,0 @@
// Copyright 2016 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 java.util.Collections.singletonList;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.provider.ContactsContract;
import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger;
import com.firebase.jobdispatcher.ObservedUri.Flags;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class DefaultJobValidatorTest {
@Mock
private Context mMockContext;
private DefaultJobValidator mValidator;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mValidator = new DefaultJobValidator(mMockContext);
}
@SuppressWarnings("WrongConstant")
@Test
public void testValidate_retryStrategy() throws Exception {
Map<RetryStrategy, List<String>> testCases = new HashMap<>();
testCases.put(
new RetryStrategy(0 /* bad policy */, 30, 3600),
singletonList("Unknown retry policy provided"));
testCases.put(
new RetryStrategy(RetryStrategy.RETRY_POLICY_LINEAR, 15, 3600),
singletonList("Initial backoff must be at least 30s"));
testCases.put(
new RetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 15, 3600),
singletonList("Initial backoff must be at least 30s"));
testCases.put(
new RetryStrategy(RetryStrategy.RETRY_POLICY_LINEAR, 30, 60),
singletonList("Maximum backoff must be greater than 300s (5 minutes)"));
testCases.put(
new RetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 30, 60),
singletonList("Maximum backoff must be greater than 300s (5 minutes)"));
testCases.put(
new RetryStrategy(RetryStrategy.RETRY_POLICY_LINEAR, 301, 300),
singletonList("Maximum backoff must be greater than or equal to initial backoff"));
testCases.put(
new RetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 301, 300),
singletonList("Maximum backoff must be greater than or equal to initial backoff"));
for (Entry<RetryStrategy, List<String>> testCase : testCases.entrySet()) {
List<String> validationErrors = mValidator.validate(testCase.getKey());
assertNotNull("Expected validation errors, but got null", validationErrors);
for (String expected : testCase.getValue()) {
assertTrue(
"Expected validation errors to contain \"" + expected + "\"",
validationErrors.contains(expected));
}
}
}
@Test
public void testValidate_trigger() throws Exception {
Map<JobTrigger, String> testCases = new HashMap<>();
testCases.put(Trigger.NOW, null);
testCases.put(Trigger.executionWindow(0, 100), null);
ContentUriTrigger contentUriTrigger =
Trigger.contentUriTrigger(
Arrays.asList(
new ObservedUri(
ContactsContract.AUTHORITY_URI, Flags.FLAG_NOTIFY_FOR_DESCENDANTS)));
testCases.put(contentUriTrigger, null);
for (Entry<JobTrigger, String> testCase : testCases.entrySet()) {
List<String> validationErrors = mValidator.validate(testCase.getKey());
if (testCase.getValue() == null) {
assertNull("Expected no validation errors for trigger", validationErrors);
} else {
assertTrue(
"Expected validation errors to contain \"" + testCase.getValue() + "\"",
validationErrors.contains(testCase.getValue()));
}
}
}
}

View File

@ -1,224 +0,0 @@
// Copyright 2016 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.firebase.jobdispatcher.TestUtil.getContentUriTrigger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.support.annotation.NonNull;
import com.firebase.jobdispatcher.JobService.JobResult;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@SuppressWarnings("WrongConstant")
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 21)
public class ExecutionDelegatorTest {
private Context mMockContext;
private TestJobReceiver mReceiver;
private ExecutionDelegator mExecutionDelegator;
@Before
public void setUp() {
mMockContext = spy(RuntimeEnvironment.application);
doReturn("com.example.foo").when(mMockContext).getPackageName();
mReceiver = new TestJobReceiver();
mExecutionDelegator = new ExecutionDelegator(mMockContext, mReceiver);
}
@Test
public void testExecuteJob_sendsBroadcastWithJobAndMessage() throws Exception {
for (JobInvocation input : TestUtil.getJobInvocationCombinations()) {
verifyExecuteJob(input);
}
}
private void verifyExecuteJob(JobInvocation input) throws Exception {
reset(mMockContext);
mReceiver.lastResult = -1;
mReceiver.setLatch(new CountDownLatch(1));
mExecutionDelegator.executeJob(input);
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
final ArgumentCaptor<ServiceConnection> connCaptor =
ArgumentCaptor.forClass(ServiceConnection.class);
verify(mMockContext).bindService(intentCaptor.capture(), connCaptor.capture(), anyInt());
final Intent result = intentCaptor.getValue();
// verify the intent was sent to the right place
assertEquals(input.getService(), result.getComponent().getClassName());
assertEquals(JobService.ACTION_EXECUTE, result.getAction());
final ServiceConnection connection = connCaptor.getValue();
ComponentName cname = mock(ComponentName.class);
JobService.LocalBinder mockLocalBinder = mock(JobService.LocalBinder.class);
final JobParameters[] out = new JobParameters[1];
JobService mockJobService = new JobService() {
@Override
public boolean onStartJob(JobParameters job) {
out[0] = job;
return false;
}
@Override
public boolean onStopJob(JobParameters job) {
return false;
}
};
when(mockLocalBinder.getService()).thenReturn(mockJobService);
connection.onServiceConnected(cname, mockLocalBinder);
TestUtil.assertJobsEqual(input, out[0]);
// make sure the countdownlatch was decremented
assertTrue(mReceiver.mLatch.await(1, TimeUnit.SECONDS));
// verify the lastResult was set correctly
assertEquals(JobService.RESULT_SUCCESS, mReceiver.lastResult);
}
@Test
public void testExecuteJob_handlesNull() {
assertFalse("Expected calling triggerExecution on null to fail and return false",
mExecutionDelegator.executeJob(null));
}
@Test
public void testHandleMessage_doesntCrashOnBadJobData() {
JobInvocation j = new JobInvocation.Builder()
.setService(TestJobService.class.getName())
.setTag("tag")
.setTrigger(Trigger.NOW)
.build();
mExecutionDelegator.executeJob(j);
ArgumentCaptor<Intent> intentCaptor =
ArgumentCaptor.forClass(Intent.class);
ArgumentCaptor<ServiceConnection> connCaptor =
ArgumentCaptor.forClass(ServiceConnection.class);
//noinspection WrongConstant
verify(mMockContext).bindService(intentCaptor.capture(), connCaptor.capture(), anyInt());
Intent executeReq = intentCaptor.getValue();
assertEquals(JobService.ACTION_EXECUTE, executeReq.getAction());
}
@Test
public void onStop_mock() throws InterruptedException {
JobInvocation job = new JobInvocation.Builder()
.setTag("TAG")
.setTrigger(getContentUriTrigger())
.setService(TestJobService.class.getName())
.setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)
.build();
reset(mMockContext);
mReceiver.lastResult = -1;
mExecutionDelegator.executeJob(job);
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
final ArgumentCaptor<ServiceConnection> connCaptor =
ArgumentCaptor.forClass(ServiceConnection.class);
verify(mMockContext).bindService(intentCaptor.capture(), connCaptor.capture(), anyInt());
final Intent result = intentCaptor.getValue();
// verify the intent was sent to the right place
assertEquals(job.getService(), result.getComponent().getClassName());
assertEquals(JobService.ACTION_EXECUTE, result.getAction());
final JobParameters[] out = new JobParameters[2];
JobService mockJobService = new JobService() {
@Override
public boolean onStartJob(JobParameters job) {
out[0] = job;
return true;
}
@Override
public boolean onStopJob(JobParameters job) {
out[1] = job;
return false;
}
};
JobService.LocalBinder mockLocalBinder = mock(JobService.LocalBinder.class);
when(mockLocalBinder.getService()).thenReturn(mockJobService);
ComponentName componentName = mock(ComponentName.class);
final ServiceConnection connection = connCaptor.getValue();
connection.onServiceConnected(componentName, mockLocalBinder);
mExecutionDelegator.stopJob(job);
TestUtil.assertJobsEqual(job, out[0]);
TestUtil.assertJobsEqual(job, out[1]);
}
private final static class TestJobReceiver implements ExecutionDelegator.JobFinishedCallback {
int lastResult;
private CountDownLatch mLatch;
@Override
public void onJobFinished(@NonNull JobInvocation js, @JobResult int result) {
lastResult = result;
if (mLatch != null) {
mLatch.countDown();
}
}
/**
* Convenience method for tests.
*/
public void setLatch(CountDownLatch latch) {
mLatch = latch;
}
}
}

View File

@ -1,76 +0,0 @@
// Copyright 2016 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 org.junit.Assert.assertEquals;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class ExecutionWindowTriggerTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void testNewInstance_withValidWindow() throws Exception {
JobTrigger.ExecutionWindowTrigger trigger = Trigger.executionWindow(0, 60);
assertEquals(0, trigger.getWindowStart());
assertEquals(60, trigger.getWindowEnd());
}
@Test
public void testNewInstance_withNegativeStart() throws Exception {
expectedException.expect(IllegalArgumentException.class);
Trigger.executionWindow(-10, 60);
}
@Test
public void testNewInstance_withNegativeEnd() throws Exception {
expectedException.expect(IllegalArgumentException.class);
Trigger.executionWindow(0, -1);
}
@Test
public void testNewInstance_withReversedValues() throws Exception {
expectedException.expect(IllegalArgumentException.class);
Trigger.executionWindow(60, 0);
}
@Test
public void testNewInstance_withTooSmallWindow_now() throws Exception {
expectedException.expect(IllegalArgumentException.class);
Trigger.executionWindow(60, 59);
}
@Test
public void testNewInstance_withTooSmallWindow_inFuture() throws Exception {
expectedException.expect(IllegalArgumentException.class);
Trigger.executionWindow(200, 100);
}
}

View File

@ -1,40 +0,0 @@
package com.firebase.jobdispatcher;
import android.os.IBinder;
import android.os.Parcel;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.shadows.ShadowParcel;
/**
* ShadowParcel doesn't correctly handle {@link Parcel#writeStrongBinder(IBinder)} or {@link
* Parcel#readStrongBinder()}, so we shim a simple implementation that uses an in-memory map to read
* and write Binder objects.
*/
@Implements(Parcel.class)
public class ExtendedShadowParcel extends ShadowParcel {
@RealObject private Parcel realObject;
// Map each IBinder to an integer, and use the super's int-writing capability to fake Binder
// read/writes.
private final AtomicInteger nextBinderId = new AtomicInteger(1);
private final Map<Integer, IBinder> binderMap =
Collections.synchronizedMap(new HashMap<Integer, IBinder>());
@Implementation
public void writeStrongBinder(IBinder binder) {
int id = nextBinderId.getAndIncrement();
binderMap.put(id, binder);
realObject.writeInt(id);
}
@Implementation
public IBinder readStrongBinder() {
return binderMap.get(realObject.readInt());
}
}

View File

@ -1,205 +0,0 @@
// Copyright 2016 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.firebase.jobdispatcher.FirebaseJobDispatcher.ScheduleFailedException;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class FirebaseJobDispatcherTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Mock
private Driver mDriver;
@Mock
private JobValidator mValidator;
private FirebaseJobDispatcher mDispatcher;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mDriver.getValidator()).thenReturn(mValidator);
mDispatcher = new FirebaseJobDispatcher(mDriver);
setDriverAvailability(true);
}
@Test
public void testSchedule_passThrough() throws Exception {
final int[] possibleResults = {
FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS,
FirebaseJobDispatcher.SCHEDULE_RESULT_NO_DRIVER_AVAILABLE,
FirebaseJobDispatcher.SCHEDULE_RESULT_BAD_SERVICE,
FirebaseJobDispatcher.SCHEDULE_RESULT_UNKNOWN_ERROR,
FirebaseJobDispatcher.SCHEDULE_RESULT_UNSUPPORTED_TRIGGER};
for (int result : possibleResults) {
when(mDriver.schedule(null)).thenReturn(result);
assertEquals(result, mDispatcher.schedule(null));
}
verify(mDriver, times(possibleResults.length)).schedule(null);
}
@Test
public void testSchedule_unavailable() throws Exception {
setDriverAvailability(false);
assertEquals(
FirebaseJobDispatcher.SCHEDULE_RESULT_NO_DRIVER_AVAILABLE,
mDispatcher.schedule(null));
verify(mDriver, never()).schedule(null);
}
@Test
public void testCancelJob() throws Exception {
final String tag = "foo";
// simulate success
when(mDriver.cancel(tag))
.thenReturn(FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS);
assertEquals(
"Expected dispatcher to pass the result of Driver#cancel(String, Class) through",
FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS,
mDispatcher.cancel(tag));
// verify the driver was indeed called
verify(mDriver).cancel(tag);
}
@Test
public void testCancelJob_unavailable() throws Exception {
setDriverAvailability(false); // driver is unavailable
assertEquals(
FirebaseJobDispatcher.CANCEL_RESULT_NO_DRIVER_AVAILABLE,
mDispatcher.cancel("foo"));
// verify the driver was never even consulted
verify(mDriver, never()).cancel("foo");
}
@Test
public void testCancelAllJobs() throws Exception {
when(mDriver.cancelAll()).thenReturn(FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS);
assertEquals(FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS, mDispatcher.cancelAll());
verify(mDriver).cancelAll();
}
@Test
public void testCancelAllJobs_unavailable() throws Exception {
setDriverAvailability(false); // driver is unavailable
assertEquals(
FirebaseJobDispatcher.CANCEL_RESULT_NO_DRIVER_AVAILABLE,
mDispatcher.cancelAll());
verify(mDriver, never()).cancelAll();
}
@Test
public void testMustSchedule_success() throws Exception {
when(mDriver.schedule(null)).thenReturn(FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS);
/* assert no exception is thrown */
mDispatcher.mustSchedule(null);
}
@Test
public void testMustSchedule_unavailable() throws Exception {
setDriverAvailability(false); // driver is unavailable
expectedException.expect(FirebaseJobDispatcher.ScheduleFailedException.class);
mDispatcher.mustSchedule(null);
}
@Test
public void testMustSchedule_failure() throws Exception {
final int[] possibleErrors = {
FirebaseJobDispatcher.SCHEDULE_RESULT_NO_DRIVER_AVAILABLE,
FirebaseJobDispatcher.SCHEDULE_RESULT_BAD_SERVICE,
FirebaseJobDispatcher.SCHEDULE_RESULT_UNKNOWN_ERROR,
FirebaseJobDispatcher.SCHEDULE_RESULT_UNSUPPORTED_TRIGGER};
for (int scheduleError : possibleErrors) {
when(mDriver.schedule(null)).thenReturn(scheduleError);
try {
mDispatcher.mustSchedule(null);
fail("Expected mustSchedule() with error code " + scheduleError + " to fail");
} catch (ScheduleFailedException expected) { /* expected */ }
}
verify(mDriver, times(possibleErrors.length)).schedule(null);
}
@Test
public void testNewRetryStrategyBuilder() {
// custom validator that only approves strategies where initialbackoff == 30s
when(mValidator.validate(any(RetryStrategy.class))).thenAnswer(new Answer<List<String>>() {
@Override
public List<String> answer(InvocationOnMock invocation) throws Throwable {
RetryStrategy rs = (RetryStrategy) invocation.getArguments()[0];
// only succeed if initialBackoff == 30s
return rs.getInitialBackoff() == 30 ? null : Arrays.asList("foo", "bar");
}
});
try {
mDispatcher.newRetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 0, 30);
fail("Expected initial backoff != 30s to fail using custom validator");
} catch (Exception unused) { /* unused */ }
try {
mDispatcher.newRetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 30, 30);
} catch (Exception unused) {
fail("Expected initial backoff == 30s not to fail using custom validator");
}
}
public void setDriverAvailability(boolean driverAvailability) {
when(mDriver.isAvailable()).thenReturn(driverAvailability);
}
}

View File

@ -1,153 +0,0 @@
// Copyright 2016 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Pair;
import com.firebase.jobdispatcher.TestUtil.InspectableBinder;
import com.firebase.jobdispatcher.TestUtil.TransactionArguments;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(
constants = BuildConfig.class,
manifest = Config.NONE,
sdk = 23,
shadows = {ExtendedShadowParcel.class}
)
public final class GooglePlayCallbackExtractorTest {
@Mock
private IBinder mBinder;
private GooglePlayCallbackExtractor mExtractor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mExtractor = new GooglePlayCallbackExtractor();
}
@Test
public void testExtractCallback_nullBundle() {
assertNull(mExtractor.extractCallback(null));
}
@Test
public void testExtractCallback_nullParcelable() {
Bundle emptyBundle = new Bundle();
assertNull(extractCallback(emptyBundle));
}
@Test
public void testExtractCallback_badParcelable() {
Bundle misconfiguredBundle = new Bundle();
misconfiguredBundle.putParcelable("callback", new BadParcelable(1));
assertNull(extractCallback(misconfiguredBundle));
}
@Test
public void testExtractCallback_goodParcelable() {
InspectableBinder binder = new InspectableBinder();
Bundle validBundle = new Bundle();
validBundle.putParcelable("callback", binder.toPendingCallback());
Pair<JobCallback, Bundle> extraction = extractCallback(validBundle);
assertNotNull(extraction);
assertEquals("should have stripped the 'callback' entry from the extracted bundle",
0, extraction.second.keySet().size());
extraction.first.jobFinished(JobService.RESULT_SUCCESS);
// Check our homemade Binder is doing the right things:
TransactionArguments args = binder.getArguments().get(0);
// Should have set the transaction code:
assertEquals("transaction code", IBinder.FIRST_CALL_TRANSACTION + 1, args.code);
// strong mode bit
args.data.readInt();
// interface token
assertEquals("com.google.android.gms.gcm.INetworkTaskCallback", args.data.readString());
// result
assertEquals("result", JobService.RESULT_SUCCESS, args.data.readInt());
}
@Test
public void testExtractCallback_extraMapValues() {
Bundle validBundle = new Bundle();
validBundle.putString("foo", "bar");
validBundle.putInt("bar", 3);
validBundle.putParcelable("parcelable", new Bundle());
validBundle.putParcelable("callback", new InspectableBinder().toPendingCallback());
Pair<JobCallback, Bundle> extraction = extractCallback(validBundle);
assertNotNull(extraction);
assertEquals("should have stripped the 'callback' entry from the extracted bundle",
3, extraction.second.keySet().size());
}
private Pair<JobCallback, Bundle> extractCallback(Bundle bundle) {
return mExtractor.extractCallback(bundle);
}
private static final class BadParcelable implements Parcelable {
public static final Parcelable.Creator<BadParcelable> CREATOR
= new Parcelable.Creator<BadParcelable>() {
@Override
public BadParcelable createFromParcel(Parcel in) {
return new BadParcelable(in);
}
@Override
public BadParcelable[] newArray(int size) {
return new BadParcelable[size];
}
};
private final int mNum;
public BadParcelable(int i) {
mNum = i;
}
private BadParcelable(Parcel in) {
mNum = in.readInt();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dst, int flags) {
dst.writeInt(mNum);
}
}
}

View File

@ -1,203 +0,0 @@
// Copyright 2016 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class GooglePlayDriverTest {
@Mock
public Context mMockContext;
private TestJobDriver mDriver;
private FirebaseJobDispatcher mDispatcher;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mDriver = new TestJobDriver(new GooglePlayDriver(mMockContext));
mDispatcher = new FirebaseJobDispatcher(mDriver);
when(mMockContext.getPackageName()).thenReturn("foo.bar.whatever");
}
@Test
public void testSchedule_failsWhenPlayServicesIsUnavailable() throws Exception {
markBackendUnavailable();
mockPackageManagerInfo();
Job job = null;
try {
job = mDispatcher.newJobBuilder()
.setService(TestJobService.class)
.setTag("foobar")
.setConstraints(Constraint.DEVICE_CHARGING)
.setTrigger(Trigger.executionWindow(0, 60))
.build();
} catch (ValidationEnforcer.ValidationException ve) {
fail(TextUtils.join("\n", ve.getErrors()));
}
assertEquals("Expected schedule() request to fail when backend is unavailable",
FirebaseJobDispatcher.SCHEDULE_RESULT_NO_DRIVER_AVAILABLE,
mDispatcher.schedule(job));
}
@Test
public void testCancelJobs_backendUnavailable() throws Exception {
markBackendUnavailable();
assertEquals("Expected cancelAll() request to fail when backend is unavailable",
FirebaseJobDispatcher.CANCEL_RESULT_NO_DRIVER_AVAILABLE,
mDispatcher.cancelAll());
}
@Test
public void testSchedule_sendsAppropriateBroadcast() {
ArgumentCaptor<Intent> pmQueryIntentCaptor = mockPackageManagerInfo();
Job job = mDispatcher.newJobBuilder()
.setConstraints(Constraint.DEVICE_CHARGING)
.setService(TestJobService.class)
.setTrigger(Trigger.executionWindow(0, 60))
.setRecurring(false)
.setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)
.setTag("foobar")
.build();
Intent pmQueryIntent = pmQueryIntentCaptor.getValue();
assertEquals(JobService.ACTION_EXECUTE, pmQueryIntent.getAction());
assertEquals(TestJobService.class.getName(), pmQueryIntent.getComponent().getClassName());
assertEquals("Expected schedule() request to succeed",
FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS,
mDispatcher.schedule(job));
final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext).sendBroadcast(captor.capture());
Intent broadcast = captor.getValue();
assertNotNull(broadcast);
assertEquals("com.google.android.gms.gcm.ACTION_SCHEDULE", broadcast.getAction());
assertEquals("SCHEDULE_TASK", broadcast.getStringExtra("scheduler_action"));
assertEquals("com.google.android.gms", broadcast.getPackage());
assertEquals(8, broadcast.getIntExtra("source", -1));
assertEquals(1, broadcast.getIntExtra("source_version", -1));
final Parcelable parcelablePendingIntent = broadcast.getParcelableExtra("app");
assertTrue("Expected 'app' value to be a PendingIntent",
parcelablePendingIntent instanceof PendingIntent);
}
private ArgumentCaptor<Intent> mockPackageManagerInfo() {
PackageManager packageManager = mock(PackageManager.class);
when(mMockContext.getPackageManager()).thenReturn(packageManager);
ArgumentCaptor<Intent> intentArgCaptor = ArgumentCaptor.forClass(Intent.class);
ResolveInfo info = new ResolveInfo();
info.serviceInfo = new ServiceInfo();
info.serviceInfo.enabled = true;
//noinspection WrongConstant
when(packageManager.queryIntentServices(intentArgCaptor.capture(), eq(0)))
.thenReturn(Arrays.asList(info));
return intentArgCaptor;
}
@Test
public void testCancel_sendsAppropriateBroadcast() {
mDispatcher.cancel("foobar");
ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
verify(mMockContext).sendBroadcast(captor.capture());
Intent broadcast = captor.getValue();
assertNotNull(broadcast);
assertEquals("foobar", broadcast.getStringExtra("tag"));
}
private void markBackendUnavailable() {
mDriver.available = false;
}
public final static class TestJobDriver implements Driver {
public boolean available = true;
private final Driver wrappedDriver;
public TestJobDriver(Driver wrappedDriver) {
this.wrappedDriver = wrappedDriver;
}
@Override
public int schedule(@NonNull Job job) {
return this.wrappedDriver.schedule(job);
}
@Override
public int cancel(@NonNull String tag) {
return this.wrappedDriver.cancel(tag);
}
@Override
public int cancelAll() {
return this.wrappedDriver.cancelAll();
}
@NonNull
@Override
public JobValidator getValidator() {
return this.wrappedDriver.getValidator();
}
@Override
public boolean isAvailable() {
return available;
}
}
}

View File

@ -1,268 +0,0 @@
// Copyright 2016 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import com.firebase.jobdispatcher.Job.Builder;
import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger;
import com.firebase.jobdispatcher.ObservedUri.Flags;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class GooglePlayJobWriterTest {
private static final boolean[] ALL_BOOLEANS = {true, false};
private GooglePlayJobWriter mWriter;
private static Builder initializeDefaultBuilder() {
return TestUtil.getBuilderWithNoopValidator()
.setConstraints(Constraint.DEVICE_CHARGING)
.setExtras(null)
.setLifetime(Lifetime.FOREVER)
.setRecurring(false)
.setReplaceCurrent(false)
.setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)
.setService(TestJobService.class)
.setTag("tag")
.setTrigger(Trigger.NOW);
}
@Before
public void setUp() throws Exception {
mWriter = new GooglePlayJobWriter();
}
@Test
public void testWriteToBundle_tags() {
for (String tag : Arrays.asList("foo", "bar", "foobar", "this is a tag")) {
Bundle b = mWriter.writeToBundle(
initializeDefaultBuilder().setTag(tag).build(),
new Bundle());
assertEquals("tag", tag, b.getString("tag"));
}
}
@Test
public void testWriteToBundle_updateCurrent() {
for (boolean replaceCurrent : ALL_BOOLEANS) {
Bundle b = mWriter.writeToBundle(
initializeDefaultBuilder().setReplaceCurrent(replaceCurrent).build(),
new Bundle());
assertEquals("update_current", replaceCurrent, b.getBoolean("update_current"));
}
}
@Test
public void testWriteToBundle_persisted() {
Bundle b = mWriter.writeToBundle(
initializeDefaultBuilder().setLifetime(Lifetime.FOREVER).build(),
new Bundle());
assertTrue("persisted", b.getBoolean("persisted"));
for (int lifetime : new int[]{Lifetime.UNTIL_NEXT_BOOT}) {
b = mWriter.writeToBundle(
initializeDefaultBuilder().setLifetime(lifetime).build(),
new Bundle());
assertFalse("persisted", b.getBoolean("persisted"));
}
}
@Test
public void testWriteToBundle_service() {
Bundle b = mWriter.writeToBundle(
initializeDefaultBuilder().setService(TestJobService.class).build(),
new Bundle());
assertEquals("service", GooglePlayReceiver.class.getName(), b.getString("service"));
}
@Test
public void testWriteToBundle_requiredNetwork() {
Map<Integer, Integer> mapping = new HashMap<>();
mapping.put(Constraint.ON_ANY_NETWORK, GooglePlayJobWriter.LEGACY_NETWORK_CONNECTED);
mapping.put(Constraint.ON_UNMETERED_NETWORK, GooglePlayJobWriter.LEGACY_NETWORK_UNMETERED);
mapping.put(0, GooglePlayJobWriter.LEGACY_NETWORK_ANY);
for (Entry<Integer, Integer> testCase : mapping.entrySet()) {
@SuppressWarnings("WrongConstant")
Bundle b = mWriter.writeToBundle(
initializeDefaultBuilder().setConstraints(testCase.getKey()).build(),
new Bundle());
assertEquals("requiredNetwork", (int) testCase.getValue(), b.getInt("requiredNetwork"));
}
}
@Test
public void testWriteToBundle_unmeteredConstraintShouldTakePrecendence() {
Bundle b = mWriter.writeToBundle(
initializeDefaultBuilder()
.setConstraints(Constraint.ON_ANY_NETWORK, Constraint.ON_UNMETERED_NETWORK)
.build(),
new Bundle());
assertEquals("expected ON_UNMETERED_NETWORK to take precendence over ON_ANY_NETWORK",
GooglePlayJobWriter.LEGACY_NETWORK_UNMETERED, b.getInt("requiredNetwork"));
}
@Test
public void testWriteToBundle_requiresCharging() {
assertTrue("requiresCharging", mWriter.writeToBundle(
initializeDefaultBuilder().setConstraints(Constraint.DEVICE_CHARGING).build(),
new Bundle()).getBoolean("requiresCharging"));
for (Integer constraint : Arrays.asList(
Constraint.ON_ANY_NETWORK,
Constraint.ON_UNMETERED_NETWORK)) {
assertFalse("requiresCharging", mWriter.writeToBundle(
initializeDefaultBuilder().setConstraints(constraint).build(),
new Bundle()).getBoolean("requiresCharging"));
}
}
@Test
public void testWriteToBundle_requiresIdle() {
assertTrue("requiresIdle", mWriter.writeToBundle(
initializeDefaultBuilder().setConstraints(Constraint.DEVICE_IDLE).build(),
new Bundle()).getBoolean("requiresIdle"));
for (Integer constraint : Arrays.asList(
Constraint.ON_ANY_NETWORK,
Constraint.ON_UNMETERED_NETWORK)) {
assertFalse("requiresIdle", mWriter.writeToBundle(
initializeDefaultBuilder().setConstraints(constraint).build(),
new Bundle()).getBoolean("requiresIdle"));
}
}
@Test
public void testWriteToBundle_retryPolicy() {
assertEquals("retry_policy",
GooglePlayJobWriter.LEGACY_RETRY_POLICY_EXPONENTIAL,
mWriter.writeToBundle(
initializeDefaultBuilder()
.setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)
.build(),
new Bundle()).getBundle("retryStrategy").getInt("retry_policy"));
assertEquals("retry_policy",
GooglePlayJobWriter.LEGACY_RETRY_POLICY_LINEAR,
mWriter.writeToBundle(
initializeDefaultBuilder()
.setRetryStrategy(RetryStrategy.DEFAULT_LINEAR)
.build(),
new Bundle()).getBundle("retryStrategy").getInt("retry_policy"));
}
@Test
public void testWriteToBundle_backoffSeconds() {
for (RetryStrategy retryStrategy : Arrays
.asList(RetryStrategy.DEFAULT_EXPONENTIAL, RetryStrategy.DEFAULT_LINEAR)) {
Bundle b = mWriter.writeToBundle(
initializeDefaultBuilder().setRetryStrategy(retryStrategy).build(),
new Bundle()).getBundle("retryStrategy");
assertEquals("initial_backoff_seconds",
retryStrategy.getInitialBackoff(),
b.getInt("initial_backoff_seconds"));
assertEquals("maximum_backoff_seconds",
retryStrategy.getMaximumBackoff(),
b.getInt("maximum_backoff_seconds"));
}
}
@Test
public void testWriteToBundle_triggers() {
// immediate
Bundle b = mWriter.writeToBundle(
initializeDefaultBuilder().setTrigger(Trigger.NOW).build(),
new Bundle());
assertEquals("window_start", 0, b.getLong("window_start"));
assertEquals("window_end", 30, b.getLong("window_end"));
// execution window (oneoff)
JobTrigger.ExecutionWindowTrigger t = Trigger.executionWindow(631, 978);
b = mWriter.writeToBundle(
initializeDefaultBuilder().setTrigger(t).build(),
new Bundle());
assertEquals("window_start", t.getWindowStart(), b.getLong("window_start"));
assertEquals("window_end", t.getWindowEnd(), b.getLong("window_end"));
// execution window (periodic)
b = mWriter.writeToBundle(
initializeDefaultBuilder().setRecurring(true).setTrigger(t).build(),
new Bundle());
assertEquals("period", t.getWindowEnd(), b.getLong("period"));
assertEquals("period_flex", t.getWindowEnd() - t.getWindowStart(), b.getLong("period_flex"));
}
@Test
public void testWriteToBundle_contentUriTrigger() {
ObservedUri observedUri = new ObservedUri(ContactsContract.AUTHORITY_URI,
Flags.FLAG_NOTIFY_FOR_DESCENDANTS);
ContentUriTrigger contentUriTrigger = Trigger.contentUriTrigger(Arrays.asList(observedUri));
Bundle bundle = mWriter.writeToBundle(
initializeDefaultBuilder().setTrigger(contentUriTrigger).build(), new Bundle());
Uri[] uris =
(Uri[]) bundle.getParcelableArray(BundleProtocol.PACKED_PARAM_CONTENT_URI_ARRAY);
int[] flags = bundle.getIntArray(BundleProtocol.PACKED_PARAM_CONTENT_URI_FLAGS_ARRAY);
assertTrue("Array size", uris.length == flags.length && flags.length == 1);
assertEquals(BundleProtocol.PACKED_PARAM_CONTENT_URI_ARRAY,
ContactsContract.AUTHORITY_URI, uris[0]);
assertEquals(BundleProtocol.PACKED_PARAM_CONTENT_URI_FLAGS_ARRAY,
Flags.FLAG_NOTIFY_FOR_DESCENDANTS, flags[0]);
}
@Test
public void testWriteToBundle_extras() {
Bundle extras = new Bundle();
Bundle result = mWriter.writeToBundle(
initializeDefaultBuilder().setExtras(extras).build(),
new Bundle());
assertEquals("extras", extras, result.getBundle("extras"));
}
}

View File

@ -1,153 +0,0 @@
// Copyright 2016 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.firebase.jobdispatcher.GooglePlayJobWriter.REQUEST_PARAM_TAG;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.AppOpsManager;
import android.content.Context;
import android.os.Bundle;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import com.firebase.jobdispatcher.JobInvocation.Builder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/**
* Tests {@link GooglePlayMessageHandler}.
*/
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 21)
public class GooglePlayMessageHandlerTest {
@Mock
Looper looper;
@Mock
GooglePlayReceiver receiverMock;
@Mock
Context context;
@Mock
AppOpsManager appOpsManager;
@Mock
Messenger messengerMock;
@Mock
ExecutionDelegator executionDelegatorMock;
GooglePlayMessageHandler handler;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
handler = new GooglePlayMessageHandler(looper, receiverMock);
when(receiverMock.getExecutionDelegator()).thenReturn(executionDelegatorMock);
when(receiverMock.getApplicationContext()).thenReturn(context);
when(context.getSystemService(Context.APP_OPS_SERVICE)).thenReturn(appOpsManager);
}
@Test
public void handleMessage_nullNoException() throws Exception {
handler.handleMessage(null);
}
@Test
public void handleMessage_ignoreIfSenderIsNotGcm() throws Exception {
Message message = Message.obtain();
message.what = GooglePlayMessageHandler.MSG_START_EXEC;
Bundle data = new Bundle();
data.putString(REQUEST_PARAM_TAG, "TAG");
message.setData(data);
message.replyTo = messengerMock;
doThrow(new SecurityException()).when(appOpsManager)
.checkPackage(message.sendingUid, GooglePlayDriver.BACKEND_PACKAGE);
handler.handleMessage(message);
verify(receiverMock, never()).prepareJob(any(GooglePlayMessengerCallback.class), eq(data));
}
@Test
public void handleMessage_startExecution_noData() throws Exception {
Message message = Message.obtain();
message.what = GooglePlayMessageHandler.MSG_START_EXEC;
message.replyTo = messengerMock;
handler.handleMessage(message);
verify(receiverMock, never())
.prepareJob(any(GooglePlayMessengerCallback.class), any(Bundle.class));
}
@Test
public void handleMessage_startExecution() throws Exception {
Message message = Message.obtain();
message.what = GooglePlayMessageHandler.MSG_START_EXEC;
Bundle data = new Bundle();
data.putString(REQUEST_PARAM_TAG, "TAG");
message.setData(data);
message.replyTo = messengerMock;
JobInvocation jobInvocation = new Builder()
.setTag("tag")
.setService(TestJobService.class.getName())
.setTrigger(Trigger.NOW).build();
when(receiverMock.prepareJob(any(GooglePlayMessengerCallback.class), eq(data)))
.thenReturn(jobInvocation);
handler.handleMessage(message);
verify(executionDelegatorMock).executeJob(jobInvocation);
}
@Test
public void handleMessage_stopExecution() throws Exception {
Message message = Message.obtain();
message.what = GooglePlayMessageHandler.MSG_STOP_EXEC;
JobCoder jobCoder = GooglePlayReceiver.getJobCoder();
Bundle data = TestUtil.encodeContentUriJob(TestUtil.getContentUriTrigger(), jobCoder);
JobInvocation jobInvocation = jobCoder.decode(data).build();
message.setData(data);
message.replyTo = messengerMock;
handler.handleMessage(message);
final ArgumentCaptor<JobInvocation> captor = ArgumentCaptor.forClass(JobInvocation.class);
verify(executionDelegatorMock).stopJob(captor.capture());
TestUtil.assertJobsEqual(jobInvocation, captor.getValue());
}
@Test
public void handleMessage_stopExecution_invalidNoCrash() throws Exception {
Message message = Message.obtain();
message.what = GooglePlayMessageHandler.MSG_STOP_EXEC;
message.replyTo = messengerMock;
handler.handleMessage(message);
verify(executionDelegatorMock, never()).stopJob(any(JobInvocation.class));
}
}

View File

@ -1,63 +0,0 @@
// Copyright 2016 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.firebase.jobdispatcher.GooglePlayJobWriter.REQUEST_PARAM_TAG;
import static org.junit.Assert.assertEquals;
import android.os.Message;
import android.os.Messenger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/**
* Tests {@link GooglePlayMessengerCallback}.
*/
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 21)
public class GooglePlayMessengerCallbackTest {
@Mock
Messenger messengerMock;
GooglePlayMessengerCallback callback;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
callback = new GooglePlayMessengerCallback(messengerMock, "tag");
}
@Test
public void jobFinished() throws Exception {
final ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
callback.jobFinished(JobService.RESULT_SUCCESS);
Mockito.verify(messengerMock).send(messageCaptor.capture());
Message message = messageCaptor.getValue();
assertEquals(message.what, GooglePlayMessageHandler.MSG_RESULT);
assertEquals(message.arg1, JobService.RESULT_SUCCESS);
assertEquals(message.getData().getString(REQUEST_PARAM_TAG), "tag");
}
}

View File

@ -1,327 +0,0 @@
// Copyright 2016 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.firebase.jobdispatcher.TestUtil.encodeContentUriJob;
import static com.firebase.jobdispatcher.TestUtil.encodeRecurringContentUriJob;
import static com.firebase.jobdispatcher.TestUtil.getContentUriTrigger;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Messenger;
import android.os.Parcel;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.MediaStore.Images.Media;
import android.support.annotation.NonNull;
import com.firebase.jobdispatcher.GooglePlayReceiverTest.ShadowMessenger;
import com.firebase.jobdispatcher.JobInvocation.Builder;
import com.firebase.jobdispatcher.TestUtil.InspectableBinder;
import com.google.android.gms.gcm.PendingCallback;
import java.util.ArrayList;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implements;
@RunWith(RobolectricTestRunner.class)
@Config(
constants = BuildConfig.class,
manifest = Config.NONE,
sdk = 21,
shadows = {ShadowMessenger.class}
)
public class GooglePlayReceiverTest {
/**
* The default ShadowMessenger implementation causes NPEs when using the
* {@link Messenger#Messenger(Handler)} constructor. We create our own empty Shadow so we can
* just use the standard Android implementation, which is totally fine.
*
* @see <a href="https://github.com/robolectric/robolectric/issues/2246">Robolectric issue</a>
*
*/
@Implements(Messenger.class)
public static class ShadowMessenger {}
GooglePlayReceiver receiver;
JobCoder jobCoder = new JobCoder(BundleProtocol.PACKED_PARAM_BUNDLE_PREFIX, true);
@Mock
Messenger messengerMock;
@Mock
IBinder binderMock;
@Mock
JobCallback callbackMock;
@Mock
ExecutionDelegator executionDelegatorMock;
@Mock
Driver driverMock;
@Captor
ArgumentCaptor<Job> jobArgumentCaptor;
ArrayList<Uri> triggeredUris = new ArrayList<>();
{
triggeredUris.add(ContactsContract.AUTHORITY_URI);
triggeredUris.add(Media.EXTERNAL_CONTENT_URI);
}
Builder jobInvocationBuilder = new Builder()
.setTag("tag")
.setService(TestJobService.class.getName())
.setTrigger(Trigger.NOW);
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
receiver = spy(new GooglePlayReceiver());
when(receiver.getExecutionDelegator()).thenReturn(executionDelegatorMock);
receiver.driver = driverMock;
receiver.validationEnforcer = new ValidationEnforcer(new NoopJobValidator());
}
@Test
public void onJobFinished_unknownJobCallbackIsNotPresent_ignoreNoException() {
receiver.onJobFinished(jobInvocationBuilder.build(), JobService.RESULT_SUCCESS);
verifyZeroInteractions(driverMock);
}
@Test
public void onJobFinished_notRecurringContentJob_sendResult() {
jobInvocationBuilder.setTrigger(
Trigger.contentUriTrigger(Arrays.asList(new ObservedUri(Contacts.CONTENT_URI, 0))));
JobInvocation jobInvocation = receiver
.prepareJob(callbackMock, getBundleForContentJobExecution());
receiver.onJobFinished(jobInvocation, JobService.RESULT_SUCCESS);
verify(callbackMock).jobFinished(JobService.RESULT_SUCCESS);
verifyZeroInteractions(driverMock);
}
@Test
public void onJobFinished_successRecurringContentJob_reschedule() {
JobInvocation jobInvocation = receiver
.prepareJob(callbackMock, getBundleForContentJobExecutionRecurring());
receiver.onJobFinished(jobInvocation, JobService.RESULT_SUCCESS);
verify(driverMock).schedule(jobArgumentCaptor.capture());
// No need to callback when job finished.
// Reschedule request is treated as two events: completion of old job and scheduling of new
// job with the same parameters.
verifyZeroInteractions(callbackMock);
Job rescheduledJob = jobArgumentCaptor.getValue();
TestUtil.assertJobsEqual(jobInvocation, rescheduledJob);
}
@Test
public void onJobFinished_failWithRetryRecurringContentJob_sendResult() {
JobInvocation jobInvocation = receiver
.prepareJob(callbackMock, getBundleForContentJobExecutionRecurring());
receiver.onJobFinished(jobInvocation, JobService.RESULT_FAIL_RETRY);
// If a job finishes with RESULT_FAIL_RETRY we don't need to send a reschedule request.
// Rescheduling will erase previously triggered URIs.
verify(callbackMock).jobFinished(JobService.RESULT_FAIL_RETRY);
verifyZeroInteractions(driverMock);
}
@Test
public void prepareJob() {
Intent intent = new Intent();
Bundle encode = encodeContentUriJob(getContentUriTrigger(), jobCoder);
intent.putExtra(GooglePlayJobWriter.REQUEST_PARAM_EXTRAS, encode);
Parcel container = Parcel.obtain();
container.writeStrongBinder(new Binder());
PendingCallback pcb = new PendingCallback(container);
intent.putExtra("callback", pcb);
ArrayList<Uri> uris = new ArrayList<>();
uris.add(ContactsContract.AUTHORITY_URI);
uris.add(Media.EXTERNAL_CONTENT_URI);
intent.putParcelableArrayListExtra(BundleProtocol.PACKED_PARAM_TRIGGERED_URIS, uris);
JobInvocation jobInvocation = receiver.prepareJob(intent);
assertEquals(jobInvocation.getTriggerReason().getTriggeredContentUris(), uris);
}
@Test
public void prepareJob_messenger() {
JobInvocation jobInvocation = receiver.prepareJob(callbackMock, new Bundle());
assertNull(jobInvocation);
verify(callbackMock).jobFinished(JobService.RESULT_FAIL_NORETRY);
}
@Test
public void prepareJob_messenger_noExtras() {
Bundle bundle = getBundleForContentJobExecution();
JobInvocation jobInvocation = receiver.prepareJob(callbackMock, bundle);
assertEquals(jobInvocation.getTriggerReason().getTriggeredContentUris(), triggeredUris);
}
@NonNull
private Bundle getBundleForContentJobExecution() {
Bundle bundle = new Bundle();
Bundle encode = encodeContentUriJob(getContentUriTrigger(), jobCoder);
bundle.putBundle(GooglePlayJobWriter.REQUEST_PARAM_EXTRAS, encode);
bundle.putParcelableArrayList(BundleProtocol.PACKED_PARAM_TRIGGERED_URIS, triggeredUris);
return bundle;
}
@NonNull
private Bundle getBundleForContentJobExecutionRecurring() {
Bundle bundle = new Bundle();
Bundle encode = encodeRecurringContentUriJob(getContentUriTrigger(), jobCoder);
bundle.putBundle(GooglePlayJobWriter.REQUEST_PARAM_EXTRAS, encode);
bundle.putParcelableArrayList(BundleProtocol.PACKED_PARAM_TRIGGERED_URIS, triggeredUris);
return bundle;
}
@Test
public void onBind() {
Intent intent = new Intent(GooglePlayReceiver.ACTION_EXECUTE);
IBinder binderA = receiver.onBind(intent);
IBinder binderB = receiver.onBind(intent);
assertEquals(binderA, binderB);
}
@Test
public void onBind_nullIntent() {
IBinder binder = receiver.onBind(null);
assertNull(binder);
}
@Test
public void onBind_wrongAction() {
Intent intent = new Intent("test");
IBinder binder = receiver.onBind(intent);
assertNull(binder);
}
@Test
@Config(sdk = VERSION_CODES.KITKAT)
public void onBind_wrongBuild() {
Intent intent = new Intent(GooglePlayReceiver.ACTION_EXECUTE);
IBinder binder = receiver.onBind(intent);
assertNull(binder);
}
@Test
public void onStartCommand_nullIntent() {
assertResultWasStartNotSticky(receiver.onStartCommand(null, 0, 101));
verify(receiver).stopSelf(101);
}
@Test
public void onStartCommand_initAction() {
Intent initIntent = new Intent("com.google.android.gms.gcm.SERVICE_ACTION_INITIALIZE");
assertResultWasStartNotSticky(receiver.onStartCommand(initIntent, 0, 101));
verify(receiver).stopSelf(101);
}
@Test
public void onStartCommand_unknownAction() {
Intent unknownIntent = new Intent("com.example.foo.bar");
assertResultWasStartNotSticky(receiver.onStartCommand(unknownIntent, 0, 101));
assertResultWasStartNotSticky(receiver.onStartCommand(unknownIntent, 0, 102));
assertResultWasStartNotSticky(receiver.onStartCommand(unknownIntent, 0, 103));
InOrder inOrder = inOrder(receiver);
inOrder.verify(receiver).stopSelf(101);
inOrder.verify(receiver).stopSelf(102);
inOrder.verify(receiver).stopSelf(103);
}
@Test
public void onStartCommand_executeActionWithEmptyExtras() {
Intent execIntent = new Intent("com.google.android.gms.gcm.ACTION_TASK_READY");
assertResultWasStartNotSticky(receiver.onStartCommand(execIntent, 0, 101));
verify(receiver).stopSelf(101);
}
@Test
public void onStartCommand_executeAction() {
JobInvocation job = new JobInvocation.Builder()
.setTag("tag")
.setService("com.example.foo.FooService")
.setTrigger(Trigger.NOW)
.setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)
.setLifetime(Lifetime.UNTIL_NEXT_BOOT)
.setConstraints(new int[]{Constraint.DEVICE_IDLE})
.build();
Intent execIntent = new Intent("com.google.android.gms.gcm.ACTION_TASK_READY")
.putExtra("extras", new JobCoder(BundleProtocol.PACKED_PARAM_BUNDLE_PREFIX, true)
.encode(job, new Bundle()))
.putExtra("callback", new InspectableBinder().toPendingCallback());
when(executionDelegatorMock.executeJob(any(JobInvocation.class))).thenReturn(true);
assertResultWasStartNotSticky(receiver.onStartCommand(execIntent, 0, 101));
verify(receiver, never()).stopSelf(anyInt());
verify(executionDelegatorMock).executeJob(any(JobInvocation.class));
receiver.onJobFinished(job, JobService.RESULT_SUCCESS);
verify(receiver).stopSelf(101);
}
private void assertResultWasStartNotSticky(int result) {
assertEquals(
"Result for onStartCommand wasn't START_NOT_STICKY", Service.START_NOT_STICKY, result);
}
}

View File

@ -1,34 +0,0 @@
// Copyright 2016 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 org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class ImmediateTriggerTest {
/**
* Code coverage.
*/
@Test
public void testPrivateConstructor() throws Exception {
TestUtil.assertHasSinglePrivateConstructor(JobTrigger.ImmediateTrigger.class);
}
}

View File

@ -1,65 +0,0 @@
// Copyright 2016 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 org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class JobBuilderTest {
private static final int[] ALL_LIFETIMES = {Lifetime.UNTIL_NEXT_BOOT, Lifetime.FOREVER};
private Job.Builder mBuilder;
@Before
public void setUp() throws Exception {
mBuilder = TestUtil.getBuilderWithNoopValidator();
}
@Test
public void testAddConstraints() {
mBuilder.setConstraints()
.addConstraint(Constraint.DEVICE_CHARGING)
.addConstraint(Constraint.ON_UNMETERED_NETWORK);
int[] expected = {Constraint.DEVICE_CHARGING, Constraint.ON_UNMETERED_NETWORK};
assertEquals(Constraint.compact(expected), Constraint.compact(mBuilder.getConstraints()));
}
@Test
public void testSetLifetime() {
for (int lifetime : ALL_LIFETIMES) {
mBuilder.setLifetime(lifetime);
assertEquals(lifetime, mBuilder.getLifetime());
}
}
@Test
public void testSetShouldReplaceCurrent() {
for (boolean replace : new boolean[]{true, false}) {
mBuilder.setReplaceCurrent(replace);
assertEquals(replace, mBuilder.shouldReplaceCurrent());
}
}
}

View File

@ -1,158 +0,0 @@
// Copyright 2016 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.firebase.jobdispatcher.TestUtil.encodeContentUriJob;
import static com.firebase.jobdispatcher.TestUtil.getContentUriTrigger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.MediaStore.Images.Media;
import com.firebase.jobdispatcher.Job.Builder;
import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger;
import java.util.ArrayList;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class JobCoderTest {
private final JobCoder mCoder = new JobCoder(PREFIX, true);
private static final String PREFIX = "prefix";
private Builder mBuilder;
private static Builder setValidBuilderDefaults(Builder mBuilder) {
return mBuilder
.setTag("tag")
.setTrigger(Trigger.NOW)
.setService(TestJobService.class)
.setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL);
}
@Before
public void setUp() throws Exception {
mBuilder = TestUtil.getBuilderWithNoopValidator();
}
@Test
public void testCodingIsLossless() {
for (JobParameters input : TestUtil.getJobCombinations(mBuilder)) {
JobParameters output = mCoder.decode(mCoder.encode(input, input.getExtras())).build();
TestUtil.assertJobsEqual(input, output);
}
}
@Test(expected = IllegalArgumentException.class)
public void testEncode_throwsOnNullBundle() {
mCoder.encode(mBuilder.build(), null);
}
@Test(expected = IllegalArgumentException.class)
public void testDecode_throwsOnNullBundle() {
mCoder.decode(null);
}
@Test
public void testDecode_failsWhenMissingFields() {
assertNull("Expected null tag to cause decoding to fail",
mCoder.decode(mCoder.encode(
setValidBuilderDefaults(mBuilder).setTag(null).build(),
new Bundle())));
assertNull("Expected null service to cause decoding to fail",
mCoder.decode(mCoder.encode(
setValidBuilderDefaults(mBuilder).setService(null).build(),
new Bundle())));
}
@Test(expected = IllegalArgumentException.class)
public void testDecode_failsUnsupportedTrigger() {
mCoder.decode(mCoder.encode(setValidBuilderDefaults(mBuilder).setTrigger(null).build(),
new Bundle()));
}
@Test
public void testDecode_ignoresMissingRetryStrategy() {
assertNotNull("Expected null retry strategy to cause decode to use a default",
mCoder.decode(mCoder.encode(
setValidBuilderDefaults(mBuilder).setRetryStrategy(null).build(),
new Bundle())));
}
@Test
public void encode_contentUriTrigger() {
Bundle encode = TestUtil.encodeContentUriJob(TestUtil.getContentUriTrigger(), mCoder);
int triggerType = encode.getInt(PREFIX + BundleProtocol.PACKED_PARAM_TRIGGER_TYPE);
assertEquals("Trigger type", BundleProtocol.TRIGGER_TYPE_CONTENT_URI, triggerType);
String json = encode.getString(PREFIX + BundleProtocol.PACKED_PARAM_OBSERVED_URI);
String expectedJson = "{\"uri_flags\":[1,0],\"uris\":[\"content:\\/\\/com.android.contacts"
+ "\",\"content:\\/\\/media\\/external\\/images\\/media\"]}";
assertEquals("Json trigger", expectedJson, json);
}
@Test
public void decode_contentUriTrigger() {
ContentUriTrigger contentUriTrigger = TestUtil.getContentUriTrigger();
Bundle bundle = TestUtil.encodeContentUriJob(contentUriTrigger, mCoder);
JobInvocation decode = mCoder.decode(bundle).build();
ContentUriTrigger trigger = (ContentUriTrigger) decode.getTrigger();
assertEquals(contentUriTrigger.getUris(), trigger.getUris());
}
@Test
public void decode_addBundleAsExtras() {
ContentUriTrigger contentUriTrigger = TestUtil.getContentUriTrigger();
Bundle bundle = TestUtil.encodeContentUriJob(contentUriTrigger, mCoder);
bundle.putString("test_key", "test_value");
JobInvocation decode = mCoder.decode(bundle).build();
assertEquals("test_value", decode.getExtras().getString("test_key"));
}
@Test
public void decodeIntentBundle() {
Bundle bundle = new Bundle();
ContentUriTrigger uriTrigger = getContentUriTrigger();
Bundle encode = encodeContentUriJob(uriTrigger, mCoder);
bundle.putBundle(GooglePlayJobWriter.REQUEST_PARAM_EXTRAS, encode);
ArrayList<Uri> uris = new ArrayList<>();
uris.add(ContactsContract.AUTHORITY_URI);
uris.add(Media.EXTERNAL_CONTENT_URI);
bundle.putParcelableArrayList(BundleProtocol.PACKED_PARAM_TRIGGERED_URIS, uris);
JobInvocation jobInvocation = mCoder.decodeIntentBundle(bundle);
assertEquals(uris, jobInvocation.getTriggerReason().getTriggeredContentUris());
assertEquals("TAG", jobInvocation.getTag());
assertEquals(uriTrigger.getUris(), ((ContentUriTrigger) jobInvocation.getTrigger())
.getUris());
assertEquals(TestJobService.class.getName(), jobInvocation.getService());
assertEquals(RetryStrategy.DEFAULT_EXPONENTIAL.getPolicy(),
jobInvocation.getRetryStrategy().getPolicy());
}
}

View File

@ -1,83 +0,0 @@
// Copyright 2016 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.os.Bundle;
import com.firebase.jobdispatcher.JobInvocation.Builder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class JobInvocationTest {
private Builder builder;
@Before
public void setUp() {
builder = new Builder()
.setTag("tag")
.setService(TestJobService.class.getName())
.setTrigger(Trigger.NOW);
}
@SuppressWarnings("ConstantConditions")
@Test
public void testShouldReplaceCurrent() throws Exception {
assertTrue("Expected shouldReplaceCurrent() to return value passed in constructor",
builder.setReplaceCurrent(true).build().shouldReplaceCurrent());
assertFalse("Expected shouldReplaceCurrent() to return value passed in constructor",
builder.setReplaceCurrent(false).build().shouldReplaceCurrent());
}
@Test
public void extras() throws Exception {
assertNotNull(builder.build().getExtras());
Bundle bundle = new Bundle();
bundle.putLong("test", 1L);
Bundle extras = builder.addExtras(bundle).build().getExtras();
assertEquals(1, extras.size());
assertEquals(1L, extras.getLong("test"));
}
@Test
public void contract_hashCode_equals() {
JobInvocation jobInvocation = builder.build();
assertEquals(jobInvocation, builder.build());
assertEquals(jobInvocation.hashCode(), builder.build().hashCode());
JobInvocation jobInvocationNew = builder.setTag("new").build();
assertNotEquals(jobInvocation, jobInvocationNew);
assertNotEquals(jobInvocation.hashCode(), jobInvocationNew.hashCode());
}
@Test
public void contract_hashCode_equals_triggerShouldBeIgnored() {
JobInvocation jobInvocation = builder.build();
JobInvocation periodic = builder.setTrigger(Trigger.executionWindow(0, 1)).build();
assertEquals(jobInvocation, periodic);
assertEquals(jobInvocation.hashCode(), periodic.hashCode());
}
}

View File

@ -1,116 +0,0 @@
// Copyright 2016 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 junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.os.IBinder;
import android.os.Message;
import com.firebase.jobdispatcher.JobInvocation.Builder;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/**
* Test for {@link JobServiceConnection}.
*/
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class JobServiceConnectionTest {
JobInvocation job = new Builder()
.setTag("tag")
.setService(TestJobService.class.getName())
.setTrigger(Trigger.NOW)
.build();
@Mock
Message messageMock;
@Mock
JobService.LocalBinder binderMock;
@Mock
JobService jobServiceMock;
JobServiceConnection connection;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(binderMock.getService()).thenReturn(jobServiceMock);
connection = new JobServiceConnection(job, messageMock);
}
@Test
public void fullConnectionCycle() {
assertFalse(connection.isBound());
connection.onServiceConnected(null, binderMock);
verify(jobServiceMock).start(job, messageMock);
assertTrue(connection.isBound());
connection.onStop();
verify(jobServiceMock).stop(job);
assertTrue(connection.isBound());
connection.onServiceDisconnected(null);
assertFalse(connection.isBound());
}
@Test
public void onServiceConnected_shouldNotSendExecutionRequestTwice() {
assertFalse(connection.isBound());
connection.onServiceConnected(null, binderMock);
verify(jobServiceMock).start(job, messageMock);
assertTrue(connection.isBound());
reset(jobServiceMock);
connection.onServiceConnected(null, binderMock);
verify(jobServiceMock, never()).start(job, messageMock); // start should not be called again
connection.onStop();
verify(jobServiceMock).stop(job);
assertTrue(connection.isBound());
connection.onServiceDisconnected(null);
assertFalse(connection.isBound());
}
@Test
public void stopOnUnboundConnection() {
assertFalse(connection.isBound());
connection.onStop();
verify(jobServiceMock, never()).onStopJob(job);
}
@Test
public void onServiceConnectedWrongBinder() {
IBinder binder = mock(IBinder.class);
connection.onServiceConnected(null, binder);
assertFalse(connection.isBound());
}
}

View File

@ -1,346 +0,0 @@
// Copyright 2016 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.Parcel;
import com.firebase.jobdispatcher.JobInvocation.Builder;
import com.google.android.gms.gcm.PendingCallback;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class JobServiceTest {
private static CountDownLatch countDownLatch;
@Before
public void setUp() throws Exception {}
@After
public void tearDown() throws Exception {
countDownLatch = null;
}
@Test
public void testOnStartCommand_handlesNullIntent() throws Exception {
JobService service = spy(new ExampleJobService());
int startId = 7;
try {
service.onStartCommand(null, 0, startId);
verify(service).stopSelf(startId);
} catch (NullPointerException npe) {
fail("Unexpected NullPointerException after calling onStartCommand with a null Intent.");
}
}
@Test
public void testOnStartCommand_handlesNullAction() throws Exception {
JobService service = spy(new ExampleJobService());
int startId = 7;
Intent nullActionIntent = new Intent();
service.onStartCommand(nullActionIntent, 0, startId);
verify(service).stopSelf(startId);
}
@Test
public void testOnStartCommand_handlesEmptyAction() throws Exception {
JobService service = spy(new ExampleJobService());
int startId = 7;
Intent emptyActionIntent = new Intent("");
service.onStartCommand(emptyActionIntent, 0, startId);
verify(service).stopSelf(startId);
}
@Test
public void testOnStartCommand_handlesUnknownAction() throws Exception {
JobService service = spy(new ExampleJobService());
int startId = 7;
Intent emptyActionIntent = new Intent("foo.bar.baz");
service.onStartCommand(emptyActionIntent, 0, startId);
verify(service).stopSelf(startId);
}
@Test
public void testOnStartCommand_handlesStartJob_nullData() {
JobService service = spy(new ExampleJobService());
int startId = 7;
Intent executeJobIntent = new Intent(JobService.ACTION_EXECUTE);
service.onStartCommand(executeJobIntent, 0, startId);
verify(service).stopSelf(startId);
}
@Test
public void testOnStartCommand_handlesStartJob_noTag() {
JobService service = spy(new ExampleJobService());
int startId = 7;
Intent executeJobIntent = new Intent(JobService.ACTION_EXECUTE);
Parcel p = Parcel.obtain();
p.writeStrongBinder(mock(IBinder.class));
executeJobIntent.putExtra("callback", new PendingCallback(p));
service.onStartCommand(executeJobIntent, 0, startId);
verify(service).stopSelf(startId);
p.recycle();
}
@Test
public void testOnStartCommand_handlesStartJob_noCallback() {
JobService service = spy(new ExampleJobService());
int startId = 7;
Intent executeJobIntent = new Intent(JobService.ACTION_EXECUTE);
executeJobIntent.putExtra("tag", "foobar");
service.onStartCommand(executeJobIntent, 0, startId);
verify(service).stopSelf(startId);
}
@Test
public void testOnStartCommand_handlesStartJob_validRequest() throws InterruptedException {
JobService service = spy(new ExampleJobService());
HandlerThread ht = new HandlerThread("handler");
ht.start();
Handler h = new Handler(ht.getLooper());
Intent executeJobIntent = new Intent(JobService.ACTION_EXECUTE);
Job jobSpec = TestUtil.getBuilderWithNoopValidator()
.setTag("tag")
.setService(ExampleJobService.class)
.setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)
.setTrigger(Trigger.NOW)
.setLifetime(Lifetime.FOREVER)
.build();
countDownLatch = new CountDownLatch(1);
((JobService.LocalBinder) service.onBind(executeJobIntent))
.getService()
.start(jobSpec, h.obtainMessage(ExecutionDelegator.JOB_FINISHED, jobSpec));
assertTrue("Expected job to run to completion", countDownLatch.await(5, TimeUnit.SECONDS));
}
@Test
public void testOnStartCommand_handlesStartJob_doNotStartRunningJobAgain() {
StoppableJobService service = new StoppableJobService(false);
Job jobSpec = TestUtil.getBuilderWithNoopValidator()
.setTag("tag")
.setService(StoppableJobService.class)
.setTrigger(Trigger.NOW)
.build();
((JobService.LocalBinder) service.onBind(null)).getService().start(jobSpec, null);
((JobService.LocalBinder) service.onBind(null)).getService().start(jobSpec, null);
assertEquals(1, service.getNumberOfExecutionRequestsReceived());
}
@Test
public void stop_noCallback_finished() {
JobService service = spy(new StoppableJobService(false));
JobInvocation job = new Builder()
.setTag("Tag")
.setTrigger(Trigger.NOW)
.setService(StoppableJobService.class.getName())
.build();
service.stop(job);
verify(service, never()).onStopJob(job);
}
@Test
public void stop_withCallback_retry() {
JobService service = spy(new StoppableJobService(false));
JobInvocation job = new Builder()
.setTag("Tag")
.setTrigger(Trigger.NOW)
.setService(StoppableJobService.class.getName())
.build();
Handler handlerMock = mock(Handler.class);
Message message = Message.obtain(handlerMock);
service.start(job, message);
service.stop(job);
verify(service).onStopJob(job);
verify(handlerMock).sendMessage(message);
assertEquals(message.arg1, JobService.RESULT_SUCCESS);
}
@Test
public void stop_withCallback_done() {
JobService service = spy(new StoppableJobService(true));
JobInvocation job = new Builder()
.setTag("Tag")
.setTrigger(Trigger.NOW)
.setService(StoppableJobService.class.getName())
.build();
Handler handlerMock = mock(Handler.class);
Message message = Message.obtain(handlerMock);
service.start(job, message);
service.stop(job);
verify(service).onStopJob(job);
verify(handlerMock).sendMessage(message);
assertEquals(message.arg1, JobService.RESULT_FAIL_RETRY);
}
@Test
public void onStartJob_jobFinishedReschedule() {
// Verify that a retry request from within onStartJob will cause the retry result to be sent
// to the bouncer service's handler, regardless of what value is ultimately returned from
// onStartJob.
JobService reschedulingService = new JobService() {
@Override
public boolean onStartJob(JobParameters job) {
// Reschedules job.
jobFinished(job, true /* retry this job */);
return false;
}
@Override
public boolean onStopJob(JobParameters job) {
return false;
}
};
Job jobSpec = TestUtil.getBuilderWithNoopValidator()
.setTag("tag")
.setService(reschedulingService.getClass())
.setTrigger(Trigger.NOW)
.build();
Handler mock = mock(Handler.class);
Message message = new Message();
message.setTarget(mock);
reschedulingService.start(jobSpec, message);
verify(mock).sendMessage(message);
assertEquals(message.arg1, JobService.RESULT_FAIL_RETRY);
}
@Test
public void onStartJob_jobFinishedNotReschedule() {
// Verify that a termination request from within onStartJob will cause the result to be sent
// to the bouncer service's handler, regardless of what value is ultimately returned from
// onStartJob.
JobService reschedulingService = new JobService() {
@Override
public boolean onStartJob(JobParameters job) {
jobFinished(job, false /* don't retry this job */);
return false;
}
@Override
public boolean onStopJob(JobParameters job) {
return false;
}
};
Job jobSpec = TestUtil.getBuilderWithNoopValidator()
.setTag("tag")
.setService(reschedulingService.getClass())
.setTrigger(Trigger.NOW)
.build();
Handler mock = mock(Handler.class);
Message message = new Message();
message.setTarget(mock);
reschedulingService.start(jobSpec, message);
verify(mock).sendMessage(message);
assertEquals(message.arg1, JobService.RESULT_SUCCESS);
}
public static class ExampleJobService extends JobService {
@Override
public boolean onStartJob(JobParameters job) {
countDownLatch.countDown();
return false;
}
@Override
public boolean onStopJob(JobParameters job) {
return false;
}
}
public static class StoppableJobService extends JobService {
private final boolean shouldReschedule;
public int getNumberOfExecutionRequestsReceived() {
return amountOfExecutionRequestReceived.get();
}
private final AtomicInteger amountOfExecutionRequestReceived = new AtomicInteger();
public StoppableJobService(boolean shouldReschedule) {
this.shouldReschedule = shouldReschedule;
}
@Override
public boolean onStartJob(JobParameters job) {
amountOfExecutionRequestReceived.incrementAndGet();
return true;
}
@Override
public boolean onStopJob(JobParameters job) {
return shouldReschedule;
}
}
}

View File

@ -1,187 +0,0 @@
// Copyright 2016 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, manifest = Config.NONE, sdk = 23)
public class ValidationEnforcerTest {
private static final List<String> ERROR_LIST = Collections.singletonList("error: foo");
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Mock
private JobValidator mValidator;
@Mock
private JobParameters mMockJobParameters;
@Mock
private JobTrigger mMockTrigger;
private ValidationEnforcer mEnforcer;
private RetryStrategy mRetryStrategy = RetryStrategy.DEFAULT_EXPONENTIAL;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mEnforcer = new ValidationEnforcer(mValidator);
}
@Test
public void testValidate_retryStrategy() throws Exception {
mEnforcer.validate(mRetryStrategy);
verify(mValidator).validate(mRetryStrategy);
}
@Test
public void testValidate_jobSpec() throws Exception {
mEnforcer.validate(mMockJobParameters);
verify(mValidator).validate(mMockJobParameters);
}
@Test
public void testValidate_trigger() throws Exception {
mEnforcer.validate(mMockTrigger);
verify(mValidator).validate(mMockTrigger);
}
@Test
public void testIsValid_retryStrategy_invalid() throws Exception {
when(mValidator.validate(mRetryStrategy))
.thenReturn(Collections.singletonList("error: foo"));
assertFalse("isValid", mEnforcer.isValid(mRetryStrategy));
}
@Test
public void testIsValid_retryStrategy_valid() throws Exception {
when(mValidator.validate(mRetryStrategy)).thenReturn(null);
assertTrue("isValid", mEnforcer.isValid(mRetryStrategy));
}
@Test
public void testIsValid_trigger_invalid() throws Exception {
when(mValidator.validate(mMockTrigger))
.thenReturn(Collections.singletonList("error: foo"));
assertFalse("isValid", mEnforcer.isValid(mMockTrigger));
}
@Test
public void testIsValid_trigger_valid() throws Exception {
when(mValidator.validate(mMockTrigger)).thenReturn(null);
assertTrue("isValid", mEnforcer.isValid(mMockTrigger));
}
@Test
public void testIsValid_jobSpec_invalid() throws Exception {
when(mValidator.validate(mMockJobParameters)).thenReturn(ERROR_LIST);
assertFalse("isValid", mEnforcer.isValid(mMockJobParameters));
}
@Test
public void testIsValid_jobSpec_valid() throws Exception {
when(mValidator.validate(mMockJobParameters)).thenReturn(null);
assertTrue("isValid", mEnforcer.isValid(mMockJobParameters));
}
@Test
public void testEnsureValid_retryStrategy_valid() throws Exception {
when(mValidator.validate(mRetryStrategy)).thenReturn(null);
mEnforcer.ensureValid(mRetryStrategy);
}
@Test
public void testEnsureValid_trigger_valid() throws Exception {
when(mValidator.validate(mMockTrigger)).thenReturn(null);
mEnforcer.ensureValid(mMockTrigger);
}
@Test
public void testEnsureValid_jobSpec_valid() throws Exception {
when(mValidator.validate(mMockJobParameters)).thenReturn(null);
mEnforcer.ensureValid(mMockJobParameters);
}
@Test
public void testEnsureValid_retryStrategy_invalid() throws Exception {
expectedException.expect(ValidationEnforcer.ValidationException.class);
when(mValidator.validate(mRetryStrategy)).thenReturn(ERROR_LIST);
mEnforcer.ensureValid(mRetryStrategy);
}
@Test
public void testEnsureValid_trigger_invalid() throws Exception {
expectedException.expect(ValidationEnforcer.ValidationException.class);
when(mValidator.validate(mMockTrigger)).thenReturn(ERROR_LIST);
mEnforcer.ensureValid(mMockTrigger);
}
@Test
public void testEnsureValid_jobSpec_invalid() throws Exception {
expectedException.expect(ValidationEnforcer.ValidationException.class);
when(mValidator.validate(mMockJobParameters)).thenReturn(ERROR_LIST);
mEnforcer.ensureValid(mMockJobParameters);
}
@Test
public void testValidationMessages() throws Exception {
when(mValidator.validate(mMockJobParameters)).thenReturn(ERROR_LIST);
try {
mEnforcer.ensureValid(mMockJobParameters);
fail("Expected ensureValid to fail");
} catch (ValidationEnforcer.ValidationException ve) {
assertEquals("Expected ValidationException to have 1 error message",
1,
ve.getErrors().size());
}
}
}

View File

@ -1,44 +0,0 @@
// Copyright 2016 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.support.annotation.Nullable;
import java.util.List;
/**
* A very simple Validator that thinks that everything is ok. Used for testing.
*/
class NoopJobValidator implements JobValidator {
@Nullable
@Override
public List<String> validate(JobParameters job) {
return null;
}
@Nullable
@Override
public List<String> validate(JobTrigger trigger) {
return null;
}
@Nullable
@Override
public List<String> validate(RetryStrategy retryStrategy) {
return null;
}
}

View File

@ -1,71 +0,0 @@
// Copyright 2016 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;
/** A very simple JobService that can be configured for individual tests. */
public class TestJobService extends JobService {
public interface JobServiceProxy {
boolean onStartJob(JobParameters job);
boolean onStopJob(JobParameters job);
}
public static final JobServiceProxy NOOP_PROXY =
new JobServiceProxy() {
@Override
public boolean onStartJob(JobParameters job) {
return false;
}
@Override
public boolean onStopJob(JobParameters job) {
return false;
}
};
private static final Object lock = new Object();
// GuardedBy("lock")
private static JobServiceProxy currentProxy = NOOP_PROXY;
public static void setProxy(JobServiceProxy proxy) {
synchronized (lock) {
currentProxy = proxy;
}
}
public static void reset() {
synchronized (lock) {
currentProxy = NOOP_PROXY;
}
}
@Override
public boolean onStartJob(JobParameters job) {
synchronized (lock) {
return currentProxy.onStartJob(job);
}
}
@Override
public boolean onStopJob(JobParameters job) {
synchronized (lock) {
return currentProxy.onStopJob(job);
}
}
}

View File

@ -1,375 +0,0 @@
// Copyright 2016 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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.os.Binder;
import android.os.Bundle;
import android.os.Parcel;
import android.provider.ContactsContract;
import android.provider.MediaStore.Images.Media;
import android.support.annotation.NonNull;
import com.firebase.jobdispatcher.Job.Builder;
import com.firebase.jobdispatcher.JobTrigger.ContentUriTrigger;
import com.firebase.jobdispatcher.ObservedUri.Flags;
import com.google.android.gms.gcm.PendingCallback;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* Provides common utilities helpful for testing.
*/
public class TestUtil {
private static final String TAG = "TAG";
private static final String[] TAG_COMBINATIONS = {"tag", "foobar", "fooooooo", "bz", "100"};
private static final int[] LIFETIME_COMBINATIONS = {
Lifetime.UNTIL_NEXT_BOOT,
Lifetime.FOREVER};
private static final JobTrigger[] TRIGGER_COMBINATIONS = {
Trigger.executionWindow(0, 30),
Trigger.executionWindow(300, 600),
Trigger.executionWindow(86400, 86400 * 2),
Trigger.NOW,
Trigger.contentUriTrigger(
Arrays.asList(new ObservedUri(ContactsContract.AUTHORITY_URI, 0))),
Trigger.contentUriTrigger(Arrays.asList(new ObservedUri(ContactsContract.AUTHORITY_URI, 0),
new ObservedUri(ContactsContract.AUTHORITY_URI, Flags.FLAG_NOTIFY_FOR_DESCENDANTS)))
};
@SuppressWarnings("unchecked")
private static final List<Class<TestJobService>> SERVICE_COMBINATIONS =
Arrays.asList(TestJobService.class);
private static final RetryStrategy[] RETRY_STRATEGY_COMBINATIONS = {
RetryStrategy.DEFAULT_LINEAR,
new RetryStrategy(RetryStrategy.RETRY_POLICY_LINEAR, 60, 300),
RetryStrategy.DEFAULT_EXPONENTIAL,
new RetryStrategy(RetryStrategy.RETRY_POLICY_EXPONENTIAL, 300, 600),
};
public static void assertHasSinglePrivateConstructor(Class<?> cls) throws Exception {
Constructor<?>[] constructors = cls.getDeclaredConstructors();
assertEquals("expected number of constructors to be == 1", 1, constructors.length);
Constructor<?> constructor = constructors[0];
assertFalse("expected constructor to be inaccessible", constructor.isAccessible());
constructor.setAccessible(true);
constructor.newInstance();
}
static List<List<Integer>> getAllConstraintCombinations() {
List<List<Integer>> combos = new LinkedList<>();
combos.add(Collections.<Integer>emptyList());
for (Integer cur : Constraint.ALL_CONSTRAINTS) {
for (int l = combos.size() - 1; l >= 0; l--) {
List<Integer> oldCombo = combos.get(l);
List<Integer> newCombo = Arrays.asList(new Integer[oldCombo.size() + 1]);
Collections.copy(newCombo, oldCombo);
newCombo.set(oldCombo.size(), cur);
combos.add(newCombo);
}
combos.add(Collections.singletonList(cur));
}
return combos;
}
static int[] toIntArray(List<Integer> list) {
int[] input = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
input[i] = list.get(i);
}
return input;
}
static List<Job> getJobCombinations(Builder builder) {
return getCombination(new JobBuilder(builder));
}
static List<JobInvocation> getJobInvocationCombinations() {
return getCombination(new JobInvocationBuilder());
}
private static <T extends JobParameters> List<T> getCombination(
JobParameterBuilder<T> buildJobParam) {
List<T> result = new ArrayList<>();
for (String tag : TAG_COMBINATIONS) {
for (List<Integer> constraintList : getAllConstraintCombinations()) {
for (boolean recurring : new boolean[]{true, false}) {
for (boolean replaceCurrent : new boolean[]{true, false}) {
for (int lifetime : LIFETIME_COMBINATIONS) {
for (JobTrigger trigger : TRIGGER_COMBINATIONS) {
for (Class<TestJobService> service : SERVICE_COMBINATIONS) {
for (Bundle extras : getBundleCombinations()) {
for (RetryStrategy rs : RETRY_STRATEGY_COMBINATIONS) {
result.add(buildJobParam.build(
tag,
replaceCurrent,
constraintList,
recurring,
lifetime,
trigger,
service,
extras,
rs));
}
}
}
}
}
}
}
}
}
return result;
}
private static Bundle[] getBundleCombinations() {
List<Bundle> bundles = new LinkedList<>();
bundles.add(new Bundle());
Bundle b = new Bundle();
b.putString("foo", "bar");
b.putInt("bar", 1);
b.putLong("baz", 3L);
bundles.add(b);
return bundles.toArray(new Bundle[bundles.size()]);
}
static void assertJobsEqual(JobParameters input, JobParameters output) {
assertNotNull("input", input);
assertNotNull("output", output);
assertEquals("isRecurring()", input.isRecurring(), output.isRecurring());
assertEquals("shouldReplaceCurrent()",
input.shouldReplaceCurrent(),
output.shouldReplaceCurrent());
assertEquals("getLifetime()", input.getLifetime(), output.getLifetime());
assertEquals("getTag()", input.getTag(), output.getTag());
assertEquals("getService()", input.getService(), output.getService());
assertEquals("getConstraints()",
Constraint.compact(input.getConstraints()),
Constraint.compact(output.getConstraints()));
assertTriggersEqual(input.getTrigger(), output.getTrigger());
assertBundlesEqual(input.getExtras(), output.getExtras());
assertRetryStrategiesEqual(input.getRetryStrategy(), output.getRetryStrategy());
}
static void assertRetryStrategiesEqual(RetryStrategy in, RetryStrategy out) {
String prefix = "getRetryStrategy().";
assertEquals(prefix + "getPolicy()",
in.getPolicy(), out.getPolicy());
assertEquals(prefix + "getInitialBackoff()",
in.getInitialBackoff(), out.getInitialBackoff());
assertEquals(prefix + "getMaximumBackoff()",
in.getMaximumBackoff(), out.getMaximumBackoff());
}
static void assertBundlesEqual(Bundle inExtras, Bundle outExtras) {
if (inExtras == null || outExtras == null) {
assertNull(inExtras);
assertNull(outExtras);
return;
}
assertEquals("getExtras().size()", inExtras.size(), outExtras.size());
final Set<String> inKeys = inExtras.keySet();
for (String key : inKeys) {
assertTrue("getExtras().containsKey(\"" + key + "\")", outExtras.containsKey(key));
assertEquals("getExtras().get(\"" + key + "\")", inExtras.get(key), outExtras.get(key));
}
}
static void assertTriggersEqual(JobTrigger inTrigger, JobTrigger outTrigger) {
assertEquals("", inTrigger.getClass(), outTrigger.getClass());
if (inTrigger instanceof JobTrigger.ExecutionWindowTrigger) {
assertEquals("getTrigger().getWindowStart()",
((JobTrigger.ExecutionWindowTrigger) inTrigger).getWindowStart(),
((JobTrigger.ExecutionWindowTrigger) outTrigger).getWindowStart());
assertEquals("getTrigger().getWindowEnd()",
((JobTrigger.ExecutionWindowTrigger) inTrigger).getWindowEnd(),
((JobTrigger.ExecutionWindowTrigger) outTrigger).getWindowEnd());
} else if (inTrigger == Trigger.NOW) {
assertEquals(inTrigger, outTrigger);
} else if (inTrigger instanceof JobTrigger.ContentUriTrigger) {
assertEquals("Collection of URIs",
((ContentUriTrigger) inTrigger).getUris(),
((ContentUriTrigger) outTrigger).getUris());
} else {
fail("Unknown Trigger class: " + inTrigger.getClass());
}
}
@NonNull
public static Builder getBuilderWithNoopValidator() {
return new Builder(new ValidationEnforcer(new NoopJobValidator()));
}
@NonNull
static Bundle encodeContentUriJob(ContentUriTrigger trigger, JobCoder coder) {
Job job = getBuilderWithNoopValidator()
.setTag(TAG)
.setTrigger(trigger)
.setService(TestJobService.class)
.setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)
.build();
return coder.encode(job, new Bundle());
}
@NonNull
static Bundle encodeRecurringContentUriJob(ContentUriTrigger trigger, JobCoder coder) {
Job job = getBuilderWithNoopValidator()
.setTag(TAG)
.setTrigger(trigger)
.setService(TestJobService.class)
.setReplaceCurrent(true)
.setRecurring(true)
.build();
return coder.encode(job, new Bundle());
}
static ContentUriTrigger getContentUriTrigger() {
ObservedUri contactUri = new ObservedUri(
ContactsContract.AUTHORITY_URI, Flags.FLAG_NOTIFY_FOR_DESCENDANTS);
ObservedUri imageUri = new ObservedUri(Media.EXTERNAL_CONTENT_URI, 0);
return Trigger.contentUriTrigger(Arrays.asList(contactUri, imageUri));
}
public static class TransactionArguments {
public final int code;
public final Parcel data;
public final int flags;
public TransactionArguments(int code, Parcel data, int flags) {
this.code = code;
this.data = data;
this.flags = flags;
}
}
public static class InspectableBinder extends Binder {
private final List<TransactionArguments> transactionArguments = new LinkedList<>();
public InspectableBinder() {}
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
transactionArguments.add(new TransactionArguments(code, copyParcel(data), flags));
return true;
}
public PendingCallback toPendingCallback() {
Parcel container = Parcel.obtain();
try {
container.writeStrongBinder(this);
container.setDataPosition(0);
return new PendingCallback(container);
} finally {
container.recycle();
}
}
private Parcel copyParcel(Parcel data) {
Parcel clone = Parcel.obtain();
clone.appendFrom(data, 0, data.dataSize());
clone.setDataPosition(0);
return clone;
}
public List<TransactionArguments> getArguments() {
return Collections.unmodifiableList(transactionArguments);
}
}
private static class JobInvocationBuilder implements
JobParameterBuilder<JobInvocation> {
@Override
public JobInvocation build(String tag, boolean replaceCurrent, List<Integer> constraintList,
boolean recurring, int lifetime, JobTrigger trigger, Class<TestJobService> service,
Bundle extras, RetryStrategy rs) {
//noinspection WrongConstant
return new JobInvocation.Builder()
.setTag(tag)
.setReplaceCurrent(replaceCurrent)
.setRecurring(recurring)
.setConstraints(toIntArray(constraintList))
.setLifetime(lifetime)
.setTrigger(trigger)
.setService(service.getName())
.addExtras(extras)
.setRetryStrategy(rs)
.build();
}
}
private static class JobBuilder implements JobParameterBuilder<Job> {
private final Builder builder;
public JobBuilder(Builder builder){
this.builder = builder;
}
@Override
public Job build(String tag, boolean replaceCurrent, List<Integer> constraintList,
boolean recurring, int lifetime, JobTrigger trigger, Class<TestJobService> service,
Bundle extras, RetryStrategy rs) {
//noinspection WrongConstant
return builder
.setTag(tag)
.setReplaceCurrent(replaceCurrent)
.setRecurring(recurring)
.setConstraints(toIntArray(constraintList))
.setLifetime(lifetime)
.setTrigger(trigger)
.setService(service)
.setExtras(extras)
.setRetryStrategy(rs)
.build();
}
}
private interface JobParameterBuilder<T extends JobParameters> {
T build(String tag, boolean replaceCurrent, List<Integer> constraintList, boolean recurring,
int lifetime, JobTrigger trigger, Class<TestJobService> service, Bundle extras,
RetryStrategy rs);
}
}

View File

@ -1,61 +0,0 @@
// Copyright 2016 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.google.android.gms.gcm;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.Keep;
/**
* Parcelable class to wrap the binder we send to the client over IPC. Only included for the benefit
* of tests.
*/
@Keep
public final class PendingCallback implements Parcelable {
public static final Creator<PendingCallback> CREATOR =
new Creator<PendingCallback>() {
@Override
public PendingCallback createFromParcel(Parcel parcel) {
return new PendingCallback(parcel);
}
@Override
public PendingCallback[] newArray(int i) {
return new PendingCallback[i];
}
};
private final IBinder mBinder;
public PendingCallback(Parcel in) {
mBinder = in.readStrongBinder();
}
public IBinder getIBinder() {
return mBinder;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeStrongBinder(mBinder);
}
}

View File

@ -1 +1 @@
include ':app', ':jobdispatcher'
include ':app'