WebView images from Android SD card

649 Views Asked by At

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?

2

There are 2 best solutions below

3
Martin Zeitler On

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.

0
Super Toptal On

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?