2

I am making a webpage on-the-fly with Android's WebView, using Android Studio and a phone with OS version 11 connected by USB in developer mode.

Despite many questions about this, I can't load a file from external storage. This minimal example shows that I can do the job with an image in the assets folder.

package com.example.hellowebview
    
import android.os.Bundle
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
    
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val myWebView = WebView(this)
        val myImage = "file:///android_asset/MonaLisa.jpg"
        val myHtml = "<HTML><BODY><h3>Hello, WebView!</h3><img src='$myImage'></BODY></HTML>"
        myWebView.loadDataWithBaseURL(null, myHtml, "text/html", "utf-8", null);
        setContentView(myWebView)
    }
}

But it won't load an image from the SD card. Here is my attempt, with variations.

package com.example.hellowebview
    
import android.Manifest
import android.os.Bundle
import android.os.Environment
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
    
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val myWebView = WebView(this)

        val PERMISSION_EXTERNAL_STORAGE = 1
        requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSION_EXTERNAL_STORAGE)
        myWebView.getSettings().setAllowFileAccess(true);
        
        val sdCard = Environment.getExternalStorageDirectory().absolutePath.toString()
        
        val myImage = "file://$sdCard/Download/MonaLisa.jpg"
        //val myImage = "file:///$sdCard/Download/MonaLisa.jpg"
        //val myImage = "$sdCard/Download/MonaLisa.jpg"
        //val myImage = "file:///Download/MonaLisa.jpg"
        //val myImage = "file:///sdcard/Download/MonaLisa.jpg"
        //val myImage = "file://sdcard/Download/MonaLisa.jpg"

        val myHtml = "<HTML><BODY><h3>Hello, WebView!</h3><img src='$myImage'></BODY></HTML>"
        
        //myWebView.loadDataWithBaseURL(sdCard, myHtml, "text/html", "utf-8", null)
        myWebView.loadDataWithBaseURL(null, myHtml, "text/html", "utf-8", null)
        
        setContentView(myWebView)
    }
}

I also added these two lines to AndroidManifest.xml

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

and permission was requested (and given). The value of sdCard is /storage/emulated/0 and the image file does exist:

enter image description here

What have I overlooked, or done wrong?


I can make an arbitrary folder on the SD card by hand, copy files from my PC by hand by USB, and view those files with other Android apps. So why can't I load those files with my app?


Bounty

I am aware that Google changed its policy with Android 11 to improve security. Yet it is possible to read images from the SD card as this example from GeeksforGeeks demonstrates:

How to Build a Photo Viewing Application in Android?

It is java not kotlin but no matter: it displays images in accessible folders, one of which I created at the phone and copied images from the PC. It works (although for some reason won't run twice but has to be uninstalled). It doesn't use a WebView and I haven't been able to imitate it.

It does not have MANAGE_EXTERNAL_STORAGE in the manifest, only READ_EXTERNAL_STORAGE.

I have not posted the code requested by a comment, because I do not want the app to copy images to a folder from its assets. I want to be able to copy images from a PC directly, without placing them in assets and rebuilding the app.

So I would welcome answers to my question:

How do I use images from accessible SD card folders in an HTML <img> tag in a WebView using Android version 11?

Weather Vane
  • 33,872
  • 7
  • 36
  • 56
  • Presumably, your app did not download the file to `Download/`. Hence, it has no access to that file on Android 11+, regardless of permissions. – CommonsWare Jun 03 '22 at 17:48
  • @CommonsWare I copied the file there by hand, and I don't care which folder it uses. I also explored using the app's private external storage on `Android/data/com.example.hellowebview` but a) the OS won't let me create a folder, and b) I don't want to have to reinstall the (eventually large set of) files every time I uninstall and start over. This is only a simple example of what will be a very large set of files in nested folders: hence not wanting to use `assets`. – Weather Vane Jun 03 '22 at 17:54
  • @CommonsWare then how can a photo viewer possibly work? – Weather Vane Jun 03 '22 at 17:58
  • "I don't care which folder it uses" -- then perhaps try a folder designed for pictures, such as `Pictures/`. "This is only a simple example of what will be a very large set of files in nested folders: hence not wanting to use `assets`" -- what you are using is not really comparable to `assets`. `assets` is "I want to ship content with the app". "then how can a photo viewer possibly work?" -- I do not know if photo viewers are capable of displaying photos stored in `Download/` on modern versions of Android. Most photos are not stored there AFAIK. – CommonsWare Jun 03 '22 at 18:10
  • @CommonsWare I've also tried `DCIM/Camera`. I don't understand your remark about `assets`. I used one image in `assets` to get me going, and for this exercise I want to use one image from the SD card. I've looked at a large number of similar posts and no-one ever says you can only load an image which the app has stored there. Can you offer any solution please? – Weather Vane Jun 03 '22 at 18:22
  • Also tried `Pictures/` – Weather Vane Jun 03 '22 at 18:30
  • Copy your image first from assets to getFilesDir() or getExternalFilesDir(). Then try again. Or to /storage/emulated/0/Documents. – blackapps Jun 03 '22 at 18:49
  • @blackapps but if it has to be in the `assets` folder in the first place.... – Weather Vane Jun 03 '22 at 18:51
  • No. It is just a test for yourself to see that webview can load your files if your app owns that file. Also files copied 'by hand' to getExternalFilesDir() can be used by webview. Your app could also download the file to become owner, as you can read in first comment. – blackapps Jun 03 '22 at 18:52
  • @blackapps I will try to create the folder and one file programmatically, and then see if I can copy by hand (that is, by a file manager on the PC connected by USB). – Weather Vane Jun 03 '22 at 18:55
  • Which folder? Call getExternalFilesDir(null). Then copy files from pc. – blackapps Jun 03 '22 at 18:56
  • @blackapps to the folder it creates. I'll try that, thanks. – Weather Vane Jun 03 '22 at 18:57
  • @blackapps I've applied [this code](https://stackoverflow.com/a/69941051/4142924) to copy the assets folder, but there is nothing to be seen in a file browser. I'm utterly mystified by all this: previously I made a folder on the SD card by hand, copied some images by hand, and can use the Gallery app or Photo viewer to look at them. Which is what I originally did here, only resorting to using folders that were in common use when it failed. – Weather Vane Jun 03 '22 at 19:33
  • File managers and 'gallery' have no access to your app's getExternalFIlesDir(). You did not tell if you copied to getExternalFilesDir() and i did not follow your code link. – blackapps Jun 03 '22 at 19:42
  • @blackapps that's not what I said. I meant that if I can make an arbitrary folder on the SD card, copy files there by hand, and view them with other Android apps, then why can't I load those files from my app? I have no idea what you mean by "call getExternalFilesDir(null)". I put that in the code and it does nothing, but I found that linked example which uses it to copy files from assets to SD card, which i thought is [what you advised](https://stackoverflow.com/questions/72493259/webview-images-from-android-sd-card#comment128062106_72493259). – Weather Vane Jun 03 '22 at 19:54
  • Those other apps use MANAGE_EXTERNAL_STORAGE permission or use SAF. – blackapps Jun 03 '22 at 20:25
  • SD card? I mentioned four possible storage locations. Show your copy code where you copy to public Documents directory. Post your code. Dont use links. – blackapps Jun 03 '22 at 20:27
  • Have you tried adding `MANAGE_EXTERNAL_STORAGE` and using a direct path e.g. `/sdcard/Download/MonaLisa.jpg` (without `file://`) – Darkman Jun 11 '22 at 10:13
  • @Darkman I have solved it now. I could use an image from assets and I could use an image from the internet, but not from the SD card. The `getExternalStorageDirectory()` returned a path such as `/storage/emulated/0/` but I actually needed `/storage/xxxx-yyyy/` (where *xxxx* and *yyyy* are 4-digit numbers) which was found by `getContentResolver().query()`. I could not find a SO question that answered this. Perhaps someone can claim the bounty by providing a canonical answer for how to access the SD card on Android 11. – Weather Vane Jun 11 '22 at 11:14

2 Answers2

2

ContentResolver provides access to "well-defined media collections" alike MediaStore.Downloads - also on MediaStore.VOLUME_EXTERNAL. Unless requesting android:requestLegacyExternalStorage="true", this may be the only option left.

This explains what needs to be done; Query a media collection:

Uri collection;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    collection = MediaStore.Downloads.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
} else {
    collection = MediaStore.Downloads.Media.EXTERNAL_CONTENT_URI;
}

In order to inject the content URI into WebView, using a data URI might be the least complicated. On Android, ByteArrayOutputStream.toByteArray() can convert InputStream to byte[]:
https://stackoverflow.com/questions/1264709/convert-inputstream-to-byte-array-in-java

// Using the content Uri, which the ContentResolver returned.
InputStream is = requireContext().getContentResolver().openInputStream(uri);

ByteArrayOutputStream os = new ByteArrayOutputStream(); 
byte[] buffer = new byte[0xFFFF];
for (int len = is.read(buffer); len != -1; len = is.read(buffer)) { 
    os.write(buffer, 0, len);
}
byte[] bytes = os.toByteArray();
is.close();
os.flush();
os.close();

String base64 = Base64.encodeToString(bytes, Base64.DEFAULT);
String dataUri = "data:image/jpeg;base64, " + base64;

Then embed the content as "<img src=\"" + dataUri + "\"/>".

image/jpeg and image/png must match the actual image format.


I'd wonder, if not getExternalCacheDir() might be better suitable:

File externalCacheFile = new File(context.getExternalCacheDir(), filename);

The question is where the image file originally came from (if it's app-specific, whether or not).
The advance hereby is, that these files will not show up in the MediaStore query results.

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
  • Thanks for the answer. I don't want cache folders because I would have to reinstall the images every time I start over. I am now able to copy a file from assets to the external storage obtained with `getExternalFilesDir()` (and later from the PC), but it is in `Phone/Android/` not in `Card/Android/`. If I use `getFilesDir()` the file is copied *somewhere* and then can be used later in WebView without copying it. But it does not show up anywhere that is visible from the PC. It needs to be a folder on the SD card which is copyable from the PC (and ideally not visible by Gallery etc apps). – Weather Vane Jun 14 '22 at 10:28
  • A `.nomedia` file inside a directory used to skip the media scanning, but then it likely cannot be retrieved from the `MediaStore` (database) either (just an assumption, haven't tired). With `adb` or Device File Explorer, one can at least access the internal storage of apps in debug mode - without `FileProvider` they're not visible. Fetching from Cloud Storage might be the alternative to uploading from PC; still better than installed from app resources. – Martin Zeitler Jun 14 '22 at 14:43
  • The `.nomedia` file does what you say, but unfortunately prevents my app from reading the files too. It isn't using network connections: it must work offline. Incidentally the [Total Commander](https://total-commander.en.uptodown.com/android) utility does show me the proper folder names such as `/storage/1234-5678/Myfolder`. – Weather Vane Jun 15 '22 at 08:31
1

Assuming that you added uses-permission in your AndroidManifest.xml;

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

and WebView control in your Activity.xml.

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <WebView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center_parent"
        android:background="@color/white" />
</RelativeLayout>

And in your MainActivity.java, you can import image like this;

First, prepare Uri as String. This can be done by AsyncTask or other in your onCreate function.

private String dataUri = null;
...

    InputStream is = requireContext().getContentResolver().openInputStream(uri);
    int sz = is.available();
    byte[] buff = new byte [sz];
    is.read(buff);
    is.close();

    String base64 = Base64.encodeToString(bytes, Base64.DEFAULT);
    String dataUri = "data:image/jpeg;base64, " + base64;


And when it's ready, load dataUri to your component.

    mWebView.loadDataWithBaseURL(null, "<html><head><style>img {margin-top:auto;margin-bottom:auto}</style></head><body><img src=\"" + dataUri + "\"></body></html>", "html/css", "utf-8", null);

    mWebView.setBackgroundColor(getResources().getColor(R.color.transparent));
    mWebView.setWebViewClient(new CustomWebViewClient());

How does it look like?

Acode
  • 507
  • 5
  • 14