Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File access and Android 11+ #1743

Open
MKergall opened this issue Sep 17, 2021 · 26 comments
Open

File access and Android 11+ #1743

MKergall opened this issue Sep 17, 2021 · 26 comments

Comments

@MKergall
Copy link
Collaborator

From Android 11, Google introduced drastical constraints on file access.
In short, an app cannot access any file, except files located in its "private" directory, directory which is not accessible to other apps, including File Explorers.
And you cannot publish an app on Google Play anymore, if the app is not targeting Android 11.

This raises various issues for apps using osmdroid features.

  1. Good news: If you let osmdroid define its default cache directory, it will target the app "private" directory => usual file read/write is allowed, online tile sources will work fine.

  2. Bad news:
    Offline tiles: outside the app (using File Explorer), you cannot drop files in this cache directory.
    MapsForge maps: outside the app (using File Explorer), you cannot put MapsForge files in a directory than could be read by the app.
    Geopackage maps: same issue.

Workaround: you could request MANAGE_EXTERNAL_STORAGE permission. But again, when publishing your app on Google Play, you can anticipate that it will never be approved (trust me, I tried...).

The "good" direction defined by Google for file access is based on SAF framework: forget java.io.File class...

I think this subject needs investigations and suggestions for solutions.

Issue Type

[X] Bug / Improvement

If it's a bug, version(s) of android this affects:

Android 11+ (API Level 30 and above)

Version of osmdroid the issue relates to:

At least up to 6.1.11

@spyhunter99
Copy link
Collaborator

for starters, we can make a new config manager class (or adjust the existing one) that handles api 30+ and only looks in app private storage, bypassing the storage utils class. This should resolve a lot of issues people are having with the current setup.

@MKergall
Copy link
Collaborator Author

For starters, I think the system already in place (osmdroid locating its cache directory in the app private storage) is the good solution.

The issue is much more for advanced users, wanting to manage the all kinds of offline content supported by osmdroid (huge work behind that!):

  • Be able to build a MOBAC or MBTiles file on a PC and use it.
  • Be able to download a MapsForge file on Internet and use it. To customize its rendertheme.xml file, and use it.
    Using the private app storage is not possible, as it is accessible to nothing else than the app.

@monsieurtanuki
Copy link
Collaborator

The issue is much more for advanced users, wanting to manage the all kinds of offline content supported by osmdroid

Unless you copy the file in the app private storage. Is it possible under the new rules to have to end-user select a file wherever and read it? (in order to copy it)
That means that at some time you'll have 2 copies of the same file.

@MKergall
Copy link
Collaborator Author

My understanding of the new rules:

  • using usual "java.io.File" methods (as done in most osmdroid code), the app cannot read outside its private storage
  • the end user cannot read/write (using a File Explorer, or any other app) in the app private storage

@monsieurtanuki
Copy link
Collaborator

I guess we must slalom around each details: there's no way the new rules would prevent an end-user to select a picture in the Gallery and save it in the app private storage.
If we replace "a picture" with "a map file", we should be OK, shouldn't we?

@MKergall
Copy link
Collaborator Author

Unfortunately, there is not way for a user to select a picture in the Gallery and save it in the app private storage...
Yes, we have a real problem.

@monsieurtanuki
Copy link
Collaborator

Really? How do you add your profile pic to an app like whatsapp?
I'll try that in an app of mine.

@MKergall
Copy link
Collaborator Author

Whatsapp is certainly using the SAF framework, which exists since Android 4.4.
And this is likely the move we have to handle for osmdroid features based on "public" files.

@monsieurtanuki
Copy link
Collaborator

Because the user is involved in selecting the files or directories that your app can access, this mechanism doesn't require any system permissions

That's what I had in mind: the end-user selecting the file. That looks OK for the read part, doesn't it?

@MKergall
Copy link
Collaborator Author

Yes, as long as you replace everything based on java.io.File by something based on InputStream.
Not impossible (I did it for the KML save/load features in OSMNavigator).
But at osmdroid scale, not a 1h work.
And I anticipate some issues on missing APIs: Managing of ZIP files for instance. Or MapsForge external lib.

@monsieurtanuki
Copy link
Collaborator

I meant to say that it was possible, not quickly done.
And I don't share your worries regarding missing APIs, as my assumption is that we just have to call the file picker and copy the map file. Then, once the map file is in the app private storage, it's business as usual and there's nothing to change.

I understood that if you invoke the file picker, as the end-user is involved, you're in a grace period long enough to copy the file. But you cannot reuse the file path later for instance.

Disclaimer: I have only an old Lollipop smartphone, and I only care about "standard" files like pictures and MP3. But my tests with API30 on emulators are OK (so far).

@MKergall
Copy link
Collaborator Author

That's a way. Drawbacks:

  • offline map files are big
  • how the user will be able to manage this private app storage (list files, delete files)? Reimplement a "file explorer" ?...
  • this approach will need explanations to the user.

A point: osmdroid method to choose its cache directory do not warranty the same place at each launch. The offline files may be copied to a directory which will not be used later.

I was more in favor of a real move to SAF.
Then, let the user select his "working directory", via ACTION_OPEN_DOCUMENT_TREE.

Note that the app can reuse later a file which has been authorized once. With additional complications after a reboot.
See https://developer.android.com/training/data-storage/shared/documents-files#persist-permissions

@MKergall
Copy link
Collaborator Author

Mapsforge team already worked on supporting SAF: mapsforge/mapsforge#1186

@monsieurtanuki
Copy link
Collaborator

@MKergall Fair enough. Actually I don't use offline maps; I was just reacting to your rather alarming OP.
In my apps I just need to copy, in the app private storage, files (music and pics) that the end-user explicitly selected.
And I wanted to share "my" solution. Which is probably not appropriate for larger files, files likely to be refreshed, or a large number of files.

A point: osmdroid method to choose its cache directory do not warranty the same place at each launch. The offline files may be copied to a directory which will not be used later.

I'm not sure it's correct: maybe the very first time, but then the path is stored in the preferences and is reused the next time. To be checked.

@MKergall
Copy link
Collaborator Author

I'm not sure it's correct: maybe the very first time, but then the path is stored in the preferences and is reused the next time. To be checked.

I just checked: no storage of the path in preferences.

@monsieurtanuki
Copy link
Collaborator

@MKergall Maybe we're talking about different things, but this is what I see in https://github.com/osmdroid/osmdroid/blob/master/osmdroid-android/src/main/java/org/osmdroid/config/DefaultConfigurationProvider.java#L328:

if (!prefs.contains("osmdroid.basePath")) {
     //this is the first time startup. run the discovery bit
     File discoveredBasePath = getOsmdroidBasePath(ctx);
// ...
 } else {
     //normal startup, load user preferences and populate the config object
     setOsmdroidBasePath(new File(prefs.getString("osmdroid.basePath", getOsmdroidBasePath(ctx).getAbsolutePath())));

@MKergall
Copy link
Collaborator Author

Oh, OK, thanks. I never noticed the introduction of this Configuration.getInstance().load method, so I wasn't calling it.

@eclectice
Copy link

eclectice commented Sep 21, 2021

I'm attempting to fix the Android 10+ Storage Model issue on my own using the SimpleStorage third-party package, which makes extensive use of the AndroidX DocumentFile class.

Perhaps we can start using the DocumentFile class to totally replace the java.io.File class, eliminating the need to convert DocumentFile instances to java.io.File in several of the codes below that involve Configuration.getInstance().setOsmdroidBasePath(), Configuration.getInstance().setOsmdroidTileCache(), and GeoPackageProvider().

Because the java.io.File class is not supported by the Storage Access Framework (SAF, in this example, the public Documents folder,<rootpath>/Documents, which requires a document provider), the cache could not be created by OSMDroid v6.1.10, used in OSMBonusPack 6.7.0, or OSMDroid v6.1.11, used in OSMBonusPack 6.8.0. The cache can only be created using the API29+ on my test Android 7.0 device.

If the java.io.File object is utilized in the internal storage of app-specific "private" folders like /data/user/0/<applicationid>/files/, there will be no issues. These private folders, on the other hand, are hidden from any file manager. 😒

/**
 * to scan for folder paths that match "tiles" to find OSMDroid database files
 *
 * @return maps a set of DocumentFiles, each store the found folder path
 */
protected Set<DocumentFile> findCacheFiles(@NotNull Context context, DocumentFile path) {
	Set<DocumentFile> maps = new HashSet<>();

	if (path != null) {
		Regex regex = new Regex("^.*tiles.*$");
		String[] mimetypes = {MimeType.UNKNOWN};
		List<DocumentFile> list = DocumentFileUtils.search(path, true, DocumentFileType.FOLDER, null, "", regex);
		LOG.log(Level.INFO, "OSM GPKG file list size: " + list.size());
		list.stream().forEach((c) -> {
			LOG.log(Level.INFO, "OSM GPKG file item: " + DocumentFileUtils.getAbsolutePath(c, context));
		});
		maps.addAll(list);
	}
	return maps;
}
/**
 * to scan for file paths that match "*.gpkg" to find GeoPackage files
 *
 * @return maps a set of DocumentFiles, each store the found file path
 */
protected Set<DocumentFile> findMapFiles(@NotNull Context context, DocumentFile path) {
	Set<DocumentFile> maps = new HashSet<>();

	if (path != null) {
		Regex regex = new Regex("^.*gpkg$");
		String[] mimetypes = {MimeType.UNKNOWN};
		List<DocumentFile> list = DocumentFileUtils.search(path, true, DocumentFileType.FILE, null, "", regex);
		LOG.log(Level.INFO, "OSM GPKG file list size: " + list.size());
		list.stream().forEach((c) -> {
			LOG.log(Level.INFO, "OSM GPKG file item: " + DocumentFileUtils.getAbsolutePath(c, context));
		});
		maps.addAll(list);
	}
	return maps;
}
//Assume the default Android public Documents folder at "/storage/emulated/0/Documents"
//set OSMDroid default folder at "/storage/emulated/0/Documents/APP/maps"
Stream<String> mapTree = Arrays.asList(SimpleStorage.getExternalStoragePath(), PublicDirectory.DOCUMENTS.getFolderName(), Constants.APP_NAME, "maps").stream();
String mapPathTree = mapTree.collect(Collectors.joining(File.separator));

Configuration.getInstance().setOsmdroidBasePath(new File(mapPathTree));
LOG.log(Level.INFO, "OSM GPKG baseDir created: " + Configuration.getInstance().getOsmdroidBasePath(MainApp.context()));

Boolean hasAccess = SimpleStorage.hasStorageAccess(MainApp.context(), mapPathTree);
LOG.log(Level.INFO, "OSM GPKG SimpleStorage.hasStorageAccess("+mapPathTree+"): " + hasAccess);

DocumentFile osmdroidDefaultFolder = DocumentFileCompat.fromFullPath(MainApp.context(), mapPathTree);

Set<DocumentFile> tileCachefiles = findCacheFiles(MainApp.context(), osmdroidDefaultFolder);
File [] tileCaches = tileCachefiles.stream().map((c)-> DocumentFileUtils.toRawFile(c, MainApp.context())).toArray(File[]::new);
if (tileCaches.length >= 1) {
	LOG.log(Level.INFO, "OSM GPKG cacheDir found: " + tileCaches[0].getAbsolutePath());
	Configuration.getInstance().setOsmdroidTileCache(tileCaches[0]);
} else {
	Configuration.getInstance().setOsmdroidTileCache(new File(mapPathTree + "/tiles"));
	LOG.log(Level.INFO, "OSM GPKG cacheDir created: " + Configuration.getInstance().getOsmdroidTileCache(MainApp.context()));
}

//now, save changes to the base and cache database path
Configuration.getInstance().save(MainApp.context(), PreferenceManager.getDefaultSharedPreferences(MainApp.context()));

Set<DocumentFile> mapfiles = findMapFiles(MainApp.context(), osmdroidDefaultFolder);
File [] maps = mapfiles.stream().map((c)-> DocumentFileUtils.toRawFile(c, MainApp.context())).toArray(File[]::new);
LOG.log(Level.INFO, "OSM GPKG maps size " + maps.length);

if (maps.length == 0 && hasAccess) {
	//show a warning that no map files were found...
}

surfaceView = view.findViewById(R.id.drawingOsmSurfaceView);
if (surfaceView != null) {
	geoPackageProvider = new GeoPackageProvider(maps, this.getContext());
	// do whatever we want with GeoPackage files...
}

The app will output the following logs if the user has granted the appropriate storage permission access...

2021-09-21 12:47:09.230 9125-9125/? I/OsmMapFragment: OSM GPKG baseDir created: /storage/emulated/0/Documents/BFT/maps
2021-09-21 12:47:09.233 9125-9125/? I/OsmMapFragment: OSM GPKG SimpleStorage.hasStorageAccess(/storage/emulated/0/Documents/BFT/maps): true
2021-09-21 12:47:09.241 9125-9125/? I/OsmMapFragment: OSM GPKG file list size: 1
2021-09-21 12:47:09.242 9125-9125/? I/OsmMapFragment: OSM GPKG file item: /storage/emulated/0/Documents/BFT/maps/tiles
2021-09-21 12:47:09.243 9125-9125/? I/OsmMapFragment: OSM GPKG cacheDir found: /storage/emulated/0/Documents/BFT/maps/tiles
2021-09-21 12:47:09.248 9125-9125/? I/OsmMapFragment: OSM GPKG file list size: 3
2021-09-21 12:47:09.249 9125-9125/? I/OsmMapFragment: OSM GPKG file item: /storage/emulated/0/Documents/BFT/maps/navy/EA200003.gpkg
2021-09-21 12:47:09.250 9125-9125/? I/OsmMapFragment: OSM GPKG file item: /storage/emulated/0/Documents/BFT/maps/navy/MY2C0005.gpkg
2021-09-21 12:47:09.251 9125-9125/? I/OsmMapFragment: OSM GPKG file item: /storage/emulated/0/Documents/BFT/maps/navy/MY2C0006.gpkg
2021-09-21 12:47:09.251 9125-9125/? I/OsmMapFragment: OSM GPKG maps size 3

Otherwise,

2021-09-21 12:46:33.887 9125-9125/? I/OsmMapFragment: OSM GPKG baseDir created: /storage/emulated/0/Documents/BFT/maps
2021-09-21 12:46:33.888 9125-9125/? I/OsmMapFragment: OSM GPKG SimpleStorage.hasStorageAccess(/storage/emulated/0/Documents/BFT/maps): false
2021-09-21 12:46:33.930 9125-9125/? I/OsmMapFragment: OSM GPKG cacheDir created: /storage/emulated/0/Documents/BFT/maps/tiles
2021-09-21 12:46:33.932 9125-9125/? I/OsmMapFragment: OSM GPKG maps size 0

UPDATE (2021-09-23):

The raw java.io.File instances (exposed by the DocumentFileUtils.toRawFile() method) did not work on the Android 11 device after I allowed proper Storage permission access (I needed to choose the Allow management of all files option where the app would only be allowed to access storage while in use), but the DocumentFile and SimpleStorage classes did. The cache database could not be created, and the GeoPackage GPKG files in the folder could not be read by OSMDroid.

I believe the OSMDroid library should provide full SAF support via Intent requests, replace java.io.File objects with DocumentFile objects, and use DocumentContract objects; it can also implement DocumentsProvider classes to read and write the cache database and offline maps.

@eclectice
Copy link

eclectice commented Sep 23, 2021

I found this article by @CommonsWare about undocumented Documents support in MediaStore and tested the following codes in an Android 11 device using the synthetic external volume name MediaStore.VOLUME EXTERNAL. Instead of utilizing the java.io.File class, I used the DocumentFile class to modify the file in the folder.

The point to underline here is that OSMDroid may need to figure out how to establish the cache database as well as to read and write folders and files that aren't in the app's "private" folders, such as the shared public Documents folder.

NOTES:

  1. Using ContentResolver to insert a file from a URI with the same filename will make a copy if another file with the same filename already exists. ContentResolver.query() helps to find the URI with the same filename.
  2. The Allow management of all files option in the Storage permission appears only when MANAGE_EXTERNAL_STORAGE is included in the app's manifest file. Hence, it is not the best solution for those who want to publish their app on the Google Play Store.
  3. Without MANAGE_EXTERNAL_STORAGE permission, the MediaStore approach only allows the app to act upon the folders and files created by the app only. To access those that aren't created by the app, the app needs to use MediaStore APIs to open them.
Stream<String> baseTree = Arrays.asList(Constants.APP_NAME, "maps").stream();
String basePathTree = baseTree.collect(Collectors.joining(File.separator));
LOG.log(Level.INFO, "OSM GPKG basePathTree: " + basePathTree);

Stream<String> docTree = Arrays.asList(PublicDirectory.DOCUMENTS.getFolderName(), basePathTree).stream();
String docPathTree = docTree.collect(Collectors.joining(File.separator));
LOG.log(Level.INFO, "OSM GPKG docPathTree: " + docPathTree);

//Assume the default Android public Documents folder at "/storage/emulated/0/Documents"
//set OSMDroid default folder at "/storage/emulated/0/Documents/APP/maps"
Stream<String> mapTree = Arrays.asList(SimpleStorage.getExternalStoragePath(), docPathTree).stream();
String mapPathTree = mapTree.collect(Collectors.joining(File.separator));
LOG.log(Level.INFO, "OSM GPKG mapPathTree: " + mapPathTree);

LOG.log(Level.INFO, "OSM GPKG mapPathTree DocumentFileCompat.doesExist(mapPathTree): " + DocumentFileCompat.doesExist(MainApp.context(), mapPathTree));

if (Build.VERSION.SDK_INT >=29 )
{
	String filename = "test.txt";
	ContentValues contentValues = new ContentValues();
	contentValues.put(MediaStore.Files.FileColumns.DISPLAY_NAME, filename);
	contentValues.put(MediaStore.Files.FileColumns.MIME_TYPE, MimeType.TEXT);
	contentValues.put(MediaStore.Files.FileColumns.DATE_ADDED, System.currentTimeMillis()/1000);
	contentValues.put(MediaStore.Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis()/1000);
	contentValues.put(MediaStore.Files.FileColumns.RELATIVE_PATH, docPathTree); // "Documents/APP/maps"
	contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, true);

	Uri uri = getContext().getContentResolver().insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), contentValues);
	if (uri != null) {
		LOG.log(Level.INFO, "OSM GPKG MediaStore uri: " + uri);
		try {
			OutputStream out = getContext().getContentResolver().openOutputStream(uri);
			out.write("Hello\n".getBytes(StandardCharsets.UTF_8));
			out.close();
			LOG.log(Level.INFO, "OSM GPKG MediaStore written into: " + uri);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		//let's verify the file exists and get its properties!
		//do note that it is not a raw file but a file that contains "content://" URI scheme!
		DocumentFile testFile = DocumentFileCompat.fromUri(MainApp.context(), uri);
		if (testFile != null) {
			LOG.log(Level.INFO, "OSM GPKG testFile.getUri(): " + testFile.getUri());
			LOG.log(Level.INFO, "OSM GPKG testFile.exists(): " + testFile.exists());
			LOG.log(Level.INFO, "OSM GPKG testFile.isDirectory(): " + testFile.isDirectory());
			LOG.log(Level.INFO, "OSM GPKG testFile.isFile(): " + testFile.isFile());
			LOG.log(Level.INFO, "OSM GPKG testFile.getParentFile(): " + testFile.getParentFile());
			LOG.log(Level.INFO, "OSM GPKG testFile.canRead(): " + testFile.canRead());
			LOG.log(Level.INFO, "OSM GPKG testFile.canWrite(): " + testFile.canWrite());
		}
	}
	contentValues.clear();
	contentValues.put(MediaStore.Files.FileColumns.IS_PENDING, 0);
	getContext().getContentResolver().update(uri, contentValues, null, null);

	if (uri != null) {
		//let's open this file as raw file instead by using the actual raw file path (should have "file://" URI scheme)
		Stream<String> fileTree = Arrays.asList(mapPathTree, filename).stream();
		String filePathTree = fileTree.collect(Collectors.joining(File.separator));
		LOG.log(Level.INFO, "OSM GPKG filePathTree: " + filePathTree);

		LOG.log(Level.INFO, "OSM GPKG filePathTree DocumentFileCompat.doesExist(filePathTree): " + DocumentFileCompat.doesExist(MainApp.context(), filePathTree));

		DocumentFile rawFile = DocumentFileCompat.fromFullPath(MainApp.context(), filePathTree);
		if (rawFile != null) {
			LOG.log(Level.INFO, "OSM GPKG rawFile.getUri(): " + rawFile.getUri());
			LOG.log(Level.INFO, "OSM GPKG rawFile.exists(): " + rawFile.exists());
			LOG.log(Level.INFO, "OSM GPKG rawFile.isDirectory(): " + rawFile.isDirectory());
			LOG.log(Level.INFO, "OSM GPKG rawFile.isFile(): " + rawFile.isFile());
			LOG.log(Level.INFO, "OSM GPKG rawFile.getParentFile(): " + rawFile.getParentFile());
			LOG.log(Level.INFO, "OSM GPKG rawFile.canRead(): " + rawFile.canRead());
			LOG.log(Level.INFO, "OSM GPKG rawFile.canWrite(): " + rawFile.canWrite());

		        try {
			        byte[] bytes = new byte[(int) rawFile.length()];
			        InputStream in = getContext().getContentResolver().openInputStream(uri);
			        in.read(bytes, 0, (int) rawFile.length());
			        in.close();
			        LOG.log(Level.INFO, "OSM GPKG read from rawFile: " + DocumentFileUtils.getAbsolutePath(rawFile, MainApp.context()));
			        LOG.log(Level.INFO, "OSM GPKG content in rawFile: " + new String(bytes));
		        } catch (FileNotFoundException e) {
			        e.printStackTrace();
		        } catch (IOException e) {
			        e.printStackTrace();
		        }
		}
	}
}

With the Allow management of all files option enabled, The codes successfully created and read the test.txt file from the shared public Documents folder, resulting in the following logs:

2021-09-23 11:39:53.079 2727-2727/? I/OsmMapFragment: OSM GPKG basePathTree: APP/maps
2021-09-23 11:39:53.080 2727-2727/? I/OsmMapFragment: OSM GPKG docPathTree: Documents/APP/maps
2021-09-23 11:39:53.081 2727-2727/? I/OsmMapFragment: OSM GPKG mapPathTree: /storage/emulated/0/Documents/APP/maps
2021-09-23 11:39:53.083 2727-2727/? I/OsmMapFragment: OSM GPKG mapPathTree DocumentFileCompat.doesExist(mapPathTree): true
2021-09-23 11:39:53.096 2727-2727/? I/OsmMapFragment: OSM GPKG MediaStore uri: content://media/external/file/868
2021-09-23 11:39:53.103 2727-2727/? I/OsmMapFragment: OSM GPKG MediaStore written into: content://media/external/file/868
2021-09-23 11:39:53.103 2727-2727/? I/OsmMapFragment: OSM GPKG testFile.getUri(): content://media/external/file/868
2021-09-23 11:39:53.110 2727-2727/? I/OsmMapFragment: OSM GPKG testFile.exists(): true
2021-09-23 11:39:53.116 2727-2727/? I/OsmMapFragment: OSM GPKG testFile.isDirectory(): false
2021-09-23 11:39:53.123 2727-2727/? I/OsmMapFragment: OSM GPKG testFile.isFile(): true
2021-09-23 11:39:53.123 2727-2727/? I/OsmMapFragment: OSM GPKG testFile.getParentFile(): null
2021-09-23 11:39:53.123 2727-2727/? I/OsmMapFragment: OSM GPKG testFile.canRead(): false
2021-09-23 11:39:53.124 2727-2727/? I/OsmMapFragment: OSM GPKG testFile.canWrite(): false
2021-09-23 11:39:53.149 2727-2727/? I/OsmMapFragment: OSM GPKG filePathTree: /storage/emulated/0/Documents/APP/maps/test.txt
2021-09-23 11:39:53.151 2727-2727/? I/OsmMapFragment: OSM GPKG filePathTree DocumentFileCompat.doesExist(filePathTree): true
2021-09-23 11:39:53.152 2727-2727/? I/OsmMapFragment: OSM GPKG rawFile.getUri(): file:///storage/emulated/0/Documents/APP/maps/test.txt
2021-09-23 11:39:53.152 2727-2727/? I/OsmMapFragment: OSM GPKG rawFile.exists(): true
2021-09-23 11:39:53.152 2727-2727/? I/OsmMapFragment: OSM GPKG rawFile.isDirectory(): false
2021-09-23 11:39:53.152 2727-2727/? I/OsmMapFragment: OSM GPKG rawFile.isFile(): true
2021-09-23 11:39:53.152 2727-2727/? I/OsmMapFragment: OSM GPKG rawFile.getParentFile(): null
2021-09-23 11:39:53.153 2727-2727/? I/OsmMapFragment: OSM GPKG rawFile.canRead(): true
2021-09-23 11:39:53.154 2727-2727/? I/OsmMapFragment: OSM GPKG rawFile.canWrite(): true
2021-09-23 11:39:53.161 2727-2727/? I/OsmMapFragment: OSM GPKG read from rawFile: /storage/emulated/0/Documents/APP/maps/test.txt
2021-09-23 11:39:53.161 2727-2727/? I/OsmMapFragment: OSM GPKG content in rawFile: Hello

test file in Documents-reduced

@eclectice
Copy link

eclectice commented Sep 25, 2021

When the GeoPackageMapTileModuleProvider object in OSMDroid doesn't have adequate storage permission to read and write cache.db and GeoPackage GPKG files in the shared public Documents folder using java.io.File in Android10+:

mTileProviderList.add(MapTileProviderBasic.getMapTileFileStorageProviderBase(pRegisterReceiver, pTileSource, tileWriter));
geopackage = new GeoPackageMapTileModuleProvider(databases, pContext, tileWriter);

If the Storage permission is set to "Allow access to media only" or "Allow management of all files", the following will happen (Android 11 on Samsung A51):

2021-09-25 12:09:15.052 17694-17694/? I/OsmDroid: Using tile source: Mapnik
2021-09-25 12:09:15.053 17694-17694/? I/OsmDroid: Tile cache increased from 0 to 9
2021-09-25 12:09:15.129 17694-17694/? I/OsmDroid: Tile cache increased from 0 to 9
2021-09-25 12:09:15.129 17694-17694/? I/OsmDroid: Geopackage support is BETA. Please report any issues
2021-09-25 12:09:15.130 17694-17694/? I/OsmDroid: Geopackage support is BETA. Please report any issues
2021-09-25 12:09:15.276 17694-17694/? E/SQLiteDatabase: Error inserting 
    android.database.sqlite.SQLiteException: near "null": syntax error (code 1 SQLITE_ERROR[1]): , while compiling: INSERT INTO "geopackage"(null) VALUES (NULL)
        at android.database.sqlite.SQLiteConnection.nativePrepareStatement(Native Method)
        at android.database.sqlite.SQLiteConnection.acquirePreparedStatement(SQLiteConnection.java:1463)
        at android.database.sqlite.SQLiteConnection.prepare(SQLiteConnection.java:901)
        at android.database.sqlite.SQLiteSession.prepare(SQLiteSession.java:590)
        at android.database.sqlite.SQLiteProgram.<init>(SQLiteProgram.java:62)
        at android.database.sqlite.SQLiteStatement.<init>(SQLiteStatement.java:33)
        at android.database.sqlite.SQLiteDatabase.insertWithOnConflict(SQLiteDatabase.java:2217)
        at android.database.sqlite.SQLiteDatabase.insert(SQLiteDatabase.java:2088)
        at mil.nga.geopackage.db.GeoPackageDatabase.insert(GeoPackageDatabase.java:127)
        at mil.nga.geopackage.db.metadata.GeoPackageMetadataDataSource.create(GeoPackageMetadataDataSource.java:51)
        at mil.nga.geopackage.factory.GeoPackageManagerImpl.importGeoPackage(GeoPackageManagerImpl.java:1319)
        at mil.nga.geopackage.factory.GeoPackageManagerImpl.importGeoPackage(GeoPackageManagerImpl.java:658)
        at mil.nga.geopackage.factory.GeoPackageManagerImpl.importGeoPackage(GeoPackageManagerImpl.java:572)
        at org.osmdroid.gpkg.tiles.raster.GeoPackageMapTileModuleProvider.<init>(GeoPackageMapTileModuleProvider.java:64)
        at org.osmdroid.gpkg.tiles.raster.GeoPackageProvider.<init>(GeoPackageProvider.java:66)
        at org.osmdroid.gpkg.tiles.raster.GeoPackageProvider.<init>(GeoPackageProvider.java:42)

I suspect this problem will also affect the GeoPackageFactory class in the Geopackage Android library.

@eclectice
Copy link

eclectice commented Sep 25, 2021

DocumentFile is already supported by the GeoPackageManager implementation in the GeoPackage Android framework since v5.0.0. OSMDroid should make use of it.

public boolean importGeoPackage(DocumentFile file);

I'm not sure if other offline tile database frameworks support DocumentFile.

@eclectice
Copy link

eclectice commented Oct 2, 2021

I'm not familiar with the git system (I'm only familiar with SVN), so I just made a unified diff since HEAD patch for the osmdroid-geopackage update to version 6.0.2, which adds support for the DocumentFile objects.

geopackage-update-to-6.0.2.zip (updated with a note below)

As a result, we can pass DocumentFile objects in the following way:

//using anggrayudi/SimpleStorage API
DocumentFile osmdroidDefaultFolder = DocumentFileCompat.fromUri(getContext(), uri);

/**
 * to scan for file paths that match "*.gpkg" to find GeoPackage files
 *
 * @return maps a set of DocumentFiles, each stores the found file path
 */
protected Set<DocumentFile> findMapFiles(@NotNull Context context, DocumentFile path) {
	Set<DocumentFile> maps = new HashSet<>();

	if (path != null) {
		Regex regex = new Regex("^.*gpkg$");
		String[] mimetypes = {MimeType.UNKNOWN};
		List<DocumentFile> list = DocumentFileUtils.search(path, true, DocumentFileType.FILE, null, "", regex);
		maps.addAll(list);
	}
	return maps;
}

Set<DocumentFile> mapfiles = findMapFiles(getContext(), osmdroidDefaultFolder);

DocumentFile[] mapDocs = mapfiles.stream().toArray(DocumentFile[]::new);
geoPackageProvider = new GeoPackageProvider(mapDocs, getContext());
boolean sourceSet = false;

List<GeopackageRasterTileSource> tileSources = geoPackageProvider.geoPackageMapTileModuleProvider().getTileSources();
if (!tileSources.isEmpty()) {
	surfaceView.setTileProvider(geoPackageProvider);
	surfaceView.setTileSource(tileSources.get(0));

	surfaceView.zoomToBoundingBox(tileSources.get(0).getBounds(), true);
	surfaceView.getController().setZoom(tileSources.get(0).getMinimumZoomLevel());
	sourceSet = true;
}

// Get a manager
GeoPackageManager manager = GeoPackageFactory.getManager(getContext());

// Available databases
List<String> databases = manager.databases();

It's worth noting that the file extension from the filename must be omitted when comparing a DocumentFile object to the names in the GeoPackage database (I found this in version 6.0.2).

@mguignes
Copy link

Hello,
Has this problem been resolved ?
Has it been integrated into the master branch ?
Has it been released in a new version ?

Because today, it's getting critical for any apps to upload to Google Play

Thanks !

@ryanbehr
Copy link

ryanbehr commented Feb 24, 2022

@mguignes There really isn't a fix for this issue. The closest I have gone is to do something like this: ryanbehr@13e5fbe where you can request permission to access a directory and provide the DocumentFile to the MBTiles file to OSMDroid. After that it will copy the document file into a temporary app storage, then access that file using the usual "File" method. It works great for smaller MBTiles, but if you're using 50 GB MBTiles files, it's rough.

@drogatkin
Copy link

@MKergall , have you solved the issue? I work on a completely different project, but have a similar issue and it got even worse with Android 13. I think all android developers should unite and send a petition to Google to reconsider current approach. It didn't make Android safer, but killed many good apps. So far, Android clearly loses to iOS specifically for bad designed API.

@MKergall
Copy link
Collaborator Author

I solved it on important sections of my own app (OsmNavigator).
I'm not using GeoPackage, so I didn't look at @eclectice feedback about that.
I lost capability to handle MapQuest maps => this will certainy require an upgrade to latest MapQuest lib, and some rework on its osmdroid integration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants