From d90bdc8874d35683f23aeb05abec6c543ba9cff4 Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Wed, 10 Jul 2024 05:49:13 +0200 Subject: [PATCH] NF: Mark receivers are exported This ensure there is no change in behaviour. I checked whether we could try to not export the download of decks, but it does not works. The app does not crash on api 24 on simulator. There was a slight problem, with variables that could potentially be null. To ensure the type checker agreed with the code, without using `!!`, I had to rewrite some of the logic. I also ensured that some of the code that was copy/pasted in various activity now entirely belong to AnkiActivity. --- .../com/ichi2/anki/AbstractFlashcardViewer.kt | 107 +++++++++++++----- .../main/java/com/ichi2/anki/AnkiActivity.kt | 58 +++++++++- .../main/java/com/ichi2/anki/CardBrowser.kt | 84 ++++++++------ .../main/java/com/ichi2/anki/DeckPicker.kt | 42 ++++--- .../main/java/com/ichi2/anki/NoteEditor.kt | 32 ------ .../ichi2/anki/SharedDecksDownloadFragment.kt | 11 +- .../src/main/java/com/ichi2/compat/Compat.kt | 9 ++ .../main/java/com/ichi2/compat/CompatV23.kt | 12 ++ .../main/java/com/ichi2/compat/CompatV33.kt | 12 ++ .../com/ichi2/widget/AnkiDroidWidgetSmall.kt | 68 ++++++----- 10 files changed, 281 insertions(+), 154 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index 9dfad3b7c467..3fede5ce6c84 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -22,21 +22,48 @@ package com.ichi2.anki import android.annotation.SuppressLint import android.annotation.TargetApi -import android.content.* +import android.content.ActivityNotFoundException +import android.content.BroadcastReceiver +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Color import android.hardware.SensorManager import android.net.Uri -import android.os.* -import android.view.* +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.view.GestureDetector import android.view.GestureDetector.SimpleOnGestureListener +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View import android.view.View.OnTouchListener +import android.view.ViewGroup +import android.view.ViewParent +import android.view.WindowManager import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager -import android.webkit.* +import android.webkit.CookieManager +import android.webkit.JsResult +import android.webkit.PermissionRequest +import android.webkit.RenderProcessGoneDetail +import android.webkit.WebChromeClient +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView import android.webkit.WebView.HitTestResult -import android.widget.* +import android.webkit.WebViewClient +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.RelativeLayout import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultCallback import androidx.activity.result.contract.ActivityResultContracts @@ -54,9 +81,23 @@ import com.google.android.material.snackbar.Snackbar import com.ichi2.anim.ActivityTransitionAnimation import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.CollectionManager.withCol -import com.ichi2.anki.cardviewer.* +import com.ichi2.anki.cardviewer.AndroidCardRenderContext import com.ichi2.anki.cardviewer.AndroidCardRenderContext.Companion.createInstance +import com.ichi2.anki.cardviewer.CardMediaPlayer +import com.ichi2.anki.cardviewer.Gesture +import com.ichi2.anki.cardviewer.GestureProcessor +import com.ichi2.anki.cardviewer.JavascriptEvaluator +import com.ichi2.anki.cardviewer.MediaErrorHandler +import com.ichi2.anki.cardviewer.OnRenderProcessGoneDelegate +import com.ichi2.anki.cardviewer.RenderedCard +import com.ichi2.anki.cardviewer.SingleCardSide +import com.ichi2.anki.cardviewer.TTS +import com.ichi2.anki.cardviewer.TypeAnswer import com.ichi2.anki.cardviewer.TypeAnswer.Companion.createInstance +import com.ichi2.anki.cardviewer.ViewerCommand +import com.ichi2.anki.cardviewer.ViewerRefresh +import com.ichi2.anki.cardviewer.handledGamepadKeyDown +import com.ichi2.anki.cardviewer.handledGamepadKeyUp import com.ichi2.anki.dialogs.TtsVoicesDialogFragment import com.ichi2.anki.dialogs.tags.TagsDialog import com.ichi2.anki.dialogs.tags.TagsDialogFactory @@ -68,11 +109,16 @@ 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.* +import com.ichi2.anki.reviewer.AutomaticAnswer import com.ichi2.anki.reviewer.AutomaticAnswer.AutomaticallyAnswered +import com.ichi2.anki.reviewer.AutomaticAnswerAction +import com.ichi2.anki.reviewer.CardSide +import com.ichi2.anki.reviewer.EaseButton +import com.ichi2.anki.reviewer.FullScreenMode import com.ichi2.anki.reviewer.FullScreenMode.Companion.DEFAULT import com.ichi2.anki.reviewer.FullScreenMode.Companion.fromPreference +import com.ichi2.anki.reviewer.MotionEventHandler +import com.ichi2.anki.reviewer.PreviousAnswerIndicator import com.ichi2.anki.servicelayer.LanguageHintService.applyLanguageHint import com.ichi2.anki.servicelayer.NoteService.isMarked import com.ichi2.anki.services.migrationServiceWhileStartedOrNull @@ -84,26 +130,45 @@ import com.ichi2.anki.utils.OnlyOnce.preventSimultaneousExecutions import com.ichi2.annotations.NeedsTest import com.ichi2.compat.CompatHelper.Companion.resolveActivityCompat import com.ichi2.compat.ResolveInfoFlagsCompat -import com.ichi2.libanki.* +import com.ichi2.libanki.Card +import com.ichi2.libanki.CardId +import com.ichi2.libanki.ChangeManager import com.ichi2.libanki.Collection +import com.ichi2.libanki.Consts import com.ichi2.libanki.Consts.BUTTON_TYPE +import com.ichi2.libanki.DeckId +import com.ichi2.libanki.Decks import com.ichi2.libanki.Sound.getAvTag +import com.ichi2.libanki.SoundOrVideoTag +import com.ichi2.libanki.TTSTag +import com.ichi2.libanki.Utils +import com.ichi2.libanki.note +import com.ichi2.libanki.renderOutput +import com.ichi2.libanki.setTagsFromStr +import com.ichi2.libanki.undoableOp import com.ichi2.themes.Themes import com.ichi2.themes.Themes.getResFromAttr import com.ichi2.ui.FixedEditText -import com.ichi2.utils.* +import com.ichi2.utils.BlocksSchemaUpgrade import com.ichi2.utils.ClipboardUtil.getText import com.ichi2.utils.HandlerUtils.executeFunctionWithDelay import com.ichi2.utils.HandlerUtils.newHandler import com.ichi2.utils.HashUtil.hashSetInit +import com.ichi2.utils.KotlinCleanup +import com.ichi2.utils.Stopwatch import com.ichi2.utils.WebViewDebugging.initializeDebugging +import com.ichi2.utils.message +import com.ichi2.utils.negativeButton +import com.ichi2.utils.positiveButton +import com.ichi2.utils.show +import com.ichi2.utils.title import com.squareup.seismic.ShakeDetector import kotlinx.coroutines.Job import kotlinx.coroutines.runBlocking import timber.log.Timber -import java.io.* +import java.io.File +import java.io.UnsupportedEncodingException import java.net.URLDecoder -import java.util.* import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReadWriteLock import java.util.concurrent.locks.ReentrantReadWriteLock @@ -743,24 +808,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) - registerReceiver(unmountReceiver, iFilter) - } - } - 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 e10b2fda369d..5e8a01b09e5f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt @@ -6,14 +6,21 @@ 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 import android.net.Uri import android.os.Build import android.os.Bundle -import android.view.* +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 import androidx.activity.result.ActivityResultLauncher @@ -27,7 +34,9 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.widget.ThemeUtils import androidx.appcompat.widget.Toolbar import androidx.browser.customtabs.CustomTabColorSchemeParams -import androidx.browser.customtabs.CustomTabsIntent.* +import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK +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.fragment.app.DialogFragment @@ -36,7 +45,8 @@ import androidx.fragment.app.FragmentManager import com.google.android.material.color.MaterialColors import com.ichi2.anim.ActivityTransitionAnimation import com.ichi2.anim.ActivityTransitionAnimation.Direction -import com.ichi2.anim.ActivityTransitionAnimation.Direction.* +import com.ichi2.anim.ActivityTransitionAnimation.Direction.DEFAULT +import com.ichi2.anim.ActivityTransitionAnimation.Direction.NONE import com.ichi2.anki.analytics.UsageAnalytics import com.ichi2.anki.dialogs.AsyncDialogFragment import com.ichi2.anki.dialogs.DialogHandler @@ -45,9 +55,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 import com.ichi2.compat.customtabs.CustomTabActivityHelper import com.ichi2.compat.customtabs.CustomTabsFallback import com.ichi2.compat.customtabs.CustomTabsHelper @@ -62,6 +74,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 unmountReceiver: BroadcastReceiver? = null + /** The name of the parent class (example: 'Reviewer') */ private val activityName: String val dialogHandler = DialogHandler(this) @@ -106,6 +123,15 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { customTabActivityHelper.unbindCustomTabsService(this) } + override fun onDestroy() { + super.onDestroy() + val unmountReceiver = this.unmountReceiver + this.unmountReceiver = null + if (unmountReceiver != null) { + unregisterReceiver(unmountReceiver) + } + } + override fun onResume() { super.onResume() UsageAnalytics.sendAnalyticsScreenView(this) @@ -135,6 +161,32 @@ open class AnkiActivity : AppCompatActivity, SimpleMessageDialogListener { hideProgressBar() } + /** + * Show/dismiss dialog when sd card is ejected/remounted (collection is saved by SdCardReceiver) + */ + protected open fun registerExternalStorageListener() { + if (unmountReceiver != null) { + // Receiver already registered + return + } + val unmountReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == SdCardReceiver.MEDIA_EJECT) { + onSdCardNotMounted() + } + } + } + val iFilter = IntentFilter() + iFilter.addAction(SdCardReceiver.MEDIA_EJECT) + CompatHelper.compat.registerExportedReceiver(this, unmountReceiver, iFilter) + this.unmountReceiver = unmountReceiver + } + + 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 e0937cd68c32..b717e3707a55 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt @@ -18,14 +18,30 @@ package com.ichi2.anki -import android.content.* +import android.content.Context +import android.content.DialogInterface +import android.content.Intent import android.content.res.Configuration import android.os.Bundle import android.os.SystemClock import android.text.TextUtils import android.util.TypedValue -import android.view.* -import android.widget.* +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.SubMenu +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.AbsListView +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.BaseAdapter +import android.widget.CheckBox +import android.widget.ListView +import android.widget.Spinner +import android.widget.TextView import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.annotation.CheckResult @@ -46,19 +62,24 @@ import com.ichi2.anki.browser.CardBrowserColumn.Companion.COLUMN1_KEYS import com.ichi2.anki.browser.CardBrowserColumn.Companion.COLUMN2_KEYS import com.ichi2.anki.browser.CardBrowserLaunchOptions import com.ichi2.anki.browser.CardBrowserViewModel -import com.ichi2.anki.browser.CardBrowserViewModel.* +import com.ichi2.anki.browser.CardBrowserViewModel.SearchState import com.ichi2.anki.browser.PreviewerIdsFile import com.ichi2.anki.browser.SaveSearchResult import com.ichi2.anki.browser.SharedPreferencesLastDeckIdRepository import com.ichi2.anki.browser.getLabel import com.ichi2.anki.browser.toCardBrowserLaunchOptions import com.ichi2.anki.common.utils.android.isRobolectric -import com.ichi2.anki.dialogs.* +import com.ichi2.anki.dialogs.BrowserOptionsDialog +import com.ichi2.anki.dialogs.CardBrowserMySearchesDialog import com.ichi2.anki.dialogs.CardBrowserMySearchesDialog.Companion.newInstance import com.ichi2.anki.dialogs.CardBrowserMySearchesDialog.MySearchesDialogListener +import com.ichi2.anki.dialogs.CardBrowserOrderDialog +import com.ichi2.anki.dialogs.DeckSelectionDialog import com.ichi2.anki.dialogs.DeckSelectionDialog.Companion.newInstance import com.ichi2.anki.dialogs.DeckSelectionDialog.DeckSelectionListener import com.ichi2.anki.dialogs.DeckSelectionDialog.SelectableDeck +import com.ichi2.anki.dialogs.IntegerDialog +import com.ichi2.anki.dialogs.SimpleMessageDialog import com.ichi2.anki.dialogs.tags.TagsDialog import com.ichi2.anki.dialogs.tags.TagsDialogFactory import com.ichi2.anki.dialogs.tags.TagsDialogListener @@ -68,13 +89,13 @@ import com.ichi2.anki.export.ExportDialogsFactory import com.ichi2.anki.export.ExportDialogsFactoryProvider import com.ichi2.anki.model.CardStateFilter import com.ichi2.anki.model.CardsOrNotes -import com.ichi2.anki.model.CardsOrNotes.* +import com.ichi2.anki.model.CardsOrNotes.CARDS +import com.ichi2.anki.model.CardsOrNotes.NOTES import com.ichi2.anki.model.SortType import com.ichi2.anki.noteeditor.EditCardDestination import com.ichi2.anki.noteeditor.toIntent 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 @@ -90,15 +111,31 @@ import com.ichi2.anki.utils.ext.ifNotZero import com.ichi2.anki.utils.roundedTimeSpanUnformatted import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener import com.ichi2.annotations.NeedsTest -import com.ichi2.async.* -import com.ichi2.libanki.* +import com.ichi2.async.renderBrowserQA +import com.ichi2.libanki.Card +import com.ichi2.libanki.CardId +import com.ichi2.libanki.ChangeManager import com.ichi2.libanki.Collection +import com.ichi2.libanki.Consts +import com.ichi2.libanki.DeckId +import com.ichi2.libanki.DeckNameId +import com.ichi2.libanki.NoteId +import com.ichi2.libanki.SortOrder +import com.ichi2.libanki.Sound +import com.ichi2.libanki.TemplateManager +import com.ichi2.libanki.Utils +import com.ichi2.libanki.setTagsFromStr +import com.ichi2.libanki.stripAvRefs +import com.ichi2.libanki.undoableOp import com.ichi2.libanki.utils.TimeManager import com.ichi2.ui.CardBrowserSearchView import com.ichi2.ui.FixedTextView -import com.ichi2.utils.* +import com.ichi2.utils.HandlerUtils import com.ichi2.utils.HandlerUtils.postDelayedOnNewHandler +import com.ichi2.utils.KotlinCleanup +import com.ichi2.utils.LanguageUtil import com.ichi2.utils.TagsUtil.getUpdatedTags +import com.ichi2.utils.increaseHorizontalPaddingOfOverflowMenuIcons import com.ichi2.widget.WidgetStatus.updateInBackground import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -108,7 +145,6 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import net.ankiweb.rsdroid.RustCleanup import timber.log.Timber -import java.util.* import kotlin.math.abs import kotlin.math.ceil @@ -231,11 +267,6 @@ open class CardBrowser : private var postAutoScroll = false private val onboarding = Onboarding.CardBrowser(this) - /** - * Broadcast that informs us when the sd card is about to be unmounted - */ - private var unmountReceiver: BroadcastReceiver? = null - init { ChangeManager.subscribe(this) } @@ -702,9 +733,6 @@ open class CardBrowser : override fun onDestroy() { invalidate() super.onDestroy() - if (unmountReceiver != null) { - unregisterReceiver(unmountReceiver) - } } @Deprecated("Deprecated in Java") @@ -2082,24 +2110,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) - registerReceiver(unmountReceiver, iFilter) - } - } - /** * 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 656493333cac..ad7daec262e3 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -125,6 +125,7 @@ import com.ichi2.anki.worker.SyncWorker import com.ichi2.anki.worker.UniqueWorkNames import com.ichi2.annotations.NeedsTest import com.ichi2.async.* +import com.ichi2.compat.CompatHelper import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat import com.ichi2.compat.CompatHelper.Companion.sdkVersion import com.ichi2.libanki.* @@ -217,7 +218,7 @@ open class DeckPicker : private lateinit var reviewSummaryTextView: TextView @KotlinCleanup("make lateinit, but needs more changes") - private var unmountReceiver: BroadcastReceiver? = null + private var mountReceiver: BroadcastReceiver? = null private lateinit var floatingActionMenu: DeckPickerFloatingActionMenu // flag asking user to do a full sync which is used in upgrade path @@ -1249,8 +1250,8 @@ open class DeckPicker : override fun onDestroy() { super.onDestroy() - if (unmountReceiver != null) { - unregisterReceiver(unmountReceiver) + if (mountReceiver != null) { + unregisterReceiver(mountReceiver) } if (progressDialog != null && progressDialog!!.isShowing) { progressDialog!!.dismiss() @@ -1688,11 +1689,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") @@ -1895,24 +1891,26 @@ open class DeckPicker : } /** - * Show a message when the SD card is ejected + * Also recreate the deck picker when a media is mounted. + * @see AnkiActivity.registerExternalStorageListener */ - 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) - } + override fun registerExternalStorageListener() { + super.registerExternalStorageListener() + if (mountReceiver != null) { + // Receiver already registered. + return + } + val mountReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == SdCardReceiver.MEDIA_MOUNT) { + ActivityCompat.recreate(this@DeckPicker) } } - val iFilter = IntentFilter() - iFilter.addAction(SdCardReceiver.MEDIA_EJECT) - iFilter.addAction(SdCardReceiver.MEDIA_MOUNT) - registerReceiver(unmountReceiver, iFilter) } + val iFilter = IntentFilter() + iFilter.addAction(SdCardReceiver.MEDIA_MOUNT) + CompatHelper.compat.registerExportedReceiver(this, mountReceiver, iFilter) + this.mountReceiver = mountReceiver } fun openAnkiWebSharedDecks() { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt index 9e42f0ac0e39..f17edc53b742 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt @@ -19,12 +19,10 @@ package com.ichi2.anki import android.annotation.SuppressLint -import android.content.BroadcastReceiver import android.content.ClipData 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 @@ -86,7 +84,6 @@ import com.ichi2.anki.pages.ImageOcclusion import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.previewer.TemplatePreviewerArguments import com.ichi2.anki.previewer.TemplatePreviewerFragment -import com.ichi2.anki.receiver.SdCardReceiver import com.ichi2.anki.servicelayer.LanguageHintService import com.ichi2.anki.servicelayer.NoteService import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider @@ -139,10 +136,6 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags */ 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 @@ -1133,13 +1126,6 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags closeNoteEditor() } - override fun onDestroy() { - super.onDestroy() - if (unmountReceiver != null) { - unregisterReceiver(unmountReceiver) - } - } - override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) updateToolbar() @@ -1340,24 +1326,6 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags 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) { - finish() - } - } - } - val iFilter = IntentFilter() - iFilter.addAction(SdCardReceiver.MEDIA_EJECT) - registerReceiver(unmountReceiver, iFilter) - } - } - private fun setTags(tags: Array) { selectedTags = tags.toCollection(ArrayList()) updateTags() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/SharedDecksDownloadFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/SharedDecksDownloadFragment.kt index e39c94b12360..bbf8a2d7e586 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/SharedDecksDownloadFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/SharedDecksDownloadFragment.kt @@ -18,7 +18,11 @@ package com.ichi2.anki import android.app.DownloadManager -import android.content.* +import android.content.ActivityNotFoundException +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.database.Cursor import android.net.Uri import android.os.Bundle @@ -35,6 +39,7 @@ import androidx.core.content.FileProvider import androidx.fragment.app.Fragment import com.ichi2.anki.SharedDecksActivity.Companion.DOWNLOAD_FILE import com.ichi2.anki.snackbar.showSnackbar +import com.ichi2.compat.CompatHelper import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat import com.ichi2.utils.ImportUtils import com.ichi2.utils.create @@ -151,7 +156,9 @@ class SharedDecksDownloadFragment : Fragment(R.layout.fragment_shared_decks_down } // Register broadcast receiver for download completion. Timber.d("Registering broadcast receiver for download completion") - activity?.registerReceiver(onComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) + activity?.let { + CompatHelper.compat.registerExportedReceiver(it, onComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) + } val currentFileName = URLUtil.guessFileName( fileToBeDownloaded.url, diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt b/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt index 1b1d99bb517c..00b7b3430262 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/Compat.kt @@ -17,8 +17,11 @@ package com.ichi2.compat +import android.content.BroadcastReceiver import android.content.Context +import android.content.ContextWrapper import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.NameNotFoundException @@ -78,6 +81,12 @@ interface Compat { fun resolveService(packageManager: PackageManager, intent: Intent, flags: ResolveInfoFlagsCompat): ResolveInfo? fun queryIntentActivities(packageManager: PackageManager, intent: Intent, flags: ResolveInfoFlagsCompat): List + /** + * Call [Intent.registerReceiver], ignores the flag before API 33. + * @see Intent.registerReceiver + */ + fun registerExportedReceiver(context: ContextWrapper, receiver: BroadcastReceiver, filter: IntentFilter) + /** * Retrieve extended data from the intent. * @param name – The name of the desired item. diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt index 65326f20e0ee..521a70f1e91e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV23.kt @@ -16,8 +16,11 @@ package com.ichi2.compat +import android.content.BroadcastReceiver import android.content.Context +import android.content.ContextWrapper import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo @@ -225,6 +228,15 @@ open class CompatV23 : Compat { return packageManager.queryIntentActivities(intent, flags.value.toInt()) } + // Until API 32 + override fun registerExportedReceiver( + context: ContextWrapper, + receiver: BroadcastReceiver, + filter: IntentFilter + ) { + context.registerReceiver(receiver, filter) + } + // Until API 33 override fun getSerializableExtra( intent: Intent, diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV33.kt b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV33.kt index cd59d96b4c92..5c86f0c39838 100644 --- a/AnkiDroid/src/main/java/com/ichi2/compat/CompatV33.kt +++ b/AnkiDroid/src/main/java/com/ichi2/compat/CompatV33.kt @@ -17,11 +17,15 @@ package com.ichi2.compat import android.annotation.TargetApi +import android.content.BroadcastReceiver +import android.content.ContextWrapper import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity.RECEIVER_EXPORTED import java.io.Serializable @TargetApi(33) @@ -35,6 +39,14 @@ open class CompatV33 : CompatV31(), Compat { return packageManager.resolveActivity(intent, PackageManager.ResolveInfoFlags.of(flags.value)) } + override fun registerExportedReceiver( + context: ContextWrapper, + receiver: BroadcastReceiver, + filter: IntentFilter + ) { + context.registerReceiver(receiver, filter, RECEIVER_EXPORTED) + } + override fun getSerializableExtra(intent: Intent, name: String, className: Class): T? { return intent.getSerializableExtra(name, className) } diff --git a/AnkiDroid/src/main/java/com/ichi2/widget/AnkiDroidWidgetSmall.kt b/AnkiDroid/src/main/java/com/ichi2/widget/AnkiDroidWidgetSmall.kt index dbea6971f6bb..17ce62830d3d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/widget/AnkiDroidWidgetSmall.kt +++ b/AnkiDroid/src/main/java/com/ichi2/widget/AnkiDroidWidgetSmall.kt @@ -18,7 +18,11 @@ import android.app.PendingIntent import android.app.Service import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider -import android.content.* +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.content.res.Configuration import android.os.IBinder import android.util.TypedValue @@ -32,6 +36,7 @@ import com.ichi2.anki.IntentHandler.Companion.grantedStoragePermissions import com.ichi2.anki.R import com.ichi2.anki.analytics.UsageAnalytics import com.ichi2.anki.preferences.sharedPrefs +import com.ichi2.compat.CompatHelper import com.ichi2.utils.KotlinCleanup import timber.log.Timber import kotlin.math.sqrt @@ -96,32 +101,7 @@ class AnkiDroidWidgetSmall : AppWidgetProvider() { updateViews.setViewVisibility(R.id.widget_due, View.INVISIBLE) updateViews.setViewVisibility(R.id.widget_eta, View.INVISIBLE) updateViews.setViewVisibility(R.id.ankidroid_widget_small_finish_layout, View.GONE) - if (mMountReceiver == null) { - mMountReceiver = object : BroadcastReceiver() { - @KotlinCleanup("Change parameter context name below, should not be used") - override fun onReceive(context: Context, intent: Intent) { - // baseContext() is null, applicationContext() throws a NPE, - // context may not have the locale override from AnkiDroidApp - val action = intent.action - if (action != null && action == Intent.ACTION_MEDIA_MOUNTED) { - Timber.d("mMountReceiver - Action = Media Mounted") - if (remounted) { - WidgetStatus.updateInBackground(AnkiDroidApp.instance) - remounted = false - if (mMountReceiver != null) { - AnkiDroidApp.instance.unregisterReceiver(mMountReceiver) - } - } else { - remounted = true - } - } - } - } - val iFilter = IntentFilter() - iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED) - iFilter.addDataScheme("file") - AnkiDroidApp.instance.registerReceiver(mMountReceiver, iFilter) - } + registerExternalStorageListener() } else { // If we do not have a cached version, always update. if (dueCardsCount == 0 || updateDueDecksNow) { @@ -170,6 +150,38 @@ class AnkiDroidWidgetSmall : AppWidgetProvider() { return updateViews } + private fun registerExternalStorageListener() { + if (mountReceiver != null) { + // The receiver is already registered. + return + } + val mountReceiver = object : BroadcastReceiver() { + @KotlinCleanup("Change parameter context name below, should not be used") + override fun onReceive(context: Context, intent: Intent) { + // baseContext() is null, applicationContext() throws a NPE, + // context may not have the locale override from AnkiDroidApp + val action = intent.action + if (action != null && action == Intent.ACTION_MEDIA_MOUNTED) { + Timber.d("mMountReceiver - Action = Media Mounted") + if (remounted) { + WidgetStatus.updateInBackground(AnkiDroidApp.instance) + remounted = false + if (mountReceiver != null) { + AnkiDroidApp.instance.unregisterReceiver(mountReceiver) + } + } else { + remounted = true + } + } + } + } + val iFilter = IntentFilter() + iFilter.addAction(Intent.ACTION_MEDIA_MOUNTED) + iFilter.addDataScheme("file") + CompatHelper.compat.registerExportedReceiver(AnkiDroidApp.instance, mountReceiver, iFilter) + AnkiDroidWidgetSmall.mountReceiver = mountReceiver + } + override fun onBind(arg0: Intent): IBinder? { Timber.d("onBind") return null @@ -177,7 +189,7 @@ class AnkiDroidWidgetSmall : AppWidgetProvider() { } companion object { - private var mMountReceiver: BroadcastReceiver? = null + private var mountReceiver: BroadcastReceiver? = null private var remounted = false private fun updateWidgetDimensions(context: Context, updateViews: RemoteViews, cls: Class<*>) { val manager = getAppWidgetManager(context) ?: return