Thank you to all who have contributed either via reporting bugs or via code contribution.
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index d24120f8..6dacd3b7 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -191,7 +191,7 @@
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f3425700..66812982 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -91,6 +91,7 @@
Unlock
Unlock Everything
Feeds
-
Loading, please wait…
@@ -556,4 +556,13 @@
Edit Gist
Content
expand
+ Copy SHA
+ View as code
+ In App Animations
+ Disable in App animations everywhere.
+ This PR can\'t be merged now.
+ Projects
+ No Projects
+ No Cards
+ Added by %s
diff --git a/app/src/main/res/values/theme_amlod.xml b/app/src/main/res/values/theme_amlod.xml
index 6d2e6912..90f2ab1d 100644
--- a/app/src/main/res/values/theme_amlod.xml
+++ b/app/src/main/res/values/theme_amlod.xml
@@ -18,7 +18,7 @@
- @color/amlodWindowBackground
- #040408
- #010102
- - #483078
+ - #2962FF
- ?colorAccent
- ?colorPrimary
- true
@@ -150,7 +150,7 @@
- @style/TimeLineBackgroundAmlod
- #040408
- #08080F
- - #483078
+ - #2962FF
- @color/amlodWindowBackground
@@ -218,7 +218,7 @@
- @style/Theme.Mal.Dark.PopupOverlay
- #040408
- #08080F
- - #483078
+ - #2962FF
- false
- #eee
- #ffe0e0e0
diff --git a/app/src/main/res/values/theme_bluish.xml b/app/src/main/res/values/theme_bluish.xml
index 77dcb5d0..4d37c0a3 100644
--- a/app/src/main/res/values/theme_bluish.xml
+++ b/app/src/main/res/values/theme_bluish.xml
@@ -154,10 +154,14 @@
- @style/CommentBoxDarkBluish
- #eee
- @style/TimeLineBackgroundBluish
+ - @color/dark_patch_addition_color
+ - @color/dark_patch_deletion_color
+ - @color/dark_patch_ref_color
+ - @color/bluishDivider
+ - @color/bluishWindowBackground
- @color/bluish_primary
- @color/bluish_primary_dark
- @color/bluish_accent
- - @color/bluishWindowBackground
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/theme_midnight_blue.xml b/app/src/main/res/values/theme_midnight_blue.xml
deleted file mode 100644
index 1dddbdd0..00000000
--- a/app/src/main/res/values/theme_midnight_blue.xml
+++ /dev/null
@@ -1,204 +0,0 @@
-
-
-
- #FAFAFA
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/xml/behaviour_settings.xml b/app/src/main/res/xml/behaviour_settings.xml
index 4195416f..bbdbbe8e 100644
--- a/app/src/main/res/xml/behaviour_settings.xml
+++ b/app/src/main/res/xml/behaviour_settings.xml
@@ -4,7 +4,7 @@
diff --git a/app/src/main/res/xml/customization_settings.xml b/app/src/main/res/xml/customization_settings.xml
index 5efd0847..48c34194 100644
--- a/app/src/main/res/xml/customization_settings.xml
+++ b/app/src/main/res/xml/customization_settings.xml
@@ -10,29 +10,37 @@
+
+
+
diff --git a/app/src/release/java/com/fastaccess/provider/fabric/FabricProvider.java b/app/src/release/java/com/fastaccess/provider/fabric/FabricProvider.java
deleted file mode 100644
index b77fdf12..00000000
--- a/app/src/release/java/com/fastaccess/provider/fabric/FabricProvider.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.fastaccess.provider.fabric;
-
-import android.content.Context;
-import android.support.annotation.NonNull;
-
-import com.fastaccess.BuildConfig;
-
-/**
- * Created by kosh on 14/08/2017.
- */
-
-public class FabricProvider {
-
- public static void initFabric(@NonNull Context context) {
- Fabric fabric = new Fabric.Builder(context)
- .kits(new Crashlytics.Builder()
- .core(new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
- .build())
- .debuggable(BuildConfig.DEBUG)
- .build();
- Fabric.with(fabric);
- }
-
- public static void logPurchase(@NonNull String productKey) {
- Answers.getInstance().logPurchase(PurchaseEvent().putItemName(productKey).putSuccess(true));
- }
-}
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index 238f4a76..00000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,63 +0,0 @@
-image: Visual Studio 2017
-clone_folder: 'C:\FastHub'
-
-# skip branch build if there is an active pull request
-skip_branch_with_pr: true
-skip_commits:
- files:
- - '**/*.md'
- message: \[(skip app veyor|app veyor skip|skip appveyor|appveyor skip)\]
-
-environment:
- ANDROID_HOME: 'A:\'
- GRADLE_USER_HOME: 'G:\'
-
-init:
- - ps: |
- subst F: C:\FastHub
- mkdir C:\gradle.home
- subst G: C:\gradle.home
- mkdir C:\Android\android-sdk
- subst A: C:\Android\android-sdk
- appveyor DownloadFile "https://dl.google.com/android/repository/sdk-tools-windows-3859397.zip" -FileName "C:\android-tools.zip"
- Write-Host "Extracting SDK tools..."
- 7z x "C:\android-tools.zip" -o"$env:ANDROID_HOME" | Out-Null
-
-install:
- - ps: |
- Write-Output "" > ~\.android\repositories.cfg
- if ($env:APPVEYOR_PULL_REQUEST_NUMBER){
- if (($env:APPVEYOR_REPO_COMMIT_AUTHOR -ne 'Yakov') -and ($env:APPVEYOR_REPO_COMMIT_AUTHOR -ne 'Kosh Sergani')) {
- Write-Host "PR detected. Installing C# Script Engine and doing translations check:"
- cinst cs-script --version 3.26.2.0
- cscs
- cscs -ac:2 -nl $env:APPVEYOR_BUILD_FOLDER\.github\check_translations.cs
- }
- }
- Write-Host "Installing Android packages:"
- $pkgs = '"platform-tools"', '"extras;android;m2repository"', '"extras;google;m2repository"', '"build-tools;26.0.1"', '"platforms;android-26"'
- foreach ($pkg in $pkgs) {
- Write-Host "Installing ${pkg}:"
- echo "y" | & $env:ANDROID_HOME\tools\bin\sdkmanager.bat ${pkg}
- }
-build_script:
- - cmd: |
- CD /D F:
- F:\gradlew clean assembleDebug --stacktrace
-
-after_build:
- - ps: Rename-Item -Path "$env:APPVEYOR_BUILD_FOLDER\app\build\outputs\apk\debug\app-debug.apk" -NewName "fasthub-debug-$env:APPVEYOR_BUILD_VERSION.apk"
-
-test: off
-
-artifacts:
-- path: \app\build\outputs\apk\debug\fasthub-debug-%APPVEYOR_BUILD_VERSION%.apk
-
-deploy: off
-
-notifications:
-- provider: GitHubPullRequest
- template: ':x: [Build {{&projectName}} {{buildVersion}} {{status}}]({{buildUrl}}) (commit {{commitUrl}} by @{{&commitAuthorUsername}})
**Message(s):**
{{#jobs}}{{#messages}}
{{message}}
{{/messages}}{{/jobs}}'
- on_build_success: false
- on_build_failure: true
- on_build_status_changed: false
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 3147699c..0b0e929f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,8 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
- taskRequests = getGradle().getStartParameter().getTaskRequests().toString()
- isProduction = taskRequests.toLowerCase().contains("release")
butterKnifeVersion = '8.5.1'
state_version = '1.1.0'
lombokVersion = '1.12.6'
@@ -15,7 +13,7 @@ buildscript {
assertjVersion = '2.5.0'
espresseVersion = '2.2.2'
requery = '1.3.2'
- kotlin_version = '1.1.4'
+ kotlin_version = '1.1.4-2'
commonmark = '0.9.0'
}
repositories {
@@ -24,11 +22,11 @@ buildscript {
google()
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.0.0-beta2'
+ classpath 'com.android.tools.build:gradle:3.0.0-beta5'
classpath 'com.google.gms:google-services:3.0.0'
classpath 'com.novoda:gradle-build-properties-plugin:0.3'
classpath 'com.dicedmelon.gradle:jacoco-android:0.1.2'
- if (isProduction) classpath 'io.fabric.tools:gradle:1.22.2'
+ classpath 'io.fabric.tools:gradle:1.24.1'
classpath 'com.apollographql.apollo:gradle-plugin:0.4.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.github.viswaramamoorthy:gradle-util-plugins:0.1.0-RELEASE"
diff --git a/debug_gradle.properties b/debug_gradle.properties
index d5332fd0..1303a8bd 100644
--- a/debug_gradle.properties
+++ b/debug_gradle.properties
@@ -1,5 +1,5 @@
# Below API Keys are meant for debugging purpose & they aren't being used in production.
-org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
+org.gradle.jvmargs=-Xmx2536M
android_store_password=kosh2010
android_key_password=kosh2010
android_key_alias=FastAccess
@@ -8,5 +8,4 @@ github_secret=b2d158f949d3615078eaf570ff99eba81cfa1ff9
imgur_client_id=5fced7f255e1dc9
imgur_secret=03025033403196a4b68b48f0738e67ef136ad64f
redirect_url=fasthub://login
-android.enableD8=true
android.sdk.channel=2
diff --git a/jobdispatcher/build.gradle b/jobdispatcher/build.gradle
new file mode 100644
index 00000000..d326ddf2
--- /dev/null
+++ b/jobdispatcher/build.gradle
@@ -0,0 +1,79 @@
+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"
+}
diff --git a/jobdispatcher/coverage.gradle b/jobdispatcher/coverage.gradle
new file mode 100644
index 00000000..204f2dc2
--- /dev/null
+++ b/jobdispatcher/coverage.gradle
@@ -0,0 +1,40 @@
+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"
+ }
+}
diff --git a/jobdispatcher/src/androidTest/AndroidManifest.xml b/jobdispatcher/src/androidTest/AndroidManifest.xml
new file mode 100644
index 00000000..c87f27fc
--- /dev/null
+++ b/jobdispatcher/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jobdispatcher/src/androidTest/java/com/firebase/jobdispatcher/EndToEndTest.java b/jobdispatcher/src/androidTest/java/com/firebase/jobdispatcher/EndToEndTest.java
new file mode 100644
index 00000000..48b1048b
--- /dev/null
+++ b/jobdispatcher/src/androidTest/java/com/firebase/jobdispatcher/EndToEndTest.java
@@ -0,0 +1,69 @@
+// 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));
+ }
+}
diff --git a/jobdispatcher/src/main/AndroidManifest.xml b/jobdispatcher/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..71205bd8
--- /dev/null
+++ b/jobdispatcher/src/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/BundleProtocol.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/BundleProtocol.java
new file mode 100644
index 00000000..774a8340
--- /dev/null
+++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/BundleProtocol.java
@@ -0,0 +1,49 @@
+// 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() {
+ }
+}
diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Constraint.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Constraint.java
new file mode 100644
index 00000000..ff413fb2
--- /dev/null
+++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Constraint.java
@@ -0,0 +1,108 @@
+// 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;
+ }
+}
diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/DefaultJobValidator.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/DefaultJobValidator.java
new file mode 100644
index 00000000..4804804e
--- /dev/null
+++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/DefaultJobValidator.java
@@ -0,0 +1,288 @@
+// 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.
+ *
+ * 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 Lists} together. */
+ @Nullable
+ private static List mergeErrorLists(@Nullable List errors,
+ @Nullable List newErrors) {
+ if (errors == null) {
+ return newErrors;
+ }
+ if (newErrors == null) {
+ return errors;
+ }
+
+ errors.addAll(newErrors);
+ return errors;
+ }
+
+ @Nullable
+ private static List addError(@Nullable List errors, String newError) {
+ if (newError == null) {
+ return errors;
+ }
+ if (errors == null) {
+ return getMutableSingletonList(newError);
+ }
+
+ Collections.addAll(errors, newError);
+
+ return errors;
+ }
+
+ @Nullable
+ private static List addErrorsIf(boolean condition, List 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 validate(JobParameters job) {
+ List 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.
+ *
+ * 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 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 validate(RetryStrategy retryStrategy) {
+ List 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 validateForPersistence(Bundle extras) {
+ List 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 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 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 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 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 getMutableSingletonList(String msg) {
+ ArrayList strings = new ArrayList<>();
+ strings.add(msg);
+ return strings;
+ }
+}
diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Driver.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Driver.java
new file mode 100644
index 00000000..fe832721
--- /dev/null
+++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/Driver.java
@@ -0,0 +1,62 @@
+// 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();
+}
diff --git a/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ExecutionDelegator.java b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ExecutionDelegator.java
new file mode 100644
index 00000000..1dbacb3c
--- /dev/null
+++ b/jobdispatcher/src/main/java/com/firebase/jobdispatcher/ExecutionDelegator.java
@@ -0,0 +1,160 @@
+// 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 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