Messages: Replace (Safe)AsyncTask

* AsyncTask is deprecated
* Executors and Handlers can achieve the same thing and
  are not deprecated

Change-Id: I5271bb73b848ce885eeaf5632c1da27853c56c4a
This commit is contained in:
Michael W
2025-03-04 18:52:05 +01:00
parent 94c0b8734b
commit f83b05ea0a
19 changed files with 293 additions and 407 deletions
@@ -20,6 +20,8 @@ import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -41,10 +43,12 @@ import com.android.messaging.datamodel.data.PersonItemData;
import com.android.messaging.datamodel.data.VCardContactItemData;
import com.android.messaging.datamodel.data.PersonItemData.PersonItemDataListener;
import com.android.messaging.util.Assert;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.UiUtils;
import com.android.messaging.util.UriUtil;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* A fragment that shows the content of a VCard that contains one or more contacts.
*/
@@ -130,19 +134,18 @@ public class VCardDetailFragment extends Fragment implements PersonItemDataListe
final Uri vCardUri = mBinding.getData().getVCardUri();
// We have to do things in the background in case we need to copy the vcard data.
new SafeAsyncTask<Void, Void, Uri>() {
@Override
protected Uri doInBackgroundTimed(final Void... params) {
// We can't delete the persisted vCard file because we don't know when to
// delete it, since the app that uses it (contacts, dialer) may start or
// shut down at any point. Therefore, we rely on the system to clean up
// the cache directory for us.
return mScratchSpaceUri != null ? mScratchSpaceUri :
UriUtil.persistContentToScratchSpace(vCardUri);
}
ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());
@Override
protected void onPostExecute(final Uri result) {
executor.execute(() -> {
// We can't delete the persisted vCard file because we don't know when to
// delete it, since the app that uses it (contacts, dialer) may start or
// shut down at any point. Therefore, we rely on the system to clean up
// the cache directory for us.
Uri result = mScratchSpaceUri != null ? mScratchSpaceUri :
UriUtil.persistContentToScratchSpace(vCardUri);
handler.post(() -> {
if (result != null) {
mScratchSpaceUri = result;
if (getActivity() != null) {
@@ -152,8 +155,8 @@ public class VCardDetailFragment extends Fragment implements PersonItemDataListe
result);
}
}
}
}.executeOnThreadPool();
});
});
return true;
}
return super.onOptionsItemSelected(item);
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 The Android Open Source Project
* Copyright (C) 2024 The LineageOS Project
* Copyright (C) 2024-2025 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,8 @@ import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.TextPaint;
import android.text.TextWatcher;
@@ -49,6 +50,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
@@ -147,66 +149,81 @@ public class ContactRecipientAutoCompleteView extends RecipientEditTextView {
}
/**
* An AsyncTask that cleans up contact chips on every chips commit (i.e. get or create a new
* A class that cleans up contact chips on every chips commit (i.e. get or create a new
* conversation with the given chips).
*/
private class AsyncContactChipSanitizeTask extends
AsyncTask<Void, ChipReplacementTuple, Integer> {
private class AsyncContactChipSanitizeTask {
@Override
protected Integer doInBackground(final Void... params) {
final DrawableRecipientChip[] recips = getText()
.getSpans(0, getText().length(), DrawableRecipientChip.class);
int invalidChipsRemoved = 0;
for (final DrawableRecipientChip recipient : recips) {
final RecipientEntry entry = recipient.getEntry();
if (entry != null) {
if (entry.isValid()) {
if (RecipientEntry.isCreatedRecipient(entry.getContactId()) ||
ContactRecipientEntryUtils.isSendToDestinationContact(entry)) {
// This is a generated/send-to contact chip, try to look it up and
// display a chip for the corresponding local contact.
try (final Cursor lookupResult =
ContactUtil.lookupDestination(
getContext(), entry.getDestination())
.performSynchronousQuery()) {
if (lookupResult != null && lookupResult.moveToNext()) {
// Found a match, remove the generated entry and replace with a
// better local entry.
publishProgress(
new ChipReplacementTuple(
recipient,
ContactUtil.createRecipientEntryForPhoneQuery(
lookupResult, true)));
} else if (PhoneUtils.isValidSmsMmsDestination(
entry.getDestination())) {
// No match was found, but we have a valid destination so let's
// at least create an entry that shows an avatar.
publishProgress(
new ChipReplacementTuple(
recipient,
ContactRecipientEntryUtils
.constructNumberWithAvatarEntry(
entry.getDestination())));
} else {
// Not a valid contact. Remove and show an error.
publishProgress(new ChipReplacementTuple(recipient, null));
invalidChipsRemoved++;
private ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private Handler mHandler = new Handler(Looper.getMainLooper());
private boolean mIsCancelled;
protected void execute() {
mExecutor.execute(() -> {
final DrawableRecipientChip[] recips = getText()
.getSpans(0, getText().length(), DrawableRecipientChip.class);
int invalidChipsRemoved = 0;
for (final DrawableRecipientChip recipient : recips) {
if (mIsCancelled) {
break;
}
final RecipientEntry entry = recipient.getEntry();
if (entry != null) {
if (entry.isValid()) {
if (RecipientEntry.isCreatedRecipient(entry.getContactId()) ||
ContactRecipientEntryUtils.isSendToDestinationContact(entry)) {
// This is a generated/send-to contact chip, try to look it up and
// display a chip for the corresponding local contact.
try (final Cursor lookupResult = ContactUtil.lookupDestination(
getContext(), entry.getDestination())
.performSynchronousQuery()) {
if (mIsCancelled) {
break;
}
if (lookupResult != null && lookupResult.moveToNext()) {
// Found a match, remove the generated entry and replace
// with abetter local entry.
publishProgress(
new ChipReplacementTuple(recipient,
ContactUtil.
createRecipientEntryForPhoneQuery(
lookupResult, true)));
} else if (PhoneUtils.isValidSmsMmsDestination(
entry.getDestination())) {
// No match was found, but we have a valid destination so
// let's at least create an entry that shows an avatar.
publishProgress(
new ChipReplacementTuple(
recipient,
ContactRecipientEntryUtils
.constructNumberWithAvatarEntry(
entry.getDestination())));
} else {
// Not a valid contact. Remove and show an error.
publishProgress(new ChipReplacementTuple(recipient, null));
invalidChipsRemoved++;
}
}
}
} else {
publishProgress(new ChipReplacementTuple(recipient, null));
invalidChipsRemoved++;
}
} else {
publishProgress(new ChipReplacementTuple(recipient, null));
invalidChipsRemoved++;
}
}
}
return invalidChipsRemoved;
final int finalInvalidChipsRemoved = invalidChipsRemoved;
mHandler.post(() -> {
mCurrentSanitizeTask = null;
if (finalInvalidChipsRemoved > 0) {
mChipsChangeListener.onInvalidContactChipsPruned(finalInvalidChipsRemoved);
}
});
});
}
@Override
protected void onProgressUpdate(final ChipReplacementTuple... values) {
for (final ChipReplacementTuple tuple : values) {
private void publishProgress(final ChipReplacementTuple tuple) {
mHandler.post(() -> {
if (tuple.removedChip != null) {
final Editable text = getText();
final int chipStart = text.getSpanStart(tuple.removedChip);
@@ -219,24 +236,18 @@ public class ContactRecipientAutoCompleteView extends RecipientEditTextView {
appendRecipientEntry(tuple.replacedChipEntry);
}
}
}
});
}
@Override
protected void onPostExecute(final Integer invalidChipsRemoved) {
mCurrentSanitizeTask = null;
if (invalidChipsRemoved > 0) {
mChipsChangeListener.onInvalidContactChipsPruned(invalidChipsRemoved);
}
public void cancel() {
mIsCancelled = true;
}
public boolean isCancelled() {
return mIsCancelled;
}
}
/**
* We don't use SafeAsyncTask but instead use a single threaded executor to ensure that
* all sanitization tasks are serially executed so as not to interfere with each other.
*/
private static final Executor SANITIZE_EXECUTOR = Executors.newSingleThreadExecutor();
private AsyncContactChipSanitizeTask mCurrentSanitizeTask;
/**
@@ -251,11 +262,11 @@ public class ContactRecipientAutoCompleteView extends RecipientEditTextView {
*/
private void sanitizeContactChips() {
if (mCurrentSanitizeTask != null && !mCurrentSanitizeTask.isCancelled()) {
mCurrentSanitizeTask.cancel(false);
mCurrentSanitizeTask.cancel();
mCurrentSanitizeTask = null;
}
mCurrentSanitizeTask = new AsyncContactChipSanitizeTask();
mCurrentSanitizeTask.executeOnExecutor(SANITIZE_EXECUTOR);
mCurrentSanitizeTask.execute();
}
/**
@@ -21,6 +21,8 @@ import android.content.res.Resources;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.Html;
import android.text.InputFilter;
@@ -70,13 +72,14 @@ import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.MediaUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.UiUtils;
import com.android.messaging.util.UriUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* This view contains the UI required to generate and send messages.
@@ -596,10 +599,11 @@ public class ComposeMessageView extends LinearLayout
mConversationDataModel.getData().getParticipantsLoaded();
}
private static class AsyncUpdateMessageBodySizeTask
extends SafeAsyncTask<List<MessagePartData>, Void, Long> {
private static class AsyncUpdateMessageBodySizeTask {
private final Context mContext;
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final TextView mSizeTextView;
public AsyncUpdateMessageBodySizeTask(final Context context, final TextView tv) {
@@ -607,20 +611,23 @@ public class ComposeMessageView extends LinearLayout
mSizeTextView = tv;
}
@Override
protected Long doInBackgroundTimed(final List<MessagePartData>... params) {
final List<MessagePartData> attachments = params[0];
long totalSize = 0;
for (final MessagePartData attachment : attachments) {
final Uri contentUri = attachment.getContentUri();
if (contentUri != null) {
totalSize += UriUtil.getContentSize(attachment.getContentUri());
protected void execute(final List<MessagePartData> attachments) {
mExecutor.execute(() -> {
long totalSize = 0;
for (final MessagePartData attachment : attachments) {
final Uri contentUri = attachment.getContentUri();
if (contentUri != null) {
totalSize += UriUtil.getContentSize(attachment.getContentUri());
}
}
}
return totalSize;
final long size = totalSize;
mHandler.post(() -> {
onPostExecute(size);
});
});
}
@Override
protected void onPostExecute(Long size) {
if (mSizeTextView != null) {
mSizeTextView.setText(Formatter.formatFileSize(mContext, size));
@@ -656,7 +663,7 @@ public class ComposeMessageView extends LinearLayout
if (hasAttachmentsChanged) {
// Calculate message attachments size and show it.
new AsyncUpdateMessageBodySizeTask(getContext(), mMessageBodySize)
.executeOnThreadPool(attachments, null, null);
.execute(attachments);
} else {
// No update. Just show previous size.
mMessageBodySize.setVisibility(View.VISIBLE);
@@ -34,6 +34,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcelable;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
@@ -105,7 +106,6 @@ import com.android.messaging.util.ImeUtil;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.TextUtil;
import com.android.messaging.util.UiUtils;
import com.android.messaging.util.UriUtil;
@@ -113,6 +113,8 @@ import com.android.messaging.util.UriUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Shows a list of messages/parts comprising a conversation.
@@ -312,7 +314,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
part.getContentType());
}
if (saveAttachmentTask.getAttachmentCount() > 0) {
saveAttachmentTask.executeOnThreadPool();
saveAttachmentTask.execute();
mHost.dismissActionMode();
}
return true;
@@ -1266,8 +1268,10 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
}
public static class SaveAttachmentTask extends SafeAsyncTask<Void, Void, Void> {
public static class SaveAttachmentTask {
private final Context mContext;
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final List<AttachmentToSave> mAttachmentsToSave = new ArrayList<>();
public SaveAttachmentTask(final Context context, final Uri contentUri,
@@ -1288,8 +1292,14 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
return mAttachmentsToSave.size();
}
@Override
protected Void doInBackgroundTimed(final Void... arg) {
public void execute() {
mExecutor.execute(() -> {
onExecute();
mHandler.post(this::onPostExecute);
});
}
protected void onExecute() {
final File appDir = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES),
mContext.getResources().getString(R.string.app_name));
@@ -1301,11 +1311,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
attachment.persistedUri = UriUtil.persistContent(attachment.uri,
isImageOrVideo ? appDir : downloadDir, attachment.contentType);
}
return null;
}
@Override
protected void onPostExecute(final Void result) {
protected void onPostExecute() {
int failCount = 0;
int imageCount = 0;
int videoCount = 0;
@@ -46,10 +46,11 @@ import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.MediaUtil;
import com.android.messaging.util.MediaUtil.OnCompletionListener;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.ThreadUtil;
import com.android.messaging.util.UiUtils;
import java.util.concurrent.Executors;
/**
* Hosts an audio recorder with tap and hold to record functionality.
*/
@@ -260,7 +261,7 @@ public class AudioRecordView extends FrameLayout implements
// "tap+hold" to record audio.
final Uri outputUri = stopRecording();
if (outputUri != null) {
SafeAsyncTask.executeOnThreadPool(() ->
Executors.newSingleThreadExecutor().execute(() ->
Factory.get().getApplicationContext().getContentResolver().delete(
outputUri, null, null));
}
@@ -34,9 +34,10 @@ import com.android.messaging.R;
import com.android.messaging.datamodel.data.PendingAttachmentData;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.UiUtils;
import java.util.concurrent.Executors;
/**
* Chooser which allows the user to select an existing contact from contacts apps on this device.
* Note that this chooser requires the Manifest.permission.READ_CONTACTS which is one of the miminum
@@ -71,7 +72,7 @@ class ContactMediaChooser extends MediaChooser {
final Uri vCardUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI,
lookupKey);
if (vCardUri != null) {
SafeAsyncTask.executeOnThreadPool(() -> {
Executors.newSingleThreadExecutor().execute(() -> {
final PendingAttachmentData pendingItem =
PendingAttachmentData.createPendingAttachmentData(
ContentType.TEXT_X_VCARD.toLowerCase(), vCardUri);
@@ -19,6 +19,8 @@ package com.android.messaging.ui.mediapicker;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.PickVisualMediaRequest;
@@ -33,10 +35,11 @@ import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.FileUtil;
import com.android.messaging.util.ImageUtils;
import com.android.messaging.util.SafeAsyncTask;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Wraps around the functionalities to allow the user to pick an image/video/audio from the document
@@ -108,24 +111,24 @@ public class DocumentImagePicker {
// Notify our listener with a PendingAttachmentData containing the metadata.
// Asynchronously get the content type for the picked image since
// ImageUtils.getContentType() potentially involves I/O and can be expensive.
new SafeAsyncTask<Void, Void, String>() {
@Override
protected String doInBackgroundTimed(final Void... params) {
if (FileUtil.isInPrivateDir(documentUri) &&
!MediaScratchFileProvider.isMediaScratchSpaceUri(documentUri)) {
// hacker sending private app data. Bail out
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.ERROR)) {
LogUtil.e(LogUtil.BUGLE_TAG, "Aborting attach of private app data ("
+ documentUri + ")");
}
return null;
ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());
executor.execute(() -> {
final String contentType;
if (FileUtil.isInPrivateDir(documentUri) &&
!MediaScratchFileProvider.isMediaScratchSpaceUri(documentUri)) {
// hacker sending private app data. Bail out
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.ERROR)) {
LogUtil.e(LogUtil.BUGLE_TAG, "Aborting attach of private app data ("
+ documentUri + ")");
}
return ImageUtils.getContentType(
contentType = null;
} else {
contentType = ImageUtils.getContentType(
Factory.get().getApplicationContext().getContentResolver(), documentUri);
}
@Override
protected void onPostExecute(final String contentType) {
handler.post(() -> {
if (contentType == null) {
return; // bad uri on input
}
@@ -134,7 +137,7 @@ public class DocumentImagePicker {
PendingAttachmentData.createPendingAttachmentData(contentType,
documentUri);
mListener.onDocumentSelected(pendingItem);
}
}.executeOnThreadPool();
});
});
}
}
@@ -26,10 +26,10 @@ import com.android.messaging.datamodel.MediaScratchFileProvider;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContentType;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.SafeAsyncTask;
import com.android.messaging.util.UiUtils;
import java.io.IOException;
import java.util.concurrent.Executors;
/**
* Wraps around the functionalities of MediaRecorder, performs routine setup for audio recording
@@ -147,7 +147,7 @@ public class LevelTrackingMediaRecorder {
"media recorder. " + ex);
if (mOutputUri != null) {
final Uri outputUri = mOutputUri;
SafeAsyncTask.executeOnThreadPool(() ->
Executors.newSingleThreadExecutor().execute(() ->
Factory.get().getApplicationContext().getContentResolver().delete(
outputUri, null, null));
mOutputUri = null;
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2015 The Android Open Source Project
* Copyright (C) 2024 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -157,7 +158,7 @@ public class BuglePhotoViewController extends PhotoViewController {
}
final String photoUri = adapter.getPhotoUri(cursor);
new ConversationFragment.SaveAttachmentTask(((Activity) getActivity()),
Uri.parse(photoUri), adapter.getContentType(cursor)).executeOnThreadPool();
Uri.parse(photoUri), adapter.getContentType(cursor)).execute();
return true;
} else {
return super.onOptionsItemSelected(item);