├── .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 | 
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 |
--------------------------------------------------------------------------------