diff --git a/src/com/android/messaging/datamodel/ParticipantRefresh.java b/src/com/android/messaging/datamodel/ParticipantRefresh.java index 7978466..4be83fd 100644 --- a/src/com/android/messaging/datamodel/ParticipantRefresh.java +++ b/src/com/android/messaging/datamodel/ParticipantRefresh.java @@ -39,7 +39,6 @@ import com.android.messaging.util.ContactUtil; 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.google.common.base.Joiner; import java.util.ArrayList; @@ -47,6 +46,7 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -146,7 +146,7 @@ public class ParticipantRefresh { if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { LogUtil.v(TAG, "Started full participant refresh"); } - SafeAsyncTask.executeOnThreadPool(sFullRefreshRunnable); + Executors.newSingleThreadExecutor().execute(sFullRefreshRunnable); } else if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { LogUtil.v(TAG, "Skipped full participant refresh"); } @@ -156,7 +156,7 @@ public class ParticipantRefresh { * Refresh self participants on subscription or settings change. */ public static void refreshSelfParticipants() { - SafeAsyncTask.executeOnThreadPool(sSelfOnlyRefreshRunnable); + Executors.newSingleThreadExecutor().execute(sSelfOnlyRefreshRunnable); } private static boolean getNeedFullRefresh() { diff --git a/src/com/android/messaging/datamodel/data/DraftMessageData.java b/src/com/android/messaging/datamodel/data/DraftMessageData.java index fa9cc19..95f8f05 100644 --- a/src/com/android/messaging/datamodel/data/DraftMessageData.java +++ b/src/com/android/messaging/datamodel/data/DraftMessageData.java @@ -18,6 +18,8 @@ package com.android.messaging.datamodel.data; import android.net.Uri; +import android.os.Handler; +import android.os.Looper; import android.text.TextUtils; import com.android.messaging.datamodel.MessageTextStats; @@ -37,7 +39,6 @@ import com.android.messaging.util.Assert.RunsOnMainThread; import com.android.messaging.util.BugleGservicesKeys; import com.android.messaging.util.LogUtil; import com.android.messaging.util.PhoneUtils; -import com.android.messaging.util.SafeAsyncTask; import java.util.ArrayList; import java.util.Collection; @@ -45,6 +46,11 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; public class DraftMessageData extends BindableData implements ReadDraftDataActionListener { @@ -678,7 +684,7 @@ public class DraftMessageData extends BindableData implements ReadDraftDataActio // Any change in the draft will cancel any pending draft checking task, since the // size/status of the draft may have changed. if (mCheckDraftForSendTask != null) { - mCheckDraftForSendTask.cancel(true /* mayInterruptIfRunning */); + mCheckDraftForSendTask.cancel(); mCheckDraftForSendTask = null; } mListeners.onDraftChanged(this, changeFlags); @@ -708,7 +714,7 @@ public class DraftMessageData extends BindableData implements ReadDraftDataActio public void checkDraftForAction(final boolean checkMessageSize, final int selfSubId, final CheckDraftTaskCallback callback, final Binding binding) { new CheckDraftForSendTask(checkMessageSize, selfSubId, callback, binding) - .executeOnThreadPool((Void) null); + .execute(); } /** @@ -750,7 +756,7 @@ public class DraftMessageData extends BindableData implements ReadDraftDataActio void onDraftChecked(DraftMessageData data, int result); } - public class CheckDraftForSendTask extends SafeAsyncTask { + public class CheckDraftForSendTask { public static final int RESULT_PASSED = 0; public static final int RESULT_HAS_PENDING_ATTACHMENTS = 1; public static final int RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS = 2; @@ -762,6 +768,10 @@ public class DraftMessageData extends BindableData implements ReadDraftDataActio private final CheckDraftTaskCallback mCallback; private final String mBindingId; private final List mAttachmentsCopy; + ScheduledExecutorService mExecutor = Executors.newScheduledThreadPool(2); + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private Future mFuture; + private boolean mCancelled; private int mPreExecuteResult = RESULT_PASSED; public CheckDraftForSendTask(final boolean checkMessageSize, final int selfSubId, @@ -777,7 +787,51 @@ public class DraftMessageData extends BindableData implements ReadDraftDataActio mCheckDraftForSendTask = this; } - @Override + public void execute() { + onPreExecute(); + + mFuture = mExecutor.submit(() -> { + final int result; + if (mPreExecuteResult != RESULT_PASSED) { + result = mPreExecuteResult; + } else if (mCheckMessageSize && getIsMessageOverLimit()) { + result = RESULT_MESSAGE_OVER_LIMIT; + } else { + result = RESULT_PASSED; + } + + mHandler.post(() -> { + mCheckDraftForSendTask = null; + // Only call back if we are bound to the original binding. + if (isBound(mBindingId) && !mCancelled) { + mCallback.onDraftChecked(DraftMessageData.this, result); + } else { + if (!isBound(mBindingId)) { + LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft not bound"); + } + if (mCancelled) { + LogUtil.w(LogUtil.BUGLE_TAG, + "Message can't be sent: draft is cancelled"); + } + } + }); + }); + + mExecutor.schedule(this::cancel, 10, TimeUnit.SECONDS); + } + + public void cancel() { + if (!mFuture.isDone()) { + mFuture.cancel(true); + mCancelled = true; + mCheckDraftForSendTask = null; + } + } + + public boolean isCancelled() { + return mCancelled; + } + protected void onPreExecute() { // Perform checking work that can happen on the main thread. if (hasPendingAttachments()) { @@ -803,39 +857,6 @@ public class DraftMessageData extends BindableData implements ReadDraftDataActio } } - @Override - protected Integer doInBackgroundTimed(Void... params) { - if (mPreExecuteResult != RESULT_PASSED) { - return mPreExecuteResult; - } - - if (mCheckMessageSize && getIsMessageOverLimit()) { - return RESULT_MESSAGE_OVER_LIMIT; - } - return RESULT_PASSED; - } - - @Override - protected void onPostExecute(Integer result) { - mCheckDraftForSendTask = null; - // Only call back if we are bound to the original binding. - if (isBound(mBindingId) && !isCancelled()) { - mCallback.onDraftChecked(DraftMessageData.this, result); - } else { - if (!isBound(mBindingId)) { - LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft not bound"); - } - if (isCancelled()) { - LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: draft is cancelled"); - } - } - } - - @Override - protected void onCancelled() { - mCheckDraftForSendTask = null; - } - /** * 1. Check if the draft message contains too many attachments to send * 2. Computes the minimum size that this message could be compressed/downsampled/encoded diff --git a/src/com/android/messaging/datamodel/data/MessagePartData.java b/src/com/android/messaging/datamodel/data/MessagePartData.java index d5030c5..bbfebf7 100644 --- a/src/com/android/messaging/datamodel/data/MessagePartData.java +++ b/src/com/android/messaging/datamodel/data/MessagePartData.java @@ -43,10 +43,10 @@ import com.android.messaging.util.ContentType; import com.android.messaging.util.GifTranscoder; import com.android.messaging.util.ImageUtils; import com.android.messaging.util.LogUtil; -import com.android.messaging.util.SafeAsyncTask; import com.android.messaging.util.UriUtil; import java.util.Arrays; +import java.util.concurrent.Executors; /** * Represents a single message part. Messages consist of one or more parts which may contain @@ -444,7 +444,7 @@ public class MessagePartData implements Parcelable { public void destroyAsync() { final Uri contentUri = shouldDestroy(); if (contentUri != null) { - SafeAsyncTask.executeOnThreadPool(() -> + Executors.newSingleThreadExecutor().execute(() -> Factory.get().getApplicationContext().getContentResolver().delete( contentUri, null, null)); } diff --git a/src/com/android/messaging/datamodel/data/PendingAttachmentData.java b/src/com/android/messaging/datamodel/data/PendingAttachmentData.java index 34b3b30..30c3dcc 100644 --- a/src/com/android/messaging/datamodel/data/PendingAttachmentData.java +++ b/src/com/android/messaging/datamodel/data/PendingAttachmentData.java @@ -17,6 +17,8 @@ package com.android.messaging.datamodel.data; import android.net.Uri; +import android.os.Handler; +import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; @@ -24,9 +26,13 @@ import androidx.annotation.NonNull; 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.UriUtil; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + /** * Represents a "pending" message part that acts as a placeholder for the actual attachment being * loaded. It handles the task to load and persist the attachment from a Uri to local scratch @@ -98,37 +104,26 @@ public class PendingAttachmentData extends MessagePartData { } mCurrentState = STATE_LOADING; - // Kick off a SafeAsyncTask to load the content of the media and persist it locally. + // Kick off loading the content of the media and persist it locally. // Note: we need to persist the media locally even if it's not remote, because we // want to be able to resend the media in case the message failed to send. - new SafeAsyncTask(LOAD_MEDIA_TIME_LIMIT_MILLIS, - true /* cancelExecutionOnTimeout */) { - @Override - protected MessagePartData doInBackgroundTimed(final Void... params) { - final Uri contentUri = getContentUri(); - final Uri persistedUri = UriUtil.persistContentToScratchSpace(contentUri); - if (persistedUri != null) { - return MessagePartData.createMediaMessagePart( - getText(), - getContentType(), - persistedUri, - getWidth(), - getHeight()); - } - return null; + ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); + Handler handler = new Handler(Looper.getMainLooper()); + final Future future = executor.submit(() -> { + MessagePartData data = null; + final Uri contentUri = getContentUri(); + final Uri persistedUri = UriUtil.persistContentToScratchSpace(contentUri); + if (persistedUri != null) { + data = MessagePartData.createMediaMessagePart( + getText(), + getContentType(), + persistedUri, + getWidth(), + getHeight()); } - @Override - protected void onCancelled() { - LogUtil.w(LogUtil.BUGLE_TAG, "Timeout while retrieving media"); - mCurrentState = STATE_FAILED; - if (draftMessageData.isBound(bindingId)) { - draftMessageData.removePendingAttachment(PendingAttachmentData.this); - } - } - - @Override - protected void onPostExecute(final MessagePartData attachment) { + final MessagePartData attachment = data; + handler.post(() -> { if (attachment != null) { mCurrentState = STATE_LOADED; if (draftMessageData.isBound(bindingId)) { @@ -147,8 +142,18 @@ public class PendingAttachmentData extends MessagePartData { draftMessageData.removePendingAttachment(PendingAttachmentData.this); } } + }); + }); + executor.schedule(() -> { + if (!future.isDone()) { + future.cancel(true); + LogUtil.w(LogUtil.BUGLE_TAG, "Timeout while retrieving media"); + mCurrentState = STATE_FAILED; + if (draftMessageData.isBound(bindingId)) { + draftMessageData.removePendingAttachment(PendingAttachmentData.this); + } } - }.executeOnThreadPool(); + }, LOAD_MEDIA_TIME_LIMIT_MILLIS, TimeUnit.MILLISECONDS); } protected PendingAttachmentData(final Parcel in) { diff --git a/src/com/android/messaging/datamodel/media/MediaResourceManager.java b/src/com/android/messaging/datamodel/media/MediaResourceManager.java index c529ac9..1a14834 100644 --- a/src/com/android/messaging/datamodel/media/MediaResourceManager.java +++ b/src/com/android/messaging/datamodel/media/MediaResourceManager.java @@ -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. @@ -16,7 +16,8 @@ */ package com.android.messaging.datamodel.media; -import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; import com.android.messaging.Factory; import com.android.messaging.util.Assert; @@ -227,35 +228,30 @@ public class MediaResourceManager { if (bindableRequest != null && !bindableRequest.isBound()) { return; // Request is obsolete } - // We don't use SafeAsyncTask here since it enforces the shared thread pool executor - // whereas we want a dedicated thread pool executor. - AsyncTask> mediaLoadingTask = new AsyncTask<>() { - private Exception mException; - @Override - protected MediaLoadingResult doInBackground(Void... params) { - // Double check the request is still valid by the time we start processing it - if (bindableRequest != null && !bindableRequest.isBound()) { - return null; // Request is obsolete - } + Handler handler = new Handler(Looper.getMainLooper()); + executor.execute(() -> { + Exception exception = null; + + MediaLoadingResult tmpResult = null; + // Double check the request is still valid by the time we start processing it + if (bindableRequest != null && bindableRequest.isBound()) { try { - return processMediaRequestInternal(mediaRequest); + tmpResult = processMediaRequestInternal(mediaRequest); } catch (Exception e) { - mException = e; - return null; + exception = e; } } - @Override - protected void onPostExecute(final MediaLoadingResult result) { + final Exception mException = exception; + final MediaLoadingResult result = tmpResult; + handler.post(() -> { if (result != null) { Assert.isNull(mException); Assert.isTrue(result.loadedResource.getRefCount() > 0); try { - if (bindableRequest != null) { - bindableRequest.onMediaResourceLoaded( - bindableRequest, result.loadedResource, result.fromCache); - } + bindableRequest.onMediaResourceLoaded( + bindableRequest, result.loadedResource, result.fromCache); } finally { result.loadedResource.release(); result.scheduleChainedRequests(); @@ -263,9 +259,7 @@ public class MediaResourceManager { } else if (mException != null) { LogUtil.e(LogUtil.BUGLE_TAG, "Asynchronous media loading failed, key=" + mediaRequest.getKey(), mException); - if (bindableRequest != null) { - bindableRequest.onMediaResourceLoadError(bindableRequest, mException); - } + bindableRequest.onMediaResourceLoadError(bindableRequest, mException); } else { Assert.isTrue(bindableRequest == null || !bindableRequest.isBound()); if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { @@ -273,9 +267,8 @@ public class MediaResourceManager { LogUtil.sanitizePII(mediaRequest.getKey()) /* key with phone# */); } } - } - }; - mediaLoadingTask.executeOnExecutor(executor, (Void) null); + }); + }); } @RunsOnAnyThread diff --git a/src/com/android/messaging/datamodel/media/VCardResourceEntry.java b/src/com/android/messaging/datamodel/media/VCardResourceEntry.java index 0b33d16..47f0111 100644 --- a/src/com/android/messaging/datamodel/media/VCardResourceEntry.java +++ b/src/com/android/messaging/datamodel/media/VCardResourceEntry.java @@ -32,7 +32,6 @@ import com.android.messaging.datamodel.MediaScratchFileProvider; import com.android.messaging.datamodel.data.PersonItemData; import com.android.messaging.util.ContactUtil; import com.android.messaging.util.LogUtil; -import com.android.messaging.util.SafeAsyncTask; import com.android.vcard.VCardEntry; import com.android.vcard.VCardEntry.EmailData; import com.android.vcard.VCardEntry.ImData; @@ -47,6 +46,7 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Executors; /** * Holds one entry item (i.e. a single contact) within a VCard resource. It is able to take @@ -72,7 +72,7 @@ public class VCardResourceEntry { void close() { // If the avatar image was temporarily saved in the scratch folder, remove that. if (MediaScratchFileProvider.isMediaScratchSpaceUri(mAvatarUri)) { - SafeAsyncTask.executeOnThreadPool(() -> + Executors.newSingleThreadExecutor().execute(() -> Factory.get().getApplicationContext().getContentResolver().delete( mAvatarUri, null, null)); } diff --git a/src/com/android/messaging/sms/MmsConfig.java b/src/com/android/messaging/sms/MmsConfig.java index c157b05..ae5449c 100644 --- a/src/com/android/messaging/sms/MmsConfig.java +++ b/src/com/android/messaging/sms/MmsConfig.java @@ -26,12 +26,12 @@ import com.android.messaging.datamodel.data.ParticipantData; import com.android.messaging.util.Assert; import com.android.messaging.util.LogUtil; import com.android.messaging.util.PhoneUtils; -import com.android.messaging.util.SafeAsyncTask; import com.google.common.collect.Maps; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Executors; /** * MMS configuration. @@ -134,7 +134,7 @@ public class MmsConfig { * Same as load() but doing it using an async thread from SafeAsyncTask thread pool. */ public static void loadAsync() { - SafeAsyncTask.executeOnThreadPool(MmsConfig::load); + Executors.newSingleThreadExecutor().execute(MmsConfig::load); } /** diff --git a/src/com/android/messaging/ui/VCardDetailFragment.java b/src/com/android/messaging/ui/VCardDetailFragment.java index 96d269d..e7ddb7c 100644 --- a/src/com/android/messaging/ui/VCardDetailFragment.java +++ b/src/com/android/messaging/ui/VCardDetailFragment.java @@ -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() { - @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); diff --git a/src/com/android/messaging/ui/contact/ContactRecipientAutoCompleteView.java b/src/com/android/messaging/ui/contact/ContactRecipientAutoCompleteView.java index 24cf001..24f70a5 100644 --- a/src/com/android/messaging/ui/contact/ContactRecipientAutoCompleteView.java +++ b/src/com/android/messaging/ui/contact/ContactRecipientAutoCompleteView.java @@ -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 { + 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(); } /** diff --git a/src/com/android/messaging/ui/conversation/ComposeMessageView.java b/src/com/android/messaging/ui/conversation/ComposeMessageView.java index 839a587..68ec952 100644 --- a/src/com/android/messaging/ui/conversation/ComposeMessageView.java +++ b/src/com/android/messaging/ui/conversation/ComposeMessageView.java @@ -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, 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... params) { - final List 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 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); diff --git a/src/com/android/messaging/ui/conversation/ConversationFragment.java b/src/com/android/messaging/ui/conversation/ConversationFragment.java index e0224bf..d211c9a 100644 --- a/src/com/android/messaging/ui/conversation/ConversationFragment.java +++ b/src/com/android/messaging/ui/conversation/ConversationFragment.java @@ -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 { + 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 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; diff --git a/src/com/android/messaging/ui/mediapicker/AudioRecordView.java b/src/com/android/messaging/ui/mediapicker/AudioRecordView.java index ae63c71..29eaa91 100644 --- a/src/com/android/messaging/ui/mediapicker/AudioRecordView.java +++ b/src/com/android/messaging/ui/mediapicker/AudioRecordView.java @@ -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)); } diff --git a/src/com/android/messaging/ui/mediapicker/ContactMediaChooser.java b/src/com/android/messaging/ui/mediapicker/ContactMediaChooser.java index d476dab..8dcd042 100644 --- a/src/com/android/messaging/ui/mediapicker/ContactMediaChooser.java +++ b/src/com/android/messaging/ui/mediapicker/ContactMediaChooser.java @@ -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); diff --git a/src/com/android/messaging/ui/mediapicker/DocumentImagePicker.java b/src/com/android/messaging/ui/mediapicker/DocumentImagePicker.java index 3d3a7ce..305fdd4 100644 --- a/src/com/android/messaging/ui/mediapicker/DocumentImagePicker.java +++ b/src/com/android/messaging/ui/mediapicker/DocumentImagePicker.java @@ -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() { - @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(); + }); + }); } } diff --git a/src/com/android/messaging/ui/mediapicker/LevelTrackingMediaRecorder.java b/src/com/android/messaging/ui/mediapicker/LevelTrackingMediaRecorder.java index 2507b36..07150fd 100644 --- a/src/com/android/messaging/ui/mediapicker/LevelTrackingMediaRecorder.java +++ b/src/com/android/messaging/ui/mediapicker/LevelTrackingMediaRecorder.java @@ -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; diff --git a/src/com/android/messaging/ui/photoviewer/BuglePhotoViewController.java b/src/com/android/messaging/ui/photoviewer/BuglePhotoViewController.java index 3d49e73..eee9f43 100644 --- a/src/com/android/messaging/ui/photoviewer/BuglePhotoViewController.java +++ b/src/com/android/messaging/ui/photoviewer/BuglePhotoViewController.java @@ -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); diff --git a/src/com/android/messaging/util/SafeAsyncTask.java b/src/com/android/messaging/util/SafeAsyncTask.java deleted file mode 100644 index e2495ba..0000000 --- a/src/com/android/messaging/util/SafeAsyncTask.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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. - * 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 com.android.messaging.util; - -import android.content.Intent; -import android.os.AsyncTask; -import android.os.Debug; -import android.os.SystemClock; - -import com.android.messaging.Factory; -import com.android.messaging.util.Assert.RunsOnAnyThread; - -/** - * Wrapper class which provides explicit API for: - *
    - *
  1. Threading policy choice - Users of this class should use the explicit API instead of - * {@link #execute} which uses different threading policy on different OS versions. - *
  2. Enforce creation on main thread as required by AsyncTask - *
  3. Enforce that the background task does not take longer than expected. - *
- */ -public abstract class SafeAsyncTask - extends AsyncTask { - private static final long DEFAULT_MAX_EXECUTION_TIME_MILLIS = 10 * 1000; // 10 seconds - - /** This is strongly discouraged as it can block other AsyncTasks indefinitely. */ - public static final long UNBOUNDED_TIME = Long.MAX_VALUE; - - private static final String WAKELOCK_ID = "bugle_safe_async_task_wakelock"; - protected static final int WAKELOCK_OP = 1000; - private static final WakeLockHelper sWakeLock = new WakeLockHelper(WAKELOCK_ID); - - private final long mMaxExecutionTimeMillis; - private final boolean mCancelExecutionOnTimeout; - private boolean mThreadPoolRequested; - - public SafeAsyncTask() { - this(DEFAULT_MAX_EXECUTION_TIME_MILLIS, false); - } - - public SafeAsyncTask(final long maxTimeMillis) { - this(maxTimeMillis, false); - } - - /** - * @param maxTimeMillis maximum expected time for the background operation. This is just - * a diagnostic tool to catch unexpectedly long operations. If an operation does take - * longer than expected, it is fine to increase this argument. If the value is larger - * than a minute, you should consider using a dedicated thread so as not to interfere - * with other AsyncTasks. - * - *

Use {@link #UNBOUNDED_TIME} if you do not know the maximum expected time. This - * is strongly discouraged as it can block other AsyncTasks indefinitely. - * - * @param cancelExecutionOnTimeout whether to attempt to cancel the task execution on timeout. - * If this is set, at execution timeout we will call cancel(), so doInBackgroundTimed() - * should periodically check if the task is to be cancelled and finish promptly if - * possible, and handle the cancel event in onCancelled(). Also, at the end of execution - * we will not crash the execution if it went over limit since we explicitly canceled it. - */ - public SafeAsyncTask(final long maxTimeMillis, final boolean cancelExecutionOnTimeout) { - Assert.isMainThread(); // AsyncTask has to be created on the main thread - mMaxExecutionTimeMillis = maxTimeMillis; - mCancelExecutionOnTimeout = cancelExecutionOnTimeout; - } - - public final SafeAsyncTask executeOnThreadPool( - final Params... params) { - Assert.isMainThread(); // AsyncTask requires this - mThreadPoolRequested = true; - executeOnExecutor(THREAD_POOL_EXECUTOR, params); - return this; - } - - protected abstract Result doInBackgroundTimed(final Params... params); - - @Override - protected final Result doInBackground(final Params... params) { - // This enforces that executeOnThreadPool was called, not execute. Ideally, we would - // make execute throw an exception, but since it is final, we cannot override it. - Assert.isTrue(mThreadPoolRequested); - - if (mCancelExecutionOnTimeout) { - ThreadUtil.getMainThreadHandler().postDelayed(() -> { - if (getStatus() == Status.RUNNING) { - // Cancel the task if it's still running. - LogUtil.w(LogUtil.BUGLE_TAG, String.format("%s timed out and is canceled", - this)); - cancel(true /* mayInterruptIfRunning */); - } - }, mMaxExecutionTimeMillis); - } - - final long startTime = SystemClock.elapsedRealtime(); - try { - return doInBackgroundTimed(params); - } finally { - final long executionTime = SystemClock.elapsedRealtime() - startTime; - if (executionTime > mMaxExecutionTimeMillis) { - LogUtil.w(LogUtil.BUGLE_TAG, String.format("%s took %dms", this, executionTime)); - // Don't crash if debugger is attached or if we are asked to cancel on timeout. - if (!Debug.isDebuggerConnected() && !mCancelExecutionOnTimeout) { - Assert.fail(this + " took too long"); - } - } - } - - } - - @Override - protected void onPostExecute(final Result result) { - // No need to use AsyncTask at all if there is no onPostExecute - Assert.fail("Use SafeAsyncTask.executeOnThreadPool"); - } - - /** - * This provides a way for people to run async tasks but without onPostExecute. - * This can be called on any thread. - * - * Run code in a thread using AsyncTask's thread pool. - * - * To enable wakelock during the execution, see {@link #executeOnThreadPool(Runnable, boolean)} - * - * @param runnable The Runnable to execute asynchronously - */ - @RunsOnAnyThread - public static void executeOnThreadPool(final Runnable runnable) { - executeOnThreadPool(runnable, false); - } - - /** - * This provides a way for people to run async tasks but without onPostExecute. - * This can be called on any thread. - * - * Run code in a thread using AsyncTask's thread pool. - * - * @param runnable The Runnable to execute asynchronously - * @param withWakeLock when set, a wake lock will be held for the duration of the runnable - * execution - */ - public static void executeOnThreadPool(final Runnable runnable, final boolean withWakeLock) { - if (withWakeLock) { - final Intent intent = new Intent(); - sWakeLock.acquire(Factory.get().getApplicationContext(), intent, WAKELOCK_OP); - THREAD_POOL_EXECUTOR.execute(() -> { - try { - runnable.run(); - } finally { - sWakeLock.release(intent, WAKELOCK_OP); - } - }); - } else { - THREAD_POOL_EXECUTOR.execute(runnable); - } - } -} diff --git a/src/com/android/messaging/widget/BugleWidgetProvider.java b/src/com/android/messaging/widget/BugleWidgetProvider.java index fd565ad..34cf351 100644 --- a/src/com/android/messaging/widget/BugleWidgetProvider.java +++ b/src/com/android/messaging/widget/BugleWidgetProvider.java @@ -28,9 +28,10 @@ import com.android.messaging.R; import com.android.messaging.ui.UIIntents; import com.android.messaging.util.LogUtil; import com.android.messaging.util.OsUtil; -import com.android.messaging.util.SafeAsyncTask; import com.android.messaging.util.UiUtils; +import java.util.concurrent.Executors; + public class BugleWidgetProvider extends BaseWidgetProvider { public static final String ACTION_NOTIFY_CONVERSATIONS_CHANGED = "com.android.Bugle.intent.action.ACTION_NOTIFY_CONVERSATIONS_CHANGED"; @@ -43,7 +44,7 @@ public class BugleWidgetProvider extends BaseWidgetProvider { @Override protected void updateWidget(final Context context, final int appWidgetId) { if (OsUtil.hasRequiredPermissions()) { - SafeAsyncTask.executeOnThreadPool(() -> rebuildWidget(context, appWidgetId)); + Executors.newSingleThreadExecutor().execute(() -> rebuildWidget(context, appWidgetId)); } else { AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, UiUtils.getWidgetMissingPermissionView(context)); diff --git a/src/com/android/messaging/widget/WidgetConversationProvider.java b/src/com/android/messaging/widget/WidgetConversationProvider.java index 6f76c84..fa79044 100644 --- a/src/com/android/messaging/widget/WidgetConversationProvider.java +++ b/src/com/android/messaging/widget/WidgetConversationProvider.java @@ -36,9 +36,10 @@ import com.android.messaging.ui.UIIntents; import com.android.messaging.ui.WidgetPickConversationActivity; import com.android.messaging.util.LogUtil; import com.android.messaging.util.OsUtil; -import com.android.messaging.util.SafeAsyncTask; import com.android.messaging.util.UiUtils; +import java.util.concurrent.Executors; + public class WidgetConversationProvider extends BaseWidgetProvider { public static final String ACTION_NOTIFY_MESSAGES_CHANGED = "com.android.Bugle.intent.action.ACTION_NOTIFY_MESSAGES_CHANGED"; @@ -149,7 +150,8 @@ public class WidgetConversationProvider extends BaseWidgetProvider { // widget dependent on ConversationListItemData. However, we have to update // the widget regardless, even with those missing pieces. Here we update the // widget again in the background. - SafeAsyncTask.executeOnThreadPool(() -> rebuildWidget(context, appWidgetId)); + Executors.newSingleThreadExecutor().execute(() -> + rebuildWidget(context, appWidgetId)); } }