/*
* Copyright (C) 2015 Simon Vig Therkildsen
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.simonvt.cathode.jobqueue;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import net.simonvt.cathode.jobqueue.JobDatabase.Tables;
import net.simonvt.cathode.jobqueue.database.JobDatabase;
import net.simonvt.cathode.util.MainHandler;
import net.simonvt.schematic.Cursors;
import timber.log.Timber;
public final class JobManager {
private Context context;
private JobInjector jobInjector;
private JobDatabase database;
private Converter converter;
private final ArrayList<Job> jobs = new ArrayList<>();
private final SerialExecutor serialExecutor;
private List<WeakReference<JobListener>> jobListeners = new ArrayList<>();
public JobManager(Context context, JobInjector jobInjector) {
this.context = context.getApplicationContext();
this.jobInjector = jobInjector;
database = JobDatabase.getInstance(this.context);
converter = new Converter();
serialExecutor = SerialExecutor.newInstance();
serialExecutor.execute(new Runnable() {
@Override public void run() {
loadJobs();
}
});
}
public void addJobListener(JobListener listener) {
WeakReference<JobListener> jobListener = new WeakReference<>(listener);
jobListeners.add(jobListener);
}
public void removeJobListener(JobListener listener) {
for (int i = jobListeners.size() - 1; i >= 0; i--) {
WeakReference<JobListener> listenerRef = jobListeners.get(i);
JobListener jobListener = listenerRef.get();
if (jobListener == null || listener == jobListener) {
jobListeners.remove(listenerRef);
}
}
}
private synchronized void postOnJobsLoaded() {
MainHandler.post(new Runnable() {
@Override public void run() {
for (int i = jobListeners.size() - 1; i >= 0; i--) {
WeakReference<JobListener> listenerRef = jobListeners.get(i);
JobListener jobListener = listenerRef.get();
if (jobListener == null) {
jobListeners.remove(listenerRef);
} else {
jobListener.onJobsLoaded(JobManager.this);
}
}
}
});
}
private synchronized void postOnJobAdded(final Job job) {
MainHandler.post(new Runnable() {
@Override public void run() {
for (int i = jobListeners.size() - 1; i >= 0; i--) {
WeakReference<JobListener> listenerRef = jobListeners.get(i);
JobListener jobListener = listenerRef.get();
if (jobListener == null) {
jobListeners.remove(listenerRef);
} else {
jobListener.onJobAdded(JobManager.this, job);
}
}
}
});
}
private synchronized void postOnJobRemoved(final Job job) {
MainHandler.post(new Runnable() {
@Override public void run() {
for (int i = jobListeners.size() - 1; i >= 0; i--) {
WeakReference<JobListener> listenerRef = jobListeners.get(i);
JobListener jobListener = listenerRef.get();
if (jobListener == null) {
jobListeners.remove(listenerRef);
} else {
jobListener.onJobRemoved(JobManager.this, job);
}
}
}
});
}
private void loadJobs() {
synchronized (jobs) {
SQLiteDatabase db = database.getReadableDatabase();
Cursor c = db.query(Tables.JOBS, null, null, null, null, null, null);
while (c.moveToNext()) {
byte[] bytes = Cursors.getBlob(c, JobColumns.JOB);
Job job = converter.from(bytes);
addJobInternal(job);
}
c.close();
postOnJobsLoaded();
Timber.d("Loaded %d jobs", jobs.size());
}
}
private boolean isMoreImportantThan(Job job1, Job job2) {
return job1.getPriority() > job2.getPriority();
}
public void addJob(final Job job) {
serialExecutor.execute(new Runnable() {
@Override public void run() {
addJobNow(job);
}
});
}
public void addJobNow(final Job job) {
Timber.d("Adding job: %s", job.key());
postOnJobAdded(job);
synchronized (jobs) {
if (!job.allowDuplicates()) {
final String key = job.key();
for (Job existingJob : jobs) {
if (key.equals(existingJob.key())) {
List<WeakReference<Job.OnDoneListener>> listeners = job.getOnDoneRefs();
if (listeners != null) {
for (WeakReference<Job.OnDoneListener> ref : listeners) {
Job.OnDoneListener listener = ref.get();
if (listener != null) {
existingJob.registerOnDoneListener(listener);
}
}
}
Timber.d("Job %s matched %s", key, existingJob.key());
return;
}
}
}
addJobInternal(job);
}
persistJob(job);
}
private void addJobInternal(Job job) {
synchronized (jobs) {
boolean added = false;
jobInjector.injectInto(job);
for (int i = 0; i < jobs.size(); i++) {
if (isMoreImportantThan(job, jobs.get(i))) {
added = true;
jobs.add(i, job);
break;
}
}
if (!added) {
jobs.add(job);
}
}
}
private void persistJob(Job job) {
ContentValues values = new ContentValues();
values.put(JobColumns.KEY, job.key());
byte[] bytes = converter.to(job);
values.put(JobColumns.JOB, bytes);
values.put(JobColumns.JOB_NAME, job.getClass().getName());
values.put(JobColumns.FLAGS, job.getFlags());
SQLiteDatabase db = database.getWritableDatabase();
db.insert(Tables.JOBS, null, values);
}
public Job checkoutJob(int withFlags, int withoutFlags) {
synchronized (jobs) {
for (Job job : jobs) {
if (!job.isCheckedOut()) {
if (withFlags != 0 && !job.hasFlags(withFlags)) {
continue;
}
if (withoutFlags != 0 && job.hasFlags(withoutFlags)) {
continue;
}
job.setCheckedOut(true);
return job;
}
}
return null;
}
}
public void checkinJob(Job job) {
job.setCheckedOut(false);
}
public void removeJob(final Job job) {
synchronized (jobs) {
Timber.d("Removing job: %s", job.key());
jobs.remove(job);
postOnJobRemoved(job);
}
removeJobFromDatabase(job);
}
private void removeJobFromDatabase(final Job job) {
serialExecutor.execute(new Runnable() {
@Override public void run() {
SQLiteDatabase db = database.getWritableDatabase();
db.delete(Tables.JOBS, JobColumns.JOB_NAME + "=? AND " + JobColumns.KEY + "=?",
new String[] {
job.getClass().getName(), job.key(),
});
}
});
}
public boolean hasJobs(int withFlags, int withoutFlags) {
synchronized (jobs) {
for (Job job : jobs) {
if (withFlags != 0 && !job.hasFlags(withFlags)) {
continue;
}
if (withoutFlags != 0 && job.hasFlags(withoutFlags)) {
continue;
}
return true;
}
return false;
}
}
public void removeJobsWithFlag(final int flag) {
serialExecutor.execute(new Runnable() {
@Override public void run() {
synchronized (jobs) {
for (int i = jobs.size() - 1; i >= 0; i--) {
Job job = jobs.get(i);
if (job.hasFlags(flag)) {
jobs.remove(i);
removeJobFromDatabase(job);
}
}
}
}
});
}
public int jobCount() {
synchronized (jobs) {
return jobs.size();
}
}
public void clear() {
serialExecutor.clear();
serialExecutor.execute(new Runnable() {
@Override public void run() {
synchronized (jobs) {
jobs.clear();
SQLiteDatabase db = database.getWritableDatabase();
db.delete(Tables.JOBS, null, null);
}
}
});
}
}