Fix a bunch of lint issues.

This commit is contained in:
Greyson Parrelli
2026-06-16 09:56:12 -04:00
parent d44bef0eda
commit b16481616a
15 changed files with 790 additions and 194 deletions
@@ -600,7 +600,18 @@ public final class ContactSelectionListFragment extends LoggingFragment {
boolean isUnknown = contact instanceof ContactSearchKey.UnknownRecipientKey;
SelectedContact selectedContact = contact.requireSelectedContact();
if (!canSelectSelf && !selectedContact.hasUsername() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId())) {
boolean needsSelfCheck = !canSelectSelf && !selectedContact.hasUsername();
if (needsSelfCheck) {
lifecycleDisposable.add(contactChipViewModel.isSelf(selectedContact)
.subscribe(isSelf -> onItemClickResolved(contact, selectedContact, isUnknown, isSelf)));
} else {
onItemClickResolved(contact, selectedContact, isUnknown, false);
}
}
private void onItemClickResolved(ContactSearchKey contact, SelectedContact selectedContact, boolean isUnknown, boolean isSelf) {
if (isSelf) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
return;
}
@@ -1,107 +1,27 @@
package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.activity.ComponentActivity;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.conversation.NewConversationActivity;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Rfc5724Uri;
import java.net.URISyntaxException;
public class SystemContactsEntrypointActivity extends Activity {
private static final String TAG = Log.tag(SystemContactsEntrypointActivity.class);
public class SystemContactsEntrypointActivity extends ComponentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
startActivity(getNextIntent(getIntent()));
finish();
super.onCreate(savedInstanceState);
}
private Intent getNextIntent(Intent original) {
DestinationAndBody destination;
SystemContactsEntrypointViewModel viewModel = new ViewModelProvider(this).get(SystemContactsEntrypointViewModel.class);
if (original.getData() != null && "content".equals(original.getData().getScheme())) {
destination = getDestinationForSyncAdapter(original);
} else {
destination = getDestinationForView(original);
}
final Intent nextIntent;
if (TextUtils.isEmpty(destination.destination)) {
nextIntent = NewConversationActivity.createIntent(this, destination.getBody());
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
} else {
Recipient recipient = Recipient.external(destination.getDestination());
if (recipient != null) {
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
nextIntent = ConversationIntents.createBuilderSync(this, recipient.getId(), threadId)
.withDraftText(destination.getBody())
.build();
} else {
nextIntent = NewConversationActivity.createIntent(this, destination.getBody());
viewModel.getContactAction().observe(this, nextStep -> {
if (nextStep.getShowSpecifyRecipientToast()) {
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
}
}
return nextIntent;
}
startActivity(nextStep.getIntent());
finish();
});
private @NonNull DestinationAndBody getDestinationForView(Intent intent) {
try {
Rfc5724Uri smsUri = new Rfc5724Uri(intent.getData().toString());
return new DestinationAndBody(smsUri.getPath(), smsUri.getQueryParams().get("body"));
} catch (URISyntaxException e) {
Log.w(TAG, "unable to parse RFC5724 URI from intent", e);
return new DestinationAndBody("", "");
}
}
private @NonNull DestinationAndBody getDestinationForSyncAdapter(Intent intent) {
Cursor cursor = null;
try {
cursor = getContentResolver().query(intent.getData(), null, null, null, null);
if (cursor != null && cursor.moveToNext()) {
return new DestinationAndBody(cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.Data.DATA1)), "");
}
return new DestinationAndBody("", "");
} finally {
if (cursor != null) cursor.close();
}
}
private static class DestinationAndBody {
private final String destination;
private final String body;
private DestinationAndBody(String destination, String body) {
this.destination = destination;
this.body = body;
}
public String getDestination() {
return destination;
}
public String getBody() {
return body;
}
viewModel.resolveNextStep(getIntent());
}
}
@@ -0,0 +1,100 @@
package org.thoughtcrime.securesms
import android.content.Context
import android.content.Intent
import android.provider.ContactsContract
import android.text.TextUtils
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.conversation.NewConversationActivity
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.Rfc5724Uri
import java.net.URISyntaxException
class SystemContactsEntrypointViewModel : ViewModel() {
companion object {
private val TAG = Log.tag(SystemContactsEntrypointViewModel::class.java)
}
private val internalContactAction = MutableLiveData<ContactAction>()
val contactAction: LiveData<ContactAction> = internalContactAction
fun resolveNextStep(original: Intent) {
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
getContactAction(AppDependencies.application, original)
}
internalContactAction.value = result
}
}
@WorkerThread
private fun getContactAction(context: Context, original: Intent): ContactAction {
val destination = if (original.data != null && "content" == original.data?.scheme) {
getDestinationForSyncAdapter(context, original)
} else {
getDestinationForView(original)
}
val destinationAddress = destination.destination
if (TextUtils.isEmpty(destinationAddress)) {
return ContactAction(NewConversationActivity.createIntent(context, destination.body), true)
}
val recipient = Recipient.external(destinationAddress!!)
if (recipient != null) {
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val nextIntent = ConversationIntents.createBuilderSync(context, recipient.id, threadId)
.withDraftText(destination.body)
.build()
return ContactAction(nextIntent, false)
}
return ContactAction(NewConversationActivity.createIntent(context, destination.body), true)
}
private fun getDestinationForView(intent: Intent): DestinationAndBody {
return try {
val smsUri = Rfc5724Uri(intent.data.toString())
DestinationAndBody(smsUri.path, smsUri.queryParams["body"])
} catch (e: URISyntaxException) {
Log.w(TAG, "unable to parse RFC5724 URI from intent", e)
DestinationAndBody("", "")
}
}
private fun getDestinationForSyncAdapter(context: Context, intent: Intent): DestinationAndBody {
context.contentResolver.query(intent.data!!, null, null, null, null).use { cursor ->
if (cursor != null && cursor.moveToNext()) {
return DestinationAndBody(cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.Data.DATA1)), "")
}
return DestinationAndBody("", "")
}
}
data class ContactAction(
val intent: Intent,
val showSpecifyRecipientToast: Boolean
)
private data class DestinationAndBody(
val destination: String?,
val body: String?
)
}
@@ -72,6 +72,12 @@ class ContactChipViewModel : ViewModel() {
}
}
fun isSelf(selectedContact: SelectedContact): Single<Boolean> {
return Single.fromCallable { Recipient.self().id == selectedContact.getOrCreateRecipientId() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
private fun getOrCreateRecipientId(selectedContact: SelectedContact): Single<RecipientId> {
return Single.fromCallable {
selectedContact.getOrCreateRecipientId()
@@ -45,7 +45,6 @@ object JumboEmoji {
private var currentVersion: Int = -1
@JvmStatic
@MainThread
fun updateCurrentVersion(context: Context) {
SignalExecutors.BOUNDED.execute {
val version: EmojiFiles.Version = EmojiFiles.Version.readVersion(context, true) ?: return@execute
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.longmessage;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@@ -14,16 +15,19 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
class LongMessage {
private final ConversationMessage conversationMessage;
private final CharSequence fullBody;
LongMessage(@NonNull ConversationMessage conversationMessage) {
@WorkerThread
LongMessage(@NonNull ConversationMessage conversationMessage, @NonNull Context context) {
this.conversationMessage = conversationMessage;
this.fullBody = conversationMessage.getDisplayBody(context);
}
@NonNull MessageRecord getMessageRecord() {
return conversationMessage.getMessageRecord();
}
@NonNull CharSequence getFullBody(@NonNull Context context) {
return conversationMessage.getDisplayBody(context);
@NonNull CharSequence getFullBody() {
return fullBody;
}
}
@@ -122,7 +122,7 @@ public class LongMessageFragment extends FullScreenDialogFragment {
EmojiTextView text = bubble.findViewById(R.id.longmessage_text);
ConversationItemFooter footer = bubble.findViewById(R.id.longmessage_footer);
SpannableString body = new SpannableString(getTrimmedBody(message.get().getFullBody(requireContext())));
SpannableString body = new SpannableString(getTrimmedBody(message.get().getFullBody()));
V2ConversationItemUtils.linkifyUrlLinks(body,
true,
url -> CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url) ||
@@ -37,7 +37,7 @@ class LongMessageRepository {
Optional<MmsMessageRecord> record = getMmsMessage(mmsDatabase, messageId);
if (record.isPresent()) {
final ConversationMessage resolvedMessage = LongMessageResolveerKt.resolveBody(record.get(), context);
return Optional.of(new LongMessage(resolvedMessage));
return Optional.of(new LongMessage(resolvedMessage, context));
} else {
return Optional.empty();
}
@@ -1,22 +1,18 @@
package org.thoughtcrime.securesms.mediasend
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import org.signal.core.models.media.Media
import org.signal.core.util.bytes
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.MediaUtil
import java.io.IOException
import java.util.Optional
import org.thoughtcrime.securesms.mediasend.MediaSendDocumentViewModel.DocumentInfo
import org.signal.core.ui.R as CoreUiR
/**
@@ -43,6 +39,10 @@ class MediaSendDocumentFragment : Fragment(R.layout.mediasend_document_fragment)
private lateinit var uri: Uri
private lateinit var media: Media
private val viewModel: MediaSendDocumentViewModel by viewModels {
MediaSendDocumentViewModel.Factory(media)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -52,24 +52,28 @@ class MediaSendDocumentFragment : Fragment(R.layout.mediasend_document_fragment)
this.media = requireNotNull(requireArguments().getParcelableCompat(KEY_MEDIA, Media::class.java))
val fileInfo: Pair<String?, Long>? = getFileInfo()
if (fileInfo != null) {
media.fileName = fileInfo.first
name.text = fileInfo.first ?: getString(R.string.DocumentView_unnamed_file)
size.text = fileInfo.second.bytes.toUnitString()
viewModel.documentInfo.observe(viewLifecycleOwner) { documentInfoOptional ->
val documentInfo: DocumentInfo? = documentInfoOptional.orElse(null)
if (documentInfo != null) {
media.fileName = documentInfo.fileName
val extensionText: String = MediaUtil.getFileType(requireContext(), Optional.ofNullable(fileInfo.first), media.uri).orElse("")
if (extensionText.length <= 3) {
extension.text = extensionText
extension.setTextAppearance(requireContext(), CoreUiR.style.Signal_Text_BodySmall)
} else if (extensionText.length == 4) {
extension.text = extensionText
extension.setTextAppearance(requireContext(), CoreUiR.style.Signal_Text_Caption)
name.text = documentInfo.fileName ?: getString(R.string.DocumentView_unnamed_file)
size.text = documentInfo.fileSize.bytes.toUnitString()
if (documentInfo.extension.length <= 3) {
extension.text = documentInfo.extension
extension.setTextAppearance(requireContext(), CoreUiR.style.Signal_Text_BodySmall)
} else if (documentInfo.extension.length == 4) {
extension.text = documentInfo.extension
extension.setTextAppearance(requireContext(), CoreUiR.style.Signal_Text_Caption)
}
} else {
Toast.makeText(requireContext(), R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment, Toast.LENGTH_SHORT).show()
requireActivity().finishAfterTransition()
}
} else {
Toast.makeText(requireContext(), R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment, Toast.LENGTH_SHORT).show()
requireActivity().finishAfterTransition()
}
viewModel.loadDocumentInfo()
}
override fun getUri(): Uri {
@@ -85,57 +89,4 @@ class MediaSendDocumentFragment : Fragment(R.layout.mediasend_document_fragment)
override fun restoreState(state: Any) = Unit
override fun notifyHidden() = Unit
private fun getFileInfo(): Pair<String?, Long>? {
val fileInfo: Pair<String?, Long>
try {
if (PartAuthority.isLocalUri(uri)) {
fileInfo = getManuallyCalculatedFileInfo(uri)
} else {
val result = getContentResolverFileInfo(uri)
fileInfo = if ((result == null)) getManuallyCalculatedFileInfo(uri) else result
}
} catch (e: IOException) {
Log.w(TAG, e)
return null
}
return fileInfo
}
@Throws(IOException::class)
private fun getManuallyCalculatedFileInfo(uri: Uri): Pair<String?, Long> {
var fileName: String? = null
var fileSize: Long? = null
if (PartAuthority.isLocalUri(uri)) {
fileSize = PartAuthority.getAttachmentSize(requireContext(), uri)
fileName = PartAuthority.getAttachmentFileName(requireContext(), uri)
}
if (fileSize == null) {
fileSize = MediaUtil.getMediaSize(context, uri)
}
return Pair(fileName, fileSize)
}
private fun getContentResolverFileInfo(uri: Uri): Pair<String, Long>? {
var cursor: Cursor? = null
try {
cursor = requireContext().contentResolver.query(uri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
val fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
media.fileName = fileName
return Pair(fileName, fileSize)
}
} finally {
cursor?.close()
}
return null
}
}
@@ -0,0 +1,105 @@
package org.thoughtcrime.securesms.mediasend
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.models.media.Media
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.MediaUtil
import java.io.IOException
import java.util.Optional
class MediaSendDocumentViewModel(private val media: Media) : ViewModel() {
private val internalDocumentInfo = MutableLiveData<Optional<DocumentInfo>>()
val documentInfo: LiveData<Optional<DocumentInfo>> = internalDocumentInfo
fun loadDocumentInfo() {
viewModelScope.launch {
internalDocumentInfo.value = withContext(Dispatchers.IO) {
Optional.ofNullable(computeDocumentInfo())
}
}
}
private fun computeDocumentInfo(): DocumentInfo? {
val context = AppDependencies.application
val fileInfo: Pair<String?, Long> = getFileInfo(context) ?: return null
val extensionText: String = MediaUtil.getFileType(context, Optional.ofNullable(fileInfo.first), media.uri).orElse("")
return DocumentInfo(fileInfo.first, fileInfo.second, extensionText)
}
private fun getFileInfo(context: Context): Pair<String?, Long>? {
val uri = media.uri
return try {
if (PartAuthority.isLocalUri(uri)) {
getManuallyCalculatedFileInfo(context, uri)
} else {
getContentResolverFileInfo(context, uri) ?: getManuallyCalculatedFileInfo(context, uri)
}
} catch (e: IOException) {
Log.w(TAG, e)
null
}
}
@Throws(IOException::class)
private fun getManuallyCalculatedFileInfo(context: Context, uri: Uri): Pair<String?, Long> {
var fileName: String? = null
var fileSize: Long? = null
if (PartAuthority.isLocalUri(uri)) {
fileSize = PartAuthority.getAttachmentSize(context, uri)
fileName = PartAuthority.getAttachmentFileName(context, uri)
}
if (fileSize == null) {
fileSize = MediaUtil.getMediaSize(context, uri)
}
return Pair(fileName, fileSize)
}
private fun getContentResolverFileInfo(context: Context, uri: Uri): Pair<String, Long>? {
var cursor: Cursor? = null
try {
cursor = context.contentResolver.query(uri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
val fileName = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
return Pair(fileName, fileSize)
}
} finally {
cursor?.close()
}
return null
}
data class DocumentInfo(val fileName: String?, val fileSize: Long, val extension: String)
class Factory(private val media: Media) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(MediaSendDocumentViewModel(media)))
}
}
companion object {
private val TAG = Log.tag(MediaSendDocumentViewModel::class.java)
}
}
@@ -96,7 +96,7 @@ public final class Wallet {
return SignalStore.payments().mobileCoinLatestBalance();
}
@AnyThread
@WorkerThread
public @NonNull MobileCoinLedgerWrapper getCachedLedger() {
return SignalStore.payments().mobileCoinLatestFullLedger();
}
@@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.BidiUtil;
import org.signal.core.util.StringUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.groups.GroupId;
@@ -143,24 +144,27 @@ class EditProfileViewModel extends ViewModel {
return;
}
byte[] oldAvatar = originalAvatar.getValue();
byte[] newAvatar = internalAvatar.getValue();
String oldDisplayName = isGroup() ? originalDisplayName.getValue() : null;
String oldDescription = isGroup() ? originalDescription : null;
byte[] oldAvatar = originalAvatar.getValue();
byte[] newAvatar = internalAvatar.getValue();
String oldDisplayName = isGroup() ? originalDisplayName.getValue() : null;
String oldDescription = isGroup() ? originalDescription : null;
boolean isGroup = isGroup();
if (!isGroup() && SignalStore.phoneNumberPrivacy().getPhoneNumberDiscoverabilityMode() == PhoneNumberDiscoverabilityMode.UNDECIDED) {
Log.i(TAG, "Phone number discoverability mode is still UNDECIDED. Setting to DISCOVERABLE.");
SignalStore.phoneNumberPrivacy().setPhoneNumberDiscoverabilityMode(PhoneNumberDiscoverabilityMode.DISCOVERABLE);
}
SignalExecutors.BOUNDED.execute(() -> {
if (!isGroup && SignalStore.phoneNumberPrivacy().getPhoneNumberDiscoverabilityMode() == PhoneNumberDiscoverabilityMode.UNDECIDED) {
Log.i(TAG, "Phone number discoverability mode is still UNDECIDED. Setting to DISCOVERABLE.");
SignalStore.phoneNumberPrivacy().setPhoneNumberDiscoverabilityMode(PhoneNumberDiscoverabilityMode.DISCOVERABLE);
}
repository.uploadProfile(profileName,
displayName,
!Objects.equals(BidiUtil.stripBidiProtection(oldDisplayName), displayName),
description,
!Objects.equals(BidiUtil.stripBidiProtection(oldDescription), description),
newAvatar,
!Arrays.equals(oldAvatar, newAvatar),
uploadResult::postValue);
repository.uploadProfile(profileName,
displayName,
!Objects.equals(BidiUtil.stripBidiProtection(oldDisplayName), displayName),
description,
!Objects.equals(BidiUtil.stripBidiProtection(oldDescription), description),
newAvatar,
!Arrays.equals(oldAvatar, newAvatar),
uploadResult::postValue);
});
}
static class Factory implements ViewModelProvider.Factory {
@@ -41,19 +41,20 @@ final class ShareableGroupLinkRepository {
}
void toggleGroupLinkEnabled(@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) {
setGroupLinkEnabledState(toggleGroupLinkState(true, false), callback);
setGroupLinkEnabledState(true, false, callback);
}
void toggleGroupLinkApprovalRequired(@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) {
setGroupLinkEnabledState(toggleGroupLinkState(false, true), callback);
setGroupLinkEnabledState(false, true, callback);
}
private void setGroupLinkEnabledState(@NonNull GroupManager.GroupLinkState state,
private void setGroupLinkEnabledState(boolean toggleEnabled,
boolean toggleApprovalNeeded,
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
{
SignalExecutors.UNBOUNDED.execute(() -> {
try {
GroupManager.setGroupLinkEnabledState(context, groupId, state);
GroupManager.setGroupLinkEnabledState(context, groupId, toggleGroupLinkState(toggleEnabled, toggleApprovalNeeded));
callback.onComplete(null);
} catch (GroupNotAMemberException | GroupChangeFailedException | GroupInsufficientRightsException | IOException | GroupChangeBusyException e) {
callback.onError(GroupChangeFailureReason.fromException(e));
@@ -10,7 +10,6 @@ import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
@@ -84,7 +83,7 @@ public abstract class TimedEventManager<E> {
* Schedules an alarm to call {@link #scheduleIfNecessary()} after the specified delay. You can
* use {@link #setAlarm(Context, long, Class)} as a helper method.
*/
@AnyThread
@WorkerThread
protected abstract void scheduleAlarm(@NonNull Application application, E event, long delay);
/**