remove unused jobs and wrap up old job refactoring

This commit is contained in:
Ryan Zhao 2023-05-08 11:18:33 +10:00
parent 375815c719
commit 2b48b52df0
6 changed files with 104 additions and 417 deletions

View File

@ -355,7 +355,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private void initializeJobManager() {
this.jobManager = new JobManager(this, new JobManager.Configuration.Builder()
.setDataSerializer(new JsonDataSerializer())
.setJobFactories(JobManagerFactories.getJobFactories(this))
.setJobFactories(JobManagerFactories.getJobFactories())
.setConstraintFactories(JobManagerFactories.getConstraintFactories(this))
.setConstraintObservers(JobManagerFactories.getConstraintObservers(this))
.setJobStorage(new FastJobStorage(jobDatabase))

View File

@ -1,133 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.utilities.Data;
import org.session.libsession.utilities.DownloadUtilities;
import org.session.libsession.utilities.GroupRecord;
import org.session.libsignal.exceptions.InvalidMessageException;
import org.session.libsignal.exceptions.NonSuccessfulResponseCodeException;
import org.session.libsignal.messages.SignalServiceAttachmentPointer;
import org.session.libsignal.streams.AttachmentCipherInputStream;
import org.session.libsignal.utilities.Hex;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.guava.Optional;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class AvatarDownloadJob extends BaseJob {
public static final String KEY = "AvatarDownloadJob";
private static final String TAG = AvatarDownloadJob.class.getSimpleName();
private static final int MAX_AVATAR_SIZE = 20 * 1024 * 1024;
private static final String KEY_GROUP_ID = "group_id";
private String groupId;
public AvatarDownloadJob(@NonNull String groupId) {
this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(10)
.build(),
groupId);
}
private AvatarDownloadJob(@NonNull Job.Parameters parameters, @NonNull String groupId) {
super(parameters);
this.groupId = groupId;
}
@Override
public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_GROUP_ID, groupId).build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws IOException {
GroupDatabase database = DatabaseComponent.get(context).groupDatabase();
Optional<GroupRecord> record = database.getGroup(groupId);
File attachment = null;
try {
if (record.isPresent()) {
long avatarId = record.get().getAvatarId();
String contentType = record.get().getAvatarContentType();
byte[] key = record.get().getAvatarKey();
String relay = record.get().getRelay();
Optional<byte[]> digest = Optional.fromNullable(record.get().getAvatarDigest());
Optional<String> fileName = Optional.absent();
String url = record.get().getUrl();
if (avatarId == -1 || key == null || url.isEmpty()) {
return;
}
if (digest.isPresent()) {
Log.i(TAG, "Downloading group avatar with digest: " + Hex.toString(digest.get()));
}
attachment = File.createTempFile("avatar", "tmp", context.getCacheDir());
attachment.deleteOnExit();
SignalServiceAttachmentPointer pointer = new SignalServiceAttachmentPointer(avatarId, contentType, key, Optional.of(0), Optional.absent(), 0, 0, digest, fileName, false, Optional.absent(), url);
if (pointer.getUrl().isEmpty()) throw new InvalidMessageException("Missing attachment URL.");
DownloadUtilities.downloadFile(attachment, pointer.getUrl());
// Assume we're retrieving an attachment for an open group server if the digest is not set
InputStream inputStream;
if (!pointer.getDigest().isPresent()) {
inputStream = new FileInputStream(attachment);
} else {
inputStream = AttachmentCipherInputStream.createForAttachment(attachment, pointer.getSize().or(0), pointer.getKey(), pointer.getDigest().get());
}
Bitmap avatar = BitmapUtil.createScaledBitmap(context, new AttachmentModel(attachment, key, 0, digest), 500, 500);
database.updateProfilePicture(groupId, avatar);
inputStream.close();
}
} catch (BitmapDecodingException | NonSuccessfulResponseCodeException | InvalidMessageException e) {
Log.w(TAG, e);
} finally {
if (attachment != null)
attachment.delete();
}
}
@Override
public void onCanceled() {}
@Override
public boolean onShouldRetry(@NonNull Exception exception) {
if (exception instanceof IOException) return true;
return false;
}
public static final class Factory implements Job.Factory<AvatarDownloadJob> {
@Override
public @NonNull AvatarDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new AvatarDownloadJob(parameters, data.getString(KEY_GROUP_ID));
}
}
}

View File

@ -4,9 +4,9 @@ import android.app.Application;
import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Job;
import org.thoughtcrime.securesms.jobmanager.Constraint;
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.CellServiceConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
@ -28,11 +28,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.Companion.getKEY(), new LocalBackupJob.Factory());
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory(application));
put(UpdateApkJob.Companion.getKEY(), new UpdateApkJob.Factory());
put(PrepareAttachmentAudioExtrasJob.KEY, new PrepareAttachmentAudioExtrasJob.Factory());
put(LocalBackupJob.Companion.getKEY(), new LocalBackupJob.Factory());
put(RetrieveProfileAvatarJob.Companion.getKEY(), new RetrieveProfileAvatarJob.Factory());
put(UpdateApkJob.Companion.getKEY(), new UpdateApkJob.Factory());
}};
factoryKeys.addAll(factoryHashMap.keySet());
return factoryHashMap;

View File

@ -1,133 +0,0 @@
package org.thoughtcrime.securesms.jobs
import android.os.Build
import org.greenrobot.eventbus.EventBus
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachmentAudioExtras
import org.session.libsession.messaging.utilities.Data
import org.session.libsession.utilities.DecodedAudio
import org.session.libsession.utilities.InputStreamMediaDataSource
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobs.PrepareAttachmentAudioExtrasJob.AudioExtrasUpdatedEvent
import org.thoughtcrime.securesms.mms.PartAuthority
import java.util.*
import java.util.concurrent.TimeUnit
/**
* Decodes the audio content of the related attachment entry
* and caches the result with [DatabaseAttachmentAudioExtras] data.
*
* It only process attachments with "audio" mime types.
*
* Due to [DecodedAudio] implementation limitations, it only works for API 23+.
* For any lower targets fake data will be generated.
*
* You can subscribe to [AudioExtrasUpdatedEvent] to be notified about the successful result.
*/
//TODO AC: Rewrite to WorkManager API when
// https://github.com/loki-project/session-android/pull/354 is merged.
class PrepareAttachmentAudioExtrasJob : BaseJob {
companion object {
private const val TAG = "AttachAudioExtrasJob"
const val KEY = "PrepareAttachmentAudioExtrasJob"
const val DATA_ATTACH_ID = "attachment_id"
const val VISUAL_RMS_FRAMES = 32 // The amount of values to be computed for the visualization.
}
private val attachmentId: AttachmentId
constructor(attachmentId: AttachmentId) : this(Parameters.Builder()
.setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.build(),
attachmentId)
private constructor(parameters: Parameters, attachmentId: AttachmentId) : super(parameters) {
this.attachmentId = attachmentId
}
override fun serialize(): Data {
return Data.Builder().putParcelable(DATA_ATTACH_ID, attachmentId).build();
}
override fun getFactoryKey(): String { return KEY
}
override fun onShouldRetry(e: Exception): Boolean {
return false
}
override fun onCanceled() { }
override fun onRun() {
Log.v(TAG, "Processing attachment: $attachmentId")
val attachDb = DatabaseComponent.get(context).attachmentDatabase()
val attachment = attachDb.getAttachment(attachmentId)
if (attachment == null) {
throw IllegalStateException("Cannot find attachment with the ID $attachmentId")
}
if (!attachment.contentType.startsWith("audio/")) {
throw IllegalStateException("Attachment $attachmentId is not of audio type.")
}
// Check if the audio extras already exist.
if (attachDb.getAttachmentAudioExtras(attachmentId) != null) return
fun extractAttachmentRandomSeed(attachment: Attachment): Int {
return when {
attachment.digest != null -> attachment.digest!!.sum()
attachment.fileName != null -> attachment.fileName.hashCode()
else -> attachment.hashCode()
}
}
fun generateFakeRms(seed: Int, frames: Int = VISUAL_RMS_FRAMES): ByteArray {
return ByteArray(frames).apply { Random(seed.toLong()).nextBytes(this) }
}
var rmsValues: ByteArray
var totalDurationMs: Long = DatabaseAttachmentAudioExtras.DURATION_UNDEFINED
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Due to API version incompatibility, we just display some random waveform for older API.
rmsValues = generateFakeRms(extractAttachmentRandomSeed(attachment))
} else {
try {
@Suppress("BlockingMethodInNonBlockingContext")
val decodedAudio = PartAuthority.getAttachmentStream(context, attachment.dataUri!!).use {
DecodedAudio.create(InputStreamMediaDataSource(it))
}
rmsValues = decodedAudio.calculateRms(VISUAL_RMS_FRAMES)
totalDurationMs = (decodedAudio.totalDuration / 1000.0).toLong()
} catch (e: Exception) {
Log.w(TAG, "Failed to decode sample values for the audio attachment \"${attachment.fileName}\".", e)
rmsValues = generateFakeRms(extractAttachmentRandomSeed(attachment))
}
}
attachDb.setAttachmentAudioExtras(DatabaseAttachmentAudioExtras(
attachmentId,
rmsValues,
totalDurationMs
))
EventBus.getDefault().post(AudioExtrasUpdatedEvent(attachmentId))
}
class Factory : Job.Factory<PrepareAttachmentAudioExtrasJob> {
override fun create(parameters: Parameters, data: Data): PrepareAttachmentAudioExtrasJob {
return PrepareAttachmentAudioExtrasJob(parameters, data.getParcelable(DATA_ATTACH_ID, AttachmentId.CREATOR))
}
}
/** Gets dispatched once the audio extras have been updated. */
data class AudioExtrasUpdatedEvent(val attachmentId: AttachmentId)
}

View File

@ -1,144 +0,0 @@
package org.thoughtcrime.securesms.jobs;
import android.app.Application;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import org.session.libsession.avatars.AvatarHelper;
import org.session.libsession.messaging.utilities.Data;
import org.session.libsession.utilities.Address;
import org.session.libsession.utilities.DownloadUtilities;
import org.session.libsession.utilities.TextSecurePreferences;
import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.recipients.Recipient;
import org.session.libsignal.exceptions.PushNetworkException;
import org.session.libsignal.streams.ProfileCipherInputStream;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;
public class RetrieveProfileAvatarJob extends BaseJob {
public static final String KEY = "RetrieveProfileAvatarJob";
private static final String TAG = RetrieveProfileAvatarJob.class.getSimpleName();
private static final int MAX_PROFILE_SIZE_BYTES = 10 * 1024 * 1024;
private static final String KEY_PROFILE_AVATAR = "profile_avatar";
private static final String KEY_ADDRESS = "address";
private String profileAvatar;
private Recipient recipient;
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) {
this(new Job.Parameters.Builder()
.setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize())
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.HOURS.toMillis(1))
.setMaxAttempts(2)
.setMaxInstances(1)
.build(),
recipient,
profileAvatar);
}
private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) {
super(parameters);
this.recipient = recipient;
this.profileAvatar = profileAvatar;
}
@Override
public @NonNull
Data serialize() {
return new Data.Builder()
.putString(KEY_PROFILE_AVATAR, profileAvatar)
.putString(KEY_ADDRESS, recipient.getAddress().serialize())
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
public void onRun() throws IOException {
RecipientDatabase database = DatabaseComponent.get(context).recipientDatabase();
byte[] profileKey = recipient.resolve().getProfileKey();
if (profileKey == null || (profileKey.length != 32 && profileKey.length != 16)) {
Log.w(TAG, "Recipient profile key is gone!");
return;
}
if (AvatarHelper.avatarFileExists(context, recipient.resolve().getAddress()) && Util.equals(profileAvatar, recipient.resolve().getProfileAvatar())) {
Log.w(TAG, "Already retrieved profile avatar: " + profileAvatar);
return;
}
if (TextUtils.isEmpty(profileAvatar)) {
Log.w(TAG, "Removing profile avatar for: " + recipient.getAddress().serialize());
AvatarHelper.delete(context, recipient.getAddress());
database.setProfileAvatar(recipient, profileAvatar);
return;
}
File downloadDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
try {
DownloadUtilities.downloadFile(downloadDestination, profileAvatar);
InputStream avatarStream = new ProfileCipherInputStream(new FileInputStream(downloadDestination), profileKey);
File decryptDestination = File.createTempFile("avatar", ".jpg", context.getCacheDir());
Util.copy(avatarStream, new FileOutputStream(decryptDestination));
decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.getAddress()));
} finally {
if (downloadDestination != null) downloadDestination.delete();
}
if (recipient.isLocalNumber()) {
TextSecurePreferences.setProfileAvatarId(context, new SecureRandom().nextInt());
}
database.setProfileAvatar(recipient, profileAvatar);
}
@Override
public boolean onShouldRetry(@NonNull Exception e) {
if (e instanceof PushNetworkException) return true;
return false;
}
@Override
public void onCanceled() {
}
public static final class Factory implements Job.Factory<RetrieveProfileAvatarJob> {
private final Application application;
public Factory(Application application) {
this.application = application;
}
@Override
public @NonNull RetrieveProfileAvatarJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new RetrieveProfileAvatarJob(parameters,
Recipient.from(application, Address.fromSerialized(data.getString(KEY_ADDRESS)), true),
data.getString(KEY_PROFILE_AVATAR));
}
}
}

View File

@ -0,0 +1,99 @@
package org.thoughtcrime.securesms.jobs
import android.content.Context
import android.text.TextUtils
import org.session.libsession.avatars.AvatarHelper
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.DownloadUtilities.downloadFile
import org.session.libsession.utilities.TextSecurePreferences.Companion.setProfileAvatarId
import org.session.libsession.utilities.Util.copy
import org.session.libsession.utilities.Util.equals
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.streams.ProfileCipherInputStream
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.DatabaseComponent.Companion.get
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.security.SecureRandom
class RetrieveProfileAvatarJob(val profileAvatar: String, val recipientAddress: Address): 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 = RetrieveProfileAvatarJob::class.simpleName
val KEY: String = "RetrieveProfileAvatarJob"
// Keys used for database storage
private val PROFILE_AVATAR_KEY = "profileAvatar"
private val RECEIPIENT_ADDRESS_KEY = "recipient"
}
override fun execute(dispatcherName: String) {
val recipient = Recipient.from(context, recipientAddress, true)
val database = get(context).recipientDatabase()
val profileKey = recipient.resolve().profileKey
if (profileKey == null || (profileKey.size != 32 && profileKey.size != 16)) {
Log.w(TAG, "Recipient profile key is gone!")
return
}
if (AvatarHelper.avatarFileExists(context, recipient.resolve().address) && equals(profileAvatar, recipient.resolve().profileAvatar)) {
Log.w(TAG, "Already retrieved profile avatar: $profileAvatar")
return
}
if (TextUtils.isEmpty(profileAvatar)) {
Log.w(TAG, "Removing profile avatar for: " + recipient.address.serialize())
AvatarHelper.delete(context, recipient.address)
database.setProfileAvatar(recipient, profileAvatar)
return
}
val downloadDestination = File.createTempFile("avatar", ".jpg", context.cacheDir)
try {
downloadFile(downloadDestination, profileAvatar)
val avatarStream: InputStream = ProfileCipherInputStream(FileInputStream(downloadDestination), profileKey)
val decryptDestination = File.createTempFile("avatar", ".jpg", context.cacheDir)
copy(avatarStream, FileOutputStream(decryptDestination))
decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.address))
} finally {
downloadDestination.delete()
}
if (recipient.isLocalNumber) {
setProfileAvatarId(context, SecureRandom().nextInt())
}
database.setProfileAvatar(recipient, profileAvatar)
}
override fun serialize(): Data {
return Data.Builder()
.putString(PROFILE_AVATAR_KEY, profileAvatar)
.putString(RECEIPIENT_ADDRESS_KEY, recipientAddress.serialize())
.build()
}
override fun getFactoryKey(): String {
return KEY
}
class Factory: Job.Factory<RetrieveProfileAvatarJob> {
override fun create(data: Data): RetrieveProfileAvatarJob {
val profileAvatar = data.getString(PROFILE_AVATAR_KEY)
val recipientAddress = Address.fromSerialized(data.getString(RECEIPIENT_ADDRESS_KEY))
return RetrieveProfileAvatarJob(profileAvatar, recipientAddress)
}
}
}