WIP: refactor on jobs using old job table

This commit is contained in:
ryanzhao 2023-05-05 16:51:44 +10:00
parent d868021f0a
commit 375815c719
8 changed files with 266 additions and 395 deletions

View File

@ -78,35 +78,6 @@ public class JobManager implements ConstraintObserver.Notifier {
new Chain(this, Collections.singletonList(job)).enqueue();
}
/**
* Begins the creation of a job chain with a single job.
* @see Chain
*/
public Chain startChain(@NonNull Job job) {
return new Chain(this, Collections.singletonList(job));
}
/**
* Begins the creation of a job chain with a set of jobs that can be run in parallel.
* @see Chain
*/
public Chain startChain(@NonNull List<? extends Job> jobs) {
return new Chain(this, jobs);
}
/**
* Retrieves a string representing the state of the job queue. Intended for debugging.
*/
public @NonNull String getDebugInfo() {
Future<String> result = executor.submit(jobController::getDebugInfo);
try {
return result.get();
} catch (ExecutionException | InterruptedException e) {
Log.w(TAG, "Failed to retrieve Job info.", e);
return "Failed to retrieve Job info.";
}
}
/**
* Adds a listener to that will be notified when the job queue has been drained.
*/
@ -261,16 +232,6 @@ public class JobManager implements ConstraintObserver.Notifier {
private Data.Serializer dataSerializer = new JsonDataSerializer();
private JobStorage jobStorage = null;
public @NonNull Builder setJobThreadCount(int jobThreadCount) {
this.jobThreadCount = jobThreadCount;
return this;
}
public @NonNull Builder setExecutorFactory(@NonNull ExecutorFactory executorFactory) {
this.executorFactory = executorFactory;
return this;
}
public @NonNull Builder setJobFactories(@NonNull Map<String, Job.Factory> jobFactories) {
this.jobFactories = jobFactories;
return this;

View File

@ -29,9 +29,9 @@ public final class JobManagerFactories {
public static Map<String, Job.Factory> getJobFactories(@NonNull Application application) {
HashMap<String, Job.Factory> factoryHashMap = new HashMap<String, Job.Factory>() {{
put(AvatarDownloadJob.KEY, new AvatarDownloadJob.Factory());
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
put(LocalBackupJob.Companion.getKEY(), new LocalBackupJob.Factory());
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
put(UpdateApkJob.Companion.getKEY(), new UpdateApkJob.Factory());
put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory());
}};
factoryKeys.addAll(factoryHashMap.keySet());

View File

@ -1,83 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.utilities.Data;
import org.session.libsignal.utilities.NoExternalStorageException;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.database.BackupFileRecord;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.util.BackupUtil;
import java.io.IOException;
import java.util.Collections;
import network.loki.messenger.R;
public class LocalBackupJob extends BaseJob {
public static final String KEY = "LocalBackupJob";
private static final String TAG = LocalBackupJob.class.getSimpleName();
public LocalBackupJob() {
this(new Job.Parameters.Builder()
.setQueue("__LOCAL_BACKUP__")
.setMaxInstances(1)
.setMaxAttempts(3)
.build());
}
private LocalBackupJob(@NonNull Job.Parameters parameters) {
super(parameters);
}
@Override
public @NonNull
Data serialize() {
return Data.EMPTY;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws NoExternalStorageException, IOException {
Log.i(TAG, "Executing backup job...");
GenericForegroundService.startForegroundTask(context,
context.getString(R.string.LocalBackupJob_creating_backup),
NotificationChannels.BACKUPS,
R.drawable.ic_launcher_foreground);
// TODO: Maybe create a new backup icon like ic_signal_backup?
try {
BackupFileRecord record = BackupUtil.createBackupFile(context);
BackupUtil.deleteAllBackupFiles(context, Collections.singletonList(record));
} finally {
GenericForegroundService.stopForegroundTask(context);
}
}
@Override
public boolean onShouldRetry(@NonNull Exception e) {
return false;
}
@Override
public void onCanceled() {
}
public static class Factory implements Job.Factory<LocalBackupJob> {
@Override
public @NonNull LocalBackupJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new LocalBackupJob(parameters);
}
}
}

View File

@ -0,0 +1,61 @@
package org.thoughtcrime.securesms.jobs
import android.content.Context
import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.JobDelegate
import org.session.libsession.messaging.utilities.Data
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.service.GenericForegroundService
import org.thoughtcrime.securesms.util.BackupUtil.createBackupFile
import org.thoughtcrime.securesms.util.BackupUtil.deleteAllBackupFiles
import network.loki.messenger.R
class LocalBackupJob:Job {
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
override val maxFailureCount: Int = 0
lateinit var context: Context
companion object {
val TAG = LocalBackupJob::class.simpleName
val KEY: String = "LocalBackupJob"
}
override fun execute(dispatcherName: String) {
Log.i(TAG, "Executing backup job...")
GenericForegroundService.startForegroundTask(
context,
context.getString(R.string.LocalBackupJob_creating_backup),
NotificationChannels.BACKUPS,
R.drawable.ic_launcher_foreground
)
// TODO: Maybe create a new backup icon like ic_signal_backup?
try {
val record = createBackupFile(context)
deleteAllBackupFiles(context, listOf(record))
} finally {
GenericForegroundService.stopForegroundTask(context)
}
}
override fun serialize(): Data {
return Data.EMPTY
}
override fun getFactoryKey(): String {
return KEY
}
class Factory: Job.Factory<LocalBackupJob> {
override fun create(data: Data): LocalBackupJob {
return LocalBackupJob()
}
}
}

View File

@ -1,271 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.session.libsession.messaging.utilities.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.service.UpdateApkReadyListener;
import org.session.libsession.utilities.FileUtils;
import org.session.libsignal.utilities.Hex;
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import network.loki.messenger.BuildConfig;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class UpdateApkJob extends BaseJob {
public static final String KEY = "UpdateApkJob";
private static final String TAG = UpdateApkJob.class.getSimpleName();
public UpdateApkJob() {
this(new Job.Parameters.Builder()
.setQueue("UpdateApkJob")
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(3)
.build());
}
private UpdateApkJob(@NonNull Job.Parameters parameters) {
super(parameters);
}
@Override
public @NonNull
Data serialize() {
return Data.EMPTY;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws IOException, PackageManager.NameNotFoundException {
if (!BuildConfig.PLAY_STORE_DISABLED) return;
Log.i(TAG, "Checking for APK update...");
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(String.format("%s/latest.json", BuildConfig.NOPLAY_UPDATE_URL)).build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
throw new IOException("Bad response: " + response.message());
}
UpdateDescriptor updateDescriptor = JsonUtil.fromJson(response.body().string(), UpdateDescriptor.class);
byte[] digest = Hex.fromStringCondensed(updateDescriptor.getDigest());
Log.i(TAG, "Got descriptor: " + updateDescriptor);
if (updateDescriptor.getVersionCode() > getVersionCode()) {
DownloadStatus downloadStatus = getDownloadStatus(updateDescriptor.getUrl(), digest);
Log.i(TAG, "Download status: " + downloadStatus.getStatus());
if (downloadStatus.getStatus() == DownloadStatus.Status.COMPLETE) {
Log.i(TAG, "Download status complete, notifying...");
handleDownloadNotify(downloadStatus.getDownloadId());
} else if (downloadStatus.getStatus() == DownloadStatus.Status.MISSING) {
Log.i(TAG, "Download status missing, starting download...");
handleDownloadStart(updateDescriptor.getUrl(), updateDescriptor.getVersionName(), digest);
}
}
}
@Override
public boolean onShouldRetry(@NonNull Exception e) {
return e instanceof IOException;
}
@Override
public void onCanceled() {
Log.w(TAG, "Update check failed");
}
private int getVersionCode() throws PackageManager.NameNotFoundException {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionCode;
}
private DownloadStatus getDownloadStatus(String uri, byte[] theirDigest) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterByStatus(DownloadManager.STATUS_PAUSED | DownloadManager.STATUS_PENDING | DownloadManager.STATUS_RUNNING | DownloadManager.STATUS_SUCCESSFUL);
long pendingDownloadId = TextSecurePreferences.getUpdateApkDownloadId(context);
byte[] pendingDigest = getPendingDigest(context);
Cursor cursor = downloadManager.query(query);
try {
DownloadStatus status = new DownloadStatus(DownloadStatus.Status.MISSING, -1);
while (cursor != null && cursor.moveToNext()) {
int jobStatus = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
String jobRemoteUri = cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_URI));
long downloadId = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
byte[] digest = getDigestForDownloadId(downloadId);
if (jobRemoteUri != null && jobRemoteUri.equals(uri) && downloadId == pendingDownloadId) {
if (jobStatus == DownloadManager.STATUS_SUCCESSFUL &&
digest != null && pendingDigest != null &&
MessageDigest.isEqual(pendingDigest, theirDigest) &&
MessageDigest.isEqual(digest, theirDigest))
{
return new DownloadStatus(DownloadStatus.Status.COMPLETE, downloadId);
} else if (jobStatus != DownloadManager.STATUS_SUCCESSFUL) {
status = new DownloadStatus(DownloadStatus.Status.PENDING, downloadId);
}
}
}
return status;
} finally {
if (cursor != null) cursor.close();
}
}
private void handleDownloadStart(String uri, String versionName, byte[] digest) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request downloadRequest = new DownloadManager.Request(Uri.parse(uri));
downloadRequest.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
downloadRequest.setTitle("Downloading Signal update");
downloadRequest.setDescription("Downloading Signal " + versionName);
downloadRequest.setVisibleInDownloadsUi(false);
downloadRequest.setDestinationInExternalFilesDir(context, null, "signal-update.apk");
downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
long downloadId = downloadManager.enqueue(downloadRequest);
TextSecurePreferences.setUpdateApkDownloadId(context, downloadId);
TextSecurePreferences.setUpdateApkDigest(context, Hex.toStringCondensed(digest));
}
private void handleDownloadNotify(long downloadId) {
Intent intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
new UpdateApkReadyListener().onReceive(context, intent);
}
private @Nullable byte[] getDigestForDownloadId(long downloadId) {
try {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
FileInputStream fin = new FileInputStream(downloadManager.openDownloadedFile(downloadId).getFileDescriptor());
byte[] digest = FileUtils.getFileDigest(fin);
fin.close();
return digest;
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}
private @Nullable byte[] getPendingDigest(Context context) {
try {
String encodedDigest = TextSecurePreferences.getUpdateApkDigest(context);
if (encodedDigest == null) return null;
return Hex.fromStringCondensed(encodedDigest);
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}
private static class UpdateDescriptor {
@JsonProperty
private int versionCode;
@JsonProperty
private String versionName;
@JsonProperty
private String url;
@JsonProperty
private String sha256sum;
public int getVersionCode() {
return versionCode;
}
public String getVersionName() {
return versionName;
}
public String getUrl() {
return url;
}
public @NonNull String toString() {
return "[" + versionCode + ", " + versionName + ", " + url + "]";
}
public String getDigest() {
return sha256sum;
}
}
private static class DownloadStatus {
enum Status {
PENDING,
COMPLETE,
MISSING
}
private final Status status;
private final long downloadId;
DownloadStatus(Status status, long downloadId) {
this.status = status;
this.downloadId = downloadId;
}
public Status getStatus() {
return status;
}
public long getDownloadId() {
return downloadId;
}
}
public static final class Factory implements Job.Factory<UpdateApkJob> {
@Override
public @NonNull UpdateApkJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new UpdateApkJob(parameters);
}
}
}

View File

@ -0,0 +1,200 @@
package org.thoughtcrime.securesms.jobs
import androidx.annotation.Nullable
import android.app.DownloadManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import com.fasterxml.jackson.annotation.JsonProperty
import network.loki.messenger.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.Request
import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.JobDelegate
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.utilities.FileUtils
import org.session.libsession.utilities.TextSecurePreferences.Companion.getUpdateApkDigest
import org.session.libsession.utilities.TextSecurePreferences.Companion.getUpdateApkDownloadId
import org.session.libsession.utilities.TextSecurePreferences.Companion.setUpdateApkDigest
import org.session.libsession.utilities.TextSecurePreferences.Companion.setUpdateApkDownloadId
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.service.UpdateApkReadyListener
import java.io.FileInputStream
import java.io.IOException
import java.security.MessageDigest
class UpdateApkJob: Job {
override var delegate: JobDelegate? = null
override var id: String? = null
override var failureCount: Int = 0
override val maxFailureCount: Int = 0
lateinit var context: Context
companion object {
val TAG = UpdateApkJob::class.simpleName
val KEY: String = "UpdateApkJob"
}
override fun execute(dispatcherName: String) {
if (!BuildConfig.PLAY_STORE_DISABLED) return
Log.i(TAG, "Checking for APK update...")
val client = OkHttpClient()
val request = Request.Builder().url(String.format("%s/latest.json", BuildConfig.NOPLAY_UPDATE_URL)).build()
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
throw IOException("Bad response: " + response.message())
}
val updateDescriptor: UpdateDescriptor = JsonUtil.fromJson(
response.body()!!.string(),
UpdateDescriptor::class.java
)
val digest = Hex.fromStringCondensed(updateDescriptor.digest)
Log.i(
TAG,
"Got descriptor: $updateDescriptor"
)
if (updateDescriptor.versionCode > getVersionCode()) {
val downloadStatus: DownloadStatus = getDownloadStatus(updateDescriptor.url, digest)
Log.i(TAG, "Download status: " + downloadStatus.status)
if (downloadStatus.status == DownloadStatus.Status.COMPLETE) {
Log.i(TAG, "Download status complete, notifying...")
handleDownloadNotify(downloadStatus.downloadId)
} else if (downloadStatus.status == DownloadStatus.Status.MISSING) {
Log.i(TAG, "Download status missing, starting download...")
handleDownloadStart(
updateDescriptor.url,
updateDescriptor.versionName,
digest
)
}
}
}
@Throws(PackageManager.NameNotFoundException::class)
private fun getVersionCode(): Int {
val packageManager: PackageManager = context.getPackageManager()
val packageInfo: PackageInfo = packageManager.getPackageInfo(context.getPackageName(), 0)
return packageInfo.versionCode
}
private fun getDownloadStatus(uri: String, theirDigest: ByteArray): DownloadStatus {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val query = DownloadManager.Query()
query.setFilterByStatus(DownloadManager.STATUS_PAUSED or DownloadManager.STATUS_PENDING or DownloadManager.STATUS_RUNNING or DownloadManager.STATUS_SUCCESSFUL)
val pendingDownloadId = getUpdateApkDownloadId(context)
val pendingDigest = getPendingDigest(context)
val cursor = downloadManager.query(query)
return try {
var status = DownloadStatus(DownloadStatus.Status.MISSING, -1)
while (cursor != null && cursor.moveToNext()) {
val jobStatus =
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
val jobRemoteUri =
cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_URI))
val downloadId =
cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID))
val digest = getDigestForDownloadId(downloadId)
if (jobRemoteUri != null && jobRemoteUri == uri && downloadId == pendingDownloadId) {
if (jobStatus == DownloadManager.STATUS_SUCCESSFUL && digest != null && pendingDigest != null &&
MessageDigest.isEqual(pendingDigest, theirDigest) &&
MessageDigest.isEqual(digest, theirDigest)
) {
return DownloadStatus(DownloadStatus.Status.COMPLETE, downloadId)
} else if (jobStatus != DownloadManager.STATUS_SUCCESSFUL) {
status = DownloadStatus(DownloadStatus.Status.PENDING, downloadId)
}
}
}
status
} finally {
cursor?.close()
}
}
private fun handleDownloadStart(uri: String, versionName: String, digest: ByteArray) {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downloadRequest = DownloadManager.Request(Uri.parse(uri))
downloadRequest.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI)
downloadRequest.setTitle("Downloading Signal update")
downloadRequest.setDescription("Downloading Signal $versionName")
downloadRequest.setVisibleInDownloadsUi(false)
downloadRequest.setDestinationInExternalFilesDir(context, null, "signal-update.apk")
downloadRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
val downloadId = downloadManager.enqueue(downloadRequest)
setUpdateApkDownloadId(context, downloadId)
setUpdateApkDigest(context, Hex.toStringCondensed(digest))
}
private fun handleDownloadNotify(downloadId: Long) {
val intent = Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId)
UpdateApkReadyListener().onReceive(context, intent)
}
private fun getDigestForDownloadId(downloadId: Long): ByteArray? {
return try {
val downloadManager =
context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val fin = FileInputStream(downloadManager.openDownloadedFile(downloadId).fileDescriptor)
val digest = FileUtils.getFileDigest(fin)
fin.close()
digest
} catch (e: IOException) {
Log.w(TAG, e)
null
}
}
private fun getPendingDigest(context: Context): ByteArray? {
return try {
val encodedDigest = getUpdateApkDigest(context) ?: return null
Hex.fromStringCondensed(encodedDigest)
} catch (e: IOException) {
Log.w(TAG, e)
null
}
}
override fun serialize(): Data {
return Data.EMPTY
}
override fun getFactoryKey(): String {
return KEY
}
private class UpdateDescriptor(
@JsonProperty("versionCode") @Nullable val versionCode: Int,
@JsonProperty("versionName") @Nullable val versionName: String,
@JsonProperty("url") @Nullable val url: String,
@JsonProperty("sha256sum") @Nullable val digest: String)
{
override fun toString(): String {
return "[$versionCode, $versionName, $url]"
}
}
private class DownloadStatus(val status: Status, val downloadId: Long) {
enum class Status {
PENDING, COMPLETE, MISSING
}
}
class Factory: Job.Factory<UpdateApkJob> {
override fun create(data: Data): UpdateApkJob {
return UpdateApkJob()
}
}
}

View File

@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.service;
import android.content.Context;
import android.content.Intent;
import org.session.libsession.messaging.jobs.JobQueue;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.ApplicationContext;

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.sskenvironment
import android.content.Context
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.ApplicationContext