├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── spix │ │ └── jobmanager │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── spix │ │ └── jobmanager │ │ └── activity │ │ └── MainActivity.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout │ └── activity_main.xml │ └── values │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jobmanagerlib ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── LICENSE.txt │ └── java │ └── com │ └── spix │ └── jobqueue │ ├── AsyncAddCallback.java │ ├── BaseJob.java │ ├── CopyOnWriteGroupSet.java │ ├── Job.java │ ├── JobHolder.java │ ├── JobManager.java │ ├── JobQueue.java │ ├── JobStatus.java │ ├── Params.java │ ├── QueueFactory.java │ ├── cachedQueue │ └── CachedJobQueue.java │ ├── config │ └── Configuration.java │ ├── di │ └── DependencyInjector.java │ ├── executor │ └── JobConsumerExecutor.java │ ├── log │ ├── CustomLogger.java │ └── JqLog.java │ ├── network │ ├── NetworkEventProvider.java │ ├── NetworkUtil.java │ └── NetworkUtilImpl.java │ ├── nonPersistentQueue │ ├── ConsistentTimedComparator.java │ ├── CountWithGroupIdsResult.java │ ├── JobSet.java │ ├── MergedQueue.java │ ├── NetworkAwarePriorityQueue.java │ ├── NonPersistentJobSet.java │ ├── NonPersistentPriorityQueue.java │ ├── TimeAwareComparator.java │ └── TimeAwarePriorityQueue.java │ └── sqlite │ ├── DbOpenHelper.java │ ├── QueryCache.java │ ├── SqlHelper.java │ └── SqliteJobQueue.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | gradle.properties 18 | *.asc 19 | *.gpg 20 | *.gradle 21 | 22 | # Eclipse project files 23 | .classpath 24 | .project 25 | .idea 26 | .css 27 | *.html 28 | .img 29 | .DS_Store 30 | jobqueue/out 31 | coverage-report 32 | junitvmwatcher*.properties 33 | jobqueue/cobertura.ser 34 | jobqueue/javadoc 35 | out 36 | *.iml 37 | 38 | pages 39 | build 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](http://downloads.path.com/logo.png) 2 | 3 | Android Priority Job Queue (Job Manager) 4 | ========================== 5 | 6 | This fork have additional features: 7 | 8 | - JobManager.setOnAllJobsFinishedListener(); Lets you to listen, when all jobs are finished 9 | - Context now available in Job (Note: Context available after onAdded() before onRun()) Usefull when using sticky jobs 10 | - JobManager now available in Job (Note: JobManager available after onAdded() before onRun()) Usefull when want to start the job in job 11 | - JobManager object instance can be created without stating it, Just set the flag for Configurations object. 12 | 13 | Priority Job Queue is an implementation of a [Job Queue](http://en.wikipedia.org/wiki/Job_queue) specifically written for Android to easily schedule jobs (tasks) that run in the background, improving UX and application stability. 14 | 15 | It is written primarily with [flexibility][10] & [functionality][11] in mind. This is an ongoing project, which we will continue to add stability and performance improvements. 16 | 17 | - [Why ?](#why-) 18 | - [The Problem](#the-problem) 19 | - [Our Solution](#our-solution) 20 | - [Show me the code](#show-me-the-code) 21 | - [What's happening under the hood?](#under-the-hood) 22 | - [Advantages](#advantages) 23 | - [Getting Started](#getting-started) 24 | - [Version History](#version-history) 25 | - [Building](#building) 26 | - [Running Tests](#running-tests) 27 | - [wiki][9] 28 | - [Dependencies](#dependencies) 29 | - [License](#license) 30 | 31 | 32 | ### Why ? 33 | #### The Problem 34 | Almost every application does work in a background thread. These "background tasks" are expected to keep the application responsive and robust, especially during unfavorable situations (e.g. limited network connectivity). In Android applications, there are several ways to implement background work: 35 | 36 | * **Async Task:** Using an async task is the simplest approach, but it is tightly coupled with the activity lifecycle. If the activity dies (or is re-created), any ongoing async task will become wasted cycles or otherwise create unexpected behavior upon returning to the main thread. In addition, it is a terrible idea to drop a response from a network request just because a user rotated his/her phone. 37 | * **Loaders:** Loaders are a better option, as they recover themselves after a configuration change. On the other hand, they are designed to load data from disk and are not well suited for long-running network requests. 38 | * **Service with a Thread Pool:** Using a service is a much better solution, as it de-couples business logic from your UI. However, you will need a thread pool (e.g. ThreadPoolExecutor) to process requests in parallel, broadcast events to update the UI, and write additional code to persist queued requests to disk. As your application grows, the number of background operations grows, which force you to consider task prioritization and often-complicated concurrency problems. 39 | 40 | #### Our Solution 41 | Job Queue provides you a nice framework to do all of the above and more. You define your background tasks as [Jobs][11] and enqueue them to your [JobManager][10] instance. Job Manager will take care of prioritization, persistence, load balancing, delaying, network control, grouping etc. It also provides a nice lifecycle for your jobs to provide a better, consistent user experience. 42 | 43 | Although not required, it is most useful when used with an event bus. It also supports dependency injection. 44 | 45 | * Job Queue was inspired by a [Google I/O 2010 talk on REST client applications][8]. 46 | 47 | ### Show me the code 48 | 49 | Since a code example is worth thousands of documentation pages, here it is. 50 | 51 | File: [PostTweetJob.java](https://github.com/path/android-priority-jobqueue/blob/master/examples/twitter/TwitterClient/src/com/path/android/jobqueue/examples/twitter/jobs/PostTweetJob.java) 52 | ``` java 53 | // A job to send a tweet 54 | public class PostTweetJob extends Job { 55 | public static final int PRIORITY = 1; 56 | private String text; 57 | public PostTweetJob(String text) { 58 | // This job requires network connectivity, 59 | // and should be persisted in case the application exits before job is completed. 60 | super(new Params(PRIORITY).requireNetwork().persist()); 61 | } 62 | @Override 63 | public void onAdded() { 64 | // Job has been saved to disk. 65 | // This is a good place to dispatch a UI event to indicate the job will eventually run. 66 | // In this example, it would be good to update the UI with the newly posted tweet. 67 | } 68 | @Override 69 | public void onRun() throws Throwable { 70 | // Job logic goes here. In this example, the network call to post to Twitter is done here. 71 | webservice.postTweet(text); 72 | } 73 | @Override 74 | protected boolean shouldReRunOnThrowable(Throwable throwable) { 75 | // An error occurred in onRun. 76 | // Return value determines whether this job should retry running (true) or abort (false). 77 | } 78 | @Override 79 | protected void onCancel() { 80 | // Job has exceeded retry attempts or shouldReRunOnThrowable() has returned false. 81 | } 82 | } 83 | 84 | 85 | ``` 86 | 87 | File: [TweetActivity.java](https://github.com/path/android-priority-jobqueue/blob/master/examples/twitter/TwitterClient/src/com/path/android/jobqueue/examples/twitter/SampleTwitterClient.java#L53) 88 | ``` java 89 | //... 90 | public void onSendClick() { 91 | final String status = editText.getText().toString(); 92 | if(status.trim().length() > 0) { 93 | jobManager.addJobInBackground(new PostTweetJob(status)); 94 | editText.setText(""); 95 | } 96 | } 97 | ... 98 | ``` 99 | 100 | 101 | That's it. :) Job Manager allows you to enjoy: 102 | 103 | * No network calls in activity-bound async tasks 104 | * No serialization mess for important requests 105 | * No "manual" implementation of network connectivity checks or retry logic 106 | 107 | ### Under the hood 108 | * When user clicked the send button, `onSendClick()` was called, which creates a `PostTweetJob` and adds it to Job Queue for execution. 109 | It runs on a background thread because Job Queue will make a disk access to persist the job. 110 | 111 | * Right after `PostTweetJob` is synchronized to disk, Job Queue calls `DependencyInjector` (if provided) which will [inject fields](http://en.wikipedia.org/wiki/Dependency_injection) into our job instance. 112 | At `PostTweetJob.onAdded()` callback, we saved `PostTweetJob` to disk. Since there has been no network access up to this point, the time between clicking the send button and reaching `onAdded()` is within fracions of a second. This allows the implementation of `onAdded()` to inform UI to display the newly sent tweet almost instantly, creating a "fast" user experience. Beware, `onAdded()` is called on the thread job was added. 113 | 114 | * When it's time for `PostTweetJob` to run, Job Queue will call `onRun()` (and it will only be called if there is an active network connection, as dictated at the job's constructor). 115 | By default, Job Queue uses a simple connection utility that checks `ConnectivityManager` (ensure you have `ACCESS_NETWORK_STATE` permission in your manifest). You can provide a [custom implementation][1] which can 116 | add additional checks (e.g. your server stability). You should also provide a [`NetworkUtil`][1] which can notify Job Queue when network 117 | is recovered so that Job Queue will avoid a busy loop and decrease # of consumers(default configuration does it for you). 118 | 119 | * Job Queue will keep calling `onRun()` until it succeeds (or reaches a retry limit). If `onRun()` throws an exception, 120 | Job Queue will call `shouldReRunOnThrowable()` to allow you to handle the exception and decide whether to retry job execution or abort. 121 | 122 | * If all retry attempts fail (or when `shouldReRunOnThrowable()` returns false), Job Queue will call `onCancel()` to allow you to clean 123 | your database, inform the user, etc. 124 | 125 | ### Advantages 126 | * It is very easy to de-couple application logic from your activites, making your code more robust, easy to refactor, and easy to **test**. 127 | * You don't have to deal with `AsyncTask` lifecycles. This is true assuming you use an event bus to update your UI (you should). 128 | At Path, we use [greenrobot's EventBus](https://github.com/greenrobot/EventBus); however, you can also go with your favorite. (e.g. [Square's Otto] (https://github.com/square/otto)) 129 | * Job Queue takes care of prioritizing jobs, checking network connection, running them in parallel, etc. Job prioritization is especially indispensable when you have a resource-heavy app like ours. 130 | * You can delay jobs. This is helpful in cases like sending a GCM token to your server. It is very common to acquire a GCM token and send it to your server when a user logs in to your app, but you don't want it to interfere with critical network operations (e.g. fetching user-facing content). 131 | * You can group jobs to ensure their serial execution, if necessary. For example, assume you have a messaging client and your user sent a bunch of messages when their phone had no network coverage. When creating these `SendMessageToNetwork` jobs, you can group them by conversation ID. Through this approach, messages in the same conversation will send in the order they were enqueued, while messages between different conversations are still sent in parallel. This lets you effortlessly maximize network utilization and ensure data integrity. 132 | * By default, Job Queue monitors network connectivity (so you don't need to worry about it). When a device is operating offline, jobs that require the network won't run until connectivity is restored. You can even provide a custom [`NetworkUtil`][1] if you need custom logic (e.g. you can create another instance of Job Queue which runs only if there is a wireless connection). 133 | * It is unit tested and mostly documented. You can check our [code coverage report][3] and [Javadoc][4]. 134 | 135 | 136 | ### Getting Started 137 | We distribute artifacts through maven central repository. 138 | 139 | Gradle: `compile 'com.github.andrejlukasevic:job-queue-manager:--checkReleases--'` 140 | 141 | Maven: 142 | 143 | ``` xml 144 | 145 | com.github.andrejlukasevic 146 | job-queue-manager 147 | --checkReleases-- 148 | aar 149 | 150 | ``` 151 | 152 | You can also [download][5] library jar, sources and javadoc from Maven Central. 153 | 154 | We highly recommend checking how you can configure job manager and individual jobs. 155 | * [Configure job manager][10] 156 | * [Configure individual jobs][11] 157 | * [Review sample app][6] 158 | * [Review sample configuration][7] 159 | 160 | ### Version History 161 | - Watch Releases 162 | 163 | 164 | ### [Wiki][9] 165 | 166 | ### Dependencies 167 | - Job Queue does not depend on any other libraries other than Android SDK. 168 | - For testing, we use: 169 | - - [Junit 4](http://junit.org/) ([license](https://github.com/junit-team/junit/blob/master/LICENSE.txt)) 170 | - - [Robolectric](http://robolectric.org/) ([license](https://github.com/robolectric/robolectric/blob/master/LICENSE.txt)) 171 | - - [Fest Util](http://easytesting.org/) ([license](http://www.apache.org/licenses/LICENSE-2.0)) 172 | - - [Hamcrest](https://code.google.com/p/hamcrest/) ([license](http://opensource.org/licenses/BSD-3-Clause)) 173 | - For code coverage report, we use: 174 | - - [Cobertura](http://cobertura.github.io/cobertura/) ([license](https://github.com/cobertura/cobertura/blob/master/LICENSE.txt/)) 175 | - Sample Twitter client uses: 176 | - - [Twitter4j](http://twitter4j.org/en) 177 | - - [EventBus](https://github.com/greenrobot/EventBus) 178 | - - [Path's fork of greenDAO](https://github.com/path/greenDAO) . ([original repo](https://github.com/greenrobot/greenDAO)) 179 | 180 | ## License 181 | 182 | Android Priority Jobqueue is made available under the [MIT license](http://opensource.org/licenses/MIT): 183 | 184 |
185 | The MIT License (MIT)
186 | 
187 | Copyright (c) 2013 Path, Inc.
188 | 
189 | Permission is hereby granted, free of charge, to any person obtaining a copy
190 | of this software and associated documentation files (the "Software"), to deal
191 | in the Software without restriction, including without limitation the rights
192 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
193 | copies of the Software, and to permit persons to whom the Software is
194 | furnished to do so, subject to the following conditions:
195 | 
196 | The above copyright notice and this permission notice shall be included in
197 | all copies or substantial portions of the Software.
198 | 
199 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
200 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
201 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
202 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
203 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
204 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
205 | THE SOFTWARE.
206 | 
207 | 208 | 209 | [1]: https://github.com/path/android-priority-jobqueue/blob/master/jobqueue/src/com/path/android/jobqueue/network/NetworkUtil.java 210 | [2]: https://github.com/path/android-priority-jobqueue/blob/master/jobqueue/src/com/path/android/jobqueue/network/NetworkEventProvider.java 211 | [3]: http://path.github.io/android-priority-jobqueue/coverage-report/index.html 212 | [4]: http://path.github.io/android-priority-jobqueue/javadoc/index.html 213 | [5]: http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22android-priority-jobqueue%22 214 | [6]: https://github.com/path/android-priority-jobqueue/tree/master/examples 215 | [7]: https://github.com/path/android-priority-jobqueue/blob/master/examples/twitter/TwitterClient/src/com/path/android/jobqueue/examples/twitter/TwitterApplication.java#L26 216 | [8]: http://www.youtube.com/watch?v=xHXn3Kg2IQE 217 | [9]: https://github.com/path/android-priority-jobqueue/wiki 218 | [10]: https://github.com/path/android-priority-jobqueue/wiki/Job-Manager-Configuration 219 | [11]: https://github.com/path/android-priority-jobqueue/wiki/Job-Configuration 220 | [12]: https://github.com/path/android-priority-jobqueue/blob/master/jobqueue/src/com/path/android/jobqueue/Params.java 221 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 21 5 | buildToolsVersion "21.1.2" 6 | 7 | defaultConfig { 8 | applicationId "com.spix.jobmanager" 9 | minSdkVersion 15 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | compile 'com.android.support:appcompat-v7:21.0.3' 25 | compile project(':jobmanagerlib') 26 | } 27 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Programs\AndroidSDK/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/spix/jobmanager/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobmanager; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/spix/jobmanager/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobmanager.activity; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | 7 | import com.spix.jobmanager.R; 8 | import com.spix.jobqueue.Job; 9 | import com.spix.jobqueue.JobManager; 10 | import com.spix.jobqueue.Params; 11 | import com.spix.jobqueue.config.Configuration; 12 | 13 | import java.util.concurrent.atomic.AtomicInteger; 14 | 15 | public class MainActivity extends Activity { 16 | 17 | private JobManager jobManager; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_main); 23 | Configuration configs = new Configuration.Builder(getApplicationContext()).id("test").loadFactor(100).startWhenInitialized(false).build(); 24 | this.jobManager = new JobManager(getApplicationContext(), configs); 25 | jobManager.addJob(new SimpleJob()); 26 | jobManager.start(); 27 | } 28 | 29 | private static class SimpleJob extends Job { 30 | 31 | private static AtomicInteger i = new AtomicInteger(0); 32 | 33 | protected SimpleJob() { 34 | super(new Params(1).setRequiresNetwork(false).setPersistent(true)); 35 | } 36 | 37 | @Override 38 | public void onAdded() { 39 | Log.d("Job", "onAdded: ctx" + getContext()); 40 | 41 | } 42 | 43 | @Override 44 | public void onRun() throws Throwable { 45 | 46 | Log.d("Job", "onRun: ctx" + getContext()); 47 | Log.d("Job", "before sleep Threadid: " + Thread.currentThread().getId() + " job Nr: " + i.get() + getContext().getString(R.string.abc_action_bar_home_description)); 48 | Thread.sleep(10000); 49 | Log.d("Job", "after sleep Threadid: " + Thread.currentThread().getId() + " job Nr: " + i.get()); 50 | // getJobManager().addJob(new SimpleJob()); 51 | 52 | } 53 | 54 | @Override 55 | protected void onCancel() { 56 | Log.d("Job", "onCancel: ctx" + getContext()); 57 | } 58 | 59 | @Override 60 | protected boolean shouldReRunOnThrowable(Throwable throwable) { 61 | Log.d("Job", "shouldReRunOnThrowable: ctx" + getContext()); 62 | Log.d("Job", "error: " + throwable.getMessage()); 63 | return false; 64 | } 65 | 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejLukasevic/android-priority-jobqueue/3eacea61d62181e4de3d0ef5f2b6328af1080f3d/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejLukasevic/android-priority-jobqueue/3eacea61d62181e4de3d0ef5f2b6328af1080f3d/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejLukasevic/android-priority-jobqueue/3eacea61d62181e4de3d0ef5f2b6328af1080f3d/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejLukasevic/android-priority-jobqueue/3eacea61d62181e4de3d0ef5f2b6328af1080f3d/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | JobManager 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:1.0.0' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | jcenter() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndrejLukasevic/android-priority-jobqueue/3eacea61d62181e4de3d0ef5f2b6328af1080f3d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /jobmanagerlib/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /jobmanagerlib/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven' 3 | 4 | android { 5 | compileSdkVersion 21 6 | buildToolsVersion "21.1.2" 7 | 8 | defaultConfig { 9 | minSdkVersion 15 10 | targetSdkVersion 22 11 | versionCode 13000 12 | versionName "1.3.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | 21 | } 22 | 23 | apply from: 'https://raw.github.com/andrejlukasevic/gradle-mvn-push/master/gradle-mvn-push.gradle' 24 | -------------------------------------------------------------------------------- /jobmanagerlib/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:/Programs/AndroidSDK/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Path, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/AsyncAddCallback.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue; 2 | 3 | import android.app.Activity; 4 | 5 | /** 6 | * If you are adding the job via the async adder, you can provide a callback method to receive the ID. 7 | * Please keep in mind that job manager will keep a strong reference to this callback. So if the callback is an 8 | * anonymous class inside an {@link Activity} context, it may leak the activity until the job is added. 9 | */ 10 | public interface AsyncAddCallback { 11 | public void onAdded(long jobId); 12 | } 13 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/BaseJob.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue; 2 | 3 | import android.content.Context; 4 | 5 | import com.spix.jobqueue.log.JqLog; 6 | 7 | import java.io.IOException; 8 | import java.io.ObjectInputStream; 9 | import java.io.ObjectOutputStream; 10 | import java.io.Serializable; 11 | 12 | /** 13 | * This class has been deprecated and will soon be removed from public api. 14 | * Please use {@link Job} instead which provider a cleaner constructor API. 15 | * Deprecated. Use {@link Job} 16 | */ 17 | @Deprecated 18 | abstract public class BaseJob implements Serializable { 19 | public static final int DEFAULT_RETRY_LIMIT = 20; 20 | private boolean requiresNetwork; 21 | private String groupId; 22 | private boolean persistent; 23 | private transient int currentRunCount; 24 | private transient Context context; 25 | private transient JobManager jobManager; 26 | 27 | protected BaseJob(boolean requiresNetwork) { 28 | this(requiresNetwork, false, null); 29 | } 30 | 31 | protected BaseJob(String groupId) { 32 | this(false, false, groupId); 33 | } 34 | 35 | protected BaseJob(boolean requiresNetwork, String groupId) { 36 | this(requiresNetwork, false, groupId); 37 | } 38 | 39 | public BaseJob(boolean requiresNetwork, boolean persistent) { 40 | this(requiresNetwork, persistent, null); 41 | } 42 | 43 | protected BaseJob(boolean requiresNetwork, boolean persistent, String groupId) { 44 | this.requiresNetwork = requiresNetwork; 45 | this.persistent = persistent; 46 | this.groupId = groupId; 47 | } 48 | 49 | private void writeObject(ObjectOutputStream oos) throws IOException { 50 | oos.writeBoolean(requiresNetwork); 51 | oos.writeObject(groupId); 52 | oos.writeBoolean(persistent); 53 | } 54 | 55 | 56 | private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { 57 | requiresNetwork = ois.readBoolean(); 58 | groupId = (String) ois.readObject(); 59 | persistent = ois.readBoolean(); 60 | } 61 | 62 | /** 63 | * defines if we should add this job to disk or non-persistent queue 64 | * 65 | * @return 66 | */ 67 | public final boolean isPersistent() { 68 | return persistent; 69 | } 70 | 71 | /** 72 | * called when the job is added to disk and committed. 73 | * this means job will eventually run. this is a good time to update local database and dispatch events 74 | * Changes to this class will not be preserved if your job is persistent !!! 75 | * Also, if your app crashes right after adding the job, {@code onRun} might be called without an {@code onAdded} call 76 | */ 77 | abstract public void onAdded(); 78 | 79 | /** 80 | * The actual method that should to the work 81 | * It should finish w/o any exception. If it throws any exception, {@code shouldReRunOnThrowable} will be called to 82 | * decide either to dismiss the job or re-run it. 83 | * 84 | * @throws Throwable 85 | */ 86 | abstract public void onRun() throws Throwable; 87 | 88 | /** 89 | * called when a job is cancelled. 90 | */ 91 | abstract protected void onCancel(); 92 | 93 | /** 94 | * if {@code onRun} method throws an exception, this method is called. 95 | * return true if you want to run your job again, return false if you want to dismiss it. If you return false, 96 | * onCancel will be called. 97 | */ 98 | abstract protected boolean shouldReRunOnThrowable(Throwable throwable); 99 | 100 | /** 101 | * Runs the job and catches any exception 102 | * 103 | * @param currentRunCount 104 | * @return 105 | */ 106 | public final boolean safeRun(int currentRunCount) { 107 | this.currentRunCount = currentRunCount; 108 | if (JqLog.isDebugEnabled()) { 109 | JqLog.d("running job %s", this.getClass().getSimpleName()); 110 | } 111 | boolean reRun = false; 112 | boolean failed = false; 113 | try { 114 | onRun(); 115 | if (JqLog.isDebugEnabled()) { 116 | JqLog.d("finished job %s", this.getClass().getSimpleName()); 117 | } 118 | } catch (Throwable t) { 119 | failed = true; 120 | JqLog.e(t, "error while executing job"); 121 | reRun = currentRunCount < getRetryLimit(); 122 | if (reRun) { 123 | try { 124 | reRun = shouldReRunOnThrowable(t); 125 | } catch (Throwable t2) { 126 | JqLog.e(t2, "shouldReRunOnThrowable did throw an exception"); 127 | } 128 | } 129 | } finally { 130 | if (reRun) { 131 | return false; 132 | } else if (failed) { 133 | try { 134 | onCancel(); 135 | } catch (Throwable ignored) { 136 | } 137 | } 138 | } 139 | return true; 140 | } 141 | 142 | /** 143 | * before each run, JobManager sets this number. Might be useful for the {@link BaseJob#onRun()} 144 | * method 145 | * 146 | * @return 147 | */ 148 | protected int getCurrentRunCount() { 149 | return currentRunCount; 150 | } 151 | 152 | /** 153 | * if job is set to require network, it will not be called unless {@link com.spix.jobqueue.network.NetworkUtil} 154 | * reports that there is a network connection 155 | * 156 | * @return 157 | */ 158 | public final boolean requiresNetwork() { 159 | return requiresNetwork; 160 | } 161 | 162 | /** 163 | * Some jobs may require being run synchronously. For instance, if it is a job like sending a comment, we should 164 | * never run them in parallel (unless they are being sent to different conversations). 165 | * By assigning same groupId to jobs, you can ensure that that type of jobs will be run in the order they were given 166 | * (if their priority is the same). 167 | * 168 | * @return 169 | */ 170 | public final String getRunGroupId() { 171 | return groupId; 172 | } 173 | 174 | /** 175 | * By default, jobs will be retried {@code DEFAULT_RETRY_LIMIT} times. 176 | * If job fails this many times, onCancel will be called w/o calling {@code shouldReRunOnThrowable} 177 | * 178 | * @return 179 | */ 180 | protected int getRetryLimit() { 181 | return DEFAULT_RETRY_LIMIT; 182 | } 183 | 184 | /** 185 | * Gets called automatically 186 | */ 187 | protected void attachContext(Context context) { 188 | this.context = context; 189 | } 190 | 191 | protected void attachJobManager(JobManager jobManager) { 192 | this.jobManager = jobManager; 193 | } 194 | 195 | public Context getContext() { 196 | return context; 197 | } 198 | 199 | public JobManager getJobManager() { 200 | return jobManager; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/CopyOnWriteGroupSet.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.TreeSet; 6 | 7 | /** 8 | * a util class that holds running jobs sorted by name and uniq. 9 | * it behaves like CopyOnWriteLists 10 | */ 11 | public class CopyOnWriteGroupSet { 12 | private ArrayList publicClone; 13 | private final TreeSet internalSet; 14 | 15 | public CopyOnWriteGroupSet() { 16 | internalSet = new TreeSet(); 17 | } 18 | 19 | public synchronized Collection getSafe() { 20 | if (publicClone == null) { 21 | publicClone = new ArrayList(internalSet); 22 | } 23 | return publicClone; 24 | } 25 | 26 | public synchronized void add(String group) { 27 | if (internalSet.add(group)) { 28 | publicClone = null;//invalidate 29 | } 30 | } 31 | 32 | public synchronized void remove(String group) { 33 | if (internalSet.remove(group)) { 34 | publicClone = null; 35 | } 36 | } 37 | 38 | public synchronized void clear() { 39 | internalSet.clear(); 40 | publicClone = null; 41 | } 42 | 43 | public synchronized boolean isEmpty() { 44 | int counter = 0; 45 | if (publicClone != null) { 46 | counter += publicClone.size(); 47 | } 48 | if (internalSet != null) { 49 | counter += internalSet.size(); 50 | } 51 | return counter == 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/Job.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Base class for all of your jobs. 7 | * If you were using {@link BaseJob}, please move to this instance since BaseJob will be removed from the public api. 8 | */ 9 | @SuppressWarnings("deprecation") 10 | abstract public class Job extends BaseJob implements Serializable { 11 | private static final long serialVersionUID = 1L; 12 | private transient int priority; 13 | private transient long delayInMs; 14 | 15 | protected Job(Params params) { 16 | super(params.doesRequireNetwork(), params.isPersistent(), params.getGroupId()); 17 | this.priority = params.getPriority(); 18 | this.delayInMs = params.getDelayMs(); 19 | } 20 | 21 | /** 22 | * used by {@link JobManager} to assign proper priority at the time job is added. 23 | * This field is not preserved! 24 | * @return priority (higher = better) 25 | */ 26 | public final int getPriority() { 27 | return priority; 28 | } 29 | 30 | /** 31 | * used by {@link JobManager} to assign proper delay at the time job is added. 32 | * This field is not preserved! 33 | * @return delay in ms 34 | */ 35 | public final long getDelayInMs() { 36 | return delayInMs; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/JobHolder.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue; 2 | 3 | /** 4 | * Container class to address Jobs inside job manager. 5 | */ 6 | public class JobHolder { 7 | protected Long id; 8 | protected int priority; 9 | protected String groupId; 10 | protected int runCount; 11 | /** 12 | * job will be delayed until this nanotime 13 | */ 14 | protected long delayUntilNs; 15 | /** 16 | * When job is created, System.nanoTime() is assigned to {@code createdNs} value so that we know when job is created 17 | * in relation to others 18 | */ 19 | protected long createdNs; 20 | protected long runningSessionId; 21 | protected boolean requiresNetwork; 22 | transient BaseJob baseJob; 23 | 24 | /** 25 | * @param id Unique ID for the job. Should be unique per queue 26 | * @param priority Higher is better 27 | * @param groupId which group does this job belong to? default null 28 | * @param runCount Incremented each time job is fetched to run, initial value should be 0 29 | * @param baseJob Actual job to run 30 | * @param createdNs System.nanotime 31 | * @param delayUntilNs System.nanotime value where job can be run the very first time 32 | * @param runningSessionId 33 | */ 34 | public JobHolder(Long id, int priority, String groupId, int runCount, BaseJob baseJob, long createdNs, long delayUntilNs, long runningSessionId) { 35 | this.id = id; 36 | this.priority = priority; 37 | this.groupId = groupId; 38 | this.runCount = runCount; 39 | this.createdNs = createdNs; 40 | this.delayUntilNs = delayUntilNs; 41 | this.baseJob = baseJob; 42 | this.runningSessionId = runningSessionId; 43 | this.requiresNetwork = baseJob.requiresNetwork(); 44 | } 45 | 46 | public JobHolder(int priority, BaseJob baseJob, long runningSessionId) { 47 | this(null, priority, null, 0, baseJob, System.nanoTime(), Long.MIN_VALUE, runningSessionId); 48 | } 49 | 50 | public JobHolder(int priority, BaseJob baseJob, long delayUntilNs, long runningSessionId) { 51 | this(null, priority, baseJob.getRunGroupId(), 0, baseJob, System.nanoTime(), delayUntilNs, runningSessionId); 52 | } 53 | 54 | /** 55 | * runs the job w/o throwing any exceptions 56 | * @param currentRunCount 57 | * @return 58 | */ 59 | public final boolean safeRun(int currentRunCount) { 60 | return baseJob.safeRun(currentRunCount); 61 | } 62 | 63 | public Long getId() { 64 | return id; 65 | } 66 | 67 | public void setId(Long id) { 68 | this.id = id; 69 | } 70 | 71 | public boolean requiresNetwork() { 72 | return requiresNetwork; 73 | } 74 | 75 | public int getPriority() { 76 | return priority; 77 | } 78 | 79 | public void setPriority(int priority) { 80 | this.priority = priority; 81 | } 82 | 83 | public int getRunCount() { 84 | return runCount; 85 | } 86 | 87 | public void setRunCount(int runCount) { 88 | this.runCount = runCount; 89 | } 90 | 91 | public long getCreatedNs() { 92 | return createdNs; 93 | } 94 | 95 | public void setCreatedNs(long createdNs) { 96 | this.createdNs = createdNs; 97 | } 98 | 99 | public long getRunningSessionId() { 100 | return runningSessionId; 101 | } 102 | 103 | public void setRunningSessionId(long runningSessionId) { 104 | this.runningSessionId = runningSessionId; 105 | } 106 | 107 | public long getDelayUntilNs() { 108 | return delayUntilNs; 109 | } 110 | 111 | public BaseJob getBaseJob() { 112 | return baseJob; 113 | } 114 | 115 | public void setBaseJob(BaseJob baseJob) { 116 | this.baseJob = baseJob; 117 | } 118 | 119 | public String getGroupId() { 120 | return groupId; 121 | } 122 | 123 | @Override 124 | public int hashCode() { 125 | //we don't really care about overflow. 126 | if(id == null) { 127 | return super.hashCode(); 128 | } 129 | return id.intValue(); 130 | } 131 | 132 | @Override 133 | public boolean equals(Object o) { 134 | if(o instanceof JobHolder == false) { 135 | return false; 136 | } 137 | JobHolder other = (JobHolder) o; 138 | if(id == null || other.id == null) { 139 | return false; 140 | } 141 | return id.equals(other.id); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/JobManager.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue; 2 | 3 | import android.content.Context; 4 | 5 | import com.spix.jobqueue.cachedQueue.CachedJobQueue; 6 | import com.spix.jobqueue.config.Configuration; 7 | import com.spix.jobqueue.di.DependencyInjector; 8 | import com.spix.jobqueue.executor.JobConsumerExecutor; 9 | import com.spix.jobqueue.executor.JobConsumerExecutor.OnAllRunningJobsFinishedListener; 10 | import com.spix.jobqueue.log.JqLog; 11 | import com.spix.jobqueue.network.NetworkEventProvider; 12 | import com.spix.jobqueue.network.NetworkUtil; 13 | import com.spix.jobqueue.nonPersistentQueue.NonPersistentPriorityQueue; 14 | import com.spix.jobqueue.sqlite.SqliteJobQueue; 15 | 16 | import java.util.Collection; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | import java.util.concurrent.CountDownLatch; 19 | import java.util.concurrent.Executors; 20 | import java.util.concurrent.ScheduledExecutorService; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | 24 | /** 25 | * a JobManager that supports; 26 | * - Persistent / Non Persistent Jobs 27 | * - Job Priority 28 | * - Running Jobs in Parallel 29 | * - Grouping jobs so that they won't run at the same time 30 | * - Stats like waiting Job Count 31 | */ 32 | public class JobManager implements NetworkEventProvider.Listener, OnAllRunningJobsFinishedListener { 33 | public static final long NS_PER_MS = 1000000; 34 | public static final long NOT_RUNNING_SESSION_ID = Long.MIN_VALUE; 35 | public static final long NOT_DELAYED_JOB_DELAY = Long.MIN_VALUE; 36 | @SuppressWarnings("FieldCanBeLocal")//used for testing 37 | private final long sessionId; 38 | private boolean running; 39 | private OnAllJobsFinishedListener onAllJobsFinishedListener; 40 | 41 | private final AtomicInteger counterForTimedExecutor = new AtomicInteger(0); 42 | private final Context appContext; 43 | private final NetworkUtil networkUtil; 44 | private final DependencyInjector dependencyInjector; 45 | private final JobQueue persistentJobQueue; 46 | private final JobQueue nonPersistentJobQueue; 47 | private final CopyOnWriteGroupSet runningJobGroups; 48 | private final JobConsumerExecutor jobConsumerExecutor; 49 | private final Object newJobListeners = new Object(); 50 | private final ConcurrentHashMap persistentOnAddedLocks; 51 | private final ConcurrentHashMap nonPersistentOnAddedLocks; 52 | private final ScheduledExecutorService timedExecutor; 53 | private final Object getNextJobLock = new Object(); 54 | 55 | /** 56 | * Default constructor that will create a JobManager with 1 {@link SqliteJobQueue} and 1 {@link NonPersistentPriorityQueue} 57 | * 58 | * @param context job manager will use applicationContext. 59 | */ 60 | public JobManager(Context context) { 61 | this(context, "default"); 62 | } 63 | 64 | 65 | /** 66 | * Default constructor that will create a JobManager with a default {@link Configuration} 67 | * 68 | * @param context application context 69 | * @param id an id that is unique to this JobManager 70 | */ 71 | public JobManager(Context context, String id) { 72 | this(context, new Configuration.Builder(context).id(id).build()); 73 | } 74 | 75 | /** 76 | * @param context used to acquire ApplicationContext 77 | * @param config 78 | */ 79 | public JobManager(Context context, Configuration config) { 80 | if (config.getCustomLogger() != null) { 81 | JqLog.setCustomLogger(config.getCustomLogger()); 82 | } 83 | appContext = context.getApplicationContext(); 84 | running = config.isStartWhenInitialized(); 85 | 86 | runningJobGroups = new CopyOnWriteGroupSet(); 87 | sessionId = System.nanoTime(); 88 | this.persistentJobQueue = config.getQueueFactory().createPersistentQueue(context, sessionId, config.getId()); 89 | this.nonPersistentJobQueue = config.getQueueFactory().createNonPersistent(context, sessionId, config.getId()); 90 | persistentOnAddedLocks = new ConcurrentHashMap(); 91 | nonPersistentOnAddedLocks = new ConcurrentHashMap(); 92 | 93 | networkUtil = config.getNetworkUtil(); 94 | dependencyInjector = config.getDependencyInjector(); 95 | if (networkUtil instanceof NetworkEventProvider) { 96 | ((NetworkEventProvider) networkUtil).setListener(this); 97 | } 98 | //is important to initialize consumers last so that they can start running 99 | jobConsumerExecutor = new JobConsumerExecutor(config, consumerContract); 100 | timedExecutor = Executors.newSingleThreadScheduledExecutor(); 101 | 102 | if (config.isStartWhenInitialized()) { 103 | start(); 104 | } else { 105 | stop(); 106 | } 107 | 108 | } 109 | 110 | public synchronized void setOnAllJobsFinishedListener(OnAllJobsFinishedListener onAllJobsFinishedListener) { 111 | this.onAllJobsFinishedListener = onAllJobsFinishedListener; 112 | if (onAllJobsFinishedListener != null) { 113 | this.jobConsumerExecutor.setOnAllRunningJobsFinishedListener(this); 114 | } else { 115 | this.jobConsumerExecutor.setOnAllRunningJobsFinishedListener(null); 116 | } 117 | } 118 | 119 | 120 | /** 121 | * Stops consuming jobs. Currently running jobs will be finished but no new jobs will be run. 122 | */ 123 | public void stop() { 124 | running = false; 125 | } 126 | 127 | /** 128 | * restarts the JobManager. Will create a new consumer if necessary. 129 | */ 130 | public void start() { 131 | if (running) { 132 | return; 133 | } 134 | running = true; 135 | notifyJobConsumer(); 136 | } 137 | 138 | /** 139 | * returns the # of jobs that are waiting to be executed. 140 | * This might be a good place to decide whether you should wake your app up on boot etc. to complete pending jobs. 141 | * 142 | * @return # of total jobs. 143 | */ 144 | public int count() { 145 | int cnt = 0; 146 | synchronized (nonPersistentJobQueue) { 147 | cnt += nonPersistentJobQueue.count(); 148 | } 149 | synchronized (persistentJobQueue) { 150 | cnt += persistentJobQueue.count(); 151 | } 152 | return cnt; 153 | } 154 | 155 | private int countReadyJobs(boolean hasNetwork) { 156 | //TODO we can cache this 157 | int total = 0; 158 | synchronized (nonPersistentJobQueue) { 159 | total += nonPersistentJobQueue.countReadyJobs(hasNetwork, runningJobGroups.getSafe()); 160 | } 161 | synchronized (persistentJobQueue) { 162 | total += persistentJobQueue.countReadyJobs(hasNetwork, runningJobGroups.getSafe()); 163 | } 164 | return total; 165 | } 166 | 167 | /** 168 | * Adds a new Job to the list and returns an ID for it. 169 | * 170 | * @param job to add 171 | * @return id for the job. 172 | */ 173 | public long addJob(Job job) { 174 | //noinspection deprecation 175 | return addJob(job.getPriority(), job.getDelayInMs(), job); 176 | } 177 | 178 | /** 179 | * Non-blocking convenience method to add a job in background thread. 180 | * 181 | * @param job job to add 182 | * @see #addJob(Job) 183 | */ 184 | public void addJobInBackground(Job job) { 185 | //noinspection deprecation 186 | addJobInBackground(job.getPriority(), job.getDelayInMs(), job); 187 | } 188 | 189 | public void addJobInBackground(Job job, /*nullable*/ AsyncAddCallback callback) { 190 | addJobInBackground(job.getPriority(), job.getDelayInMs(), job, callback); 191 | } 192 | 193 | //need to sync on related job queue before calling this 194 | private void addOnAddedLock(ConcurrentHashMap lockMap, long id) { 195 | lockMap.put(id, new CountDownLatch(1)); 196 | } 197 | 198 | //need to sync on related job queue before calling this 199 | private void waitForOnAddedLock(ConcurrentHashMap lockMap, long id) { 200 | CountDownLatch latch = lockMap.get(id); 201 | if (latch == null) { 202 | return; 203 | } 204 | try { 205 | latch.await(); 206 | } catch (InterruptedException e) { 207 | JqLog.e(e, "could not wait for onAdded lock"); 208 | } 209 | } 210 | 211 | //need to sync on related job queue before calling this 212 | private void clearOnAddedLock(ConcurrentHashMap lockMap, long id) { 213 | CountDownLatch latch = lockMap.get(id); 214 | if (latch != null) { 215 | latch.countDown(); 216 | } 217 | lockMap.remove(id); 218 | } 219 | 220 | /** 221 | * checks next available job and returns when it will be available (if it will, otherwise returns {@link Long#MAX_VALUE}) 222 | * also creates a timer to notify listeners at that time 223 | * 224 | * @param hasNetwork . 225 | * @return time wait until next job (in milliseconds) 226 | */ 227 | private long ensureConsumerWhenNeeded(Boolean hasNetwork) { 228 | if (hasNetwork == null) { 229 | //if network util can inform us when network is recovered, we we'll check only next job that does not 230 | //require network. if it does not know how to inform us, we have to keep a busy loop. 231 | //noinspection SimplifiableConditionalExpression 232 | hasNetwork = networkUtil instanceof NetworkEventProvider ? hasNetwork() : true; 233 | } 234 | //this method is called when there are jobs but job consumer was not given any 235 | //this may happen in a race condition or when the latest job is a delayed job 236 | Long nextRunNs; 237 | synchronized (nonPersistentJobQueue) { 238 | nextRunNs = nonPersistentJobQueue.getNextJobDelayUntilNs(hasNetwork); 239 | } 240 | if (nextRunNs != null && nextRunNs <= System.nanoTime()) { 241 | notifyJobConsumer(); 242 | return 0L; 243 | } 244 | Long persistedJobRunNs; 245 | synchronized (persistentJobQueue) { 246 | persistedJobRunNs = persistentJobQueue.getNextJobDelayUntilNs(hasNetwork); 247 | } 248 | if (persistedJobRunNs != null) { 249 | if (nextRunNs == null) { 250 | nextRunNs = persistedJobRunNs; 251 | } else if (persistedJobRunNs < nextRunNs) { 252 | nextRunNs = persistedJobRunNs; 253 | } 254 | } 255 | if (nextRunNs != null) { 256 | //to avoid overflow, we need to check equality first 257 | if (nextRunNs < System.nanoTime()) { 258 | notifyJobConsumer(); 259 | return 0L; 260 | } 261 | long diff = (long) Math.ceil((double) (nextRunNs - System.nanoTime()) / NS_PER_MS); 262 | ensureConsumerOnTime(diff); 263 | return diff; 264 | } 265 | return Long.MAX_VALUE; 266 | } 267 | 268 | private void notifyJobConsumer() { 269 | synchronized (newJobListeners) { 270 | newJobListeners.notifyAll(); 271 | } 272 | jobConsumerExecutor.considerAddingConsumer(); 273 | } 274 | 275 | private final Runnable notifyRunnable = new Runnable() { 276 | @Override 277 | public void run() { 278 | notifyJobConsumer(); 279 | } 280 | }; 281 | 282 | private void ensureConsumerOnTime(long waitMs) { 283 | timedExecutor.schedule(notifyRunnable, waitMs, TimeUnit.MILLISECONDS); 284 | } 285 | 286 | private boolean hasNetwork() { 287 | return networkUtil == null || networkUtil.isConnected(appContext); 288 | } 289 | 290 | private JobHolder getNextJob() { 291 | boolean haveNetwork = hasNetwork(); 292 | JobHolder jobHolder; 293 | boolean persistent = false; 294 | synchronized (getNextJobLock) { 295 | final Collection runningJobIds = runningJobGroups.getSafe(); 296 | synchronized (nonPersistentJobQueue) { 297 | jobHolder = nonPersistentJobQueue.nextJobAndIncRunCount(haveNetwork, runningJobIds); 298 | } 299 | if (jobHolder == null) { 300 | //go to disk, there aren't any non-persistent jobs 301 | synchronized (persistentJobQueue) { 302 | jobHolder = persistentJobQueue.nextJobAndIncRunCount(haveNetwork, runningJobIds); 303 | persistent = true; 304 | } 305 | } 306 | if (jobHolder == null) { 307 | return null; 308 | } 309 | if (persistent && dependencyInjector != null) { 310 | dependencyInjector.inject(jobHolder.getBaseJob()); 311 | } 312 | if (jobHolder.getGroupId() != null) { 313 | runningJobGroups.add(jobHolder.getGroupId()); 314 | } 315 | } 316 | 317 | //wait for onAdded locks. wait for locks after job is selected so that we minimize the lock 318 | if (persistent) { 319 | waitForOnAddedLock(persistentOnAddedLocks, jobHolder.getId()); 320 | } else { 321 | waitForOnAddedLock(nonPersistentOnAddedLocks, jobHolder.getId()); 322 | } 323 | if (jobHolder != null && jobHolder.baseJob != null) { 324 | jobHolder.baseJob.attachContext(appContext); 325 | jobHolder.baseJob.attachJobManager(this); 326 | } 327 | 328 | return jobHolder; 329 | } 330 | 331 | private void reAddJob(JobHolder jobHolder) { 332 | JqLog.d("re-adding job %s", jobHolder.getId()); 333 | if (jobHolder.getBaseJob().isPersistent()) { 334 | synchronized (persistentJobQueue) { 335 | persistentJobQueue.insertOrReplace(jobHolder); 336 | } 337 | } else { 338 | synchronized (nonPersistentJobQueue) { 339 | nonPersistentJobQueue.insertOrReplace(jobHolder); 340 | } 341 | } 342 | if (jobHolder.getGroupId() != null) { 343 | runningJobGroups.remove(jobHolder.getGroupId()); 344 | } 345 | } 346 | 347 | /** 348 | * Returns the current status of a {@link Job}. 349 | *

350 | * You should not call this method on the UI thread because it may make a db request. 351 | *

352 | *

353 | * This is not a very fast call so try not to make it unless necessary. Consider using events if you need to be 354 | * informed about a job's lifecycle. 355 | *

356 | * 357 | * @param id the ID, returned by the addJob method 358 | * @param isPersistent Jobs are added to different queues depending on if they are persistent or not. This is necessary 359 | * because each queue has independent id sets. 360 | * @return 361 | */ 362 | public JobStatus getJobStatus(long id, boolean isPersistent) { 363 | if (jobConsumerExecutor.isRunning(id, isPersistent)) { 364 | return JobStatus.RUNNING; 365 | } 366 | JobHolder holder; 367 | if (isPersistent) { 368 | synchronized (persistentJobQueue) { 369 | holder = persistentJobQueue.findJobById(id); 370 | } 371 | } else { 372 | synchronized (nonPersistentJobQueue) { 373 | holder = nonPersistentJobQueue.findJobById(id); 374 | } 375 | } 376 | if (holder == null) { 377 | return JobStatus.UNKNOWN; 378 | } 379 | boolean network = hasNetwork(); 380 | if (holder.requiresNetwork() && !network) { 381 | return JobStatus.WAITING_NOT_READY; 382 | } 383 | if (holder.getDelayUntilNs() > System.nanoTime()) { 384 | return JobStatus.WAITING_NOT_READY; 385 | } 386 | 387 | return JobStatus.WAITING_READY; 388 | } 389 | 390 | private void removeJob(JobHolder jobHolder) { 391 | if (jobHolder.getBaseJob().isPersistent()) { 392 | synchronized (persistentJobQueue) { 393 | persistentJobQueue.remove(jobHolder); 394 | } 395 | } else { 396 | synchronized (nonPersistentJobQueue) { 397 | nonPersistentJobQueue.remove(jobHolder); 398 | } 399 | } 400 | if (jobHolder.getGroupId() != null) { 401 | runningJobGroups.remove(jobHolder.getGroupId()); 402 | } 403 | } 404 | 405 | 406 | public synchronized void clear() { 407 | synchronized (nonPersistentJobQueue) { 408 | nonPersistentJobQueue.clear(); 409 | nonPersistentOnAddedLocks.clear(); 410 | } 411 | synchronized (persistentJobQueue) { 412 | persistentJobQueue.clear(); 413 | persistentOnAddedLocks.clear(); 414 | } 415 | runningJobGroups.clear(); 416 | } 417 | 418 | /** 419 | * if {@link NetworkUtil} implements {@link NetworkEventProvider}, this method is called when network is recovered 420 | * 421 | * @param isConnected network connection state. 422 | */ 423 | @Override 424 | public void onNetworkChange(boolean isConnected) { 425 | ensureConsumerWhenNeeded(isConnected); 426 | } 427 | 428 | @SuppressWarnings("FieldCanBeLocal") 429 | private final JobConsumerExecutor.Contract consumerContract = new JobConsumerExecutor.Contract() { 430 | @Override 431 | public boolean isRunning() { 432 | return running; 433 | } 434 | 435 | @Override 436 | public void insertOrReplace(JobHolder jobHolder) { 437 | reAddJob(jobHolder); 438 | } 439 | 440 | @Override 441 | public void removeJob(JobHolder jobHolder) { 442 | JobManager.this.removeJob(jobHolder); 443 | } 444 | 445 | @Override 446 | public JobHolder getNextJob(int wait, TimeUnit waitDuration) { 447 | //be optimistic 448 | JobHolder nextJob = JobManager.this.getNextJob(); 449 | if (nextJob != null) { 450 | return nextJob; 451 | } 452 | long start = System.nanoTime(); 453 | long remainingWait = waitDuration.toNanos(wait); 454 | long waitUntil = remainingWait + start; 455 | //for delayed jobs, 456 | long nextJobDelay = ensureConsumerWhenNeeded(null); 457 | while (nextJob == null && waitUntil > System.nanoTime()) { 458 | //keep running inside here to avoid busy loop 459 | nextJob = running ? JobManager.this.getNextJob() : null; 460 | if (nextJob == null) { 461 | long remaining = waitUntil - System.nanoTime(); 462 | if (remaining > 0) { 463 | //if we can't detect network changes, we won't be notified. 464 | //to avoid waiting up to give time, wait in chunks of 500 ms max 465 | long maxWait = Math.min(nextJobDelay, TimeUnit.NANOSECONDS.toMillis(remaining)); 466 | if (maxWait < 1) { 467 | continue;//wait(0) will cause infinite wait. 468 | } 469 | if (networkUtil instanceof NetworkEventProvider) { 470 | //to handle delayed jobs, make sure we trigger this first 471 | //looks like there is no job available right now, wait for an event. 472 | //there is a chance that if it triggers a timer and it gets called before I enter 473 | //sync block, i am going to lose it 474 | //TODO fix above case where we may wait unnecessarily long if a job is about to become available 475 | synchronized (newJobListeners) { 476 | try { 477 | newJobListeners.wait(maxWait); 478 | } catch (InterruptedException e) { 479 | JqLog.e(e, "exception while waiting for a new job."); 480 | } 481 | } 482 | } else { 483 | //we cannot detect network changes. our best option is to wait for some time and try again 484 | //then trigger {@link ensureConsumerWhenNeeded) 485 | synchronized (newJobListeners) { 486 | try { 487 | newJobListeners.wait(Math.min(500, maxWait)); 488 | } catch (InterruptedException e) { 489 | JqLog.e(e, "exception while waiting for a new job."); 490 | } 491 | } 492 | } 493 | } 494 | } 495 | } 496 | return nextJob; 497 | } 498 | 499 | @Override 500 | public int countRemainingReadyJobs() { 501 | //if we can't detect network changes, assume we have network otherwise nothing will trigger a consumer 502 | //noinspection SimplifiableConditionalExpression 503 | return countReadyJobs(networkUtil instanceof NetworkEventProvider ? hasNetwork() : true); 504 | } 505 | }; 506 | 507 | /** 508 | * Deprecated, please use {@link #addJob(Job)}. 509 | * Adds a job with given priority and returns the JobId. 510 | * 511 | * @param priority Higher runs first 512 | * @param baseJob The actual job to run 513 | * @return job id 514 | */ 515 | @Deprecated 516 | public long addJob(int priority, BaseJob baseJob) { 517 | return addJob(priority, 0, baseJob); 518 | } 519 | 520 | /** 521 | * Deprecated, please use {@link #addJob(Job)}. 522 | * Adds a job with given priority and returns the JobId. 523 | * 524 | * @param priority Higher runs first 525 | * @param delay number of milliseconds that this job should be delayed 526 | * @param baseJob The actual job to run 527 | * @return a job id. is useless for now but we'll use this to cancel jobs in the future. 528 | */ 529 | @Deprecated 530 | public long addJob(int priority, long delay, BaseJob baseJob) { 531 | JobHolder jobHolder = new JobHolder(priority, baseJob, delay > 0 ? System.nanoTime() + delay * NS_PER_MS : NOT_DELAYED_JOB_DELAY, NOT_RUNNING_SESSION_ID); 532 | long id; 533 | if (baseJob.isPersistent()) { 534 | synchronized (persistentJobQueue) { 535 | id = persistentJobQueue.insert(jobHolder); 536 | addOnAddedLock(persistentOnAddedLocks, id); 537 | } 538 | } else { 539 | synchronized (nonPersistentJobQueue) { 540 | id = nonPersistentJobQueue.insert(jobHolder); 541 | addOnAddedLock(nonPersistentOnAddedLocks, id); 542 | } 543 | } 544 | if (JqLog.isDebugEnabled()) { 545 | JqLog.d("added job id: %d class: %s priority: %d delay: %d group : %s persistent: %s requires network: %s" 546 | , id, baseJob.getClass().getSimpleName(), priority, delay, baseJob.getRunGroupId() 547 | , baseJob.isPersistent(), baseJob.requiresNetwork()); 548 | } 549 | if (dependencyInjector != null) { 550 | //inject members b4 calling onAdded 551 | dependencyInjector.inject(baseJob); 552 | } 553 | jobHolder.getBaseJob().onAdded(); 554 | if (baseJob.isPersistent()) { 555 | synchronized (persistentJobQueue) { 556 | clearOnAddedLock(persistentOnAddedLocks, id); 557 | } 558 | } else { 559 | synchronized (nonPersistentJobQueue) { 560 | clearOnAddedLock(nonPersistentOnAddedLocks, id); 561 | } 562 | } 563 | notifyJobConsumer(); 564 | return id; 565 | } 566 | 567 | /** 568 | * Please use {@link #addJobInBackground(Job)}. 569 | *

Non-blocking convenience method to add a job in background thread.

570 | * 571 | * @see #addJob(int, BaseJob) addJob(priority, job). 572 | */ 573 | @Deprecated 574 | public void addJobInBackground(final int priority, final BaseJob baseJob) { 575 | timedExecutor.execute(new Runnable() { 576 | @Override 577 | public void run() { 578 | addJob(priority, baseJob); 579 | } 580 | }); 581 | } 582 | 583 | /** 584 | * Deprecated, please use {@link #addJobInBackground(Job)}. 585 | * Non-blocking convenience method to add a job in background thread. 586 | * 587 | * @see #addJob(int, long, BaseJob) addJob(priority, delay, job). 588 | */ 589 | @Deprecated 590 | public void addJobInBackground(final int priority, final long delay, final BaseJob baseJob) { 591 | addJobInBackground(priority, delay, baseJob, null); 592 | } 593 | 594 | protected void addJobInBackground(final int priority, final long delay, final BaseJob baseJob, 595 | /*nullable*/final AsyncAddCallback callback) { 596 | final long callTime = System.nanoTime(); 597 | counterForTimedExecutor.incrementAndGet(); 598 | timedExecutor.execute(new Runnable() { 599 | @Override 600 | public void run() { 601 | try { 602 | final long runDelay = (System.nanoTime() - callTime) / NS_PER_MS; 603 | long id = addJob(priority, Math.max(0, delay - runDelay), baseJob); 604 | if (callback != null) { 605 | callback.onAdded(id); 606 | } 607 | counterForTimedExecutor.decrementAndGet(); 608 | } catch (Throwable t) { 609 | JqLog.e(t, "addJobInBackground received an exception. job class: %s", baseJob.getClass().getSimpleName()); 610 | } 611 | } 612 | }); 613 | } 614 | 615 | //Called when no more jobs are running 616 | @Override 617 | public void onAllRunningJobsFinished() { 618 | if (count() == 0 && counterForTimedExecutor.get() == 0) { 619 | synchronized (this) { 620 | if (onAllJobsFinishedListener != null) { 621 | onAllJobsFinishedListener.onAllJobsFinished(); 622 | } 623 | } 624 | } 625 | } 626 | 627 | 628 | /** 629 | * Default implementation of QueueFactory that creates one {@link SqliteJobQueue} and one {@link NonPersistentPriorityQueue} 630 | * both are wrapped inside a {@link CachedJobQueue} to improve performance 631 | */ 632 | public static class DefaultQueueFactory implements QueueFactory { 633 | SqliteJobQueue.JobSerializer jobSerializer; 634 | 635 | public DefaultQueueFactory() { 636 | jobSerializer = new SqliteJobQueue.JavaSerializer(); 637 | } 638 | 639 | public DefaultQueueFactory(SqliteJobQueue.JobSerializer jobSerializer) { 640 | this.jobSerializer = jobSerializer; 641 | } 642 | 643 | @Override 644 | public JobQueue createPersistentQueue(Context context, Long sessionId, String id) { 645 | return new CachedJobQueue(new SqliteJobQueue(context, sessionId, id, jobSerializer)); 646 | } 647 | 648 | @Override 649 | public JobQueue createNonPersistent(Context context, Long sessionId, String id) { 650 | return new CachedJobQueue(new NonPersistentPriorityQueue(sessionId, id)); 651 | } 652 | } 653 | 654 | public interface OnAllJobsFinishedListener { 655 | public void onAllJobsFinished(); 656 | } 657 | } 658 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/JobQueue.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue; 2 | 3 | import java.util.Collection; 4 | 5 | /** 6 | * Interface that any JobQueue should implement 7 | * These job queues can be given to JobManager. 8 | */ 9 | public interface JobQueue { 10 | /** 11 | * Inserts the given JobHolder, 12 | * assigns it a unique id 13 | * and returns the id back 14 | * Is called when a job is added 15 | * @param jobHolder 16 | * @return 17 | */ 18 | long insert(JobHolder jobHolder); 19 | 20 | /** 21 | * Does the same thing with insert but the only difference is that 22 | * if job has an ID, it should replace the existing one 23 | * should also reset running session id to {@link JobManager#NOT_RUNNING_SESSION_ID} 24 | * Is called when a job is re-added (due to exception during run) 25 | * @param jobHolder 26 | * @return 27 | */ 28 | long insertOrReplace(JobHolder jobHolder); 29 | 30 | /** 31 | * Removes the job from the data store. 32 | * Is called after a job is completed (or cancelled) 33 | * @param jobHolder 34 | */ 35 | void remove(JobHolder jobHolder); 36 | 37 | /** 38 | * Returns the # of jobs that are waiting to be run 39 | * @return 40 | */ 41 | int count(); 42 | 43 | /** 44 | * counts the # of jobs that can run now. if there are more jobs from the same group, they are count as 1 since 45 | * they cannot be run in parallel 46 | * exclude groups are guaranteed to be ordered in natural order 47 | * @return 48 | */ 49 | int countReadyJobs(boolean hasNetwork, Collection excludeGroups); 50 | 51 | /** 52 | * Returns the next available job in the data set 53 | * It should also assign the sessionId as the RunningSessionId and persist that data if necessary. 54 | * It should filter out all running jobs and 55 | * exclude groups are guaranteed to be ordered in natural order 56 | * @param hasNetwork if true, should return any job, if false, should return jobs that do NOT require network 57 | * @param excludeGroups if provided, jobs from these groups will NOT be returned 58 | * @return 59 | */ 60 | JobHolder nextJobAndIncRunCount(boolean hasNetwork, Collection excludeGroups); 61 | 62 | /** 63 | * returns when the next job should run (in nanoseconds), should return null if there are no jobs to run. 64 | * @param hasNetwork if true, should return nanoseconds for any job, if false, should return nanoseconds for next 65 | * job's delay until. 66 | * @return 67 | */ 68 | Long getNextJobDelayUntilNs(boolean hasNetwork); 69 | 70 | /** 71 | * clear all jobs in the queue. should probably be called when user logs out. 72 | */ 73 | void clear(); 74 | 75 | /** 76 | * returns the job with the given id if it exists in the queue 77 | * @param id id of the job, returned by insert method 78 | * @return JobHolder with the given id or null if it does not exists 79 | */ 80 | JobHolder findJobById(long id); 81 | 82 | } 83 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/JobStatus.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue; 2 | 3 | /** 4 | * Identifies the current status of a job if it is in the queue 5 | */ 6 | public enum JobStatus { 7 | /** 8 | * Job is in the queue but cannot run yet. 9 | * As of v 1.1, this might be: 10 | * Job requires network but there is no available network connection 11 | * Job is delayed. We are waiting for the time to pass 12 | */ 13 | WAITING_NOT_READY, 14 | /** 15 | * Job is in the queue, ready to be run. Waiting for an available consumer. 16 | */ 17 | WAITING_READY, 18 | /** 19 | * Job is being executed by one of the runners. 20 | */ 21 | RUNNING, 22 | /** 23 | * Job is not known by job queue. 24 | * This might be: 25 | * Invalid ID 26 | * Job has been completed 27 | * Job has failed 28 | * Job has just been added, about to be delivered into a queue 29 | */ 30 | UNKNOWN 31 | } 32 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/Params.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue; 2 | 3 | /** 4 | * BaseJob builder object to have a more readable design. 5 | * Methods can be chained to have more readable code. 6 | */ 7 | public class Params { 8 | private boolean requiresNetwork = false; 9 | private String groupId = null; 10 | private boolean persistent = false; 11 | private int priority; 12 | private long delayMs; 13 | 14 | /** 15 | * 16 | * @param priority higher = better 17 | */ 18 | public Params(int priority) { 19 | this.priority = priority; 20 | } 21 | 22 | /** 23 | * Sets the Job as requiring network 24 | * @return this 25 | */ 26 | public Params requireNetwork() { 27 | requiresNetwork = true; 28 | return this; 29 | } 30 | 31 | /** 32 | * Sets the group id. Jobs in the same group are guaranteed to execute sequentially. 33 | * @param groupId which group this job belongs (can be null of course) 34 | * @return this 35 | */ 36 | public Params groupBy(String groupId) { 37 | this.groupId = groupId; 38 | return this; 39 | } 40 | 41 | /** 42 | * Marks the job as persistent. Make sure your job is serializable. 43 | * @return this 44 | */ 45 | public Params persist() { 46 | this.persistent = true; 47 | return this; 48 | } 49 | 50 | /** 51 | * Delays the job in given ms. 52 | * @param delayMs . 53 | * @return this 54 | */ 55 | public Params delayInMs(long delayMs) { 56 | this.delayMs = delayMs; 57 | return this; 58 | } 59 | 60 | /** 61 | * convenience method to set network requirement 62 | * @param requiresNetwork true|false 63 | * @return this 64 | */ 65 | public Params setRequiresNetwork(boolean requiresNetwork) { 66 | this.requiresNetwork = requiresNetwork; 67 | return this; 68 | } 69 | 70 | /** 71 | * convenience method to set group id. 72 | * @param groupId 73 | * @return this 74 | */ 75 | public Params setGroupId(String groupId) { 76 | this.groupId = groupId; 77 | return this; 78 | } 79 | 80 | /** 81 | * convenience method to set whether {@link JobManager} should persist this job or not. 82 | * @param persistent true|false 83 | * @return this 84 | */ 85 | public Params setPersistent(boolean persistent) { 86 | this.persistent = persistent; 87 | return this; 88 | } 89 | 90 | /** 91 | * convenience method to set delay 92 | * @param delayMs in ms 93 | * @return this 94 | */ 95 | public Params setDelayMs(long delayMs) { 96 | this.delayMs = delayMs; 97 | return this; 98 | } 99 | 100 | public boolean doesRequireNetwork() { 101 | return requiresNetwork; 102 | } 103 | 104 | public String getGroupId() { 105 | return groupId; 106 | } 107 | 108 | public boolean isPersistent() { 109 | return persistent; 110 | } 111 | 112 | public int getPriority() { 113 | return priority; 114 | } 115 | 116 | public long getDelayMs() { 117 | return delayMs; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/QueueFactory.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * Interface to supply custom {@link JobQueue}s for JobManager 7 | */ 8 | public interface QueueFactory { 9 | public JobQueue createPersistentQueue(Context context, Long sessionId, String id); 10 | public JobQueue createNonPersistent(Context context, Long sessionId, String id); 11 | } -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/cachedQueue/CachedJobQueue.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.cachedQueue; 2 | 3 | import com.spix.jobqueue.JobHolder; 4 | import com.spix.jobqueue.JobQueue; 5 | 6 | import java.util.Collection; 7 | 8 | /** 9 | * a class that implements {@link JobQueue} interface, wraps another {@link JobQueue} and caches 10 | * results to avoid unnecessary queries to wrapped JobQueue. 11 | * does very basic caching but should be sufficient for most of the repeated cases 12 | * element 13 | */ 14 | public class CachedJobQueue implements JobQueue { 15 | JobQueue delegate; 16 | private Cache cache; 17 | 18 | public CachedJobQueue(JobQueue delegate) { 19 | this.delegate = delegate; 20 | this.cache = new Cache(); 21 | } 22 | 23 | @Override 24 | public long insert(JobHolder jobHolder) { 25 | cache.invalidateAll(); 26 | return delegate.insert(jobHolder); 27 | } 28 | 29 | @Override 30 | public long insertOrReplace(JobHolder jobHolder) { 31 | cache.invalidateAll(); 32 | return delegate.insertOrReplace(jobHolder); 33 | } 34 | 35 | @Override 36 | public void remove(JobHolder jobHolder) { 37 | cache.invalidateAll(); 38 | delegate.remove(jobHolder); 39 | } 40 | 41 | @Override 42 | public int count() { 43 | if(cache.count == null) { 44 | cache.count = delegate.count(); 45 | } 46 | return cache.count; 47 | } 48 | 49 | @Override 50 | public int countReadyJobs(boolean hasNetwork, Collection excludeGroups) { 51 | if(cache.count != null && cache.count < 1) { 52 | //we know count is zero, why query? 53 | return 0; 54 | } 55 | int count = delegate.countReadyJobs(hasNetwork, excludeGroups); 56 | if(count == 0) { 57 | //warm up cache if this is an empty queue case. if not, we are creating an unncessary query. 58 | count(); 59 | } 60 | return count; 61 | } 62 | 63 | @Override 64 | public JobHolder nextJobAndIncRunCount(boolean hasNetwork, Collection excludeGroups) { 65 | if(cache.count != null && cache.count < 1) { 66 | return null;//we know we are empty, no need for querying 67 | } 68 | JobHolder holder = delegate.nextJobAndIncRunCount(hasNetwork, excludeGroups); 69 | //if holder is null, there is a good chance that there aren't any jobs in queue try to cache it by calling count 70 | if(holder == null) { 71 | //warm up empty state cache 72 | count(); 73 | } else if(cache.count != null) { 74 | //no need to invalidate cache for count 75 | cache.count--; 76 | } 77 | return holder; 78 | } 79 | 80 | @Override 81 | public Long getNextJobDelayUntilNs(boolean hasNetwork) { 82 | if(cache.delayUntil == null) { 83 | cache.delayUntil = new Cache.DelayUntil(hasNetwork, delegate.getNextJobDelayUntilNs(hasNetwork)); 84 | } else if(!cache.delayUntil.isValid(hasNetwork)) { 85 | cache.delayUntil.set(hasNetwork, delegate.getNextJobDelayUntilNs(hasNetwork)); 86 | } 87 | return cache.delayUntil.value; 88 | } 89 | 90 | @Override 91 | public void clear() { 92 | cache.invalidateAll(); 93 | delegate.clear(); 94 | } 95 | 96 | @Override 97 | public JobHolder findJobById(long id) { 98 | return delegate.findJobById(id); 99 | } 100 | 101 | private static class Cache { 102 | Integer count; 103 | DelayUntil delayUntil; 104 | 105 | public void invalidateAll() { 106 | count = null; 107 | delayUntil = null; 108 | } 109 | 110 | private static class DelayUntil { 111 | //can be null, is OK 112 | Long value; 113 | boolean hasNetwork; 114 | 115 | private DelayUntil(boolean hasNetwork, Long value) { 116 | this.value = value; 117 | this.hasNetwork = hasNetwork; 118 | } 119 | 120 | private boolean isValid(boolean hasNetwork) { 121 | return this.hasNetwork == hasNetwork; 122 | } 123 | 124 | public void set(boolean hasNetwork, Long value) { 125 | this.value = value; 126 | this.hasNetwork = hasNetwork; 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/config/Configuration.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.config; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | 6 | import com.spix.jobqueue.JobManager; 7 | import com.spix.jobqueue.JobQueue; 8 | import com.spix.jobqueue.QueueFactory; 9 | import com.spix.jobqueue.di.DependencyInjector; 10 | import com.spix.jobqueue.log.CustomLogger; 11 | import com.spix.jobqueue.network.NetworkUtil; 12 | import com.spix.jobqueue.network.NetworkUtilImpl; 13 | import com.spix.jobqueue.nonPersistentQueue.NonPersistentPriorityQueue; 14 | import com.spix.jobqueue.sqlite.SqliteJobQueue; 15 | 16 | /** 17 | * {@link com.spix.jobqueue.JobManager} configuration object 18 | */ 19 | public class Configuration { 20 | public static final String DEFAULT_ID = "default_job_manager"; 21 | public static final int DEFAULT_THREAD_KEEP_ALIVE_SECONDS = 15; 22 | public static final int DEFAULT_LOAD_FACTOR_PER_CONSUMER = 3; 23 | public static final int MAX_CONSUMER_COUNT = 5; 24 | public static final int MIN_CONSUMER_COUNT = 0; 25 | 26 | private String id = DEFAULT_ID; 27 | private boolean startWhenInitialized = true; 28 | private int maxConsumerCount = MAX_CONSUMER_COUNT; 29 | private int minConsumerCount = MIN_CONSUMER_COUNT; 30 | private int consumerKeepAlive = DEFAULT_THREAD_KEEP_ALIVE_SECONDS; 31 | private int loadFactor = DEFAULT_LOAD_FACTOR_PER_CONSUMER; 32 | private QueueFactory queueFactory; 33 | private DependencyInjector dependencyInjector; 34 | private NetworkUtil networkUtil; 35 | private CustomLogger customLogger; 36 | 37 | private Configuration() { 38 | //use builder instead 39 | } 40 | 41 | public String getId() { 42 | return id; 43 | } 44 | 45 | public boolean isStartWhenInitialized() { 46 | return startWhenInitialized; 47 | } 48 | 49 | public QueueFactory getQueueFactory() { 50 | return queueFactory; 51 | } 52 | 53 | public DependencyInjector getDependencyInjector() { 54 | return dependencyInjector; 55 | } 56 | 57 | public int getConsumerKeepAlive() { 58 | return consumerKeepAlive; 59 | } 60 | 61 | public NetworkUtil getNetworkUtil() { 62 | return networkUtil; 63 | } 64 | 65 | public int getMaxConsumerCount() { 66 | return maxConsumerCount; 67 | } 68 | 69 | public int getMinConsumerCount() { 70 | return minConsumerCount; 71 | } 72 | 73 | public CustomLogger getCustomLogger() { 74 | return customLogger; 75 | } 76 | 77 | public int getLoadFactor() { 78 | return loadFactor; 79 | } 80 | 81 | public static final class Builder { 82 | private Configuration configuration; 83 | private Context appContext; 84 | 85 | public Builder(Context context) { 86 | this.configuration = new Configuration(); 87 | appContext = context.getApplicationContext(); 88 | } 89 | 90 | /** 91 | * provide and ID for this job manager to be used while creating persistent queue. it is useful if you are going to 92 | * create multiple instances of it. 93 | * default id is {@value #DEFAULT_ID} 94 | * 95 | * @param id if you have multiple instances of job manager, you should provide an id to distinguish their persistent files. 96 | */ 97 | public Builder id(String id) { 98 | configuration.id = id; 99 | return this; 100 | } 101 | 102 | public Builder startWhenInitialized(boolean start) { 103 | configuration.startWhenInitialized = start; 104 | return this; 105 | } 106 | 107 | /** 108 | * When JobManager runs out of `ready` jobs, it will keep consumers alive for this duration. it defaults to {@value #DEFAULT_THREAD_KEEP_ALIVE_SECONDS} 109 | * 110 | * @param keepAlive in seconds 111 | */ 112 | public Builder consumerKeepAlive(int keepAlive) { 113 | configuration.consumerKeepAlive = keepAlive; 114 | return this; 115 | } 116 | 117 | /** 118 | * JobManager needs one persistent and one non-persistent {@link JobQueue} to function. 119 | * By default, it will use {@link SqliteJobQueue} and {@link NonPersistentPriorityQueue} 120 | * You can provide your own implementation if they don't fit your needs. Make sure it passes all tests in 121 | * JobQueueTestBase to ensure it will work fine. 122 | * 123 | * @param queueFactory your custom queue factory. 124 | */ 125 | public Builder queueFactory(QueueFactory queueFactory) { 126 | if (configuration.queueFactory != null) { 127 | throw new RuntimeException("already set a queue factory. This might happen if you've provided a custom " + 128 | "job serializer"); 129 | } 130 | configuration.queueFactory = queueFactory; 131 | return this; 132 | } 133 | 134 | /** 135 | * convenient configuration to replace job serializer while using {@link SqliteJobQueue} queue for persistence. 136 | * by default, it uses a {@link SqliteJobQueue.JavaSerializer} which will use default Java serialization. 137 | */ 138 | public Builder jobSerializer(SqliteJobQueue.JobSerializer jobSerializer) { 139 | configuration.queueFactory = new JobManager.DefaultQueueFactory(jobSerializer); 140 | return this; 141 | } 142 | 143 | /** 144 | * By default, Job Manager comes with a simple {@link NetworkUtilImpl} that queries {@link ConnectivityManager} 145 | * to check if network connection exists. You can provide your own if you need a custom logic (e.g. check your 146 | * server health etc). 147 | */ 148 | public Builder networkUtil(NetworkUtil networkUtil) { 149 | configuration.networkUtil = networkUtil; 150 | return this; 151 | } 152 | 153 | /** 154 | * JobManager is suitable for DependencyInjection. Just provide your DependencyInjector and it will call it 155 | * before {BaseJob#onAdded} method is called. 156 | * if job is persistent, it will also be called before run method. 157 | * 158 | * @param injector your dependency injector interface, if using one 159 | * @return 160 | */ 161 | public Builder injector(DependencyInjector injector) { 162 | configuration.dependencyInjector = injector; 163 | return this; 164 | } 165 | 166 | /** 167 | * # of max consumers to run concurrently. defaults to {@value #MAX_CONSUMER_COUNT} 168 | * 169 | * @param count 170 | */ 171 | public Builder maxConsumerCount(int count) { 172 | configuration.maxConsumerCount = count; 173 | return this; 174 | } 175 | 176 | /** 177 | * you can specify to keep minConsumers alive even if there are no ready jobs. defaults to {@value #MIN_CONSUMER_COUNT} 178 | * 179 | * @param count 180 | */ 181 | public Builder minConsumerCount(int count) { 182 | configuration.minConsumerCount = count; 183 | return this; 184 | } 185 | 186 | /** 187 | * you can provide a custom logger to get logs from JobManager. 188 | * by default, logs will go no-where. 189 | * 190 | * @param logger 191 | */ 192 | public Builder customLogger(CustomLogger logger) { 193 | configuration.customLogger = logger; 194 | return this; 195 | } 196 | 197 | /** 198 | * calculated by # of jobs (running+waiting) per thread 199 | * for instance, at a given time, if you have two consumers and 10 jobs in waiting queue (or running right now), load is 200 | * (10/2) =5 201 | * defaults to {@value #DEFAULT_LOAD_FACTOR_PER_CONSUMER} 202 | * 203 | * @param loadFactor 204 | */ 205 | public Builder loadFactor(int loadFactor) { 206 | configuration.loadFactor = loadFactor; 207 | return this; 208 | } 209 | 210 | public Configuration build() { 211 | if (configuration.queueFactory == null) { 212 | configuration.queueFactory = new JobManager.DefaultQueueFactory(); 213 | } 214 | if (configuration.networkUtil == null) { 215 | configuration.networkUtil = new NetworkUtilImpl(appContext); 216 | } 217 | return configuration; 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/di/DependencyInjector.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.di; 2 | 3 | import com.spix.jobqueue.BaseJob; 4 | 5 | /** 6 | * interface that can be provided to {@link com.spix.jobqueue.JobManager} for dependency injection 7 | * it is called before the job's onAdded method is called. for persistent jobs, also run after job is brought 8 | * back from disk. 9 | */ 10 | public interface DependencyInjector { 11 | public void inject(BaseJob job); 12 | } 13 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/executor/JobConsumerExecutor.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.executor; 2 | 3 | import com.spix.jobqueue.JobHolder; 4 | import com.spix.jobqueue.JobManager; 5 | import com.spix.jobqueue.JobQueue; 6 | import com.spix.jobqueue.config.Configuration; 7 | import com.spix.jobqueue.log.JqLog; 8 | 9 | import java.util.concurrent.ConcurrentHashMap; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | 13 | /** 14 | * An executor class that takes care of spinning consumer threads and making sure enough is alive. 15 | * works deeply coupled with {@link JobManager} 16 | */ 17 | public class JobConsumerExecutor { 18 | private int maxConsumerSize; 19 | private int minConsumerSize; 20 | private int loadFactor; 21 | private final ThreadGroup threadGroup; 22 | private final Contract contract; 23 | private final int keepAliveSeconds; 24 | private final AtomicInteger activeConsumerCount = new AtomicInteger(0); 25 | // key : id + (isPersistent) 26 | private final ConcurrentHashMap runningJobHolders; 27 | private OnAllRunningJobsFinishedListener onAllRunningJobsFinishedListener; 28 | 29 | 30 | public JobConsumerExecutor(Configuration config, Contract contract) { 31 | this.loadFactor = config.getLoadFactor(); 32 | this.maxConsumerSize = config.getMaxConsumerCount(); 33 | this.minConsumerSize = config.getMinConsumerCount(); 34 | this.keepAliveSeconds = config.getConsumerKeepAlive(); 35 | this.contract = contract; 36 | threadGroup = new ThreadGroup("JobConsumers"); 37 | runningJobHolders = new ConcurrentHashMap(); 38 | } 39 | 40 | /** 41 | * creates a new consumer thread if needed. 42 | */ 43 | public void considerAddingConsumer() { 44 | doINeedANewThread(false, true); 45 | } 46 | 47 | private boolean canIDie() { 48 | if (doINeedANewThread(true, false) == false) { 49 | return true; 50 | } 51 | return false; 52 | } 53 | 54 | public void setOnAllRunningJobsFinishedListener(OnAllRunningJobsFinishedListener onAllRunningJobsFinishedListener) { 55 | this.onAllRunningJobsFinishedListener = onAllRunningJobsFinishedListener; 56 | } 57 | 58 | private boolean doINeedANewThread(boolean inConsumerThread, boolean addIfNeeded) { 59 | //if network provider cannot notify us, we have to busy wait 60 | if (contract.isRunning() == false) { 61 | if (inConsumerThread) { 62 | activeConsumerCount.decrementAndGet(); 63 | } 64 | return false; 65 | } 66 | 67 | synchronized (threadGroup) { 68 | if (isAboveLoadFactor(inConsumerThread) && canAddMoreConsumers()) { 69 | if (addIfNeeded) { 70 | addConsumer(); 71 | } 72 | return true; 73 | } 74 | } 75 | if (inConsumerThread) { 76 | activeConsumerCount.decrementAndGet(); 77 | } 78 | return false; 79 | } 80 | 81 | private void addConsumer() { 82 | JqLog.d("adding another consumer"); 83 | synchronized (threadGroup) { 84 | Thread thread = new Thread(threadGroup, new JobConsumer(contract, this)); 85 | activeConsumerCount.incrementAndGet(); 86 | thread.start(); 87 | } 88 | } 89 | 90 | private boolean canAddMoreConsumers() { 91 | synchronized (threadGroup) { 92 | //there is a race condition for the time thread if about to finish 93 | return activeConsumerCount.intValue() < maxConsumerSize; 94 | } 95 | } 96 | 97 | private boolean isAboveLoadFactor(boolean inConsumerThread) { 98 | synchronized (threadGroup) { 99 | //if i am called from a consumer thread, don't count me 100 | int consumerCnt = activeConsumerCount.intValue() - (inConsumerThread ? 1 : 0); 101 | boolean res = 102 | consumerCnt < minConsumerSize || 103 | consumerCnt * loadFactor < contract.countRemainingReadyJobs() + runningJobHolders.size(); 104 | if (JqLog.isDebugEnabled()) { 105 | JqLog.d("%s: load factor check. %s = (%d < %d)|| (%d * %d < %d + %d). consumer thread: %s", Thread.currentThread().getName(), res, 106 | consumerCnt, minConsumerSize, 107 | consumerCnt, loadFactor, contract.countRemainingReadyJobs(), runningJobHolders.size(), inConsumerThread); 108 | } 109 | return res; 110 | } 111 | 112 | } 113 | 114 | public int getRunningJobsCount() { 115 | return runningJobHolders.size(); 116 | } 117 | 118 | private void onBeforeRun(JobHolder jobHolder) { 119 | runningJobHolders.put(createRunningJobHolderKey(jobHolder), jobHolder); 120 | } 121 | 122 | private void onAfterRun(JobHolder jobHolder) { 123 | runningJobHolders.remove(createRunningJobHolderKey(jobHolder)); 124 | if (runningJobHolders.size() == 0) { 125 | if (onAllRunningJobsFinishedListener != null) { 126 | onAllRunningJobsFinishedListener.onAllRunningJobsFinished(); 127 | } 128 | } 129 | } 130 | 131 | private String createRunningJobHolderKey(JobHolder jobHolder) { 132 | return createRunningJobHolderKey(jobHolder.getId(), jobHolder.getBaseJob().isPersistent()); 133 | } 134 | 135 | private String createRunningJobHolderKey(long id, boolean isPersistent) { 136 | return id + "_" + (isPersistent ? "t" : "f"); 137 | } 138 | 139 | /** 140 | * returns true if job is currently handled by one of the executor threads 141 | * 142 | * @param id id of the job 143 | * @param persistent boolean flag to distinguish id conflicts 144 | * @return true if job is currently handled here 145 | */ 146 | public boolean isRunning(long id, boolean persistent) { 147 | return runningJobHolders.containsKey(createRunningJobHolderKey(id, persistent)); 148 | } 149 | 150 | /** 151 | * contract between the {@link JobManager} and {@link JobConsumerExecutor} 152 | */ 153 | public static interface Contract { 154 | /** 155 | * @return if {@link JobManager} is currently running. 156 | */ 157 | public boolean isRunning(); 158 | 159 | /** 160 | * should insert the given {@link JobHolder} to related {@link JobQueue}. if it already exists, should replace the 161 | * existing one. 162 | * 163 | * @param jobHolder 164 | */ 165 | public void insertOrReplace(JobHolder jobHolder); 166 | 167 | /** 168 | * should remove the job from the related {@link JobQueue} 169 | * 170 | * @param jobHolder 171 | */ 172 | public void removeJob(JobHolder jobHolder); 173 | 174 | /** 175 | * should return the next job which is available to be run. 176 | * 177 | * @param wait 178 | * @param waitUnit 179 | * @return next job to execute or null if no jobs are available 180 | */ 181 | public JobHolder getNextJob(int wait, TimeUnit waitUnit); 182 | 183 | /** 184 | * @return the number of Jobs that are ready to be run 185 | */ 186 | public int countRemainingReadyJobs(); 187 | } 188 | 189 | /** 190 | * a simple {@link Runnable} that can take jobs from the {@link Contract} and execute them 191 | */ 192 | private static class JobConsumer implements Runnable { 193 | private final Contract contract; 194 | private final JobConsumerExecutor executor; 195 | private boolean didRunOnce = false; 196 | 197 | public JobConsumer(Contract contract, JobConsumerExecutor executor) { 198 | this.executor = executor; 199 | this.contract = contract; 200 | } 201 | 202 | @Override 203 | public void run() { 204 | boolean canDie; 205 | do { 206 | try { 207 | if (JqLog.isDebugEnabled()) { 208 | if (didRunOnce == false) { 209 | JqLog.d("starting consumer %s", Thread.currentThread().getName()); 210 | didRunOnce = true; 211 | } else { 212 | JqLog.d("re-running consumer %s", Thread.currentThread().getName()); 213 | } 214 | } 215 | JobHolder nextJob; 216 | do { 217 | nextJob = contract.isRunning() ? contract.getNextJob(executor.keepAliveSeconds, TimeUnit.SECONDS) : null; 218 | if (nextJob != null) { 219 | executor.onBeforeRun(nextJob); 220 | if (nextJob.safeRun(nextJob.getRunCount())) { 221 | contract.removeJob(nextJob); 222 | } else { 223 | contract.insertOrReplace(nextJob); 224 | } 225 | executor.onAfterRun(nextJob); 226 | } 227 | } while (nextJob != null); 228 | } finally { 229 | //to avoid creating a new thread for no reason, consider not killing this one first 230 | canDie = executor.canIDie(); 231 | if (JqLog.isDebugEnabled()) { 232 | if (canDie) { 233 | JqLog.d("finishing consumer %s", Thread.currentThread().getName()); 234 | } else { 235 | JqLog.d("didn't allow me to die, re-running %s", Thread.currentThread().getName()); 236 | } 237 | } 238 | } 239 | } while (!canDie); 240 | } 241 | } 242 | 243 | public interface OnAllRunningJobsFinishedListener { 244 | public void onAllRunningJobsFinished(); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/log/CustomLogger.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.log; 2 | 3 | /** 4 | * You can provide your own logger implementation to {@link com.spix.jobqueue.JobManager} 5 | * it is very similar to Roboguice's logger 6 | */ 7 | public interface CustomLogger { 8 | /** 9 | * JobManager may call this before logging sth that is (relatively) expensive to calculate 10 | * @return 11 | */ 12 | public boolean isDebugEnabled(); 13 | public void d(String text, Object... args); 14 | public void e(Throwable t, String text, Object... args); 15 | public void e(String text, Object... args); 16 | } 17 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/log/JqLog.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.log; 2 | 3 | /** 4 | * Wrapper around {@link CustomLogger}. by default, logs to nowhere 5 | */ 6 | public class JqLog { 7 | private static CustomLogger customLogger = new CustomLogger() { 8 | @Override 9 | public boolean isDebugEnabled() { 10 | return false; 11 | } 12 | 13 | @Override 14 | public void d(String text, Object... args) { 15 | //void 16 | } 17 | 18 | @Override 19 | public void e(Throwable t, String text, Object... args) { 20 | //void 21 | } 22 | 23 | @Override 24 | public void e(String text, Object... args) { 25 | //void 26 | } 27 | }; 28 | 29 | public static void setCustomLogger(CustomLogger customLogger) { 30 | JqLog.customLogger = customLogger; 31 | } 32 | 33 | public static boolean isDebugEnabled() { 34 | return customLogger.isDebugEnabled(); 35 | } 36 | 37 | public static void d(String text, Object... args) { 38 | customLogger.d(text, args); 39 | } 40 | 41 | public static void e(Throwable t, String text, Object... args) { 42 | customLogger.e(t, text, args); 43 | } 44 | 45 | public static void e(String text, Object... args) { 46 | customLogger.e(text, args); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/network/NetworkEventProvider.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.network; 2 | 3 | /** 4 | * An interface that NetworkUtil can implement if it supports a callback method when network state is changed 5 | * This is not mandatory but highly suggested so that {@link com.spix.jobqueue.JobManager} can avoid 6 | * busy loops when there is a job waiting for network and there is no network available 7 | */ 8 | public interface NetworkEventProvider { 9 | public void setListener(Listener listener); 10 | public static interface Listener { 11 | /** 12 | * @param isConnected can be as simple as having an internet connect or can also be customized. (e.g. if your servers are down) 13 | */ 14 | public void onNetworkChange(boolean isConnected); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/network/NetworkUtil.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.network; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * Interface which you can implement if you want to provide a custom Network callback. 7 | * Make sure you also implement {@link NetworkEventProvider} for best performance. 8 | */ 9 | public interface NetworkUtil { 10 | public boolean isConnected(Context context); 11 | } 12 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/network/NetworkUtilImpl.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.network; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.IntentFilter; 7 | import android.net.ConnectivityManager; 8 | import android.net.NetworkInfo; 9 | 10 | /** 11 | * default implementation for network Utility to observe network events 12 | */ 13 | public class NetworkUtilImpl implements NetworkUtil, NetworkEventProvider { 14 | private Listener listener; 15 | public NetworkUtilImpl(Context context) { 16 | context.getApplicationContext().registerReceiver(new BroadcastReceiver() { 17 | @Override 18 | public void onReceive(Context context, Intent intent) { 19 | if(listener == null) {//shall not be but just be safe 20 | return; 21 | } 22 | //http://developer.android.com/reference/android/net/ConnectivityManager.html#EXTRA_NETWORK_INFO 23 | //Since NetworkInfo can vary based on UID, applications should always obtain network information 24 | // through getActiveNetworkInfo() or getAllNetworkInfo(). 25 | listener.onNetworkChange(isConnected(context)); 26 | } 27 | }, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); 28 | } 29 | 30 | @Override 31 | public boolean isConnected(Context context) { 32 | ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 33 | NetworkInfo netInfo = cm.getActiveNetworkInfo(); 34 | return netInfo != null && netInfo.isConnectedOrConnecting(); 35 | } 36 | 37 | @Override 38 | public void setListener(Listener listener) { 39 | this.listener = listener; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/nonPersistentQueue/ConsistentTimedComparator.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.nonPersistentQueue; 2 | 3 | import com.spix.jobqueue.JobHolder; 4 | 5 | import java.util.Comparator; 6 | 7 | /** 8 | * A job holder comparator that checks time before checking anything else 9 | */ 10 | public class ConsistentTimedComparator implements Comparator { 11 | final Comparator baseComparator; 12 | 13 | public ConsistentTimedComparator(Comparator baseComparator) { 14 | this.baseComparator = baseComparator; 15 | } 16 | 17 | @Override 18 | public int compare(JobHolder jobHolder, JobHolder jobHolder2) { 19 | if(jobHolder.getDelayUntilNs() < jobHolder2.getDelayUntilNs()) { 20 | return -1; 21 | } else if(jobHolder.getDelayUntilNs() > jobHolder2.getDelayUntilNs()) { 22 | return 1; 23 | } 24 | return baseComparator.compare(jobHolder, jobHolder2); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/nonPersistentQueue/CountWithGroupIdsResult.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.nonPersistentQueue; 2 | 3 | import java.util.Set; 4 | 5 | public class CountWithGroupIdsResult { 6 | private int count; 7 | private Set groupIds; 8 | 9 | public CountWithGroupIdsResult(int count, Set groupIds) { 10 | this.count = count; 11 | this.groupIds = groupIds; 12 | } 13 | 14 | public int getCount() { 15 | return count; 16 | } 17 | 18 | //nullable 19 | public Set getGroupIds() { 20 | return groupIds; 21 | } 22 | 23 | public CountWithGroupIdsResult mergeWith(CountWithGroupIdsResult other) { 24 | if(groupIds == null || other.groupIds == null) { 25 | this.count += other.count; 26 | if(groupIds == null) { 27 | groupIds = other.groupIds; 28 | } 29 | return this; 30 | } 31 | //there are some groups, we need to find if any group is in both lists 32 | int sharedGroups = 0; 33 | for(String groupId : other.groupIds) { 34 | if(groupIds.add(groupId) == false) { 35 | sharedGroups ++; 36 | } 37 | } 38 | count = count + other.count - sharedGroups; 39 | return this; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/nonPersistentQueue/JobSet.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.nonPersistentQueue; 2 | 3 | import com.spix.jobqueue.JobHolder; 4 | 5 | import java.util.Collection; 6 | 7 | /** 8 | * An interface for Job Containers 9 | * It is very similar to SortedSet 10 | */ 11 | public interface JobSet { 12 | public JobHolder peek(Collection excludeGroupIds); 13 | public JobHolder poll(Collection excludeGroupIds); 14 | public JobHolder findById(long id); 15 | public boolean offer(JobHolder holder); 16 | public boolean remove(JobHolder holder); 17 | public void clear(); 18 | public int size(); 19 | public CountWithGroupIdsResult countReadyJobs(long now, Collection excludeGroups); 20 | public CountWithGroupIdsResult countReadyJobs(Collection excludeGroups); 21 | } 22 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/nonPersistentQueue/MergedQueue.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.nonPersistentQueue; 2 | 3 | import com.spix.jobqueue.JobHolder; 4 | 5 | import java.util.*; 6 | 7 | /** 8 | * A queue implementation that utilize two queues depending on one or multiple properties of the {@link JobHolder} 9 | * While retrieving items, it uses a different comparison method to handle dynamic comparisons (e.g. time) 10 | * between two queues 11 | */ 12 | abstract public class MergedQueue implements JobSet { 13 | JobSet queue0; 14 | JobSet queue1; 15 | 16 | final Comparator comparator; 17 | final Comparator retrieveComparator; 18 | 19 | /** 20 | * 21 | * @param initialCapacity passed to {@link MergedQueue#createQueue(com.spix.jobqueue.nonPersistentQueue.MergedQueue.SetId, int, java.util.Comparator)} 22 | * @param comparator passed to {@link MergedQueue#createQueue(com.spix.jobqueue.nonPersistentQueue.MergedQueue.SetId, int, java.util.Comparator)} 23 | * @param retrieveComparator upon retrieval, if both queues return items, this comparator is used to decide which 24 | * one should be returned 25 | */ 26 | public MergedQueue(int initialCapacity, Comparator comparator, Comparator retrieveComparator) { 27 | this.comparator = comparator; 28 | this.retrieveComparator = retrieveComparator; 29 | queue0 = createQueue(SetId.S0, initialCapacity, comparator); 30 | queue1 = createQueue(SetId.S1, initialCapacity, comparator); 31 | } 32 | 33 | /** 34 | * used to poll from one of the queues 35 | * @param queueId 36 | * @return 37 | */ 38 | protected JobHolder pollFromQueue(SetId queueId, Collection excludeGroupIds) { 39 | if(queueId == SetId.S0) { 40 | return queue0.poll(excludeGroupIds); 41 | } 42 | return queue1.poll(excludeGroupIds); 43 | } 44 | 45 | /** 46 | * used to peek from one of the queues 47 | * @param queueId 48 | * @return 49 | */ 50 | protected JobHolder peekFromQueue(SetId queueId, Collection excludeGroupIds) { 51 | if(queueId == SetId.S0) { 52 | return queue0.peek(excludeGroupIds); 53 | } 54 | return queue1.peek(excludeGroupIds); 55 | } 56 | 57 | /** 58 | * {@inheritDoc} 59 | */ 60 | @Override 61 | public boolean offer(JobHolder jobHolder) { 62 | SetId queueId = decideQueue(jobHolder); 63 | if(queueId == SetId.S0) { 64 | return queue0.offer(jobHolder); 65 | 66 | } 67 | return queue1.offer(jobHolder); 68 | } 69 | 70 | /** 71 | * {@inheritDoc} 72 | */ 73 | @Override 74 | public JobHolder poll(Collection excludeGroupIds) { 75 | JobHolder delayed = queue0.peek(excludeGroupIds); 76 | if(delayed == null) { 77 | return queue1.poll(excludeGroupIds); 78 | } 79 | //if queue for this job has changed, re-add it and try poll from scratch 80 | if(decideQueue(delayed) != SetId.S0) { 81 | //should be moved to the other queue 82 | queue0.remove(delayed); 83 | queue1.offer(delayed); 84 | return poll(excludeGroupIds); 85 | } 86 | JobHolder nonDelayed = queue1.peek(excludeGroupIds); 87 | if(nonDelayed == null) { 88 | queue0.remove(delayed); 89 | return delayed; 90 | } 91 | //if queue for this job has changed, re-add it and try poll from scratch 92 | if(decideQueue(nonDelayed) != SetId.S1) { 93 | queue0.offer(nonDelayed); 94 | queue1.remove(nonDelayed); 95 | return poll(excludeGroupIds); 96 | } 97 | //both are not null, need to compare and return the better 98 | int cmp = retrieveComparator.compare(delayed, nonDelayed); 99 | if(cmp == -1) { 100 | queue0.remove(delayed); 101 | return delayed; 102 | } else { 103 | queue1.remove(nonDelayed); 104 | return nonDelayed; 105 | } 106 | } 107 | 108 | /** 109 | * {@inheritDoc} 110 | */ 111 | @Override 112 | public JobHolder peek(Collection excludeGroupIds) { 113 | while (true) { 114 | JobHolder delayed = queue0.peek(excludeGroupIds); 115 | //if queue for this job has changed, re-add it and try peek from scratch 116 | if(delayed != null && decideQueue(delayed) != SetId.S0) { 117 | queue1.offer(delayed); 118 | queue0.remove(delayed); 119 | continue;//retry 120 | } 121 | JobHolder nonDelayed = queue1.peek(excludeGroupIds); 122 | //if queue for this job has changed, re-add it and try peek from scratch 123 | if(nonDelayed != null && decideQueue(nonDelayed) != SetId.S1) { 124 | queue0.offer(nonDelayed); 125 | queue1.remove(nonDelayed); 126 | continue;//retry 127 | } 128 | if(delayed == null) { 129 | return nonDelayed; 130 | } 131 | if(nonDelayed == null) { 132 | return delayed; 133 | } 134 | int cmp = retrieveComparator.compare(delayed, nonDelayed); 135 | if(cmp == -1) { 136 | return delayed; 137 | } 138 | return nonDelayed; 139 | } 140 | } 141 | 142 | 143 | 144 | /** 145 | * {@inheritDoc} 146 | */ 147 | @Override 148 | public void clear() { 149 | queue1.clear(); 150 | queue0.clear(); 151 | } 152 | 153 | /** 154 | * {@inheritDoc} 155 | */ 156 | @Override 157 | public boolean remove(JobHolder holder) { 158 | //we cannot check queue here, might be dynamic 159 | return queue1.remove(holder) || queue0.remove(holder); 160 | } 161 | 162 | /** 163 | * {@inheritDoc} 164 | */ 165 | @Override 166 | public int size() { 167 | return queue0.size() + queue1.size(); 168 | } 169 | 170 | /** 171 | * decides which queue should the job holder go 172 | * if first queue, should return 0 173 | * if second queue, should return 1 174 | * is only called when an item is inserted. methods like remove always call both queues. 175 | * @param jobHolder 176 | * @return 177 | */ 178 | abstract protected SetId decideQueue(JobHolder jobHolder); 179 | 180 | /** 181 | * called when we want to create the subsequent queues 182 | * @param initialCapacity 183 | * @param comparator 184 | * @return 185 | */ 186 | abstract protected JobSet createQueue(SetId setId, int initialCapacity, Comparator comparator); 187 | 188 | public CountWithGroupIdsResult countReadyJobs(SetId setId, long now, Collection excludeGroups) { 189 | if(setId == SetId.S0) { 190 | return queue0.countReadyJobs(now, excludeGroups); 191 | } else { 192 | return queue1.countReadyJobs(now, excludeGroups); 193 | } 194 | } 195 | 196 | public CountWithGroupIdsResult countReadyJobs(SetId setId, Collection excludeGroups) { 197 | if(setId == SetId.S0) { 198 | return queue0.countReadyJobs(excludeGroups); 199 | } else { 200 | return queue1.countReadyJobs(excludeGroups); 201 | } 202 | } 203 | 204 | 205 | 206 | /** 207 | * Returns the JobHolder that has the given id 208 | * @param id id job the job 209 | * @return 210 | */ 211 | @Override 212 | public JobHolder findById(long id) { 213 | JobHolder q0 = queue0.findById(id); 214 | return q0 == null ? queue1.findById(id) : q0; 215 | } 216 | 217 | /** 218 | * simple enum to identify queues 219 | */ 220 | protected static enum SetId { 221 | S0, 222 | S1 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/nonPersistentQueue/NetworkAwarePriorityQueue.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.nonPersistentQueue; 2 | 3 | import com.spix.jobqueue.JobHolder; 4 | 5 | import java.util.Collection; 6 | import java.util.Comparator; 7 | 8 | /** 9 | * A {@link MergedQueue} class that can separate jobs based on their network requirement 10 | */ 11 | public class NetworkAwarePriorityQueue extends MergedQueue { 12 | 13 | /** 14 | * create a network aware priority queue with given initial capacity * 2 and comparator 15 | * @param initialCapacity 16 | * @param comparator 17 | */ 18 | public NetworkAwarePriorityQueue(int initialCapacity, Comparator comparator) { 19 | super(initialCapacity, comparator, new TimeAwareComparator(comparator)); 20 | } 21 | 22 | /** 23 | * {@link java.util.Queue#peek()} implementation with network requirement filter 24 | * @param canUseNetwork if {@code true}, does not check network requirement if {@code false}, returns only from 25 | * no network queue 26 | * @return 27 | */ 28 | public JobHolder peek(boolean canUseNetwork, Collection excludeGroupIds) { 29 | if(canUseNetwork) { 30 | return super.peek(excludeGroupIds); 31 | } else { 32 | return super.peekFromQueue(SetId.S1, excludeGroupIds); 33 | } 34 | } 35 | 36 | /** 37 | * {@link java.util.Queue#poll()} implementation with network requirement filter 38 | * @param canUseNetwork if {@code true}, does not check network requirement if {@code false}, returns only from 39 | * no network queue 40 | * @return 41 | */ 42 | public JobHolder poll(boolean canUseNetwork, Collection excludeGroupIds) { 43 | if(canUseNetwork) { 44 | return super.peek(excludeGroupIds); 45 | } else { 46 | return super.peekFromQueue(SetId.S1, excludeGroupIds); 47 | } 48 | } 49 | 50 | @Override 51 | protected SetId decideQueue(JobHolder jobHolder) { 52 | return jobHolder.requiresNetwork() ? SetId.S0 : SetId.S1; 53 | } 54 | 55 | /** 56 | * create a {@link TimeAwarePriorityQueue} 57 | * @param ignoredQueueId 58 | * @param initialCapacity 59 | * @param comparator 60 | * @return 61 | */ 62 | @Override 63 | protected JobSet createQueue(SetId ignoredQueueId, int initialCapacity, Comparator comparator) { 64 | return new TimeAwarePriorityQueue(initialCapacity, comparator); 65 | } 66 | 67 | 68 | public CountWithGroupIdsResult countReadyJobs(boolean hasNetwork, Collection excludeGroups) { 69 | long now = System.nanoTime(); 70 | if(hasNetwork) { 71 | return super.countReadyJobs(SetId.S0, now, excludeGroups).mergeWith(super.countReadyJobs(SetId.S1, now, excludeGroups)); 72 | } else { 73 | return super.countReadyJobs(SetId.S1, now, excludeGroups); 74 | } 75 | } 76 | 77 | @Override 78 | public CountWithGroupIdsResult countReadyJobs(long now, Collection excludeGroups) { 79 | throw new UnsupportedOperationException("cannot call network aware priority queue count w/o providing network status"); 80 | } 81 | 82 | @Override 83 | public CountWithGroupIdsResult countReadyJobs(Collection excludeGroups) { 84 | throw new UnsupportedOperationException("cannot call network aware priority queue count w/o providing network status"); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/nonPersistentQueue/NonPersistentJobSet.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.nonPersistentQueue; 2 | 3 | import com.spix.jobqueue.JobHolder; 4 | import com.spix.jobqueue.log.JqLog; 5 | 6 | import java.util.Collection; 7 | import java.util.Comparator; 8 | import java.util.HashMap; 9 | import java.util.HashSet; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.TreeSet; 13 | 14 | /** 15 | * This is the default implementation of JobSet. 16 | * It uses TreeSet as the underlying data structure. Is currently inefficient, should be replaced w/ a more efficient 17 | * version 18 | */ 19 | public class NonPersistentJobSet implements JobSet { 20 | private final TreeSet set; 21 | //groupId -> # of jobs in that group 22 | private final Map existingGroups; 23 | private final Map idCache; 24 | 25 | public NonPersistentJobSet(Comparator comparator) { 26 | this.set = new TreeSet(comparator); 27 | this.existingGroups = new HashMap(); 28 | this.idCache = new HashMap(); 29 | } 30 | 31 | private JobHolder safeFirst() { 32 | if(set.size() < 1) { 33 | return null; 34 | } 35 | return set.first(); 36 | } 37 | 38 | @Override 39 | public JobHolder peek(Collection excludeGroupIds) { 40 | if(excludeGroupIds == null || excludeGroupIds.size() == 0) { 41 | return safeFirst(); 42 | } 43 | //there is an exclude list, we have to itereate :/ 44 | for (JobHolder holder : set) { 45 | if (holder.getGroupId() == null) { 46 | return holder; 47 | } 48 | //we have to check if it is excluded 49 | if (excludeGroupIds.contains(holder.getGroupId())) { 50 | continue; 51 | } 52 | return holder; 53 | } 54 | return null; 55 | } 56 | 57 | private JobHolder safePeek() { 58 | if(set.size() == 0) { 59 | return null; 60 | } 61 | return safeFirst(); 62 | } 63 | 64 | @Override 65 | public JobHolder poll(Collection excludeGroupIds) { 66 | JobHolder peek = peek(excludeGroupIds); 67 | if(peek != null) { 68 | remove(peek); 69 | } 70 | return peek; 71 | } 72 | 73 | @Override 74 | public JobHolder findById(long id) { 75 | return idCache.get(id); 76 | } 77 | 78 | @Override 79 | public boolean offer(JobHolder holder) { 80 | if(holder.getId() == null) { 81 | throw new RuntimeException("cannot add job holder w/o an ID"); 82 | } 83 | boolean result = set.add(holder); 84 | if(result == false) { 85 | //remove the existing element and add new one 86 | remove(holder); 87 | result = set.add(holder); 88 | } 89 | if(result) { 90 | idCache.put(holder.getId(), holder); 91 | if(holder.getGroupId() != null) { 92 | incGroupCount(holder.getGroupId()); 93 | } 94 | } 95 | 96 | return result; 97 | } 98 | 99 | private void incGroupCount(String groupId) { 100 | if(existingGroups.containsKey(groupId) == false) { 101 | existingGroups.put(groupId, 1); 102 | } else { 103 | existingGroups.put(groupId, existingGroups.get(groupId) + 1); 104 | } 105 | } 106 | 107 | private void decGroupCount(String groupId) { 108 | Integer val = existingGroups.get(groupId); 109 | if(val == null || val == 0) { 110 | //TODO should we crash? 111 | JqLog.e("detected inconsistency in NonPersistentJobSet's group id hash"); 112 | return; 113 | } 114 | val -= 1; 115 | if(val == 0) { 116 | existingGroups.remove(groupId); 117 | } 118 | } 119 | 120 | @Override 121 | public boolean remove(JobHolder holder) { 122 | boolean removed = set.remove(holder); 123 | if(removed) { 124 | idCache.remove(holder.getId()); 125 | if(holder.getGroupId() != null) { 126 | decGroupCount(holder.getGroupId()); 127 | } 128 | } 129 | return removed; 130 | } 131 | 132 | 133 | 134 | @Override 135 | public void clear() { 136 | set.clear(); 137 | existingGroups.clear(); 138 | idCache.clear(); 139 | } 140 | 141 | @Override 142 | public int size() { 143 | return set.size(); 144 | } 145 | 146 | @Override 147 | public CountWithGroupIdsResult countReadyJobs(long now, Collection excludeGroups) { 148 | //TODO we can cache most of this 149 | int total = 0; 150 | int groupCnt = existingGroups.keySet().size(); 151 | Set groupIdSet = null; 152 | if(groupCnt > 0) { 153 | groupIdSet = new HashSet();//we have to track :/ 154 | } 155 | for(JobHolder holder : set) { 156 | if(holder.getDelayUntilNs() < now) { 157 | //we should not need to check groupCnt but what if sth is wrong in hashmap, be defensive till 158 | //we write unit tests around NonPersistentJobSet 159 | if(holder.getGroupId() != null) { 160 | if(excludeGroups != null && excludeGroups.contains(holder.getGroupId())) { 161 | continue; 162 | } 163 | //we should not need to check groupCnt but what if sth is wrong in hashmap, be defensive till 164 | //we write unit tests around NonPersistentJobSet 165 | if(groupCnt > 0) { 166 | if(groupIdSet.add(holder.getGroupId())) { 167 | total++; 168 | } 169 | } 170 | //else skip, we already counted this group 171 | } else { 172 | total ++; 173 | } 174 | } 175 | } 176 | return new CountWithGroupIdsResult(total, groupIdSet); 177 | } 178 | 179 | @Override 180 | public CountWithGroupIdsResult countReadyJobs(Collection excludeGroups) { 181 | if(existingGroups.size() == 0) { 182 | return new CountWithGroupIdsResult(set.size(), null); 183 | } else { 184 | //todo we can actually count from existingGroups set if we start counting numbers there as well 185 | int total = 0; 186 | Set existingGroupIds = null; 187 | for(JobHolder holder : set) { 188 | if(holder.getGroupId() != null) { 189 | if(excludeGroups != null && excludeGroups.contains(holder.getGroupId())) { 190 | continue; 191 | } else if(existingGroupIds == null) { 192 | existingGroupIds = new HashSet(); 193 | existingGroupIds.add(holder.getGroupId()); 194 | } else if(existingGroupIds.add(holder.getGroupId()) == false) { 195 | continue; 196 | } 197 | 198 | } 199 | total ++; 200 | } 201 | return new CountWithGroupIdsResult(total, existingGroupIds); 202 | } 203 | 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/nonPersistentQueue/NonPersistentPriorityQueue.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.nonPersistentQueue; 2 | 3 | import com.spix.jobqueue.JobHolder; 4 | import com.spix.jobqueue.JobManager; 5 | import com.spix.jobqueue.JobQueue; 6 | 7 | import java.util.*; 8 | 9 | public class NonPersistentPriorityQueue implements JobQueue { 10 | private long nonPersistentJobIdGenerator = Integer.MIN_VALUE; 11 | //TODO implement a more efficient priority queue where we can mark jobs as removed but don't remove for real 12 | private NetworkAwarePriorityQueue jobs; 13 | private final String id; 14 | private final long sessionId; 15 | 16 | public NonPersistentPriorityQueue(long sessionId, String id) { 17 | this.id = id; 18 | this.sessionId = sessionId; 19 | jobs = new NetworkAwarePriorityQueue(5, jobComparator); 20 | } 21 | 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | @Override 26 | public synchronized long insert(JobHolder jobHolder) { 27 | nonPersistentJobIdGenerator++; 28 | jobHolder.setId(nonPersistentJobIdGenerator); 29 | jobs.offer(jobHolder); 30 | return jobHolder.getId(); 31 | } 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | @Override 37 | public long insertOrReplace(JobHolder jobHolder) { 38 | remove(jobHolder); 39 | jobHolder.setRunningSessionId(JobManager.NOT_RUNNING_SESSION_ID); 40 | jobs.offer(jobHolder); 41 | return jobHolder.getId(); 42 | } 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | @Override 48 | public void remove(JobHolder jobHolder) { 49 | jobs.remove(jobHolder); 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | @Override 56 | public int count() { 57 | return jobs.size(); 58 | } 59 | 60 | @Override 61 | public int countReadyJobs(boolean hasNetwork, Collection excludeGroups) { 62 | return jobs.countReadyJobs(hasNetwork, excludeGroups).getCount(); 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | @Override 69 | public JobHolder nextJobAndIncRunCount(boolean hasNetwork, Collection excludeGroups) { 70 | JobHolder jobHolder = jobs.peek(hasNetwork, excludeGroups); 71 | 72 | if (jobHolder != null) { 73 | //check if job can run 74 | if(jobHolder.getDelayUntilNs() > System.nanoTime()) { 75 | jobHolder = null; 76 | } else { 77 | jobHolder.setRunningSessionId(sessionId); 78 | jobHolder.setRunCount(jobHolder.getRunCount() + 1); 79 | jobs.remove(jobHolder); 80 | } 81 | } 82 | return jobHolder; 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | */ 88 | @Override 89 | public Long getNextJobDelayUntilNs(boolean hasNetwork) { 90 | JobHolder next = jobs.peek(hasNetwork, null); 91 | return next == null ? null : next.getDelayUntilNs(); 92 | } 93 | 94 | /** 95 | * {@inheritDoc} 96 | */ 97 | @Override 98 | public void clear() { 99 | jobs.clear(); 100 | } 101 | 102 | /** 103 | * {@inheritDoc} 104 | */ 105 | @Override 106 | public JobHolder findJobById(long id) { 107 | return jobs.findById(id); 108 | } 109 | 110 | public final Comparator jobComparator = new Comparator() { 111 | @Override 112 | public int compare(JobHolder holder1, JobHolder holder2) { 113 | //we should not check delay here. TimeAwarePriorityQueue does it for us. 114 | //high priority first 115 | int cmp = compareInt(holder1.getPriority(), holder2.getPriority()); 116 | if(cmp != 0) { 117 | return cmp; 118 | } 119 | 120 | //if run counts are also equal, older job first 121 | cmp = -compareLong(holder1.getCreatedNs(), holder2.getCreatedNs()); 122 | if(cmp != 0) { 123 | return cmp; 124 | } 125 | 126 | //if jobs were created at the same time, smaller id first 127 | return -compareLong(holder1.getId(), holder2.getId()); 128 | } 129 | }; 130 | 131 | private static int compareInt(int i1, int i2) { 132 | if (i1 > i2) { 133 | return -1; 134 | } 135 | if (i2 > i1) { 136 | return 1; 137 | } 138 | return 0; 139 | } 140 | 141 | private static int compareLong(long l1, long l2) { 142 | if (l1 > l2) { 143 | return -1; 144 | } 145 | if (l2 > l1) { 146 | return 1; 147 | } 148 | return 0; 149 | } 150 | 151 | 152 | } 153 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/nonPersistentQueue/TimeAwareComparator.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.nonPersistentQueue; 2 | 3 | import com.spix.jobqueue.JobHolder; 4 | 5 | import java.util.Comparator; 6 | 7 | /** 8 | * A real-time comparator class that checks current time to decide of both jobs are valid or not. 9 | * Return values from this comparator are inconsistent as time may change. 10 | */ 11 | public class TimeAwareComparator implements Comparator { 12 | final Comparator baseComparator; 13 | 14 | public TimeAwareComparator(Comparator baseComparator) { 15 | this.baseComparator = baseComparator; 16 | } 17 | 18 | @Override 19 | public int compare(JobHolder jobHolder, JobHolder jobHolder2) { 20 | long now = System.nanoTime(); 21 | boolean job1Valid = jobHolder.getDelayUntilNs() <= now; 22 | boolean job2Valid = jobHolder2.getDelayUntilNs() <= now; 23 | if(job1Valid) { 24 | return job2Valid ? baseComparator.compare(jobHolder, jobHolder2) : -1; 25 | } 26 | if(job2Valid) { 27 | return job1Valid ? baseComparator.compare(jobHolder, jobHolder2) : 1; 28 | } 29 | //if both jobs are invalid, return the job that can run earlier. if the want to run at the same time, use base 30 | //comparison 31 | if(jobHolder.getDelayUntilNs() < jobHolder2.getDelayUntilNs()) { 32 | return -1; 33 | } else if(jobHolder.getDelayUntilNs() > jobHolder2.getDelayUntilNs()) { 34 | return 1; 35 | } 36 | return baseComparator.compare(jobHolder, jobHolder2); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/nonPersistentQueue/TimeAwarePriorityQueue.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.nonPersistentQueue; 2 | 3 | import com.spix.jobqueue.JobHolder; 4 | 5 | import java.util.*; 6 | 7 | /** 8 | * This is a {@link MergedQueue} class that can handle queue updates based on time. 9 | * It uses two queues, one for jobs that can run now and the other for jobs that should wait. 10 | * Upon retrieval, if it detects a job in delayed queue that can run now, it removes it from there, adds it to S0 11 | * and re-runs the operation. This is not very efficient but provides proper ordering for delayed jobs. 12 | */ 13 | public class TimeAwarePriorityQueue extends MergedQueue { 14 | 15 | /** 16 | * When retrieving jobs, considers current system nanotime to check if jobs are valid. if both jobs are valid 17 | * or both jobs are invalid, returns based on regular comparison 18 | * @param initialCapacity 19 | * @param comparator 20 | */ 21 | public TimeAwarePriorityQueue(int initialCapacity, Comparator comparator) { 22 | super(initialCapacity, comparator, new TimeAwareComparator(comparator)); 23 | } 24 | 25 | @Override 26 | protected SetId decideQueue(JobHolder jobHolder) { 27 | return jobHolder.getDelayUntilNs() <= System.nanoTime() ? SetId.S0 : SetId.S1; 28 | } 29 | 30 | /** 31 | * create a {@link PriorityQueue} with given comparator 32 | * @param setId 33 | * @param initialCapacity 34 | * @param comparator 35 | * @return 36 | */ 37 | @Override 38 | protected JobSet createQueue(SetId setId, int initialCapacity, Comparator comparator) { 39 | if(setId == SetId.S0) { 40 | return new NonPersistentJobSet(comparator); 41 | } else { 42 | return new NonPersistentJobSet(new ConsistentTimedComparator(comparator)); 43 | } 44 | } 45 | 46 | @Override 47 | public CountWithGroupIdsResult countReadyJobs(long now, Collection excludeGroups) { 48 | return super.countReadyJobs(SetId.S0, excludeGroups).mergeWith(super.countReadyJobs(SetId.S1, now, excludeGroups)); 49 | } 50 | 51 | @Override 52 | public CountWithGroupIdsResult countReadyJobs(Collection excludeGroups) { 53 | throw new UnsupportedOperationException("cannot call time aware priority queue's count ready jobs w/o providing a time"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/sqlite/DbOpenHelper.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.sqlite; 2 | 3 | import android.content.Context; 4 | import android.database.sqlite.SQLiteDatabase; 5 | import android.database.sqlite.SQLiteOpenHelper; 6 | 7 | /** 8 | * Helper class for {@link SqliteJobQueue} to handle database connection 9 | */ 10 | public class DbOpenHelper extends SQLiteOpenHelper { 11 | private static final int DB_VERSION = 3; 12 | /*package*/ static final String JOB_HOLDER_TABLE_NAME = "job_holder"; 13 | /*package*/ static final SqlHelper.Property ID_COLUMN = new SqlHelper.Property("_id", "integer", 0); 14 | /*package*/ static final SqlHelper.Property PRIORITY_COLUMN = new SqlHelper.Property("priority", "integer", 1); 15 | /*package*/ static final SqlHelper.Property GROUP_ID_COLUMN = new SqlHelper.Property("group_id", "text", 2); 16 | /*package*/ static final SqlHelper.Property RUN_COUNT_COLUMN = new SqlHelper.Property("run_count", "integer", 3); 17 | /*package*/ static final SqlHelper.Property BASE_JOB_COLUMN = new SqlHelper.Property("base_job", "byte", 4); 18 | /*package*/ static final SqlHelper.Property CREATED_NS_COLUMN = new SqlHelper.Property("created_ns", "long", 5); 19 | /*package*/ static final SqlHelper.Property DELAY_UNTIL_NS_COLUMN = new SqlHelper.Property("delay_until_ns", "long", 6); 20 | /*package*/ static final SqlHelper.Property RUNNING_SESSION_ID_COLUMN = new SqlHelper.Property("running_session_id", "long", 7); 21 | /*package*/ static final SqlHelper.Property REQUIRES_NETWORK_COLUMN = new SqlHelper.Property("requires_network", "integer", 8); 22 | 23 | /*package*/ static final int COLUMN_COUNT = 9; 24 | 25 | public DbOpenHelper(Context context, String name) { 26 | super(context, name, null, DB_VERSION); 27 | } 28 | 29 | @Override 30 | public void onCreate(SQLiteDatabase sqLiteDatabase) { 31 | String createQuery = SqlHelper.create(JOB_HOLDER_TABLE_NAME, 32 | ID_COLUMN, 33 | PRIORITY_COLUMN, 34 | GROUP_ID_COLUMN, 35 | RUN_COUNT_COLUMN, 36 | BASE_JOB_COLUMN, 37 | CREATED_NS_COLUMN, 38 | DELAY_UNTIL_NS_COLUMN, 39 | RUNNING_SESSION_ID_COLUMN, 40 | REQUIRES_NETWORK_COLUMN 41 | ); 42 | sqLiteDatabase.execSQL(createQuery); 43 | } 44 | 45 | @Override 46 | public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { 47 | sqLiteDatabase.execSQL(SqlHelper.drop(JOB_HOLDER_TABLE_NAME)); 48 | onCreate(sqLiteDatabase); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/sqlite/QueryCache.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.sqlite; 2 | 3 | import java.util.Collection; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | /** 8 | * a class to cache ready jobs queries so that we can avoid unnecessary memory allocations for them 9 | */ 10 | public class QueryCache { 11 | private static final String KEY_EMPTY_WITH_NETWORK = "w_n"; 12 | private static final String KEY_EMPTY_WITHOUT_NETWORK = "wo_n"; 13 | //never reach this outside sync block 14 | private final StringBuilder reusedBuilder; 15 | private final Map cache; 16 | 17 | public QueryCache() { 18 | reusedBuilder = new StringBuilder(); 19 | cache = new HashMap(); 20 | } 21 | 22 | public synchronized String get(boolean hasNetwork, Collection excludeGroups) { 23 | String key = cacheKey(hasNetwork, excludeGroups); 24 | return cache.get(key); 25 | } 26 | 27 | public synchronized void set(String query, boolean hasNetwork, Collection excludeGroups) { 28 | String key = cacheKey(hasNetwork, excludeGroups); 29 | cache.put(key, query); 30 | return; 31 | } 32 | 33 | /** 34 | * create a cache key for an exclude group set. exclude groups are guaranteed to be ordered so we rely on that 35 | * @param hasNetwork 36 | * @param excludeGroups 37 | * @return 38 | */ 39 | private String cacheKey(boolean hasNetwork, Collection excludeGroups) { 40 | if(excludeGroups == null || excludeGroups.size() == 0) { 41 | return hasNetwork ? KEY_EMPTY_WITH_NETWORK : KEY_EMPTY_WITHOUT_NETWORK; 42 | } 43 | reusedBuilder.setLength(0); 44 | reusedBuilder.append(hasNetwork ? "X" : "Y"); 45 | for(String group : excludeGroups) { 46 | reusedBuilder.append("-").append(group); 47 | } 48 | return reusedBuilder.toString(); 49 | } 50 | 51 | public synchronized void clear() { 52 | cache.clear(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/sqlite/SqlHelper.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.sqlite; 2 | 3 | import android.database.sqlite.SQLiteDatabase; 4 | import android.database.sqlite.SQLiteStatement; 5 | import com.spix.jobqueue.log.JqLog; 6 | 7 | /** 8 | * Helper class for {@link SqliteJobQueue} to generate sql queries and statements. 9 | */ 10 | public class SqlHelper { 11 | 12 | /**package**/ String FIND_BY_ID_QUERY; 13 | 14 | private SQLiteStatement insertStatement; 15 | private SQLiteStatement insertOrReplaceStatement; 16 | private SQLiteStatement deleteStatement; 17 | private SQLiteStatement onJobFetchedForRunningStatement; 18 | private SQLiteStatement countStatement; 19 | private SQLiteStatement nextJobDelayedUntilWithNetworkStatement; 20 | private SQLiteStatement nextJobDelayedUntilWithoutNetworkStatement; 21 | 22 | 23 | final SQLiteDatabase db; 24 | final String tableName; 25 | final String primaryKeyColumnName; 26 | final int columnCount; 27 | final long sessionId; 28 | 29 | public SqlHelper(SQLiteDatabase db, String tableName, String primaryKeyColumnName, int columnCount, long sessionId) { 30 | this.db = db; 31 | this.tableName = tableName; 32 | this.columnCount = columnCount; 33 | this.primaryKeyColumnName = primaryKeyColumnName; 34 | this.sessionId = sessionId; 35 | FIND_BY_ID_QUERY = "SELECT * FROM " + tableName + " WHERE " + DbOpenHelper.ID_COLUMN.columnName + " = ?"; 36 | } 37 | 38 | public static String create(String tableName, Property primaryKey, Property... properties) { 39 | StringBuilder builder = new StringBuilder("CREATE TABLE "); 40 | builder.append(tableName).append(" ("); 41 | builder.append(primaryKey.columnName).append(" "); 42 | builder.append(primaryKey.type); 43 | builder.append(" primary key autoincrement "); 44 | for (Property property : properties) { 45 | builder.append(", `").append(property.columnName).append("` ").append(property.type); 46 | } 47 | builder.append(" );"); 48 | JqLog.d(builder.toString()); 49 | return builder.toString(); 50 | } 51 | 52 | public static String drop(String tableName) { 53 | return "DROP TABLE IF EXISTS " + tableName; 54 | } 55 | 56 | public SQLiteStatement getInsertStatement() { 57 | if (insertStatement == null) { 58 | StringBuilder builder = new StringBuilder("INSERT INTO ").append(tableName); 59 | builder.append(" VALUES ("); 60 | for (int i = 0; i < columnCount; i++) { 61 | if (i != 0) { 62 | builder.append(","); 63 | } 64 | builder.append("?"); 65 | } 66 | builder.append(")"); 67 | insertStatement = db.compileStatement(builder.toString()); 68 | } 69 | return insertStatement; 70 | } 71 | 72 | public SQLiteStatement getCountStatement() { 73 | if (countStatement == null) { 74 | countStatement = db.compileStatement("SELECT COUNT(*) FROM " + tableName + " WHERE " + 75 | DbOpenHelper.RUNNING_SESSION_ID_COLUMN.columnName + " != ?"); 76 | } 77 | return countStatement; 78 | } 79 | 80 | public SQLiteStatement getInsertOrReplaceStatement() { 81 | if (insertOrReplaceStatement == null) { 82 | StringBuilder builder = new StringBuilder("INSERT OR REPLACE INTO ").append(tableName); 83 | builder.append(" VALUES ("); 84 | for (int i = 0; i < columnCount; i++) { 85 | if (i != 0) { 86 | builder.append(","); 87 | } 88 | builder.append("?"); 89 | } 90 | builder.append(")"); 91 | insertOrReplaceStatement = db.compileStatement(builder.toString()); 92 | } 93 | return insertOrReplaceStatement; 94 | } 95 | 96 | public SQLiteStatement getDeleteStatement() { 97 | if (deleteStatement == null) { 98 | deleteStatement = db.compileStatement("DELETE FROM " + tableName + " WHERE " + primaryKeyColumnName + " = ?"); 99 | } 100 | return deleteStatement; 101 | } 102 | 103 | public SQLiteStatement getOnJobFetchedForRunningStatement() { 104 | if (onJobFetchedForRunningStatement == null) { 105 | String sql = "UPDATE " + tableName + " SET " 106 | + DbOpenHelper.RUN_COUNT_COLUMN.columnName + " = ? , " 107 | + DbOpenHelper.RUNNING_SESSION_ID_COLUMN.columnName + " = ? " 108 | + " WHERE " + primaryKeyColumnName + " = ? "; 109 | onJobFetchedForRunningStatement = db.compileStatement(sql); 110 | } 111 | return onJobFetchedForRunningStatement; 112 | } 113 | 114 | public SQLiteStatement getNextJobDelayedUntilWithNetworkStatement() { 115 | if(nextJobDelayedUntilWithNetworkStatement == null) { 116 | String sql = "SELECT " + DbOpenHelper.DELAY_UNTIL_NS_COLUMN.columnName 117 | + " FROM " + tableName + " WHERE " 118 | + DbOpenHelper.RUNNING_SESSION_ID_COLUMN.columnName + " != " + sessionId 119 | + " ORDER BY " + DbOpenHelper.DELAY_UNTIL_NS_COLUMN.columnName + " ASC" 120 | + " LIMIT 1"; 121 | nextJobDelayedUntilWithNetworkStatement = db.compileStatement(sql); 122 | } 123 | return nextJobDelayedUntilWithNetworkStatement; 124 | } 125 | 126 | public SQLiteStatement getNextJobDelayedUntilWithoutNetworkStatement() { 127 | if(nextJobDelayedUntilWithoutNetworkStatement == null) { 128 | String sql = "SELECT " + DbOpenHelper.DELAY_UNTIL_NS_COLUMN.columnName 129 | + " FROM " + tableName + " WHERE " 130 | + DbOpenHelper.RUNNING_SESSION_ID_COLUMN.columnName + " != " + sessionId 131 | + " AND " + DbOpenHelper.REQUIRES_NETWORK_COLUMN.columnName + " != 1" 132 | + " ORDER BY " + DbOpenHelper.DELAY_UNTIL_NS_COLUMN.columnName + " ASC" 133 | + " LIMIT 1"; 134 | nextJobDelayedUntilWithoutNetworkStatement = db.compileStatement(sql); 135 | } 136 | return nextJobDelayedUntilWithoutNetworkStatement; 137 | } 138 | 139 | public String createSelect(String where, Integer limit, Order... orders) { 140 | StringBuilder builder = new StringBuilder("SELECT * FROM "); 141 | builder.append(tableName); 142 | if (where != null) { 143 | builder.append(" WHERE ").append(where); 144 | } 145 | boolean first = true; 146 | for (Order order : orders) { 147 | if (first) { 148 | builder.append(" ORDER BY "); 149 | } else { 150 | builder.append(","); 151 | } 152 | first = false; 153 | builder.append(order.property.columnName).append(" ").append(order.type); 154 | } 155 | if (limit != null) { 156 | builder.append(" LIMIT ").append(limit); 157 | } 158 | return builder.toString(); 159 | } 160 | 161 | public void truncate() { 162 | db.execSQL("DELETE FROM " + DbOpenHelper.JOB_HOLDER_TABLE_NAME); 163 | vacuum(); 164 | } 165 | 166 | public void vacuum() { 167 | db.execSQL("VACUUM"); 168 | } 169 | 170 | public void resetDelayTimesTo(long newDelayTime) { 171 | db.execSQL("UPDATE " + DbOpenHelper.JOB_HOLDER_TABLE_NAME + " SET " + DbOpenHelper.DELAY_UNTIL_NS_COLUMN.columnName + "=?" 172 | , new Object[]{newDelayTime}); 173 | } 174 | 175 | public static class Property { 176 | /*package*/ final String columnName; 177 | /*package*/ final String type; 178 | public final int columnIndex; 179 | 180 | public Property(String columnName, String type, int columnIndex) { 181 | this.columnName = columnName; 182 | this.type = type; 183 | this.columnIndex = columnIndex; 184 | } 185 | } 186 | 187 | public static class Order { 188 | final Property property; 189 | final Type type; 190 | 191 | public Order(Property property, Type type) { 192 | this.property = property; 193 | this.type = type; 194 | } 195 | 196 | public static enum Type { 197 | ASC, 198 | DESC 199 | } 200 | 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /jobmanagerlib/src/main/java/com/spix/jobqueue/sqlite/SqliteJobQueue.java: -------------------------------------------------------------------------------- 1 | package com.spix.jobqueue.sqlite; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.database.sqlite.SQLiteDatabase; 6 | import android.database.sqlite.SQLiteDoneException; 7 | import android.database.sqlite.SQLiteStatement; 8 | import com.spix.jobqueue.BaseJob; 9 | import com.spix.jobqueue.JobHolder; 10 | import com.spix.jobqueue.JobManager; 11 | import com.spix.jobqueue.JobQueue; 12 | import com.spix.jobqueue.log.JqLog; 13 | 14 | import java.io.ByteArrayInputStream; 15 | import java.io.ByteArrayOutputStream; 16 | import java.io.IOException; 17 | import java.io.ObjectInputStream; 18 | import java.io.ObjectOutput; 19 | import java.io.ObjectOutputStream; 20 | import java.util.Collection; 21 | 22 | /** 23 | * Persistent Job Queue that keeps its data in an sqlite database. 24 | */ 25 | public class SqliteJobQueue implements JobQueue { 26 | DbOpenHelper dbOpenHelper; 27 | private final long sessionId; 28 | SQLiteDatabase db; 29 | SqlHelper sqlHelper; 30 | JobSerializer jobSerializer; 31 | QueryCache readyJobsQueryCache; 32 | QueryCache nextJobsQueryCache; 33 | 34 | /** 35 | * @param context application context 36 | * @param sessionId session id should match {@link JobManager} 37 | * @param id uses this value to construct database name {@code "db_" + id} 38 | */ 39 | public SqliteJobQueue(Context context, long sessionId, String id, JobSerializer jobSerializer) { 40 | this.sessionId = sessionId; 41 | dbOpenHelper = new DbOpenHelper(context, "db_" + id); 42 | db = dbOpenHelper.getWritableDatabase(); 43 | sqlHelper = new SqlHelper(db, DbOpenHelper.JOB_HOLDER_TABLE_NAME, DbOpenHelper.ID_COLUMN.columnName, DbOpenHelper.COLUMN_COUNT, sessionId); 44 | this.jobSerializer = jobSerializer; 45 | readyJobsQueryCache = new QueryCache(); 46 | nextJobsQueryCache = new QueryCache(); 47 | sqlHelper.resetDelayTimesTo(JobManager.NOT_DELAYED_JOB_DELAY); 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | @Override 54 | public long insert(JobHolder jobHolder) { 55 | SQLiteStatement stmt = sqlHelper.getInsertStatement(); 56 | long id; 57 | synchronized (stmt) { 58 | stmt.clearBindings(); 59 | bindValues(stmt, jobHolder); 60 | id = stmt.executeInsert(); 61 | } 62 | jobHolder.setId(id); 63 | return id; 64 | } 65 | 66 | private void bindValues(SQLiteStatement stmt, JobHolder jobHolder) { 67 | if (jobHolder.getId() != null) { 68 | stmt.bindLong(DbOpenHelper.ID_COLUMN.columnIndex + 1, jobHolder.getId()); 69 | } 70 | stmt.bindLong(DbOpenHelper.PRIORITY_COLUMN.columnIndex + 1, jobHolder.getPriority()); 71 | if(jobHolder.getGroupId() != null) { 72 | stmt.bindString(DbOpenHelper.GROUP_ID_COLUMN.columnIndex + 1, jobHolder.getGroupId()); 73 | } 74 | stmt.bindLong(DbOpenHelper.RUN_COUNT_COLUMN.columnIndex + 1, jobHolder.getRunCount()); 75 | byte[] baseJob = getSerializeBaseJob(jobHolder); 76 | if (baseJob != null) { 77 | stmt.bindBlob(DbOpenHelper.BASE_JOB_COLUMN.columnIndex + 1, baseJob); 78 | } 79 | stmt.bindLong(DbOpenHelper.CREATED_NS_COLUMN.columnIndex + 1, jobHolder.getCreatedNs()); 80 | stmt.bindLong(DbOpenHelper.DELAY_UNTIL_NS_COLUMN.columnIndex + 1, jobHolder.getDelayUntilNs()); 81 | stmt.bindLong(DbOpenHelper.RUNNING_SESSION_ID_COLUMN.columnIndex + 1, jobHolder.getRunningSessionId()); 82 | stmt.bindLong(DbOpenHelper.REQUIRES_NETWORK_COLUMN.columnIndex + 1, jobHolder.requiresNetwork() ? 1L : 0L); 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | */ 88 | @Override 89 | public long insertOrReplace(JobHolder jobHolder) { 90 | if (jobHolder.getId() == null) { 91 | return insert(jobHolder); 92 | } 93 | jobHolder.setRunningSessionId(JobManager.NOT_RUNNING_SESSION_ID); 94 | SQLiteStatement stmt = sqlHelper.getInsertOrReplaceStatement(); 95 | long id; 96 | synchronized (stmt) { 97 | stmt.clearBindings(); 98 | bindValues(stmt, jobHolder); 99 | id = stmt.executeInsert(); 100 | } 101 | jobHolder.setId(id); 102 | return id; 103 | } 104 | 105 | /** 106 | * {@inheritDoc} 107 | */ 108 | @Override 109 | public void remove(JobHolder jobHolder) { 110 | if (jobHolder.getId() == null) { 111 | JqLog.e("called remove with null job id."); 112 | return; 113 | } 114 | delete(jobHolder.getId()); 115 | } 116 | 117 | private void delete(Long id) { 118 | SQLiteStatement stmt = sqlHelper.getDeleteStatement(); 119 | synchronized (stmt) { 120 | stmt.clearBindings(); 121 | stmt.bindLong(1, id); 122 | stmt.execute(); 123 | } 124 | } 125 | 126 | /** 127 | * {@inheritDoc} 128 | */ 129 | @Override 130 | public int count() { 131 | SQLiteStatement stmt = sqlHelper.getCountStatement(); 132 | synchronized (stmt) { 133 | stmt.clearBindings(); 134 | stmt.bindLong(1, sessionId); 135 | return (int) stmt.simpleQueryForLong(); 136 | } 137 | } 138 | 139 | @Override 140 | public int countReadyJobs(boolean hasNetwork, Collection excludeGroups) { 141 | String sql = readyJobsQueryCache.get(hasNetwork, excludeGroups); 142 | if(sql == null) { 143 | String where = createReadyJobWhereSql(hasNetwork, excludeGroups, true); 144 | String subSelect = "SELECT count(*) group_cnt, " + DbOpenHelper.GROUP_ID_COLUMN.columnName 145 | + " FROM " + DbOpenHelper.JOB_HOLDER_TABLE_NAME 146 | + " WHERE " + where; 147 | sql = "SELECT SUM(case WHEN " + DbOpenHelper.GROUP_ID_COLUMN.columnName 148 | + " is null then group_cnt else 1 end) from (" + subSelect + ")"; 149 | readyJobsQueryCache.set(sql, hasNetwork, excludeGroups); 150 | } 151 | Cursor cursor = db.rawQuery(sql, new String[]{Long.toString(sessionId), Long.toString(System.nanoTime())}); 152 | try { 153 | if(!cursor.moveToNext()) { 154 | return 0; 155 | } 156 | return cursor.getInt(0); 157 | } finally { 158 | cursor.close(); 159 | } 160 | } 161 | 162 | /** 163 | * {@inheritDoc} 164 | */ 165 | @Override 166 | public JobHolder findJobById(long id) { 167 | Cursor cursor = db.rawQuery(sqlHelper.FIND_BY_ID_QUERY, new String[]{Long.toString(id)}); 168 | try { 169 | if(!cursor.moveToFirst()) { 170 | return null; 171 | } 172 | return createJobHolderFromCursor(cursor); 173 | } catch (InvalidBaseJobException e) { 174 | JqLog.e(e, "invalid job on findJobById"); 175 | return null; 176 | } finally { 177 | cursor.close(); 178 | } 179 | } 180 | 181 | /** 182 | * {@inheritDoc} 183 | */ 184 | @Override 185 | public JobHolder nextJobAndIncRunCount(boolean hasNetwork, Collection excludeGroups) { 186 | //we can even keep these prepared but not sure the cost of them in db layer 187 | String selectQuery = nextJobsQueryCache.get(hasNetwork, excludeGroups); 188 | if(selectQuery == null) { 189 | String where = createReadyJobWhereSql(hasNetwork, excludeGroups, false); 190 | selectQuery = sqlHelper.createSelect( 191 | where, 192 | 1, 193 | new SqlHelper.Order(DbOpenHelper.PRIORITY_COLUMN, SqlHelper.Order.Type.DESC), 194 | new SqlHelper.Order(DbOpenHelper.CREATED_NS_COLUMN, SqlHelper.Order.Type.ASC), 195 | new SqlHelper.Order(DbOpenHelper.ID_COLUMN, SqlHelper.Order.Type.ASC) 196 | ); 197 | nextJobsQueryCache.set(selectQuery, hasNetwork, excludeGroups); 198 | } 199 | Cursor cursor = db.rawQuery(selectQuery, new String[]{Long.toString(sessionId),Long.toString(System.nanoTime())}); 200 | try { 201 | if (!cursor.moveToNext()) { 202 | return null; 203 | } 204 | JobHolder holder = createJobHolderFromCursor(cursor); 205 | onJobFetchedForRunning(holder); 206 | return holder; 207 | } catch (InvalidBaseJobException e) { 208 | //delete 209 | Long jobId = cursor.getLong(0); 210 | delete(jobId); 211 | return nextJobAndIncRunCount(true, null); 212 | } finally { 213 | cursor.close(); 214 | } 215 | } 216 | 217 | private String createReadyJobWhereSql(boolean hasNetwork, Collection excludeGroups, boolean groupByRunningGroup) { 218 | String where = DbOpenHelper.RUNNING_SESSION_ID_COLUMN.columnName + " != ? " 219 | + " AND " + DbOpenHelper.DELAY_UNTIL_NS_COLUMN.columnName + " <= ? "; 220 | if(hasNetwork == false) { 221 | where += " AND " + DbOpenHelper.REQUIRES_NETWORK_COLUMN.columnName + " != 1 "; 222 | } 223 | String groupConstraint = null; 224 | if(excludeGroups != null && excludeGroups.size() > 0) { 225 | groupConstraint = DbOpenHelper.GROUP_ID_COLUMN.columnName + " IS NULL OR " + 226 | DbOpenHelper.GROUP_ID_COLUMN.columnName + " NOT IN('" + joinStrings("','", excludeGroups) + "')"; 227 | } 228 | if(groupByRunningGroup) { 229 | where += " GROUP BY " + DbOpenHelper.GROUP_ID_COLUMN.columnName; 230 | if(groupConstraint != null) { 231 | where += " HAVING " + groupConstraint; 232 | } 233 | } else if(groupConstraint != null) { 234 | where += " AND ( " + groupConstraint + " )"; 235 | } 236 | return where; 237 | } 238 | 239 | private static String joinStrings(String glue, Collection strings) { 240 | StringBuilder builder = new StringBuilder(); 241 | for(String str : strings) { 242 | if(builder.length() != 0) { 243 | builder.append(glue); 244 | } 245 | builder.append(str); 246 | } 247 | return builder.toString(); 248 | } 249 | 250 | /** 251 | * {@inheritDoc} 252 | */ 253 | @Override 254 | public Long getNextJobDelayUntilNs(boolean hasNetwork) { 255 | SQLiteStatement stmt = 256 | hasNetwork ? sqlHelper.getNextJobDelayedUntilWithNetworkStatement() 257 | : sqlHelper.getNextJobDelayedUntilWithoutNetworkStatement(); 258 | synchronized (stmt) { 259 | try { 260 | stmt.clearBindings(); 261 | return stmt.simpleQueryForLong(); 262 | } catch (SQLiteDoneException e){ 263 | return null; 264 | } 265 | } 266 | } 267 | 268 | /** 269 | * {@inheritDoc} 270 | */ 271 | @Override 272 | public void clear() { 273 | sqlHelper.truncate(); 274 | readyJobsQueryCache.clear(); 275 | nextJobsQueryCache.clear(); 276 | } 277 | 278 | private void onJobFetchedForRunning(JobHolder jobHolder) { 279 | SQLiteStatement stmt = sqlHelper.getOnJobFetchedForRunningStatement(); 280 | jobHolder.setRunCount(jobHolder.getRunCount() + 1); 281 | jobHolder.setRunningSessionId(sessionId); 282 | synchronized (stmt) { 283 | stmt.clearBindings(); 284 | stmt.bindLong(1, jobHolder.getRunCount()); 285 | stmt.bindLong(2, sessionId); 286 | stmt.bindLong(3, jobHolder.getId()); 287 | stmt.execute(); 288 | } 289 | } 290 | 291 | private JobHolder createJobHolderFromCursor(Cursor cursor) throws InvalidBaseJobException { 292 | BaseJob job = safeDeserialize(cursor.getBlob(DbOpenHelper.BASE_JOB_COLUMN.columnIndex)); 293 | if (job == null) { 294 | throw new InvalidBaseJobException(); 295 | } 296 | return new JobHolder( 297 | cursor.getLong(DbOpenHelper.ID_COLUMN.columnIndex), 298 | cursor.getInt(DbOpenHelper.PRIORITY_COLUMN.columnIndex), 299 | cursor.getString(DbOpenHelper.GROUP_ID_COLUMN.columnIndex), 300 | cursor.getInt(DbOpenHelper.RUN_COUNT_COLUMN.columnIndex), 301 | job, 302 | cursor.getLong(DbOpenHelper.CREATED_NS_COLUMN.columnIndex), 303 | cursor.getLong(DbOpenHelper.DELAY_UNTIL_NS_COLUMN.columnIndex), 304 | cursor.getLong(DbOpenHelper.RUNNING_SESSION_ID_COLUMN.columnIndex) 305 | ); 306 | 307 | } 308 | 309 | private BaseJob safeDeserialize(byte[] bytes) { 310 | try { 311 | return jobSerializer.deserialize(bytes); 312 | } catch (Throwable t) { 313 | JqLog.e(t, "error while deserializing job"); 314 | } 315 | return null; 316 | } 317 | 318 | private byte[] getSerializeBaseJob(JobHolder jobHolder) { 319 | return safeSerialize(jobHolder.getBaseJob()); 320 | } 321 | 322 | private byte[] safeSerialize(Object object) { 323 | try { 324 | return jobSerializer.serialize(object); 325 | } catch (Throwable t) { 326 | JqLog.e(t, "error while serializing object %s", object.getClass().getSimpleName()); 327 | } 328 | return null; 329 | } 330 | 331 | private static class InvalidBaseJobException extends Exception { 332 | 333 | } 334 | 335 | public static class JavaSerializer implements JobSerializer { 336 | 337 | @Override 338 | public byte[] serialize(Object object) throws IOException { 339 | if (object == null) { 340 | return null; 341 | } 342 | ByteArrayOutputStream bos = null; 343 | try { 344 | ObjectOutput out = null; 345 | bos = new ByteArrayOutputStream(); 346 | out = new ObjectOutputStream(bos); 347 | out.writeObject(object); 348 | // Get the bytes of the serialized object 349 | return bos.toByteArray(); 350 | } finally { 351 | if (bos != null) { 352 | bos.close(); 353 | } 354 | } 355 | } 356 | 357 | @Override 358 | public T deserialize(byte[] bytes) throws IOException, ClassNotFoundException { 359 | if (bytes == null || bytes.length == 0) { 360 | return null; 361 | } 362 | ObjectInputStream in = null; 363 | try { 364 | in = new ObjectInputStream(new ByteArrayInputStream(bytes)); 365 | return (T) in.readObject(); 366 | } finally { 367 | if (in != null) { 368 | in.close(); 369 | } 370 | } 371 | } 372 | } 373 | 374 | public static interface JobSerializer { 375 | public byte[] serialize(Object object) throws IOException; 376 | public T deserialize(byte[] bytes) throws IOException, ClassNotFoundException; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':jobmanagerlib' 2 | --------------------------------------------------------------------------------