diff --git a/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt b/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt new file mode 100644 index 00000000000..9244e13c53f --- /dev/null +++ b/androidshared/src/main/java/org/odk/collect/androidshared/system/UriExt.kt @@ -0,0 +1,93 @@ +package org.odk.collect.androidshared.system + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import android.webkit.MimeTypeMap +import androidx.core.net.toFile +import java.io.File +import java.io.FileOutputStream + +fun Uri.copyToFile(context: Context, dest: File) { + try { + context.contentResolver.openInputStream(this)?.use { inputStream -> + FileOutputStream(dest).use { outputStream -> + inputStream.copyTo(outputStream) + } + } + } catch (e: Exception) { + // ignore + } +} + +fun Uri.getFileExtension(context: Context): String? { + var extension = getFileName(context)?.substringAfterLast(".", "") + + if (extension.isNullOrEmpty()) { + val mimeType = context.contentResolver.getType(this) + + extension = if (scheme == ContentResolver.SCHEME_CONTENT) { + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) + } else { + MimeTypeMap.getFileExtensionFromUrl(toString()) + } + + if (extension.isNullOrEmpty()) { + extension = mimeType?.substringAfterLast("/", "") + } + } + + return if (extension.isNullOrEmpty()) { + null + } else { + ".$extension" + } +} + +fun Uri.getFileName(context: Context): String? { + var fileName: String? = null + + try { + when (scheme) { + ContentResolver.SCHEME_FILE -> fileName = toFile().name + ContentResolver.SCHEME_CONTENT -> { + val cursor = context.contentResolver.query(this, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val fileNameColumnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (fileNameColumnIndex != -1) { + fileName = it.getString(fileNameColumnIndex) + } + } + } + } + ContentResolver.SCHEME_ANDROID_RESOURCE -> { + // for uris like [android.resource://com.example.app/1234567890] + val resourceId = lastPathSegment?.toIntOrNull() + if (resourceId != null) { + fileName = context.resources.getResourceName(resourceId) + } else { + // for uris like [android.resource://com.example.app/raw/sample] + val packageName = authority + if (pathSegments.size >= 2) { + val resourceType = pathSegments[0] + val resourceEntryName = pathSegments[1] + val resId = context.resources.getIdentifier(resourceEntryName, resourceType, packageName) + if (resId != 0) { + fileName = context.resources.getResourceName(resId) + } + } + } + } + } + + if (fileName == null) { + fileName = path?.substringAfterLast("/") + } + } catch (e: Exception) { + // ignore + } + + return fileName +} diff --git a/androidshared/src/test/java/org/odk/collect/androidshared/system/UriExtTest.kt b/androidshared/src/test/java/org/odk/collect/androidshared/system/UriExtTest.kt new file mode 100644 index 00000000000..9dafdde3ef4 --- /dev/null +++ b/androidshared/src/test/java/org/odk/collect/androidshared/system/UriExtTest.kt @@ -0,0 +1,44 @@ +package org.odk.collect.androidshared.system + +import android.app.Application +import androidx.core.net.toUri +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.odk.collect.shared.TempFiles + +@RunWith(AndroidJUnit4::class) +class UriExtTest { + private val context = ApplicationProvider.getApplicationContext() + + @Test + fun `copyToFile copies the source file to the target file`() { + val sourceFile = TempFiles.createTempFile().also { + it.writeText("blah") + } + val sourceFileUri = sourceFile.toUri() + val targetFile = TempFiles.createTempFile() + + sourceFileUri.copyToFile(context, targetFile) + assertThat(targetFile.readText(), equalTo(sourceFile.readText())) + } + + @Test + fun `getFileExtension returns file extension`() { + val file = TempFiles.createTempFile(".jpg") + val fileUri = file.toUri() + + assertThat(fileUri.getFileExtension(context), equalTo(".jpg")) + } + + @Test + fun `getFileName returns file name`() { + val file = TempFiles.createTempFile() + val fileUri = file.toUri() + + assertThat(fileUri.getFileName(context), equalTo(file.name)) + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java index 984c007bb61..c1acd235054 100644 --- a/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java +++ b/collect_app/src/main/java/org/odk/collect/android/injection/config/AppDependencyModule.java @@ -137,7 +137,6 @@ import org.odk.collect.webpage.ExternalWebPageHelper; import java.io.File; -import java.util.Arrays; import javax.inject.Named; import javax.inject.Singleton; @@ -573,7 +572,8 @@ public PreferenceVisibilityHandler providesDisabledPreferencesRemover(SettingsPr @Provides public ReferenceLayerRepository providesReferenceLayerRepository(StoragePathProvider storagePathProvider, SettingsProvider settingsProvider) { return new DirectoryReferenceLayerRepository( - Arrays.asList(storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS), storagePathProvider.getOdkDirPath(StorageSubdirectory.SHARED_LAYERS)), + storagePathProvider.getOdkDirPath(StorageSubdirectory.SHARED_LAYERS), + storagePathProvider.getOdkDirPath(StorageSubdirectory.LAYERS), () -> MapConfiguratorProvider.getConfigurator( settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_BASEMAP_SOURCE) ) diff --git a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt index a20dc55e805..b6948878a19 100644 --- a/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt +++ b/collect_app/src/main/java/org/odk/collect/android/preferences/screens/MapsPreferencesFragment.kt @@ -56,7 +56,7 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP override fun onCreate(savedInstanceState: Bundle?) { childFragmentManager.fragmentFactory = FragmentFactoryBuilder() .forClass(OfflineMapLayersPicker::class) { - OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) + OfflineMapLayersPicker(requireActivity().activityResultRegistry, referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) } .build() @@ -110,8 +110,8 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP onBasemapSourceChanged(MapConfiguratorProvider.getConfigurator()) basemapSourcePref.setOnPreferenceChangeListener { _: Preference?, value: Any -> val cftor = MapConfiguratorProvider.getConfigurator(value.toString()) - if (!cftor.isAvailable(context)) { - cftor.showUnavailableMessage(context) + if (!cftor.isAvailable(requireContext())) { + cftor.showUnavailableMessage(requireContext()) false } else { onBasemapSourceChanged(cftor) @@ -142,7 +142,7 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP val baseCategory = findPreference(CATEGORY_BASEMAP) baseCategory!!.removeAll() baseCategory.addPreference(basemapSourcePref) - for (pref in cftor.createPrefs(context, settingsProvider.getUnprotectedSettings())) { + for (pref in cftor.createPrefs(requireContext(), settingsProvider.getUnprotectedSettings())) { pref.isIconSpaceReserved = false baseCategory.addPreference(pref) } diff --git a/collect_app/src/main/res/layout/reference_layer_help_footer.xml b/collect_app/src/main/res/layout/reference_layer_help_footer.xml deleted file mode 100644 index 83d0731ca8a..00000000000 --- a/collect_app/src/main/res/layout/reference_layer_help_footer.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/docs/CODE-GUIDELINES.md b/docs/CODE-GUIDELINES.md index 754eea08896..c2daebe7584 100644 --- a/docs/CODE-GUIDELINES.md +++ b/docs/CODE-GUIDELINES.md @@ -178,12 +178,18 @@ Collect is a multi module Gradle project. Modules should have a focused feature There's no easy way to define exactly when a new module should be pulled out of an existing one or when new code calls for a new module - it's best to discuss that with the team before making any decisions. Once a structure has been agreed on, to add a new module: 1. Click `File > New > New module...` in Android Studio -1. Decide whether the new module should be an "Android Library" or "Java or Kotlin Library" - ideally as much code as possible could avoid relying on Android but a lot of features will require at least one Android Library module -1. Review the generated `build.gradle` and remove any unnecessary dependencies or setup -1. Add quality checks to the module's `build.gradle`: +2. Decide whether the new module should be an "Android Library" or "Java or Kotlin Library" - ideally as much code as possible could avoid relying on Android but a lot of features will require at least one Android Library module +3. Review the generated `build.gradle` and remove any unnecessary dependencies or setup +4. Add quality checks to the module's `build.gradle`: ``` apply from: '../config/quality.gradle' ``` -1. If the module will have tests, make sure they get run on CI by adding a line to `test_modules.txt` with `:test` for a Java Library or `:testDebug` for an Android library +5. If the module will have tests, make sure they get run on CI by adding a line to `test_modules.txt` with `` and if it's a non-Android module, registering the `testDebug` task in its `build.gradle` file: + + ``` + tasks.register("testDebug") { + dependsOn("test") + } + ``` diff --git a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java index b731fc95187..071e441d7e1 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoint/GeoPointMapActivity.java @@ -141,7 +141,7 @@ public void onCreate(Bundle savedInstanceState) { getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() .forClass(MapFragment.class, () -> (Fragment) mapFragmentFactory.createMapFragment()) - .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper)) + .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(getActivityResultRegistry(), referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper)) .build() ); diff --git a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java index 634377fdf43..ae842ddf123 100644 --- a/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java +++ b/geo/src/main/java/org/odk/collect/geo/geopoly/GeoPolyActivity.java @@ -154,7 +154,7 @@ public void handleOnBackPressed() { getSupportFragmentManager().setFragmentFactory(new FragmentFactoryBuilder() .forClass(MapFragment.class, () -> (Fragment) mapFragmentFactory.createMapFragment()) - .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper)) + .forClass(OfflineMapLayersPicker.class, () -> new OfflineMapLayersPicker(getActivityResultRegistry(), referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper)) .build() ); diff --git a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt index 3ddb596d2f7..1aebdbe9a67 100644 --- a/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt +++ b/geo/src/main/java/org/odk/collect/geo/selection/SelectionMapFragment.kt @@ -97,7 +97,7 @@ class SelectionMapFragment( mapFragmentFactory.createMapFragment() as Fragment } .forClass(OfflineMapLayersPicker::class) { - OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) + OfflineMapLayersPicker(requireActivity().activityResultRegistry, referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) } .build() diff --git a/icons/src/main/res/drawable/ic_baseline_layers_24.xml b/icons/src/main/res/drawable/ic_baseline_layers_24.xml new file mode 100644 index 00000000000..2a93aa2f714 --- /dev/null +++ b/icons/src/main/res/drawable/ic_baseline_layers_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/maps/build.gradle.kts b/maps/build.gradle.kts index 1807f5b2a85..66c2fd3597b 100644 --- a/maps/build.gradle.kts +++ b/maps/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(project(":shared")) implementation(project(":androidshared")) implementation(project(":icons")) + implementation(project(":material")) implementation(project(":settings")) implementation(project(":strings")) implementation(project(":web-page")) diff --git a/maps/src/main/java/org/odk/collect/maps/MapConfigurator.java b/maps/src/main/java/org/odk/collect/maps/MapConfigurator.kt similarity index 67% rename from maps/src/main/java/org/odk/collect/maps/MapConfigurator.java rename to maps/src/main/java/org/odk/collect/maps/MapConfigurator.kt index 4f7da5e86bf..6c8af62df40 100644 --- a/maps/src/main/java/org/odk/collect/maps/MapConfigurator.java +++ b/maps/src/main/java/org/odk/collect/maps/MapConfigurator.kt @@ -1,15 +1,10 @@ -package org.odk.collect.maps; +package org.odk.collect.maps -import android.content.Context; -import android.os.Bundle; - -import androidx.preference.Preference; - -import org.odk.collect.shared.settings.Settings; - -import java.io.File; -import java.util.Collection; -import java.util.List; +import android.content.Context +import android.os.Bundle +import androidx.preference.Preference +import org.odk.collect.shared.settings.Settings +import java.io.File /** * For each MapFragment implementation class, there is one instance of this @@ -22,33 +17,33 @@ * For example, the GoogleMapConfigurator can define a "Google map style" * preference with choices such as Terrain or Satellite. */ -public interface MapConfigurator { - /** Returns true if this MapFragment implementation is available on this device. */ - boolean isAvailable(Context context); +interface MapConfigurator { + /** Returns true if this MapFragment implementation is available on this device. */ + fun isAvailable(context: Context): Boolean /** * Displays a warning to the user that this MapFragment implementation is * unavailable. This will be invoked when isSupported() is false or * createMapFragment(context) returns null. */ - void showUnavailableMessage(Context context); + fun showUnavailableMessage(context: Context) - /** Constructs any preference widgets that are specific to this map implementation. */ - List createPrefs(Context context, Settings settings); + /** Constructs any preference widgets that are specific to this map implementation. */ + fun createPrefs(context: Context, settings: Settings): List - /** Gets the set of keys for preferences that should be watched for changes. */ - Collection getPrefKeys(); + /** Gets the set of keys for preferences that should be watched for changes. */ + val prefKeys: Collection - /** Packs map-related preferences into a Bundle for MapFragment.applyConfig(). */ - Bundle buildConfig(Settings prefs); + /** Packs map-related preferences into a Bundle for MapFragment.applyConfig(). */ + fun buildConfig(prefs: Settings): Bundle /** * Returns true if map fragments obtained from this MapConfigurator are * expected to be able to render the given file as an overlay. This * check determines which files appear as available Reference Layers. */ - boolean supportsLayer(File file); + fun supportsLayer(file: File): Boolean - /** Returns a String name for a given overlay file, or null if unsupported. */ - String getDisplayName(File file); + /** Returns a String name for a given overlay file, or null if unsupported. */ + fun getDisplayName(file: File): String } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt index 4f3158c520f..384385ffb35 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepository.kt @@ -6,7 +6,8 @@ import org.odk.collect.shared.files.DirectoryUtils.listFilesRecursively import java.io.File class DirectoryReferenceLayerRepository( - private val directoryPaths: List, + private val sharedLayersDirPath: String, + private val projectLayersDirPath: String, private val getMapConfigurator: () -> MapConfigurator ) : ReferenceLayerRepository { @@ -27,7 +28,15 @@ class DirectoryReferenceLayerRepository( } } - private fun getAllFilesWithDirectory() = directoryPaths.flatMap { dir -> + override fun addLayer(file: File, shared: Boolean) { + if (shared) { + file.copyTo(File(sharedLayersDirPath, file.name), true) + } else { + file.copyTo(File(projectLayersDirPath, file.name), true) + } + } + + private fun getAllFilesWithDirectory() = listOf(sharedLayersDirPath, projectLayersDirPath).flatMap { dir -> listFilesRecursively(File(dir)).map { file -> Pair(file, dir) } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/MbtilesFile.java b/maps/src/main/java/org/odk/collect/maps/layers/MbtilesFile.java index 0e15ba40ba1..8dfbb202e8a 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/MbtilesFile.java +++ b/maps/src/main/java/org/odk/collect/maps/layers/MbtilesFile.java @@ -28,6 +28,8 @@ * See https://github.com/mapbox/mbtiles-spec for the detailed specification. */ public class MbtilesFile implements Closeable, TileSource { + public static final String FILE_EXTENSION = ".mbtiles"; + public enum LayerType { RASTER, VECTOR } private final File file; @@ -166,7 +168,7 @@ private static String detectContentType(File file) throws MbtilesException { if (!file.exists() || !file.isFile()) { throw new NotFileException(file); } - if (!file.getName().toLowerCase(Locale.US).endsWith(".mbtiles")) { + if (!file.getName().toLowerCase(Locale.US).endsWith(FILE_EXTENSION)) { throw new UnsupportedFilenameException(file); } try (SQLiteDatabase db = openSqliteReadOnly(file)) { diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt new file mode 100644 index 00000000000..a3ca996b4ba --- /dev/null +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporter.kt @@ -0,0 +1,79 @@ +package org.odk.collect.maps.layers + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.odk.collect.async.Scheduler +import org.odk.collect.maps.databinding.OfflineMapLayersImporterBinding +import org.odk.collect.material.MaterialFullScreenDialogFragment +import org.odk.collect.settings.SettingsProvider + +class OfflineMapLayersImporter( + private val referenceLayerRepository: ReferenceLayerRepository, + private val scheduler: Scheduler, + private val settingsProvider: SettingsProvider +) : MaterialFullScreenDialogFragment() { + val viewModel: OfflineMapLayersViewModel by activityViewModels { + object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider) as T + } + } + } + + private lateinit var binding: OfflineMapLayersImporterBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = OfflineMapLayersImporterBinding.inflate(inflater) + + binding.cancelButton.setOnClickListener { + dismiss() + } + + binding.addLayerButton.setOnClickListener { + viewModel.importNewLayers(binding.allProjectsOption.isChecked) + dismiss() + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.isLoading.observe(this) { isLoading -> + if (isLoading) { + binding.addLayerButton.isEnabled = false + binding.layers.visibility = View.GONE + binding.progressIndicator.visibility = View.VISIBLE + } else { + binding.addLayerButton.isEnabled = true + binding.layers.visibility = View.VISIBLE + binding.progressIndicator.visibility = View.GONE + } + } + + viewModel.layersToImport.observe(this) { layersToImport -> + val adapter = OfflineMapLayersImporterAdapter(layersToImport) + binding.layers.setAdapter(adapter) + } + } + + override fun onCloseClicked() = Unit + + override fun onBackPressed() { + dismiss() + } + + override fun getToolbar(): Toolbar { + return binding.toolbar + } +} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterAdapter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterAdapter.kt new file mode 100644 index 00000000000..d011f7e8d61 --- /dev/null +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersImporterAdapter.kt @@ -0,0 +1,24 @@ +package org.odk.collect.maps.layers + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.odk.collect.maps.databinding.OfflineMapLayersImporterItemBinding + +class OfflineMapLayersImporterAdapter( + private val layers: List, +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = OfflineMapLayersImporterItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.binding.layerName.text = layers[position].name + } + + override fun getItemCount() = layers.size + + class ViewHolder(val binding: OfflineMapLayersImporterItemBinding) : RecyclerView.ViewHolder(binding.root) +} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt index 00a177f1e28..360b692c5c7 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPicker.kt @@ -5,11 +5,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.viewModels +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.activityViewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.odk.collect.androidshared.ui.DialogFragmentUtils +import org.odk.collect.androidshared.ui.FragmentFactoryBuilder import org.odk.collect.androidshared.ui.GroupClickListener.addOnClickListener import org.odk.collect.async.Scheduler import org.odk.collect.maps.databinding.OfflineMapLayersPickerBinding @@ -17,55 +21,93 @@ import org.odk.collect.settings.SettingsProvider import org.odk.collect.webpage.ExternalWebPageHelper class OfflineMapLayersPicker( + registry: ActivityResultRegistry, private val referenceLayerRepository: ReferenceLayerRepository, private val scheduler: Scheduler, private val settingsProvider: SettingsProvider, private val externalWebPageHelper: ExternalWebPageHelper ) : BottomSheetDialogFragment() { - private val viewModel: OfflineMapLayersPickerViewModel by viewModels { + private val viewModel: OfflineMapLayersViewModel by activityViewModels { object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return OfflineMapLayersPickerViewModel(referenceLayerRepository, scheduler, settingsProvider) as T + return OfflineMapLayersViewModel(referenceLayerRepository, scheduler, settingsProvider) as T } } } - private lateinit var offlineMapLayersPickerBinding: OfflineMapLayersPickerBinding + private lateinit var binding: OfflineMapLayersPickerBinding + + private val getLayers = registerForActivityResult(ActivityResultContracts.GetMultipleContents(), registry) { uris -> + if (uris.isNotEmpty()) { + viewModel.loadLayersToImport(uris, requireContext()) + DialogFragmentUtils.showIfNotShowing( + OfflineMapLayersImporter::class.java, + childFragmentManager + ) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + childFragmentManager.fragmentFactory = FragmentFactoryBuilder() + .forClass(OfflineMapLayersImporter::class) { + OfflineMapLayersImporter(referenceLayerRepository, scheduler, settingsProvider) + } + .build() + + super.onCreate(savedInstanceState) + } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - offlineMapLayersPickerBinding = OfflineMapLayersPickerBinding.inflate(inflater) - - viewModel.data.observe(this) { data -> - offlineMapLayersPickerBinding.progressIndicator.visibility = View.GONE - offlineMapLayersPickerBinding.layers.visibility = View.VISIBLE - offlineMapLayersPickerBinding.save.isEnabled = true + binding = OfflineMapLayersPickerBinding.inflate(inflater) - val offlineMapLayersAdapter = OfflineMapLayersAdapter(data.first, data.second) { - viewModel.changeSelectedLayerId(it) - } - offlineMapLayersPickerBinding.layers.setAdapter(offlineMapLayersAdapter) - } - - offlineMapLayersPickerBinding.mbtilesInfoGroup.addOnClickListener { + binding.mbtilesInfoGroup.addOnClickListener { externalWebPageHelper.openWebPageInCustomTab( requireActivity(), Uri.parse("https://docs.getodk.org/collect-offline-maps/#transferring-offline-tilesets-to-devices") ) } - offlineMapLayersPickerBinding.cancel.setOnClickListener { + binding.addLayer.setOnClickListener { + getLayers.launch("*/*") + } + + binding.cancel.setOnClickListener { dismiss() } - offlineMapLayersPickerBinding.save.setOnClickListener { + binding.save.setOnClickListener { viewModel.saveSelectedLayer() dismiss() } - return offlineMapLayersPickerBinding.root + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.isLoading.observe(this) { isLoading -> + if (isLoading) { + binding.progressIndicator.visibility = View.VISIBLE + binding.layers.visibility = View.GONE + binding.save.isEnabled = false + } else { + binding.progressIndicator.visibility = View.GONE + binding.layers.visibility = View.VISIBLE + binding.save.isEnabled = true + } + } + + viewModel.existingLayers.observe(this) { layers -> + val adapter = OfflineMapLayersPickerAdapter(layers.first, layers.second) { + viewModel.changeSelectedLayerId(it) + } + binding.layers.setAdapter(adapter) + } } override fun onStart() { @@ -78,8 +120,4 @@ class OfflineMapLayersPicker( // ignore } } - - companion object { - const val TAG = "OfflineMapLayersPicker" - } } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersAdapter.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt similarity index 77% rename from maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersAdapter.kt rename to maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt index 18896722cf4..e4be8ce6bdb 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersAdapter.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerAdapter.kt @@ -3,17 +3,17 @@ package org.odk.collect.maps.layers import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import org.odk.collect.maps.databinding.OfflineMapLayerBinding +import org.odk.collect.maps.databinding.OfflineMapLayersPickerItemBinding import org.odk.collect.strings.localization.getLocalizedString -class OfflineMapLayersAdapter( +class OfflineMapLayersPickerAdapter( private val layers: List, private var selectedLayerId: String?, private val onSelectedLayerChanged: (String?) -> Unit -) : RecyclerView.Adapter() { +) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val binding = OfflineMapLayerBinding.inflate(LayoutInflater.from(parent.context), parent, false) + val binding = OfflineMapLayersPickerItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder(binding) } @@ -41,5 +41,5 @@ class OfflineMapLayersAdapter( override fun getItemCount() = layers.size + 1 - class ViewHolder(val binding: OfflineMapLayerBinding) : RecyclerView.ViewHolder(binding.root) + class ViewHolder(val binding: OfflineMapLayersPickerItemBinding) : RecyclerView.ViewHolder(binding.root) } diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt deleted file mode 100644 index a53aa25b82b..00000000000 --- a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersPickerViewModel.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.odk.collect.maps.layers - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.odk.collect.async.Scheduler -import org.odk.collect.settings.SettingsProvider -import org.odk.collect.settings.keys.ProjectKeys - -class OfflineMapLayersPickerViewModel( - private val referenceLayerRepository: ReferenceLayerRepository, - scheduler: Scheduler, - private val settingsProvider: SettingsProvider -) : ViewModel() { - private val _data = MutableLiveData, String?>>() - val data: LiveData, String?>> = _data - - init { - scheduler.immediate( - background = { - val layers = referenceLayerRepository.getAll() - val selectedLayerId = settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) - - _data.postValue(Pair(layers, selectedLayerId)) - }, - foreground = { } - ) - } - - fun saveSelectedLayer() { - val selectedLayerId = data.value?.second - settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, selectedLayerId) - } - - fun changeSelectedLayerId(selectedLayerId: String?) { - _data.postValue(_data.value?.copy(second = selectedLayerId)) - } -} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt new file mode 100644 index 00000000000..f23e7bc6871 --- /dev/null +++ b/maps/src/main/java/org/odk/collect/maps/layers/OfflineMapLayersViewModel.kt @@ -0,0 +1,101 @@ +package org.odk.collect.maps.layers + +import android.content.Context +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.odk.collect.androidshared.system.copyToFile +import org.odk.collect.androidshared.system.getFileExtension +import org.odk.collect.androidshared.system.getFileName +import org.odk.collect.async.Scheduler +import org.odk.collect.settings.SettingsProvider +import org.odk.collect.settings.keys.ProjectKeys +import org.odk.collect.shared.TempFiles +import java.io.File + +class OfflineMapLayersViewModel( + private val referenceLayerRepository: ReferenceLayerRepository, + private val scheduler: Scheduler, + private val settingsProvider: SettingsProvider +) : ViewModel() { + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + private val _existingLayers = MutableLiveData, String?>>() + val existingLayers: LiveData, String?>> = _existingLayers + + private val _layersToImport = MutableLiveData>() + val layersToImport: LiveData> = _layersToImport + + private lateinit var tempLayersDir: File + + init { + loadExistingLayers() + } + + private fun loadExistingLayers() { + _isLoading.value = true + scheduler.immediate( + background = { + val layers = referenceLayerRepository.getAll() + val selectedLayerId = + settingsProvider.getUnprotectedSettings().getString(ProjectKeys.KEY_REFERENCE_LAYER) + + _isLoading.postValue(false) + _existingLayers.postValue(Pair(layers, selectedLayerId)) + }, + foreground = { } + ) + } + + fun loadLayersToImport(uris: List, context: Context) { + _isLoading.value = true + scheduler.immediate( + background = { + tempLayersDir = TempFiles.createTempDir().also { + it.deleteOnExit() + } + val layers = mutableListOf() + uris.forEach { uri -> + if (uri.getFileExtension(context) == MbtilesFile.FILE_EXTENSION) { + uri.getFileName(context)?.let { fileName -> + val layerFile = File(tempLayersDir, fileName).also { file -> + uri.copyToFile(context, file) + } + layers.add(ReferenceLayer(layerFile.absolutePath, layerFile, MbtilesFile.readName(layerFile) ?: layerFile.name)) + } + } + } + _isLoading.postValue(false) + _layersToImport.postValue(layers) + }, + foreground = { } + ) + } + + fun importNewLayers(shared: Boolean) { + _isLoading.value = true + scheduler.immediate( + background = { + tempLayersDir.listFiles()?.forEach { + referenceLayerRepository.addLayer(it, shared) + } + tempLayersDir.delete() + _isLoading.postValue(false) + }, + foreground = { + loadExistingLayers() + } + ) + } + + fun saveSelectedLayer() { + val selectedLayerId = existingLayers.value?.second + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, selectedLayerId) + } + + fun changeSelectedLayerId(selectedLayerId: String?) { + _existingLayers.postValue(_existingLayers.value?.copy(second = selectedLayerId)) + } +} diff --git a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt index 58c17cadea5..f8dadbaa540 100644 --- a/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt +++ b/maps/src/main/java/org/odk/collect/maps/layers/ReferenceLayerRepository.kt @@ -6,6 +6,7 @@ interface ReferenceLayerRepository { fun getAll(): List fun get(id: String): ReferenceLayer? + fun addLayer(file: File, shared: Boolean) } data class ReferenceLayer(val id: String, val file: File, val name: String) diff --git a/maps/src/main/res/layout/offline_map_layers_importer.xml b/maps/src/main/res/layout/offline_map_layers_importer.xml new file mode 100644 index 00000000000..2b678de5f32 --- /dev/null +++ b/maps/src/main/res/layout/offline_map_layers_importer.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/maps/src/main/res/layout/offline_map_layers_importer_item.xml b/maps/src/main/res/layout/offline_map_layers_importer_item.xml new file mode 100644 index 00000000000..c564a94f6a4 --- /dev/null +++ b/maps/src/main/res/layout/offline_map_layers_importer_item.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/maps/src/main/res/layout/offline_map_layers_picker.xml b/maps/src/main/res/layout/offline_map_layers_picker.xml index 1d8d46d4fca..2c407c36d13 100644 --- a/maps/src/main/res/layout/offline_map_layers_picker.xml +++ b/maps/src/main/res/layout/offline_map_layers_picker.xml @@ -1,7 +1,6 @@ @@ -90,14 +89,13 @@ android:id="@+id/layers" android:layout_width="0dp" android:layout_height="0dp" - android:visibility="gone" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + android:layout_marginBottom="@dimen/margin_extra_small" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintBottom_toTopOf="@id/bottom_divider" + app:layout_constraintBottom_toTopOf="@id/add_layer" app:layout_constraintHeight_default="wrap" - app:layout_constraintTop_toBottomOf="@id/top_divider" - tools:visibility="visible" /> + app:layout_constraintTop_toBottomOf="@id/top_divider" /> + + - \ No newline at end of file + diff --git a/maps/src/main/res/layout/offline_map_layer.xml b/maps/src/main/res/layout/offline_map_layers_picker_item.xml similarity index 100% rename from maps/src/main/res/layout/offline_map_layer.xml rename to maps/src/main/res/layout/offline_map_layers_picker_item.xml diff --git a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt index bcd5a6637f1..e8e52ce54f2 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/DirectoryReferenceLayerRepositoryTest.kt @@ -13,55 +13,56 @@ import org.odk.collect.shared.settings.Settings import java.io.File class DirectoryReferenceLayerRepositoryTest { + private val sharedLayersDir = TempFiles.createTempDir() + private val projectLayersDir = TempFiles.createTempDir() + private val mapConfigurator = StubMapConfigurator() + private val repository = DirectoryReferenceLayerRepository( + sharedLayersDir.absolutePath, + projectLayersDir.absolutePath + ) { mapConfigurator } + @Test fun getAll_returnsAllSupportedLayersInTheDirectory() { - val dir = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir) - val file2 = TempFiles.createTempFile(dir) - val file3 = TempFiles.createTempFile(dir) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, false, file2.name) - it.addFile(file3, true, file3.name) + val file1 = TempFiles.createTempFile(sharedLayersDir) + val file2 = TempFiles.createTempFile(sharedLayersDir) + val file3 = TempFiles.createTempFile(sharedLayersDir) + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, false, file2.name) + addFile(file3, true, file3.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } @Test fun getAll_returnsAllSupportedLayersInSubDirectories() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir(dir1) - val file1 = TempFiles.createTempFile(dir2) - val file2 = TempFiles.createTempFile(dir2) - val file3 = TempFiles.createTempFile(dir2) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, false, file2.name) - it.addFile(file3, true, file3.name) - } - - val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath)) { mapConfigurator } + val subDir = TempFiles.createTempDir(sharedLayersDir) + val file1 = TempFiles.createTempFile(subDir) + val file2 = TempFiles.createTempFile(subDir) + val file3 = TempFiles.createTempFile(subDir) + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, false, file2.name) + addFile(file3, true, file3.name) + } + assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } @Test fun getAll_withMultipleDirectories_returnsAllSupportedLayersInAllDirectories() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir() - val dir3 = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir1) - val file2 = TempFiles.createTempFile(dir2) - val file3 = TempFiles.createTempFile(dir3) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, false, file2.name) - it.addFile(file3, true, file3.name) - } - - val repository = - DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath, dir3.absolutePath)) { mapConfigurator } + val file1 = TempFiles.createTempFile(sharedLayersDir) + val file2 = TempFiles.createTempFile(sharedLayersDir) + val file3 = TempFiles.createTempFile(projectLayersDir) + val file4 = TempFiles.createTempFile(projectLayersDir) + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, false, file2.name) + addFile(file3, true, file3.name) + addFile(file4, false, file4.name) + } + assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1, file3)) } @@ -72,79 +73,62 @@ class DirectoryReferenceLayerRepositoryTest { */ @Test fun getAll_withMultipleDirectoriesWithFilesWithTheSameRelativePath_onlyReturnsTheSupportedFileFromTheFirstDirectory() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir() - val dir3 = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir1, "blah", ".temp") - val file2 = TempFiles.createTempFile(dir2, "blah", ".temp") - val file3 = TempFiles.createTempFile(dir3, "blah", ".temp") - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, false, file2.name) - it.addFile(file3, true, file3.name) - } - - val repository = - DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath, dir3.absolutePath)) { mapConfigurator } + val file1 = TempFiles.createTempFile(sharedLayersDir, "blah", ".temp") + val file2 = TempFiles.createTempFile(projectLayersDir, "blah", ".temp") + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, true, file2.name) + } + assertThat(repository.getAll().map { it.file }, containsInAnyOrder(file1)) } @Test fun get_returnsLayer() { - val dir = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir) - val file2 = TempFiles.createTempFile(dir) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, true, file2.name) + val file1 = TempFiles.createTempFile(sharedLayersDir) + val file2 = TempFiles.createTempFile(sharedLayersDir) + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, true, file2.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } val file2Layer = repository.getAll().first { it.file == file2 } assertThat(repository.get(file2Layer.id)!!.file, equalTo(file2)) } @Test fun get_withMultipleDirectories_returnsLayer() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir1) - val file2 = TempFiles.createTempFile(dir2) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, true, file2.name) + val file1 = TempFiles.createTempFile(sharedLayersDir) + val file2 = TempFiles.createTempFile(projectLayersDir) + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, true, file2.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath)) { mapConfigurator } val file2Layer = repository.getAll().first { it.file == file2 } assertThat(repository.get(file2Layer.id)!!.file, equalTo(file2)) } @Test fun get_withMultipleDirectoriesWithFilesWithTheSameRelativePath_returnsLayerFromFirstDirectory() { - val dir1 = TempFiles.createTempDir() - val dir2 = TempFiles.createTempDir() - val file1 = TempFiles.createTempFile(dir1, "blah", ".temp") - val file2 = TempFiles.createTempFile(dir2, "blah", ".temp") - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file1, true, file1.name) - it.addFile(file2, true, file2.name) + val file1 = TempFiles.createTempFile(sharedLayersDir, "blah", ".temp") + val file2 = TempFiles.createTempFile(projectLayersDir, "blah", ".temp") + mapConfigurator.apply { + addFile(file1, true, file1.name) + addFile(file2, true, file2.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir1.absolutePath, dir2.absolutePath)) { mapConfigurator } val layerId = repository.getAll().first().id assertThat(repository.get(layerId)!!.file, equalTo(file1)) } @Test fun get_whenFileDoesNotExist_returnsNull() { - val dir = TempFiles.createTempDir() - val file = TempFiles.createTempFile(dir) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file, true, file.name) + val file = TempFiles.createTempFile(sharedLayersDir) + mapConfigurator.apply { + addFile(file, true, file.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } val fileLayer = repository.getAll().first { it.file == file } file.delete() @@ -153,27 +137,53 @@ class DirectoryReferenceLayerRepositoryTest { @Test fun get_returnsLayerWithCorrectName() { - val dir = TempFiles.createTempDir() - val file = TempFiles.createTempFile(dir) + val file = TempFiles.createTempFile(sharedLayersDir) - val mapConfigurator = StubMapConfigurator().also { - it.addFile(file, true, file.name) + mapConfigurator.apply { + addFile(file, true, file.name) } - val repository = DirectoryReferenceLayerRepository(listOf(dir.absolutePath)) { mapConfigurator } val fileLayer = repository.getAll().first { it.file == file } assertThat(repository.get(fileLayer.id)!!.name, equalTo(file.name)) } + @Test + fun addLayer_movesFileToTheSharedLayersDir_whenSharedIsTrue() { + val file = TempFiles.createTempFile().also { + it.writeText("blah") + } + + repository.addLayer(file, true) + + assertThat(sharedLayersDir.listFiles().size, equalTo(1)) + assertThat(sharedLayersDir.listFiles()[0].name, equalTo(file.name)) + assertThat(sharedLayersDir.listFiles()[0].readText(), equalTo("blah")) + assertThat(projectLayersDir.listFiles().size, equalTo(0)) + } + + @Test + fun addLayer_movesFileToTheProjectLayersDir_whenSharedIsFalse() { + val file = TempFiles.createTempFile().also { + it.writeText("blah") + } + + repository.addLayer(file, false) + + assertThat(sharedLayersDir.listFiles().size, equalTo(0)) + assertThat(projectLayersDir.listFiles().size, equalTo(1)) + assertThat(projectLayersDir.listFiles()[0].name, equalTo(file.name)) + assertThat(projectLayersDir.listFiles()[0].readText(), equalTo("blah")) + } + private class StubMapConfigurator : MapConfigurator { private val files = mutableMapOf>() - override fun supportsLayer(file: File?): Boolean { + override fun supportsLayer(file: File): Boolean { return files[file]!!.first } - override fun getDisplayName(file: File?): String { + override fun getDisplayName(file: File): String { return files[file]!!.second } @@ -181,21 +191,20 @@ class DirectoryReferenceLayerRepositoryTest { files[file] = Pair(isSupported, displayName) } - override fun isAvailable(context: Context?): Boolean { + override fun isAvailable(context: Context): Boolean { TODO("Not yet implemented") } - override fun showUnavailableMessage(context: Context?) { + override fun showUnavailableMessage(context: Context) { TODO("Not yet implemented") } - override fun createPrefs(context: Context?, settings: Settings?): MutableList { + override fun createPrefs(context: Context, settings: Settings): MutableList { TODO("Not yet implemented") } - override fun getPrefKeys(): MutableCollection { - TODO("Not yet implemented") - } + override val prefKeys: Collection + get() = TODO("Not yet implemented") override fun buildConfig(prefs: Settings): Bundle { TODO("Not yet implemented") diff --git a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt index 70680f7360b..36b91bd7f7a 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/MapFragmentReferenceLayerUtilsTest.kt @@ -24,7 +24,7 @@ class MapFragmentReferenceLayerUtilsTest { assertNull( MapFragmentReferenceLayerUtils.getReferenceLayerFile( config, - DirectoryReferenceLayerRepository(listOf(layersPath), mock()) + DirectoryReferenceLayerRepository(layersPath, "", mock()) ) ) } @@ -37,7 +37,7 @@ class MapFragmentReferenceLayerUtilsTest { assertNull( MapFragmentReferenceLayerUtils.getReferenceLayerFile( config, - DirectoryReferenceLayerRepository(listOf(layersPath), mock()) + DirectoryReferenceLayerRepository(layersPath, "", mock()) ) ) } @@ -57,7 +57,7 @@ class MapFragmentReferenceLayerUtilsTest { assertNotNull( MapFragmentReferenceLayerUtils.getReferenceLayerFile( config, - DirectoryReferenceLayerRepository(listOf(layersPath)) { mapConfigurator } + DirectoryReferenceLayerRepository(layersPath, "") { mapConfigurator } ) ) } diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt new file mode 100644 index 00000000000..f304774db04 --- /dev/null +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersImporterTest.kt @@ -0,0 +1,250 @@ +package org.odk.collect.maps.layers + +import androidx.core.net.toUri +import androidx.fragment.app.testing.FragmentScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.scrollTo +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.not +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.odk.collect.androidshared.ui.FragmentFactoryBuilder +import org.odk.collect.fragmentstest.FragmentScenarioLauncherRule +import org.odk.collect.settings.InMemSettingsProvider +import org.odk.collect.shared.TempFiles +import org.odk.collect.strings.R +import org.odk.collect.testshared.EspressoHelpers +import org.odk.collect.testshared.FakeScheduler +import org.odk.collect.testshared.RecyclerViewMatcher +import org.odk.collect.testshared.RobolectricHelpers +import java.io.File + +@RunWith(AndroidJUnit4::class) +class OfflineMapLayersImporterTest { + private val scheduler = FakeScheduler() + private val referenceLayerRepository = mock() + private val settingsProvider = InMemSettingsProvider() + + @get:Rule + val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule( + FragmentFactoryBuilder() + .forClass(OfflineMapLayersImporter::class) { + OfflineMapLayersImporter(referenceLayerRepository, scheduler, settingsProvider) + }.build() + ) + + @Test + fun `clicking the 'cancel' button dismisses the dialog`() { + launchFragment().onFragment { + scheduler.flush() + assertThat(it.isVisible, equalTo(true)) + EspressoHelpers.clickOnText(R.string.cancel) + assertThat(it.isVisible, equalTo(false)) + } + } + + @Test + fun `clicking the 'add layer' button dismisses the dialog`() { + launchFragment().onFragment { + scheduler.flush() + assertThat(it.isVisible, equalTo(true)) + it.viewModel.loadLayersToImport(emptyList(), it.requireContext()) + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) + scheduler.flush() + RobolectricHelpers.runLooper() + assertThat(it.isVisible, equalTo(false)) + } + } + + @Test + fun `progress indicator is displayed during loading layers`() { + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) + + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) + } + + onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(isDisplayed())) + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(not(isDisplayed()))) + + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.progress_indicator)).check(matches(not(isDisplayed()))) + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(isDisplayed())) + } + + @Test + fun `the 'cancel' button is enabled during loading layers`() { + launchFragment() + + onView(withId(org.odk.collect.maps.R.id.cancel_button)).check(matches(isEnabled())) + scheduler.flush() + onView(withId(org.odk.collect.maps.R.id.cancel_button)).check(matches(isEnabled())) + } + + @Test + fun `the 'add layer' button is disabled during loading layers`() { + launchFragment() + + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).check(matches(not(isEnabled()))) + scheduler.flush() + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).check(matches(isEnabled())) + } + + @Test + fun `'All projects' location should be selected by default`() { + launchFragment() + + onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(isChecked())) + onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(not(isChecked()))) + } + + @Test + fun `checking location sets selection correctly`() { + launchFragment() + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) + + onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(not(isChecked()))) + onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(isChecked())) + + onView(withId(org.odk.collect.maps.R.id.all_projects_option)).perform(click()) + + onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(isChecked())) + onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(not(isChecked()))) + } + + @Test + fun `recreating maintains the selected layers location`() { + val scenario = launchFragment() + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(click()) + + scenario.recreate() + + onView(withId(org.odk.collect.maps.R.id.all_projects_option)).check(matches(not(isChecked()))) + onView(withId(org.odk.collect.maps.R.id.current_project_option)).check(matches(isChecked())) + } + + @Test + fun `the list of selected layers should be displayed`() { + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) + + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) + } + + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) + onView(withText(file1.name)).check(matches(isDisplayed())) + onView(withText(file2.name)).check(matches(isDisplayed())) + } + + @Test + fun `recreating maintains the list of selected layers`() { + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) + + val scenario = launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) + } + + scheduler.flush() + + scenario.recreate() + + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(2))) + onView(withText(file1.name)).check(matches(isDisplayed())) + onView(withText(file2.name)).check(matches(isDisplayed())) + } + + @Test + fun `only mbtiles files are taken into account`() { + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", ".txt") + + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) + } + + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(1))) + onView(withText(file1.name)).check(matches(isDisplayed())) + onView(withText(file2.name)).check(doesNotExist()) + } + + @Test + fun `clicking the 'add layer' button moves the files to the shared layers dir if it is selected`() { + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) + + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) + } + + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) + scheduler.flush() + + val fileCaptor = argumentCaptor() + val booleanCaptor = argumentCaptor() + + verify(referenceLayerRepository, times(2)).addLayer(fileCaptor.capture(), booleanCaptor.capture()) + assertThat(fileCaptor.allValues.any { file -> file.name == file1.name }, equalTo(true)) + assertThat(fileCaptor.allValues.any { file -> file.name == file2.name }, equalTo(true)) + assertThat(booleanCaptor.firstValue, equalTo(true)) + assertThat(booleanCaptor.secondValue, equalTo(true)) + } + + @Test + fun `clicking the 'add layer' button moves the files to the project layers dir if it is selected`() { + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) + + launchFragment().onFragment { + it.viewModel.loadLayersToImport(listOf(file1.toUri(), file2.toUri()), it.requireContext()) + } + + scheduler.flush() + + onView(withId(org.odk.collect.maps.R.id.current_project_option)).perform(scrollTo(), click()) + + onView(withId(org.odk.collect.maps.R.id.add_layer_button)).perform(click()) + scheduler.flush() + + val fileCaptor = argumentCaptor() + val booleanCaptor = argumentCaptor() + + verify(referenceLayerRepository, times(2)).addLayer(fileCaptor.capture(), booleanCaptor.capture()) + assertThat(fileCaptor.allValues.any { file -> file.name == file1.name }, equalTo(true)) + assertThat(fileCaptor.allValues.any { file -> file.name == file2.name }, equalTo(true)) + assertThat(booleanCaptor.firstValue, equalTo(false)) + assertThat(booleanCaptor.secondValue, equalTo(false)) + } + + private fun launchFragment(): FragmentScenario { + return fragmentScenarioLauncherRule.launchInContainer(OfflineMapLayersImporter::class.java) + } +} diff --git a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt index e4e6c551b2e..cfff10481f9 100644 --- a/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt +++ b/maps/src/test/java/org/odk/collect/maps/layers/OfflineMapLayersPickerTest.kt @@ -1,10 +1,16 @@ package org.odk.collect.maps.layers import android.net.Uri +import androidx.activity.result.ActivityResultRegistry +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.app.ActivityOptionsCompat +import androidx.core.net.toUri import androidx.fragment.app.testing.FragmentScenario import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.assertThat import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -13,6 +19,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.instanceOf import org.hamcrest.CoreMatchers.not import org.junit.Rule import org.junit.Test @@ -37,24 +44,36 @@ import org.odk.collect.webpage.ExternalWebPageHelper @RunWith(AndroidJUnit4::class) class OfflineMapLayersPickerTest { - private val referenceLayerRepository = mock().also { - whenever(it.getAll()).thenReturn(emptyList()) - } + private val referenceLayerRepository = mock() private val scheduler = FakeScheduler() private val settingsProvider = InMemSettingsProvider() private val externalWebPageHelper = mock() + private val uris = mutableListOf() + private val testRegistry = object : ActivityResultRegistry() { + override fun onLaunch( + requestCode: Int, + contract: ActivityResultContract, + input: I, + options: ActivityOptionsCompat? + ) { + assertThat(contract, instanceOf(ActivityResultContracts.GetMultipleContents()::class.java)) + assertThat(input, equalTo("*/*")) + dispatchResult(requestCode, uris) + } + } + @get:Rule val fragmentScenarioLauncherRule = FragmentScenarioLauncherRule( FragmentFactoryBuilder() .forClass(OfflineMapLayersPicker::class) { - OfflineMapLayersPicker(referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) + OfflineMapLayersPicker(testRegistry, referenceLayerRepository, scheduler, settingsProvider, externalWebPageHelper) }.build() ) @Test fun `clicking the 'cancel' button dismisses the layers picker`() { - val scenario = launchFragment() + val scenario = launchOfflineMapLayersPicker() scenario.onFragment { assertThat(it.isVisible, equalTo(true)) @@ -65,11 +84,11 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'cancel' button does not save the layer`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) + ) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -79,14 +98,14 @@ class OfflineMapLayersPickerTest { @Test fun `the 'cancel' button should be enabled during loading layers`() { - launchFragment() + launchOfflineMapLayersPicker() onView(withText(string.cancel)).check(matches(isEnabled())) } @Test fun `clicking the 'save' button dismisses the layers picker`() { - val scenario = launchFragment() + val scenario = launchOfflineMapLayersPicker() scheduler.flush() @@ -99,7 +118,7 @@ class OfflineMapLayersPickerTest { @Test fun `the 'save' button should be disabled during loading layers`() { - launchFragment() + launchOfflineMapLayersPicker() onView(withText(string.save)).check(matches(not(isEnabled()))) scheduler.flush() @@ -108,11 +127,11 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'save' button saves null when 'None' option is checked`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) + ) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -122,11 +141,11 @@ class OfflineMapLayersPickerTest { @Test fun `clicking the 'save' button saves the layer id if any is checked`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) + ) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -137,11 +156,11 @@ class OfflineMapLayersPickerTest { @Test fun `when no layer id is saved in settings the 'None' option should be checked`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) + ) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -151,13 +170,16 @@ class OfflineMapLayersPickerTest { @Test fun `when layer id is saved in settings the layer it belongs to should be checked`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), - ReferenceLayer("2", TempFiles.createTempFile(), "layer2") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), + ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + ) + ) + settingsProvider.getUnprotectedSettings().save(ProjectKeys.KEY_REFERENCE_LAYER, "2") - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -168,7 +190,7 @@ class OfflineMapLayersPickerTest { @Test fun `progress indicator is displayed during loading layers`() { - launchFragment() + launchOfflineMapLayersPicker() onView(withId(R.id.progress_indicator)).check(matches(isDisplayed())) onView(withId(R.id.layers)).check(matches(not(isDisplayed()))) @@ -181,25 +203,25 @@ class OfflineMapLayersPickerTest { @Test fun `the 'learn more' button should be enabled during loading layers`() { - launchFragment() + launchOfflineMapLayersPicker() onView(withText(string.get_help_with_reference_layers)).check(matches(isEnabled())) } @Test fun `clicking the 'learn more' button opens the forum thread`() { - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() - onView(withText(string.get_help_with_reference_layers)).perform(click()) + EspressoHelpers.clickOnText(string.get_help_with_reference_layers) verify(externalWebPageHelper).openWebPageInCustomTab(any(), eq(Uri.parse("https://docs.getodk.org/collect-offline-maps/#transferring-offline-tilesets-to-devices"))) } @Test fun `if there are no layers the 'none' option is displayed`() { - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -209,12 +231,14 @@ class OfflineMapLayersPickerTest { @Test fun `if there are multiple layers all of them are displayed along with the 'None'`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), - ReferenceLayer("2", TempFiles.createTempFile(), "layer2") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", TempFiles.createTempFile(), "layer1"), + ReferenceLayer("2", TempFiles.createTempFile(), "layer2") + ) + ) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -226,11 +250,11 @@ class OfflineMapLayersPickerTest { @Test fun `checking layers sets selection correctly`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) + ) - launchFragment() + launchOfflineMapLayersPicker() scheduler.flush() @@ -245,11 +269,11 @@ class OfflineMapLayersPickerTest { @Test fun `recreating maintains selection`() { - whenever(referenceLayerRepository.getAll()).thenReturn(listOf( - ReferenceLayer("1", TempFiles.createTempFile(), "layer1") - )) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf(ReferenceLayer("1", TempFiles.createTempFile(), "layer1")) + ) - val scenario = launchFragment() + val scenario = launchOfflineMapLayersPicker() scheduler.flush() @@ -259,7 +283,90 @@ class OfflineMapLayersPickerTest { onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(isChecked())) } - private fun launchFragment(): FragmentScenario { + @Test + fun `clicking the 'add layer' and selecting layers displays the confirmation dialog`() { + val scenario = launchOfflineMapLayersPicker() + + uris.add(Uri.parse("blah")) + EspressoHelpers.clickOnText(string.add_layer) + + scenario.onFragment { + assertThat( + it.childFragmentManager.findFragmentByTag(OfflineMapLayersImporter::class.java.name), + instanceOf(OfflineMapLayersImporter::class.java) + ) + } + } + + @Test + fun `clicking the 'add layer' and selecting nothing does not display the confirmation dialog`() { + val scenario = launchOfflineMapLayersPicker() + + EspressoHelpers.clickOnText(string.add_layer) + + scenario.onFragment { + assertThat( + it.childFragmentManager.findFragmentByTag(OfflineMapLayersImporter::class.java.name), + equalTo(null) + ) + } + } + + @Test + fun `progress indicator is displayed during loading layers after receiving new ones`() { + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) + + launchOfflineMapLayersPicker() + + scheduler.flush() + + uris.add(file1.toUri()) + uris.add(file2.toUri()) + + EspressoHelpers.clickOnText(string.add_layer) + scheduler.flush() + onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(click()) + + onView(withId(R.id.progress_indicator)).check(matches(isDisplayed())) + onView(withId(R.id.layers)).check(matches(not(isDisplayed()))) + + scheduler.flush() + + onView(withId(R.id.progress_indicator)).check(matches(not(isDisplayed()))) + onView(withId(R.id.layers)).check(matches(isDisplayed())) + } + + @Test + fun `when new layers added the list should be updated`() { + val file1 = TempFiles.createTempFile("layer1", MbtilesFile.FILE_EXTENSION) + val file2 = TempFiles.createTempFile("layer2", MbtilesFile.FILE_EXTENSION) + + launchOfflineMapLayersPicker() + + scheduler.flush() + + uris.add(file1.toUri()) + uris.add(file2.toUri()) + + EspressoHelpers.clickOnText(string.add_layer) + scheduler.flush() + onView(withId(R.id.add_layer_button)).inRoot(isDialog()).perform(click()) + whenever(referenceLayerRepository.getAll()).thenReturn( + listOf( + ReferenceLayer("1", TempFiles.createTempFile(), file1.name), + ReferenceLayer("2", TempFiles.createTempFile(), file2.name) + ) + ) + scheduler.flush() + + onView(withId(R.id.layers)).check(matches(RecyclerViewMatcher.withListSize(3))) + onView(withRecyclerView(R.id.layers).atPositionOnView(0, R.id.radio_button)).check(matches(withText(string.none))) + onView(withRecyclerView(R.id.layers).atPositionOnView(1, R.id.radio_button)).check(matches(withText(file1.name))) + onView(withRecyclerView(R.id.layers).atPositionOnView(2, R.id.radio_button)).check(matches(withText(file2.name))) + } + + private fun launchOfflineMapLayersPicker(): FragmentScenario { return fragmentScenarioLauncherRule.launchInContainer(OfflineMapLayersPicker::class.java) } }