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()