Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ dependencies {
implementation libs.androidx.core
implementation libs.material
implementation libs.androidx.webkit
implementation libs.androidx.recyclerview
implementation libs.androidx.lifecycle.viewmodel
implementation libs.androidx.lifecycle.livedata

implementation libs.asset.extractor

Expand Down
175 changes: 175 additions & 0 deletions app/src/androidTest/java/app/opendocument/droid/test/LandingTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// ActivityTestRule instead of ActivityScenario, matching MainActivityTests - see the note there.
@file:Suppress("DEPRECATION")

package app.opendocument.droid.test

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.SystemClock
import androidx.core.content.FileProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import app.opendocument.droid.R
import app.opendocument.droid.background.RecentDocumentsUtil
import app.opendocument.droid.ui.activity.DocumentFragment
import app.opendocument.droid.ui.activity.MainActivity
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/** The landing screen: the recently opened documents and the actions offered next to them. */
@LargeTest
@RunWith(AndroidJUnit4::class)
class LandingTests {

// launched by hand in each test, so that the recently opened list can be seeded first -
// the landing screen reads it while it is coming up
@get:Rule val activityRule = ActivityTestRule(MainActivity::class.java, false, false)

@Before
fun setUp() {
clearLandingState()

Intents.init()
}

@After
fun tearDown() {
Intents.release()

clearLandingState()

if (activityRule.activity != null) {
activityRule.finishActivity()

InstrumentationRegistry.getInstrumentation().waitForIdleSync()
}
}

@Test
fun emptyStateIsShownWhenNothingWasOpenedYet() {
launch()

onView(withId(R.id.landing_empty)).check(matches(isDisplayed()))
}

@Test
fun aRecentDocumentIsListed() {
seedRecentDocument()

launch()

onView(withText(TEST_DOCUMENT)).check(matches(isDisplayed()))
}

@Test
fun aRecentDocumentOpensWhenTapped() {
seedRecentDocument()

val activity = launch()

onView(withText(TEST_DOCUMENT)).perform(click())

Assert.assertTrue(
"the document did not load after tapping its entry in the recent list",
waitForDocumentFragment(activity),
)
}

@Test
fun theCatchAllSettingIsOffered() {
seedRecentDocument()

launch()

onView(withText(R.string.landing_catch_all_title)).check(matches(isDisplayed()))
}

private fun launch(): MainActivity {
val activity = activityRule.launchActivity(null)

activity.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))

// the list is filled from a background executor, so give it a moment to arrive
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
SystemClock.sleep(1000)
InstrumentationRegistry.getInstrumentation().waitForIdleSync()

return activity
}

private fun seedRecentDocument() {
RecentDocumentsUtil.addRecentDocument(context(), TEST_DOCUMENT, uriOf(requireTestFile()))
}

private fun clearLandingState() {
context().deleteFile("recent_documents.json")
}

private fun waitForDocumentFragment(activity: MainActivity): Boolean {
val deadline = SystemClock.uptimeMillis() + LOAD_TIMEOUT_MS

while (SystemClock.uptimeMillis() < deadline) {
val fragment =
activity.supportFragmentManager.findFragmentByTag("document_fragment")
as DocumentFragment?
if (fragment != null) {
return true
}

SystemClock.sleep(200)
}

return false
}

private fun context(): Context = InstrumentationRegistry.getInstrumentation().targetContext

private fun uriOf(file: File): Uri =
FileProvider.getUriForFile(context(), context().packageName + ".provider", file)

private fun requireTestFile(): File = checkNotNull(testFile) { "test file was not extracted" }

companion object {
private const val TEST_DOCUMENT = "test.odt"
private const val LOAD_TIMEOUT_MS = 20000L

private var testFile: File? = null

// @JvmStatic because junit requires @BeforeClass to be static
@JvmStatic
@BeforeClass
fun extractTestFile() {
val instrumentation = InstrumentationRegistry.getInstrumentation()

val directory = File(instrumentation.targetContext.cacheDir, "test-documents")
directory.mkdirs()

val target = File(directory, TEST_DOCUMENT)
copy(instrumentation.context.assets.open(TEST_DOCUMENT), target)

testFile = target
}

private fun copy(source: InputStream, target: File) {
source.use { input -> FileOutputStream(target).use { output -> input.copyTo(output) } }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package app.opendocument.droid.background

import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager

/**
* Whether the app offers itself for every file type, or only for the document types it actually
* supports.
*
* Backed by the two activity aliases in the manifest: exactly one of them is enabled at a time, and
* which one decides how broad the intent filter the system sees is.
*/
object CatchAllSetting {

private const val PREF_CATCH_ALL_ENABLED = "catch_all_enabled"

// these keep the historical at.tomtasche.reader names on purpose: the component name is
// what the OS persists when a user picks "always open .odt with this app", and what
// setComponentEnabledSetting stores the toggle against. renaming them would drop those
// defaults for every existing install, so the manifest declares the aliases under the old
// names too.
private const val CATCH_ALL_COMPONENT = "at.tomtasche.reader.ui.activity.MainActivity.CATCH_ALL"
private const val STRICT_CATCH_COMPONENT =
"at.tomtasche.reader.ui.activity.MainActivity.STRICT_CATCH"

/**
* Puts the aliases back in the state the stored setting asks for.
*
* Has to run on every launch, not just when the landing screen is shown: users upgrading from a
* version that shipped different alias defaults are only corrected here, and the app is
* regularly started straight into a document by an external intent.
*/
fun applyOnLaunch(context: Context): Boolean {
val enabled = isEnabled(context)

apply(context, enabled)

return enabled
}

fun isEnabled(context: Context): Boolean =
// catch-all is only active if the user explicitly opted in. new installs and existing
// users who never touched the switch default to STRICT_CATCH, so we no longer volunteer
// to open unrelated file types like contacts (issue #477).
preferences(context).getBoolean(PREF_CATCH_ALL_ENABLED, false)

fun setEnabled(context: Context, enabled: Boolean) {
preferences(context).edit().putBoolean(PREF_CATCH_ALL_ENABLED, enabled).apply()

apply(context, enabled)
}

private fun apply(context: Context, enabled: Boolean) {
toggleComponent(context, CATCH_ALL_COMPONENT, enabled)
toggleComponent(context, STRICT_CATCH_COMPONENT, !enabled)
}

private fun toggleComponent(context: Context, className: String, enabled: Boolean) {
val newState =
if (enabled) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED

context.packageManager.setComponentEnabledSetting(
ComponentName(context, className),
newState,
PackageManager.DONT_KILL_APP,
)
}

/**
* The preferences android.preference.PreferenceManager used to hand out. That class is
* deprecated and its androidx replacement lives in a whole preference-ui library we do not
* otherwise need, so the default file is opened by name instead - keeping the existing
* catch-all setting of users who upgrade.
*/
private fun preferences(context: Context): SharedPreferences =
context.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE)
}
Loading
Loading