mirror of
https://github.com/ankidroid/Anki-Android.git
synced 2024-09-20 03:52:15 +02:00
feat: show that media is running in-app (#16127)
* test: remove RobolectricTestAnnotationTest We're going to introduce WorkManagerTestInitHelper and it's outlived its usefulness Change to a check on `setUp` * feat: show that media is running in-app useful if the user has notifications disabled * fix: sync badges --------- Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com>
This commit is contained in:
parent
340f2fa9ba
commit
f502ddb852
@ -403,6 +403,7 @@ dependencies {
|
||||
testImplementation(libs.androidx.espresso.contrib) {
|
||||
exclude module: "protobuf-lite"
|
||||
}
|
||||
testImplementation libs.androidx.work.testing
|
||||
|
||||
androidTestImplementation project(':testlib')
|
||||
|
||||
|
@ -601,7 +601,16 @@
|
||||
>
|
||||
<meta-data android:name="com.ichi2.anki.provider.spec" android:value="2" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="remove">
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.apkgfileprovider"
|
||||
|
@ -33,6 +33,7 @@ import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.work.Configuration
|
||||
import com.ichi2.anki.CrashReportService.sendExceptionReport
|
||||
import com.ichi2.anki.analytics.UsageAnalytics
|
||||
import com.ichi2.anki.browser.SharedPreferencesLastDeckIdRepository
|
||||
@ -69,7 +70,7 @@ import java.util.Locale
|
||||
*/
|
||||
@KotlinCleanup("lots to do")
|
||||
@KotlinCleanup("IDE Lint")
|
||||
open class AnkiDroidApp : Application() {
|
||||
open class AnkiDroidApp : Application(), Configuration.Provider {
|
||||
/** An exception if the WebView subsystem fails to load */
|
||||
private var webViewError: Throwable? = null
|
||||
private val notifications = MutableLiveData<Void?>()
|
||||
@ -80,6 +81,9 @@ open class AnkiDroidApp : Application() {
|
||||
/** Used to avoid showing extra progress dialogs when one already shown. */
|
||||
var progressDialogShown = false
|
||||
|
||||
override val workManagerConfiguration: Configuration
|
||||
get() = Configuration.Builder().build()
|
||||
|
||||
@KotlinCleanup("analytics can be moved to attachBaseContext()")
|
||||
/**
|
||||
* On application creation.
|
||||
|
@ -53,6 +53,8 @@ import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.view.MenuItemCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
@ -61,10 +63,13 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import anki.collection.OpChanges
|
||||
import anki.sync.SyncStatusResponse
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.ichi2.anim.ActivityTransitionAnimation.Direction.*
|
||||
@ -116,6 +121,7 @@ import com.ichi2.anki.utils.SECONDS_PER_DAY
|
||||
import com.ichi2.anki.widgets.DeckAdapter
|
||||
import com.ichi2.anki.worker.SyncMediaWorker
|
||||
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.Companion.getSerializableCompat
|
||||
@ -129,6 +135,7 @@ import com.ichi2.utils.*
|
||||
import com.ichi2.utils.NetworkUtils.isActiveNetworkMetered
|
||||
import com.ichi2.widget.WidgetStatus
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import makeLinksClickable
|
||||
@ -230,6 +237,8 @@ open class DeckPicker :
|
||||
addCallback(activeSnackbarCallback)
|
||||
}
|
||||
|
||||
private var syncMediaProgressJob: Job? = null
|
||||
|
||||
// flag keeping track of when the app has been paused
|
||||
var activityPaused = false
|
||||
private set
|
||||
@ -791,6 +800,7 @@ open class DeckPicker :
|
||||
floatingActionMenu.closeFloatingActionMenu(applyRiseAndShrinkAnimation = false)
|
||||
menuInflater.inflate(R.menu.deck_picker, menu)
|
||||
menu.findItem(R.id.action_export)?.title = TR.exportingExport()
|
||||
setupMediaSyncMenuItem(menu)
|
||||
setupSearchIcon(menu.findItem(R.id.deck_picker_action_filter))
|
||||
toolbarSearchView = menu.findItem(R.id.deck_picker_action_filter).actionView as SearchView
|
||||
toolbarSearchView?.maxWidth = Integer.MAX_VALUE
|
||||
@ -810,6 +820,31 @@ open class DeckPicker :
|
||||
private var migrationProgressPublishingJob: Job? = null
|
||||
private var cachedMigrationProgressMenuItemActionView: View? = null
|
||||
|
||||
private fun setupMediaSyncMenuItem(menu: Menu) {
|
||||
// shouldn't be necessary, but `invalidateOptionsMenu()` is called way more than necessary
|
||||
syncMediaProgressJob?.cancel()
|
||||
|
||||
val syncItem = menu.findItem(R.id.action_sync)
|
||||
val progressIndicator = syncItem.actionView
|
||||
?.findViewById<LinearProgressIndicator>(R.id.progress_indicator)
|
||||
|
||||
val workManager = WorkManager.getInstance(this)
|
||||
val flow = workManager.getWorkInfosForUniqueWorkFlow(UniqueWorkNames.SYNC_MEDIA)
|
||||
|
||||
syncMediaProgressJob = lifecycleScope.launch {
|
||||
flow.flowWithLifecycle(lifecycle).collectLatest {
|
||||
val workInfo = it.lastOrNull()
|
||||
if (workInfo?.state == WorkInfo.State.RUNNING && progressIndicator?.isVisible == false) {
|
||||
Timber.i("DeckPicker: Showing media sync progress indicator")
|
||||
progressIndicator.isVisible = true
|
||||
} else if (progressIndicator?.isVisible == true) {
|
||||
Timber.i("DeckPicker: Hiding media sync progress indicator")
|
||||
progressIndicator.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the menu item that shows circular progress of storage migration.
|
||||
* Can be called multiple times without harm.
|
||||
@ -956,32 +991,32 @@ open class DeckPicker :
|
||||
private fun updateSyncIconFromState(menuItem: MenuItem, state: OptionsMenuState) {
|
||||
if (state.mediaMigrationState is MediaMigrationState.Ongoing) {
|
||||
menuItem.isVisible = false
|
||||
} else {
|
||||
menuItem.isVisible = true
|
||||
|
||||
menuItem.setTitle(
|
||||
when (state.syncIcon) {
|
||||
SyncIconState.Normal, SyncIconState.PendingChanges -> R.string.button_sync
|
||||
SyncIconState.OneWay -> R.string.sync_menu_title_one_way_sync
|
||||
SyncIconState.NotLoggedIn -> R.string.sync_menu_title_no_account
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
menuItem.isVisible = true
|
||||
|
||||
menuItem.setTitle(
|
||||
when (state.syncIcon) {
|
||||
SyncIconState.Normal -> {
|
||||
BadgeDrawableBuilder.removeBadge(menuItem)
|
||||
}
|
||||
SyncIconState.PendingChanges -> {
|
||||
BadgeDrawableBuilder(this)
|
||||
.withColor(getColor(R.color.badge_warning))
|
||||
.replaceBadge(menuItem)
|
||||
}
|
||||
SyncIconState.OneWay, SyncIconState.NotLoggedIn -> {
|
||||
BadgeDrawableBuilder(this)
|
||||
.withText('!')
|
||||
.withColor(getColor(R.color.badge_error))
|
||||
.replaceBadge(menuItem)
|
||||
}
|
||||
SyncIconState.Normal, SyncIconState.PendingChanges -> R.string.button_sync
|
||||
SyncIconState.OneWay -> R.string.sync_menu_title_one_way_sync
|
||||
SyncIconState.NotLoggedIn -> R.string.sync_menu_title_no_account
|
||||
}
|
||||
)
|
||||
val provider = MenuItemCompat.getActionProvider(menuItem) as? SyncActionProvider ?: return
|
||||
when (state.syncIcon) {
|
||||
SyncIconState.Normal -> {
|
||||
BadgeDrawableBuilder.removeBadge(provider)
|
||||
}
|
||||
SyncIconState.PendingChanges -> {
|
||||
BadgeDrawableBuilder(this)
|
||||
.withColor(getColor(R.color.badge_warning))
|
||||
.replaceBadge(provider)
|
||||
}
|
||||
SyncIconState.OneWay, SyncIconState.NotLoggedIn -> {
|
||||
BadgeDrawableBuilder(this)
|
||||
.withText('!')
|
||||
.withColor(getColor(R.color.badge_error))
|
||||
.replaceBadge(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1040,7 +1075,14 @@ open class DeckPicker :
|
||||
}
|
||||
R.id.action_sync -> {
|
||||
Timber.i("DeckPicker:: Sync button pressed")
|
||||
sync()
|
||||
val actionProvider = MenuItemCompat.getActionProvider(item) as? SyncActionProvider
|
||||
if (actionProvider?.isProgressShown == true) {
|
||||
launchCatchingTask {
|
||||
monitorMediaSync(this@DeckPicker)
|
||||
}
|
||||
} else {
|
||||
sync()
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.action_scoped_storage_migrate -> {
|
||||
|
@ -22,10 +22,12 @@ import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.edit
|
||||
import anki.sync.SyncAuth
|
||||
import anki.sync.SyncCollectionResponse
|
||||
import anki.sync.syncAuth
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.ichi2.anki.CollectionManager.TR
|
||||
import com.ichi2.anki.CollectionManager.withCol
|
||||
import com.ichi2.anki.dialogs.DialogHandlerMessage
|
||||
@ -42,7 +44,15 @@ import com.ichi2.libanki.syncLogin
|
||||
import com.ichi2.libanki.utils.TimeManager
|
||||
import com.ichi2.preferences.VersatileTextWithASwitchPreference
|
||||
import com.ichi2.utils.NetworkUtils
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.ankiweb.rsdroid.Backend
|
||||
import net.ankiweb.rsdroid.exceptions.BackendInterruptedException
|
||||
import net.ankiweb.rsdroid.exceptions.BackendSyncException
|
||||
import timber.log.Timber
|
||||
|
||||
@ -342,6 +352,52 @@ fun DeckPicker.shouldFetchMedia(preferences: SharedPreferences): Boolean {
|
||||
(shouldFetchMedia == onlyIfUnmetered && !NetworkUtils.isActiveNetworkMetered())
|
||||
}
|
||||
|
||||
suspend fun monitorMediaSync(
|
||||
deckPicker: DeckPicker
|
||||
) {
|
||||
val backend = CollectionManager.getBackend()
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
val dialog = withContext(Dispatchers.Main) {
|
||||
AlertDialog.Builder(deckPicker)
|
||||
.setTitle(TR.syncMediaLogTitle())
|
||||
.setMessage("")
|
||||
.setPositiveButton(R.string.dialog_continue) { _, _ ->
|
||||
scope.cancel()
|
||||
}
|
||||
.setNegativeButton(R.string.dialog_cancel) { _, _ ->
|
||||
cancelMediaSync(backend)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun showMessage(msg: String) = deckPicker.showSnackbar(msg, Snackbar.LENGTH_SHORT)
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
while (true) {
|
||||
// this will throw if the sync exited with an error
|
||||
val resp = backend.mediaSyncStatus()
|
||||
if (!resp.active) {
|
||||
break
|
||||
}
|
||||
val text = resp.progress.run { "$added\n$removed\n$checked" }
|
||||
dialog.setMessage(text)
|
||||
delay(100)
|
||||
}
|
||||
showMessage(TR.syncMediaComplete())
|
||||
} catch (_: BackendInterruptedException) {
|
||||
showMessage(TR.syncMediaAborted())
|
||||
} catch (_: CancellationException) {
|
||||
// do nothing
|
||||
} catch (_: Exception) {
|
||||
showMessage(TR.syncMediaFailed())
|
||||
} finally {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from [DeckPicker.onMediaSyncCompleted] -> [DeckPicker.migrate] if the app is backgrounded
|
||||
*/
|
||||
|
60
AnkiDroid/src/main/java/com/ichi2/anki/SyncActionProvider.kt
Normal file
60
AnkiDroid/src/main/java/com/ichi2/anki/SyncActionProvider.kt
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Brayan Oliveira <brayandso.dev@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation; either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.ichi2.anki
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.ichi2.ui.RtlCompliantActionProvider.Companion.unwrapContext
|
||||
|
||||
class SyncActionProvider(context: Context) : ActionProviderCompat(context) {
|
||||
val activity: Activity = unwrapContext(context)
|
||||
|
||||
private var progressIndicator: LinearProgressIndicator? = null
|
||||
private var syncButton: AppCompatImageButton? = null
|
||||
|
||||
val isProgressShown: Boolean
|
||||
get() = progressIndicator?.isVisible == true
|
||||
|
||||
var icon: Drawable?
|
||||
get() = syncButton?.drawable
|
||||
set(value) {
|
||||
syncButton?.setImageDrawable(value)
|
||||
}
|
||||
|
||||
override fun onCreateActionView(forItem: MenuItem): View {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val view = inflater.inflate(R.layout.sync_progress_layout, null)
|
||||
|
||||
progressIndicator = view.findViewById(R.id.progress_indicator)
|
||||
syncButton = view.findViewById<AppCompatImageButton?>(R.id.button).apply {
|
||||
setOnClickListener {
|
||||
if (!forItem.isEnabled) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
activity.onOptionsItemSelected(forItem)
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
@ -18,9 +18,9 @@ package com.ichi2.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.MenuItem
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.ichi2.anki.R
|
||||
import com.ichi2.anki.SyncActionProvider
|
||||
import timber.log.Timber
|
||||
|
||||
class BadgeDrawableBuilder(private val context: Context) {
|
||||
@ -36,9 +36,9 @@ class BadgeDrawableBuilder(private val context: Context) {
|
||||
return this
|
||||
}
|
||||
|
||||
fun replaceBadge(menuItem: MenuItem) {
|
||||
fun replaceBadge(provider: SyncActionProvider) {
|
||||
Timber.d("Adding badge")
|
||||
var originalIcon = menuItem.icon
|
||||
var originalIcon = provider.icon
|
||||
if (originalIcon is BadgeDrawable) {
|
||||
originalIcon = originalIcon.current
|
||||
}
|
||||
@ -55,15 +55,15 @@ class BadgeDrawableBuilder(private val context: Context) {
|
||||
val mutableDrawable = badgeDrawable.mutate()
|
||||
mutableDrawable.setTint(color!!)
|
||||
badge.setBadgeDrawable(mutableDrawable)
|
||||
menuItem.icon = badge
|
||||
provider.icon = badge
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun removeBadge(menuItem: MenuItem) {
|
||||
val icon = menuItem.icon
|
||||
fun removeBadge(provider: SyncActionProvider) {
|
||||
val icon = provider.icon
|
||||
if (icon is BadgeDrawable) {
|
||||
menuItem.icon = icon.drawable
|
||||
provider.icon = icon.drawable
|
||||
Timber.d("Badge removed")
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ class RtlCompliantActionProvider(context: Context) : ActionProviderCompat(contex
|
||||
* @param context a context that may be of type [ContextWrapper]
|
||||
* @return The activity of the passed context
|
||||
*/
|
||||
private fun unwrapContext(context: Context): Activity {
|
||||
fun unwrapContext(context: Context): Activity {
|
||||
var unwrappedContext: Context? = context
|
||||
while (unwrappedContext !is Activity && unwrappedContext is ContextWrapper) {
|
||||
unwrappedContext = unwrappedContext.baseContext
|
||||
|
32
AnkiDroid/src/main/res/layout/sync_progress_layout.xml
Normal file
32
AnkiDroid/src/main/res/layout/sync_progress_layout.xml
Normal file
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:context=".SyncActionProvider">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/button"
|
||||
android:layout_width="@dimen/touch_target"
|
||||
android:layout_height="@dimen/touch_target"
|
||||
app:srcCompat="@drawable/ic_sync"
|
||||
style="@android:style/Widget.ActionButton"
|
||||
android:tint="@color/white"
|
||||
tools:tint="?attr/colorControlNormal"
|
||||
/>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress_indicator"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:layout_gravity="center|bottom"
|
||||
android:layout_marginBottom="2dp"
|
||||
app:indicatorColor="@color/slider_active_track_color"
|
||||
app:trackColor="@color/slider_inactive_track_color"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
@ -28,6 +28,7 @@
|
||||
android:id="@+id/action_sync"
|
||||
android:icon="@drawable/ic_sync"
|
||||
android:title="@string/button_sync"
|
||||
ankidroid:actionProviderClass="com.ichi2.anki.SyncActionProvider"
|
||||
ankidroid:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/action_undo"
|
||||
|
@ -30,6 +30,9 @@ import androidx.fragment.app.DialogFragment
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.testing.SynchronousExecutor
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import com.ichi2.anki.dialogs.DialogHandler
|
||||
import com.ichi2.anki.dialogs.utils.FragmentTestActivity
|
||||
import com.ichi2.anki.preferences.sharedPrefs
|
||||
@ -94,6 +97,14 @@ open class RobolectricTest : AndroidTest {
|
||||
|
||||
ChangeManager.clearSubscribers()
|
||||
|
||||
validateRunWithAnnotationPresent()
|
||||
|
||||
val config = Configuration.Builder()
|
||||
.setExecutor(SynchronousExecutor())
|
||||
.build()
|
||||
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, config)
|
||||
|
||||
// resolved issues with the collection being reused if useInMemoryDatabase is false
|
||||
CollectionManager.setColForTests(null)
|
||||
|
||||
@ -275,17 +286,7 @@ open class RobolectricTest : AndroidTest {
|
||||
}
|
||||
|
||||
val targetContext: Context
|
||||
get() {
|
||||
return try {
|
||||
ApplicationProvider.getApplicationContext()
|
||||
} catch (e: IllegalStateException) {
|
||||
if (e.message != null && e.message!!.startsWith("No instrumentation registered!")) {
|
||||
// Explicitly ignore the inner exception - generates line noise
|
||||
throw IllegalStateException("Annotate class: '${javaClass.simpleName}' with '@RunWith(AndroidJUnit4.class)'")
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
get() = ApplicationProvider.getApplicationContext()
|
||||
|
||||
/**
|
||||
* Returns an instance of [SharedPreferences] using the test context
|
||||
@ -422,6 +423,18 @@ open class RobolectricTest : AndroidTest {
|
||||
fun editPreferences(action: SharedPreferences.Editor.() -> Unit) =
|
||||
getPreferences().edit(action = action)
|
||||
|
||||
private fun validateRunWithAnnotationPresent() {
|
||||
try {
|
||||
ApplicationProvider.getApplicationContext()
|
||||
} catch (e: IllegalStateException) {
|
||||
if (e.message != null && e.message!!.startsWith("No instrumentation registered!")) {
|
||||
// Explicitly ignore the inner exception - generates line noise
|
||||
throw IllegalStateException("Annotate class: '${javaClass.simpleName}' with '@RunWith(AndroidJUnit4.class)'")
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeSetupBackend() {
|
||||
try {
|
||||
targetContext
|
||||
|
@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 David Allison <davidallisongithub@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation; either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.ichi2.anki
|
||||
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.core.StringContains.containsString
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
// explicitly missing @RunWith(AndroidJUnit4.class)
|
||||
class RobolectricTestAnnotationTest : RobolectricTest() {
|
||||
@Test
|
||||
fun readableErrorIfNotAnnotated() {
|
||||
val exception = assertFailsWith<IllegalStateException> {
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val unused = this.targetContext
|
||||
}
|
||||
assertThat(exception.message, containsString("RobolectricTestAnnotationTest"))
|
||||
assertThat(exception.message, containsString("@RunWith(AndroidJUnit4.class)"))
|
||||
}
|
||||
}
|
@ -136,6 +136,7 @@ androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "andro
|
||||
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxRunner" }
|
||||
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidxTextCore" }
|
||||
androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTextCore" }
|
||||
androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "workRuntimeKtx" }
|
||||
commons-exec = { module = "org.apache.commons:commons-exec", version.ref = "commonsExec" }
|
||||
hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
|
||||
hamcrest-library = { module = "org.hamcrest:hamcrest-library", version.ref = "hamcrest" }
|
||||
|
Loading…
Reference in New Issue
Block a user