2

Before Android Nougat ( 7.0 ) it was easy to create a KML file in external storage and launch an intent with the file uri and Google Earth mime type for KML. Google Earth app magically opened and flew down to my KML.

On Android Nougat ( 7.0 ) you must use a FileProvider with the intent. You must add a provider to AndroidManifest.xml plus a /res/xml/provider_paths.xml.

Using the FileProvider intent, the installed Google Earth app will launch, then display message "Error loading file" in Google Earth.

Logcat displays "FileNotFoundException Permission denied".

File: AndroidManifest.xml

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">

    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.provider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths"/>
    </provider>
</application>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

File: /res/xml/provider_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_files" path="."/>
</paths>
// File: MainActivity.java

// I used Android Device Monitor to push mooCow.kml to
//     /storage/emulated/0/Download/mooCow.kml
public void onClick(View arg0) {
    File folder = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
    File kmlFile =  new File(folder, "mooCow.kml");
    String pathToLocalKmlFile = kmlFile.getAbsolutePath();
    openKmlInGoogleEarth(pathToLocalKmlFile);
}
// This works with KML and file extensions supported by getMimeTypeFromExtension
public void openKmlInGoogleEarth(String pathToFileInExternalStorage) {
    try {
        String[] splits = pathToFileInExternalStorage.split("\\.");
        String ext = "";
        if(splits.length >= 2) {
            ext = splits[splits.length-1];
        }
        MimeTypeMap mime = MimeTypeMap.getSingleton();
        String mimeType = mime.getMimeTypeFromExtension(ext);
        File file = new File(pathToFileInExternalStorage);
        Uri uri;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            // Use file Uri before Android Nougat ( 7.0 )
            uri = Uri.fromFile(file);
        } else {
            // Android Nougat ( 7.0 ) and later,
            // use a FileProvider, AndroidManifest provider, and /res/xml/provider_paths.xml
            uri = FileProvider.getUriForFile(MainActivity.this,
                    BuildConfig.APPLICATION_ID + ".provider",
                    file);
        }
        final String type = "application/vnd.google-earth.kml+xml";
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(uri, mimeType);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        startActivity(intent);
    }
    catch(IllegalArgumentException e) {
        Log.e(TAG, "Unexpected exception openKmlInGoogleEarth", e);
    }
}

adb logcat point to com.google.android.apps.earth permission problem.

How can Asus File manager app open the same exact file in Google Earth but my app cannot? My non-FileProvider works fine on Android 5.2 but on Android 6 it only launches Google Earth?

E Class   : Error reading file from URI: content://com.subsite.googleearthbutton.provider/external_files/Documents/mooCow.kml
E Class   : java.io.FileNotFoundException: Permission denied
E Class   :     at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:144)
E Class   :     at android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:692)
E Class   :     at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1104)
E Class   :     at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:942)
E Class   :     at android.content.ContentResolver.openInputStream(ContentResolver.java:662)
E Class   :     at com.google.android.apps.earth.p.m.a(FileUtil.java:6)

What could be causing a file access "Permission denied" ?

enter image description here

Ed of the Mountain
  • 5,219
  • 4
  • 46
  • 54
  • Related question https://stackoverflow.com/questions/38200282/android-os-fileuriexposedexception-file-storage-emulated-0-test-txt-exposed – Ed of the Mountain Feb 08 '18 at 18:26
  • "On Android 6 I think I need to use a FileProvider" -- no, you could still use `Uri.fromFile()`, at least as far as the OS is concerned. On Android 7.0+ is where a `FileProvider` is required. Your `` element is in the wrong place (needs to be outside of ``). Beyond that, perhaps `pathToLocalKmlFile` does not exist. Also, is your LogCat entry coming from your app or from Google Earth? – CommonsWare Feb 08 '18 at 18:39
  • `"${applicationId}.provider"` does that resolve to `"com.subsite.googleearthbutton.provider"`? – greenapps Feb 08 '18 at 18:40
  • `intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);` Try without if you use no file provider. – greenapps Feb 08 '18 at 18:50
  • @CommonsWare So I do *not* need a **FileProvider** on 6.0. I pass `pathToLocalKmlFile` as argument to `openKmlInGoogleEarth`. I corrected post mistake and moved `` outside of ``. "Permission denied" is coming from com.google.android.apps.earth.p.m.a(FileUtil.java:6). **Asus File Manager** can open the KML file somehow. I cannot either with or without *FileProvider* but same code works fine on Android 5.1? – Ed of the Mountain Feb 08 '18 at 19:43
  • `readExceptionWithFileNotFoundExceptionFromParcel` suggests that your `FileProvider` is unable to serve your file (e.g., the file does not exist). – CommonsWare Feb 08 '18 at 19:51
  • Correction: The non **FileProvider** intent works fine on an **Android 6 ARM device** but * NOT * an **x86 ARM Emulator**. No clue why. The file exists because File Manager could open it in Google Earth on the x86 emulator. Maybe just an x86 quirk? Anyway, I need to find a N device or try testing on an emulator to make sure the FileProvider intent will work. – Ed of the Mountain Feb 08 '18 at 20:01
  • How does `content://com.acmeinc.googleearthbutton.provider/external_files/Download/mooCow.kml` get resolved to `/storage/emulated/0/Download/mooCow.kml` ? – Ed of the Mountain Feb 08 '18 at 20:46

2 Answers2

0

Solved.

I forgot to add a run-time check and request WRITE_EXTERNAL_STORAGE permission.

  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

I think on older versions of Android all I had to do was add a <uses-permission> element to AndroidManifest.xml

I had neither READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE until I checked and requested.

Thank you all for the helpful comments.

Ed of the Mountain
  • 5,219
  • 4
  • 46
  • 54
0

For me, nowadays this is what worked:

Uri uriFromFile = FileProvider.getUriForFile(context, GenericFileProvider.class.getName(), file); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uriFromFile, "application/vnd.google-earth.kml+xml"); intent.putExtra("com.google.earth.EXTRA.tour_feature_id", "my_track"); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(intent);

noloman
  • 11,411
  • 20
  • 82
  • 129