A template of Android Web App project with WebView and support of upload and download.
Converts Website into Android Web App.
This project demonstrates one of the ways to build an Android Web App that support uploading and downloading files.
Github (Source Code): https://github.com/adriancs2/android.webview.upload.download
References:
I’ll be using the Android Studio and Java in this project.
First, download Android Studio from the official site:
https://developer.android.com/studio
At the time of writing this, I’m using the latest version which is: Android Studio Electri Eel | 2022.1.1 Patch
At Android Studio, create a new "Phone" project, select "Empty Activity" template.
Fill in basic project info:
Read about "Package Name".
Depands on your application needs, if you wish your app to be able to run on most Android devices, select "API 19: Android 4.4 (KitKat)" (info is based at the time of writing this).
Language: Java (This article will be using Java)
Click "Finish" to start creating the app.
Wait for a moment for Android Studio to load and creates project files. Upon ready, you should be able to see something like this:
Before continue, we may first setup the App Icons (*You can do this later).
Close Android Studio, and go to this website (App Icon Generators):
Upload your favorite icon to the website and it will generate various sizes of App Icons for building your Android project.
*Acknowledgement: Thanks to icons8.com for sponsoring the icon used in this project.
Download the generated icons.
Extract the zip content and copy from the Android’s icon set folder:
zip extracted content folder...\android\
To the project icon resource folder:
project_folder...\<project name>\app\src\main\res\
Reopen Android Studio and the project.
Now, the project has 2 sets of icons.
This resulting duplicates icon existed in the project which will cause build error. Therefore, we need to delete the default icons added by Android Studio.
Go to the folder:
app > res > mipmap > ic_launcher
and delete all *.webp
files
and go to another folder:
app > res > mipmap > ic_launcher_round
and delete all *.webp
files
At the folder [ app > res > drawable
], delete the following files:
ic_launcher_background.xml
ic_launcher_foreground.xml
And replace with your own edited PNG images:
ic_launcher_background.png
ic_launcher_foreground.png
For more info on changing the App Icons, please refer to the following Android Developer Documentation:
Next, edit the "ActionBar
". Open the following theme files:
app > res > values > themes.xml
app > res > values > themes.xml (night)
Change both files of this line: (from DarkActionBar
)
<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
To (NoActionBar)
<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
After edit, the theme file’s content will look something like this:
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="21">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
Next, edit the layout file at:
app > res > layout > activity_main.xml
Click [Code
] to view the XML designer code.
Here’s the initial code:
Delete the TextView, and add a WebView.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/webView">
</WebView>
</androidx.constraintlayout.widget.ConstraintLayout>
Change the Layout from
androidx.constraintlayout.widget.ConstraintLayout
to
RelativeLayout
After edit, the code will look something like this:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/webView">
</WebView>
</RelativeLayout>
Next step is to enable the permission for the app to access internet and download/upload files.
Open the android manifest file (AndroidManifest.xml
) at:
app > manifest > AndroidManifest.xml
Insert 3 uses-permission request lines
android.permission.INTERNET
: Permission to access internet.android.permission.READ_EXTERNAL_STORAGE
: able to select files from Android storage (for uploading files)android.permission.WRITE_EXTERNAL_STORAGE
: for saving downloaded files<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"></uses-permission>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="My App"
android:supportsRtl="true"
android:theme="@style/Theme.MyApp"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Adding Asset Directory and Files
Right click the folder [app
] > New
> Directory
Select "src\main\assets
"
Right click the newly created "assets
" folder > New
> File
Name the file as "no_internet.html
".
Double click the file and enter some html, for example:
<html>
<head></head>
<body>
<h1>No Internet</h1>
Please check the internet connection.
</body>
</html>
Read more: Android Developer Documentation on WebView
Now, we come to the WebView coding part. Open the MainActivity.java
file at:
app > java > ..package name... > MainActivity
// example:
app > java > com.company.product > MainActivity
Here’s the initial code:
Import the following class libraries:
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.webkit.DownloadListener;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
Declare the objects of WebView, ProgressDialog and a few global variables.
public class MainActivity extends AppCompatActivity {
// if your website starts with www, exclude it
private static final String myWebSite = "example.com";
WebView webView;
ProgressDialog progressDialog;
// for handling file upload, set a static value, any number you like
// this value will be used by WebChromeClient during file upload
private static final int file_chooser_activity_code = 1;
private static ValueCallback<Uri[]> mUploadMessageArr;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// initialize the progressDialog
progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setCancelable(true);
progressDialog.setMessage("Loading...");
progressDialog.show();
}
}
In the class of MainActivity, create a WebViewClient to handle web browsing activities:
class myWebViewClient extends android.webkit.WebViewClient {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
//showing the progress bar once the page has started loading
progressDialog.show();
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// hide the progress bar once the page has loaded
progressDialog.dismiss();
}
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
// show the error message = no internet access
webView.loadUrl("file:///android_asset/no_internet.html");
// hide the progress bar on error in loading
progressDialog.dismiss();
Toast.makeText(getApplicationContext(),"Internet issue", Toast.LENGTH_SHORT).show();
}
}
File upload will be triggered for the html input type of file:
<html>
<head></head>
<body>
<form>
<input type="file" />
</form>
</body>
</html>
Next, in the class of MainActivity, create a WebChomeClient object to handle file uploading task.
// Calling WebChromeClient to select files from the device
public class myWebChromeClient extends WebChromeClient {
@SuppressLint("NewApi")
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> valueCallback, FileChooserParams fileChooserParams) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
// set single file type, e.g. "image/*" for images
intent.setType("*/*");
// set multiple file types
String[] mimeTypes = {"image/*", "application/pdf"};
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);
Intent chooserIntent = Intent.createChooser(intent, "Choose file");
((Activity) webView.getContext()).startActivityForResult(chooserIntent, file_chooser_activity_code);
// Save the callback for handling the selected file
mUploadMessageArr = valueCallback;
return true;
}
}
// after the file chosen handled, variables are returned back to MainActivity
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// check if the chrome activity is a file choosing session
if (requestCode == file_chooser_activity_code) {
if (resultCode == Activity.RESULT_OK && data != null) {
Uri[] results = null;
// Check if response is a multiple choice selection containing the results
if (data.getClipData() != null) {
int count = data.getClipData().getItemCount();
results = new Uri[count];
for (int i = 0; i < count; i++) {
results[i] = data.getClipData().getItemAt(i).getUri();
}
} else if (data.getData() != null) {
// Response is a single choice selection
results = new Uri[]{data.getData()};
}
mUploadMessageArr.onReceiveValue(results);
mUploadMessageArr = null;
} else {
mUploadMessageArr.onReceiveValue(null);
mUploadMessageArr = null;
Toast.makeText(MainActivity.this, "Error getting file", Toast.LENGTH_LONG).show();
}
}
}
In the class of MainActivity, create a download event listener:
DownloadListener downloadListener = new DownloadListener() {
@Override
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
progressDialog.dismiss();
Intent i = new Intent(Intent.ACTION_VIEW);
// example of URL = https://www.example.com/invoice.pdf
i.setData(Uri.parse(url));
startActivity(i);
}
};
This listener might not able to handle downloads that needs login session.
Back to the method of onCreate( ), continue the initialization of WebView:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// initialize the progressDialog
progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setCancelable(true);
progressDialog.setMessage("Loading...");
progressDialog.show();
// get the webview from the layout
webView = findViewById(R.id.webView);
// for handling Android Device [Back] key press
webView.canGoBackOrForward(99);
// handling web page browsing mechanism
webView.setWebViewClient(new myWebViewClient());
// handling file upload mechanism
webView.setWebChromeClient(new myWebChromeClient());
// some other settings
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowFileAccessFromFileURLs(true);
settings.setUserAgentString(new WebView(this).getSettings().getUserAgentString());
// set the downlaod listner
webView.setDownloadListener(downloadListener);
// load the website
webView.loadUrl("https://" + myWebSite);
}
Handling [Back] Key Pressed on Android Device: In the class of MainActivity, add a function to handle [BackPressed]:
@Override
public void onBackPressed() {
if(webView.canGoBack()){
webView.goBack();
} else {
finish();
}
}
The code will look something like this: https://github.com/adriancs2/android.webview.upload.download/blob/main/src/app/src/main/java/com/company/product/MainActivity.java
Finally, let’s test out the app.
To connect a real Android Phone to Android Studio, you may enable Developer Mode
then turn on USB Debugging
on the Android device. Connect the Android to PC through USB cable, Android Studio should be able to catch it up and display it at the device list.
To distribute it, you may build it as APK. At menu, go to [Build
] > [Generate Signed Bundle / APK…
] > select [APK
] and follow on screen instructions.
To build for uploading to Google PlayStore, you may select [Android App Bundle
] and then follow on screen instructions.
Thank you and happy coding.