From b0c3b087ded86a98be7e84410d86d04677b59506 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Sun, 18 Aug 2024 14:06:48 +0200 Subject: [PATCH] Refactor the unmount listener to anki activity. This ensure that the same receiver appears in all activities and not just the few one where the code was essentially copy pasted. The Deck Picker also listen for mounting, so I renamed its receiver as a `mountReceiver` instead --- .../com/ichi2/anki/AbstractFlashcardViewer.kt | 32 +----------- .../main/java/com/ichi2/anki/AnkiActivity.kt | 50 +++++++++++++++++++ .../main/java/com/ichi2/anki/CardBrowser.kt | 33 +----------- .../main/java/com/ichi2/anki/DeckPicker.kt | 39 +++------------ .../main/java/com/ichi2/anki/NoteEditor.kt | 35 ------------- 5 files changed, 58 insertions(+), 131 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index 6cb2b0ac3129..663c0b8f1e9b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -23,11 +23,9 @@ package com.ichi2.anki import android.annotation.SuppressLint import android.annotation.TargetApi import android.content.ActivityNotFoundException -import android.content.BroadcastReceiver import android.content.ClipboardManager import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.content.SharedPreferences import android.content.res.Configuration import android.graphics.Bitmap @@ -72,7 +70,6 @@ import androidx.annotation.CheckResult import androidx.annotation.IdRes import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.children @@ -110,7 +107,6 @@ import com.ichi2.anki.pages.AnkiServer import com.ichi2.anki.pages.CongratsPage import com.ichi2.anki.pages.PostRequestHandler import com.ichi2.anki.preferences.sharedPrefs -import com.ichi2.anki.receiver.SdCardReceiver import com.ichi2.anki.reviewer.AutomaticAnswer import com.ichi2.anki.reviewer.AutomaticAnswer.AutomaticallyAnswered import com.ichi2.anki.reviewer.AutomaticAnswerAction @@ -129,7 +125,6 @@ import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.utils.OnlyOnce.Method.ANSWER_CARD import com.ichi2.anki.utils.OnlyOnce.preventSimultaneousExecutions import com.ichi2.annotations.NeedsTest -import com.ichi2.compat.CompatHelper.Companion.registerReceiverCompat import com.ichi2.compat.CompatHelper.Companion.resolveActivityCompat import com.ichi2.compat.ResolveInfoFlagsCompat import com.ichi2.libanki.Card @@ -192,10 +187,6 @@ abstract class AbstractFlashcardViewer : @VisibleForTesting val jsApi by lazy { AnkiDroidJsAPI(this) } - /** - * Broadcast that informs us when the sd card is about to be unmounted - */ - private var unmountReceiver: BroadcastReceiver? = null private var tagsDialogFactory: TagsDialogFactory? = null /** @@ -590,7 +581,7 @@ abstract class AbstractFlashcardViewer : super.onCollectionLoaded(col) val mediaDir = col.media.dir cardMediaPlayer = CardMediaPlayer.newInstance(this, getMediaBaseUrl(mediaDir)) - registerExternalStorageListener() + registerReceiver() restoreCollectionPreferences(col) initLayout() cardRenderContext = createInstance(this, col, typeAnswer!!) @@ -653,9 +644,6 @@ abstract class AbstractFlashcardViewer : override fun onDestroy() { super.onDestroy() tts.releaseTts(this) - if (unmountReceiver != null) { - unregisterReceiver(unmountReceiver) - } // WebView.destroy() should be called after the end of use // http://developer.android.com/reference/android/webkit/WebView.html#destroy() if (cardFrame != null) { @@ -805,24 +793,6 @@ abstract class AbstractFlashcardViewer : return cardContent != null } - /** - * Show/dismiss dialog when sd card is ejected/remounted (collection is saved by SdCardReceiver) - */ - private fun registerExternalStorageListener() { - if (unmountReceiver == null) { - unmountReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == SdCardReceiver.MEDIA_EJECT) { - finish() - } - } - } - val iFilter = IntentFilter() - iFilter.addAction(SdCardReceiver.MEDIA_EJECT) - registerReceiverCompat(unmountReceiver, iFilter, ContextCompat.RECEIVER_EXPORTED) - } - } - open fun undo(): Job { return launchCatchingTask { undoAndShowSnackbar(duration = Reviewer.ACTION_SNACKBAR_TIME) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt index 8213f74e496c..339dfbfb3103 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt @@ -6,7 +6,10 @@ package com.ichi2.anki import android.app.NotificationManager import android.app.PendingIntent import android.content.ActivityNotFoundException +import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.graphics.BitmapFactory import android.graphics.Color import android.media.AudioManager @@ -16,6 +19,7 @@ import android.os.Bundle import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.view.Window import android.view.WindowManager import android.view.animation.Animation import android.widget.ProgressBar @@ -35,6 +39,7 @@ import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_SYSTEM import androidx.core.app.NotificationCompat import androidx.core.app.PendingIntentCompat +import androidx.core.content.ContextCompat import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -51,9 +56,11 @@ import com.ichi2.anki.dialogs.SimpleMessageDialog.SimpleMessageDialogListener import com.ichi2.anki.preferences.Preferences import com.ichi2.anki.preferences.Preferences.Companion.MINIMUM_CARDS_DUE_FOR_NOTIFICATION import com.ichi2.anki.preferences.sharedPrefs +import com.ichi2.anki.receiver.SdCardReceiver import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.workarounds.AppLoadedFromBackupWorkaround.showedActivityFailedScreen import com.ichi2.async.CollectionLoader +import com.ichi2.compat.CompatHelper.Companion.registerReceiverCompat import com.ichi2.compat.customtabs.CustomTabActivityHelper import com.ichi2.compat.customtabs.CustomTabsFallback import com.ichi2.compat.customtabs.CustomTabsHelper @@ -68,6 +75,11 @@ import androidx.browser.customtabs.CustomTabsIntent.Builder as CustomTabsIntentB @KotlinCleanup("set activityName") open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { + /** + * Broadcast that informs us when the sd card is about to be unmounted + */ + private var receiver: BroadcastReceiver? = null + /** The name of the parent class (example: 'Reviewer') */ private val activityName: String val dialogHandler = DialogHandler(this) @@ -112,6 +124,11 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { customTabActivityHelper.unbindCustomTabsService(this) } + override fun onDestroy() { + super.onDestroy() + receiver?.let { unregisterReceiver(it) } + } + override fun onResume() { super.onResume() UsageAnalytics.sendAnalyticsScreenView(this) @@ -150,6 +167,39 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { hideProgressBar() } + /** + * Maps from intent name action to function to run when this action is received by [receiver]. + * By default it handles [SdCardReceiver.MEDIA_EJECT], and show/dismiss dialog when sd card is ejected/remounted (collection is saved by SdCardReceiver) + */ + protected open val broadcastsActions = mapOf( + SdCardReceiver.MEDIA_EJECT to { onSdCardNotMounted() } + ) + + /** + * Register a broadcast receiver, associating an intent to an action as in [broadcastsActions]. + * Add more values in [broadcastsActions] to react to more intents. + */ + protected fun registerReceiver() { + if (receiver != null) { + // Receiver already registered + return + } + receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + broadcastsActions[intent.action]?.invoke() + } + }.also { + val iFilter = IntentFilter() + broadcastsActions.keys.map(iFilter::addAction) + registerReceiverCompat(it, iFilter, ContextCompat.RECEIVER_EXPORTED) + } + } + + protected fun onSdCardNotMounted() { + showThemedToast(this, resources.getString(R.string.sd_card_not_mounted), false) + finish() + } + /** Legacy code should migrate away from this, and use withCol {} instead. * */ val getColUnsafe: Collection diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt index 4e89b07fafe3..344d4cd887a6 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt @@ -18,11 +18,9 @@ package com.ichi2.anki -import android.content.BroadcastReceiver import android.content.Context import android.content.DialogInterface import android.content.Intent -import android.content.IntentFilter import android.content.res.Configuration import android.os.Bundle import android.os.SystemClock @@ -52,7 +50,6 @@ import androidx.annotation.MainThread import androidx.annotation.VisibleForTesting import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.ThemeUtils -import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import anki.collection.OpChanges @@ -99,7 +96,6 @@ import com.ichi2.anki.model.SortType import com.ichi2.anki.noteeditor.NoteEditorLauncher import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.previewer.PreviewerFragment -import com.ichi2.anki.receiver.SdCardReceiver import com.ichi2.anki.scheduling.ForgetCardsDialog import com.ichi2.anki.scheduling.SetDueDateDialog import com.ichi2.anki.servicelayer.NoteService @@ -116,7 +112,6 @@ import com.ichi2.anki.utils.roundedTimeSpanUnformatted import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener import com.ichi2.annotations.NeedsTest import com.ichi2.async.renderBrowserQA -import com.ichi2.compat.CompatHelper.Companion.registerReceiverCompat import com.ichi2.libanki.Card import com.ichi2.libanki.CardId import com.ichi2.libanki.ChangeManager @@ -270,11 +265,6 @@ open class CardBrowser : private var shouldRestoreScroll = false private var postAutoScroll = false - /** - * Broadcast that informs us when the sd card is about to be unmounted - */ - private var unmountReceiver: BroadcastReceiver? = null - init { ChangeManager.subscribe(this) } @@ -584,7 +574,7 @@ open class CardBrowser : override fun onCollectionLoaded(col: Collection) { super.onCollectionLoaded(col) Timber.d("onCollectionLoaded()") - registerExternalStorageListener() + registerReceiver() cards.reset() cardsListView.setOnItemClickListener { _: AdapterView<*>?, view: View?, position: Int, _: Long -> @@ -888,9 +878,6 @@ open class CardBrowser : override fun onDestroy() { invalidate() super.onDestroy() - if (unmountReceiver != null) { - unregisterReceiver(unmountReceiver) - } } @Deprecated("Deprecated in Java") @@ -2323,24 +2310,6 @@ open class CardBrowser : } } - /** - * Show/dismiss dialog when sd card is ejected/remounted (collection is saved by SdCardReceiver) - */ - private fun registerExternalStorageListener() { - if (unmountReceiver == null) { - unmountReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == SdCardReceiver.MEDIA_EJECT) { - finish() - } - } - } - val iFilter = IntentFilter() - iFilter.addAction(SdCardReceiver.MEDIA_EJECT) - registerReceiverCompat(unmountReceiver, iFilter, ContextCompat.RECEIVER_EXPORTED) - } - } - /** * The views expand / contract when switching between multi-select mode so we manually * adjust so that the vertical position of the given view is maintained diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 2f94781f045c..40746443f65d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -26,10 +26,8 @@ package com.ichi2.anki import android.app.Activity -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.content.SharedPreferences import android.database.SQLException import android.graphics.PixelFormat @@ -59,7 +57,6 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback -import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat @@ -154,7 +151,6 @@ import com.ichi2.annotations.NeedsTest import com.ichi2.async.deleteMedia import com.ichi2.compat.CompatHelper import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat -import com.ichi2.compat.CompatHelper.Companion.registerReceiverCompat import com.ichi2.compat.CompatHelper.Companion.sdkVersion import com.ichi2.libanki.ChangeManager import com.ichi2.libanki.Consts @@ -264,8 +260,6 @@ open class DeckPicker : private lateinit var reviewSummaryTextView: TextView - @KotlinCleanup("make lateinit, but needs more changes") - private var unmountReceiver: BroadcastReceiver? = null private lateinit var floatingActionMenu: DeckPickerFloatingActionMenu // flag asking user to do a full sync which is used in upgrade path @@ -503,7 +497,7 @@ open class DeckPicker : if (fragmented && !startupError) { loadStudyOptionsFragment(false) } - registerExternalStorageListener() + registerReceiver() // create inherited navigation drawer layout here so that it can be used by parent class initNavigationDrawer(mainView) @@ -1167,9 +1161,6 @@ open class DeckPicker : override fun onDestroy() { super.onDestroy() - if (unmountReceiver != null) { - unregisterReceiver(unmountReceiver) - } if (progressDialog != null && progressDialog!!.isShowing) { progressDialog!!.dismiss() } @@ -1702,11 +1693,6 @@ open class DeckPicker : showAsyncDialogFragment(newFragment, Channel.SYNC) } - fun onSdCardNotMounted() { - showThemedToast(this, resources.getString(R.string.sd_card_not_mounted), false) - finish() - } - // Callback method to submit error report fun sendErrorReport() { CrashReportService.sendExceptionReport(RuntimeException(), "DeckPicker.sendErrorReport") @@ -1898,25 +1884,12 @@ open class DeckPicker : } /** - * Show a message when the SD card is ejected + * Refresh the deck pickre when the SD card is inserted. */ - private fun registerExternalStorageListener() { - if (unmountReceiver == null) { - unmountReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == SdCardReceiver.MEDIA_EJECT) { - onSdCardNotMounted() - } else if (intent.action == SdCardReceiver.MEDIA_MOUNT) { - ActivityCompat.recreate(this@DeckPicker) - } - } - } - val iFilter = IntentFilter() - iFilter.addAction(SdCardReceiver.MEDIA_EJECT) - iFilter.addAction(SdCardReceiver.MEDIA_MOUNT) - registerReceiverCompat(unmountReceiver, iFilter, ContextCompat.RECEIVER_EXPORTED) - } - } + override val broadcastsActions = super.broadcastsActions + mapOf( + SdCardReceiver.MEDIA_MOUNT + to { ActivityCompat.recreate(this) } + ) fun openAnkiWebSharedDecks() { val intent = Intent(this, SharedDecksActivity::class.java) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt index 71f9c0b4531d..250a5fbb3a70 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt @@ -21,13 +21,11 @@ package com.ichi2.anki import android.annotation.SuppressLint import android.app.Activity import android.app.Activity.RESULT_CANCELED -import android.content.BroadcastReceiver import android.content.ClipData import android.content.ClipDescription import android.content.ClipboardManager import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.content.res.Configuration import android.net.Uri import android.os.Build @@ -62,7 +60,6 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.PopupMenu -import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.IntentCompat import androidx.core.content.edit @@ -122,7 +119,6 @@ import com.ichi2.anki.pages.ImageOcclusion import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.previewer.TemplatePreviewerArguments import com.ichi2.anki.previewer.TemplatePreviewerPage -import com.ichi2.anki.receiver.SdCardReceiver import com.ichi2.anki.servicelayer.LanguageHintService import com.ichi2.anki.servicelayer.NoteService import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider @@ -135,7 +131,6 @@ import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener import com.ichi2.annotations.NeedsTest import com.ichi2.compat.CompatHelper import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat -import com.ichi2.compat.CompatHelper.Companion.registerReceiverCompat import com.ichi2.compat.setTooltipTextCompat import com.ichi2.imagecropper.ImageCropper import com.ichi2.imagecropper.ImageCropper.Companion.CROP_IMAGE_RESULT @@ -209,10 +204,6 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su */ private var reloadRequired = false - /** - * Broadcast that informs us when the sd card is about to be unmounted - */ - private var unmountReceiver: BroadcastReceiver? = null private var fieldsLayoutContainer: LinearLayout? = null private var mediaRegistration: MediaRegistration? = null private var tagsDialogFactory: TagsDialogFactory? = null @@ -581,7 +572,6 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su super.onCollectionLoaded(col) val intent = requireActivity().intent Timber.d("NoteEditor() onCollectionLoaded: caller: %d", caller) - registerExternalStorageListener() fieldsLayoutContainer = findViewById(R.id.CardEditorEditFieldsLayout) tagsButton = findViewById(R.id.CardEditorTagButton) cardsButton = findViewById(R.id.CardEditorCardsButton) @@ -1259,13 +1249,6 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su closeNoteEditor() } - override fun onDestroy() { - super.onDestroy() - if (unmountReceiver != null) { - unregisterReceiver(unmountReceiver) - } - } - override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) updateToolbar() @@ -1461,24 +1444,6 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su startActivity(intent) } - /** - * finish when sd card is ejected - */ - private fun registerExternalStorageListener() { - if (unmountReceiver == null) { - unmountReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action != null && intent.action == SdCardReceiver.MEDIA_EJECT) { - requireActivity().finish() - } - } - } - val iFilter = IntentFilter() - iFilter.addAction(SdCardReceiver.MEDIA_EJECT) - requireContext().registerReceiverCompat(unmountReceiver, iFilter, ContextCompat.RECEIVER_EXPORTED) - } - } - private fun setTags(tags: Array) { selectedTags = tags.toCollection(ArrayList()) updateTags()