Skip to content

Commit

Permalink
Merge pull request getodk#6169 from grzesiek2010/COLLECT-5846
Browse files Browse the repository at this point in the history
Allow MBTile files to be imported
  • Loading branch information
grzesiek2010 authored Jun 7, 2024
2 parents b537b21 + 63a7a4b commit df496d4
Show file tree
Hide file tree
Showing 29 changed files with 1,187 additions and 273 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<Application>()

@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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -142,7 +142,7 @@ class MapsPreferencesFragment : BaseProjectPreferencesFragment(), Preference.OnP
val baseCategory = findPreference<PreferenceCategory>(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)
}
Expand Down
18 changes: 0 additions & 18 deletions collect_app/src/main/res/layout/reference_layer_help_footer.xml

This file was deleted.

14 changes: 10 additions & 4 deletions docs/CODE-GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<module-name>:test` for a Java Library or `<module-name>: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 `<module-name>` and if it's a non-Android module, registering the `testDebug` task in its `build.gradle` file:

```
tasks.register("testDebug") {
dependsOn("test")
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
13 changes: 13 additions & 0 deletions icons/src/main/res/drawable/ic_baseline_layers_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true"
android:tint="?colorOnSurface">

<path
android:fillColor="?colorOnSurface"
android:pathData="M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27 -7.38,5.74zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16z"/>
</vector>
1 change: 1 addition & 0 deletions maps/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Preference> createPrefs(Context context, Settings settings);
/** Constructs any preference widgets that are specific to this map implementation. */
fun createPrefs(context: Context, settings: Settings): List<Preference>

/** Gets the set of keys for preferences that should be watched for changes. */
Collection<String> getPrefKeys();
/** Gets the set of keys for preferences that should be watched for changes. */
val prefKeys: Collection<String>

/** 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import org.odk.collect.shared.files.DirectoryUtils.listFilesRecursively
import java.io.File

class DirectoryReferenceLayerRepository(
private val directoryPaths: List<String>,
private val sharedLayersDirPath: String,
private val projectLayersDirPath: String,
private val getMapConfigurator: () -> MapConfigurator
) : ReferenceLayerRepository {

Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down
Loading

0 comments on commit df496d4

Please sign in to comment.