├── src
├── main
│ ├── res
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ ├── values-w820dp
│ │ │ └── dimens.xml
│ │ └── layout
│ │ │ └── activity_main.xml
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── alextam
│ │ └── uploadimage
│ │ ├── fk_ai_model.py
│ │ ├── CryptoUtil.java
│ │ ├── MyWebChomeClient.java
│ │ ├── ImageUtil.java
│ │ ├── server.py
│ │ ├── PermissionUtil.java
│ │ ├── ContentUtil.java
│ │ ├── AiProcessor.java
│ │ └── MainActivity.java
├── test
│ └── java
│ │ └── com
│ │ └── alextam
│ │ └── uploadimage
│ │ └── ExampleUnitTest.java
└── androidTest
│ └── java
│ └── com
│ └── alextam
│ └── uploadimage
│ └── ApplicationTest.java
├── .gitignore
├── proguard-rules.pro
├── README.md
└── uploadimage.iml
/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexTam930/CrossVersionWebViewUploader/HEAD/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexTam930/CrossVersionWebViewUploader/HEAD/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexTam930/CrossVersionWebViewUploader/HEAD/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexTam930/CrossVersionWebViewUploader/HEAD/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexTam930/CrossVersionWebViewUploader/HEAD/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/src/test/java/com/alextam/uploadimage/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.alextam.uploadimage;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
9 | */
10 | public class ExampleUnitTest {
11 | @Test
12 | public void addition_isCorrect() throws Exception {
13 | assertEquals(4, 2 + 2);
14 | }
15 | }
--------------------------------------------------------------------------------
/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | UploadImage
3 |
4 | 摄像头
5 | 文件
6 | 信息
7 | 状态
8 | 定位
9 | 录音
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/androidTest/java/com/alextam/uploadimage/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | package com.alextam.uploadimage;
2 |
3 | import android.app.Application;
4 | import android.test.ApplicationTestCase;
5 |
6 | /**
7 | * Testing Fundamentals
8 | */
9 | public class ApplicationTest extends ApplicationTestCase {
10 | public ApplicationTest() {
11 | super(Application.class);
12 | }
13 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 |
15 | # Gradle files
16 | .gradle/
17 | build/
18 |
19 | # Local configuration file (sdk path, etc)
20 | local.properties
21 |
22 | # Proguard folder generated by Eclipse
23 | proguard/
24 |
25 | # Log Files
26 | *.log
27 |
28 | # Android Studio Navigation editor temp files
29 | .navigation/
30 |
31 | # Android Studio captures folder
32 | captures/
--------------------------------------------------------------------------------
/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in D:\programfiles\Android\SDK/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/main/java/com/alextam/uploadimage/fk_ai_model.py:
--------------------------------------------------------------------------------
1 | # file: fk_ai_model.py is to show a exsample to apply local AI model in mobile end, and even PC end.
2 | # So, it uses a fake AI model here to process files and data.
3 | # As long as you have a available local AI model, please consider replacing it with yours.
4 |
5 | import random
6 |
7 | def analyze_content(file_content_str: str) -> dict:
8 | """
9 | A fake AI model function that "analyzes" text content.
10 | In a real-world scenario, this would be replaced with calls to a real
11 | Large Language Model (LLM) API (like GPT, Gemini) or a locally hosted model.
12 | """
13 | # Simulate AI analysis by checking content length and picking keywords.
14 | word_count = len(file_content_str.split())
15 |
16 | summary = f"This is a fake AI-generated summary. The document contains approximately {word_count} words."
17 |
18 | possible_keywords = ["important", "confidential", "report", "image", "data", "analysis"]
19 | keywords = random.sample(possible_keywords, min(3, len(possible_keywords)))
20 |
21 | print(f"--- Fake AI Analysis Complete for content with {word_count} words. ---")
22 |
23 | return {
24 | "summary": summary,
25 | "keywords": keywords,
26 | "content_word_count": word_count
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/com/alextam/uploadimage/CryptoUtil.java:
--------------------------------------------------------------------------------
1 | package com.alextam.uploadimage;
2 |
3 | import javax.crypto.Cipher;
4 | import javax.crypto.spec.SecretKeySpec;
5 | import java.security.Key;
6 | import java.util.Base64;
7 |
8 | public class CryptoUtil {
9 |
10 | private static final String ALGORITHM = "AES";
11 | // IMPORTANT: In a real production app, never hardcode the key.
12 | // This key should be securely stored and managed.
13 | private static final String SECRET_KEY = "ThisIsASecretKey1234567890123456"; // Must be 16, 24, or 32 bytes long
14 |
15 | /**
16 | * Encrypts a byte array using AES.
17 | * @param dataToEncrypt The byte array of the original file.
18 | * @return The encrypted byte array.
19 | * @throws Exception if encryption fails.
20 | */
21 | public static byte[] encrypt(byte[] dataToEncrypt) throws Exception {
22 | Key key = new SecretKeySpec(SECRET_KEY.getBytes(), ALGORITHM);
23 | Cipher cipher = Cipher.getInstance(ALGORITHM);
24 | cipher.init(Cipher.ENCRYPT_MODE, key);
25 | return cipher.doFinal(dataToEncrypt);
26 | }
27 |
28 | // Decryption method is not needed for this flow but included for completeness.
29 | /*
30 | public static byte[] decrypt(byte[] encryptedData) throws Exception {
31 | Key key = new SecretKeySpec(SECRET_KEY.getBytes(), ALGORITHM);
32 | Cipher cipher = Cipher.getInstance(ALGORITHM);
33 | cipher.init(Cipher.DECRYPT_MODE, key);
34 | return cipher.doFinal(encryptedData);
35 | }
36 | */
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/alextam/uploadimage/MyWebChomeClient.java:
--------------------------------------------------------------------------------
1 | package com.alextam.uploadimage;
2 |
3 | import android.net.Uri;
4 | import android.webkit.ValueCallback;
5 | import android.webkit.WebChromeClient;
6 | import android.webkit.WebView;
7 |
8 | /**
9 | * MyWebChomeClient
10 | */
11 | public class MyWebChomeClient extends WebChromeClient {
12 |
13 | private OpenFileChooserCallBack mOpenFileChooserCallBack;
14 |
15 | public MyWebChomeClient(OpenFileChooserCallBack openFileChooserCallBack) {
16 | mOpenFileChooserCallBack = openFileChooserCallBack;
17 | }
18 |
19 | public void openFileChooser(ValueCallback uploadMsg, String acceptType) {
20 | mOpenFileChooserCallBack.openFileChooserCallBack(uploadMsg, acceptType);
21 | }
22 |
23 | public void openFileChooser(ValueCallback uploadMsg) {
24 | openFileChooser(uploadMsg, "");
25 | }
26 |
27 | public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture) {
28 | openFileChooser(uploadMsg, acceptType);
29 | }
30 |
31 | public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback,
32 | FileChooserParams fileChooserParams) {
33 | return mOpenFileChooserCallBack.openFileChooserCallBackAndroid5(webView, filePathCallback, fileChooserParams);
34 | }
35 |
36 | public interface OpenFileChooserCallBack {
37 | // for API - Version below 5.0.
38 | void openFileChooserCallBack(ValueCallback uploadMsg, String acceptType);
39 |
40 | // for API - Version above 5.0 (contais 5.0).
41 | boolean openFileChooserCallBackAndroid5(WebView webView, ValueCallback filePathCallback,
42 | FileChooserParams fileChooserParams);
43 | }
44 |
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/com/alextam/uploadimage/ImageUtil.java:
--------------------------------------------------------------------------------
1 | package com.alextam.uploadimage;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.net.Uri;
6 | import android.os.Environment;
7 | import android.provider.MediaStore;
8 | import android.text.TextUtils;
9 | import android.util.Log;
10 |
11 | import java.io.File;
12 |
13 |
14 | public class ImageUtil {
15 |
16 | private static final String TAG ="ImageUtil";
17 |
18 |
19 | /**
20 | * go for Album.
21 | */
22 | public static final Intent choosePicture() {
23 | Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
24 | intent.setType("image/*");
25 | return Intent.createChooser(intent, null);
26 | }
27 |
28 | /**
29 | * go for camera.
30 | */
31 | public static final Intent takeBigPicture() {
32 | Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
33 | intent.putExtra(MediaStore.EXTRA_OUTPUT, newPictureUri(getNewPhotoPath()));
34 | return intent;
35 | }
36 |
37 | public static final String getDirPath() {
38 | return Environment.getExternalStorageDirectory().getPath() + "/UploadImage";
39 | }
40 |
41 | private static final String getNewPhotoPath() {
42 | return getDirPath() + "/" + System.currentTimeMillis() + ".jpg";
43 | }
44 |
45 | public static final String retrievePath(Context context, Intent sourceIntent, Intent dataIntent) {
46 | String picPath = null;
47 | try {
48 | Uri uri;
49 | if (dataIntent != null) {
50 | uri = dataIntent.getData();
51 | if (uri != null) {
52 | picPath = ContentUtil.getPath(context, uri);
53 | }
54 | if (isFileExists(picPath)) {
55 | return picPath;
56 | }
57 |
58 | Log.w(TAG, String.format("retrievePath failed from dataIntent:%s, extras:%s", dataIntent, dataIntent.getExtras()));
59 | }
60 |
61 | if (sourceIntent != null) {
62 | uri = sourceIntent.getParcelableExtra(MediaStore.EXTRA_OUTPUT);
63 | if (uri != null) {
64 | String scheme = uri.getScheme();
65 | if (scheme != null && scheme.startsWith("file")) {
66 | picPath = uri.getPath();
67 | }
68 | }
69 | if (!TextUtils.isEmpty(picPath)) {
70 | File file = new File(picPath);
71 | if (!file.exists() || !file.isFile()) {
72 | Log.w(TAG, String.format("retrievePath file not found from sourceIntent path:%s", picPath));
73 | }
74 | }
75 | }
76 | return picPath;
77 | } finally {
78 | Log.d(TAG, "retrievePath(" + sourceIntent + "," + dataIntent + ") ret: " + picPath);
79 | }
80 | }
81 |
82 | private static final Uri newPictureUri(String path) {
83 | return Uri.fromFile(new File(path));
84 | }
85 |
86 | private static final boolean isFileExists(String path) {
87 | if (TextUtils.isEmpty(path)) {
88 | return false;
89 | }
90 | File f = new File(path);
91 | if (!f.exists()) {
92 | return false;
93 | }
94 | return true;
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/main/java/com/alextam/uploadimage/server.py:
--------------------------------------------------------------------------------
1 | # file: server.py
2 | # This server provides two APIs to run AI analysis against data using the API - ``/analyze``,
3 | # and to receive encrypted data using the API - ``/upload``.
4 |
5 | import os
6 | import base64
7 | from flask import Flask, request, jsonify
8 | from fake_ai_model import analyze_content
9 |
10 | app = Flask(__name__)
11 |
12 | # --- Configuration ---
13 | # Create a directory to store uploaded files.
14 | UPLOAD_FOLDER = 'uploaded_files'
15 | if not os.path.exists(UPLOAD_FOLDER):
16 | os.makedirs(UPLOAD_FOLDER)
17 |
18 | # --- API Endpoints ---
19 |
20 | @app.route('/analyze', methods=['POST'])
21 | def analyze_file():
22 | """
23 | AI analysis endpoint.
24 | Receives file content as a Base64 encoded string, decodes it,
25 | and returns an AI-generated analysis.
26 | """
27 | print("--- Received request for /analyze ---")
28 | data = request.json
29 | if not data or 'file_content_b64' not in data:
30 | return jsonify({"error": "Missing file_content_b64"}), 400
31 |
32 | try:
33 | # Decode the Base64 string to bytes, then to a UTF-8 string.
34 | file_content_bytes = base64.b64decode(data['file_content_b64'])
35 | file_content_str = file_content_bytes.decode('utf-8', errors='ignore')
36 |
37 | # Get analysis from our fake AI model.
38 | analysis_result = analyze_content(file_content_str)
39 |
40 | print("--- Analysis successful. Sending back results. ---")
41 | return jsonify(analysis_result)
42 |
43 | except Exception as e:
44 | print(f"Error during analysis: {e}")
45 | return jsonify({"error": str(e)}), 500
46 |
47 |
48 | @app.route('/upload', methods=['POST'])
49 | def upload_file():
50 | """
51 | File upload endpoint.
52 | Receives the encrypted file (Base64), a filename, and AI metadata.
53 | It saves the encrypted file to the server.
54 | """
55 | print("--- Received request for /upload ---")
56 | data = request.json
57 | if not data or 'encrypted_file_b64' not in data or 'filename' not in data:
58 | return jsonify({"error": "Missing required fields"}), 400
59 |
60 | try:
61 | filename = data['filename']
62 | encrypted_file_b64 = data['encrypted_file_b64']
63 | ai_metadata = data.get('ai_metadata', {}) # .get is safer
64 |
65 | # Define the path to save the encrypted file.
66 | # We add '.enc' to denote it's an encrypted file.
67 | save_path = os.path.join(UPLOAD_FOLDER, f"{filename}.enc")
68 |
69 | # Decode the Base64 string and save the encrypted bytes to a file.
70 | with open(save_path, "wb") as f:
71 | f.write(base64.b64decode(encrypted_file_b64))
72 |
73 | print(f"--- Encrypted file '{filename}.enc' saved successfully. ---")
74 | print(f"--- Associated AI Metadata: {ai_metadata} ---")
75 |
76 | return jsonify({
77 | "status": "success",
78 | "message": f"File '{filename}' uploaded securely.",
79 | "saved_path": save_path
80 | })
81 |
82 | except Exception as e:
83 | print(f"Error during upload: {e}")
84 | return jsonify({"error": str(e)}), 500
85 |
86 |
87 | # --- Main execution ---
88 | if __name__ == '__main__':
89 | # Run the app. Use host='0.0.0.0' to make it accessible from your local network (e.g., your Android device).
90 | app.run(host='0.0.0.0', port=5000, debug=True)
91 |
--------------------------------------------------------------------------------
/src/main/java/com/alextam/uploadimage/PermissionUtil.java:
--------------------------------------------------------------------------------
1 | package com.alextam.uploadimage;
2 |
3 | import android.Manifest;
4 | import android.annotation.TargetApi;
5 | import android.app.Activity;
6 | import android.content.pm.PackageManager;
7 | import android.os.Build;
8 | import android.support.v4.content.ContextCompat;
9 |
10 |
11 | import java.util.ArrayList;
12 | import java.util.List;
13 |
14 | /**
15 | * 权限管理工具 (针对Android 6.0 系统)
16 | * Created by AlexTam on 2016/10/14.
17 | */
18 | public class PermissionUtil {
19 | private static PermissionUtil permissionUtil = null;
20 | private static final String PERMISSIONS_CAMERA = Manifest.permission.CAMERA;
21 | private static final String PERMISSIONS_WRITE_STORAGE = Manifest.permission.WRITE_EXTERNAL_STORAGE;
22 | private static final String PERMISSIONS_READ_STORAGE = Manifest.permission.READ_EXTERNAL_STORAGE;
23 | private static final String PERMISSIONS_PHONE = Manifest.permission.READ_PHONE_STATE;
24 | private static final String PERMISSIONS_ACCOUNTS = Manifest.permission.GET_ACCOUNTS;
25 | private static final String PERMISSIONS_LOCATION = Manifest.permission.ACCESS_FINE_LOCATION;
26 | private static final String PERMISSIONS_AUDIO = Manifest.permission.RECORD_AUDIO;
27 |
28 |
29 | public static final boolean isOverMarshmallow() {
30 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
31 | }
32 |
33 |
34 | /**
35 | *
36 | * @param activity
37 | * @param permissionName such as Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE.
38 | * @return
39 | */
40 | public static final boolean isPermissionValid(Activity activity, String permissionName) {
41 | try {
42 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
43 | int checkCallPhonePermission = ContextCompat.checkSelfPermission(activity, permissionName);
44 | if (checkCallPhonePermission == PackageManager.PERMISSION_GRANTED) {
45 | return true;
46 | } else {
47 | return false;
48 | }
49 | } else {
50 | return true;
51 | }
52 |
53 | } catch (Exception e) {
54 | e.printStackTrace();
55 | }
56 |
57 | return false;
58 | }
59 |
60 |
61 | /**
62 | * to find the permissions which were denied in this device.
63 | */
64 | @TargetApi(value = Build.VERSION_CODES.M)
65 | public static final List findDeniedPermissions(Activity activity, List permissions) {
66 | if (permissions == null || permissions.size() == 0) {
67 | return null;
68 | } else {
69 | List denyPermissions = new ArrayList<>();
70 |
71 | for (String value : permissions) {
72 | try {
73 | if (activity.checkSelfPermission(value) != PackageManager.PERMISSION_GRANTED) {
74 | denyPermissions.add(value);
75 | }
76 | } catch (Exception e) {
77 | e.printStackTrace();
78 | }
79 | }
80 |
81 | return denyPermissions;
82 | }
83 | }
84 |
85 | /**
86 | * request Permissions.
87 | * @param activity
88 | * @param requestCode
89 | * @param mListPermissions
90 | */
91 | @TargetApi(value = Build.VERSION_CODES.M)
92 | public static final void requestPermissions(Activity activity, int requestCode, List mListPermissions) {
93 | if (mListPermissions == null || mListPermissions.size() == 0) {
94 | return;
95 | }
96 |
97 | if (!isOverMarshmallow()) {
98 | // should not be invoked when it is below Android 6.0.
99 | return;
100 |
101 | } else {
102 | List deniedPermissionList = findDeniedPermissions(activity, mListPermissions);
103 |
104 | if (deniedPermissionList != null && deniedPermissionList.size() > 0) {
105 | activity.requestPermissions(deniedPermissionList.toArray(new String[deniedPermissionList.size()]),
106 | requestCode);
107 |
108 | }
109 | }
110 |
111 | }
112 |
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/src/main/java/com/alextam/uploadimage/ContentUtil.java:
--------------------------------------------------------------------------------
1 | package com.alextam.uploadimage;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.ContentUris;
5 | import android.content.Context;
6 | import android.database.Cursor;
7 | import android.net.Uri;
8 | import android.os.Build;
9 | import android.os.Environment;
10 | import android.provider.DocumentsContract;
11 | import android.provider.MediaStore;
12 |
13 |
14 |
15 | public class ContentUtil {
16 |
17 | @SuppressLint("NewApi")
18 | public static final String getPath(final Context context, final Uri uri) {
19 |
20 | final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
21 |
22 | // DocumentProvider
23 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
24 | // ExternalStorageProvider
25 | if (isExternalStorageDocument(uri)) {
26 | final String docId = DocumentsContract.getDocumentId(uri);
27 | final String[] split = docId.split(":");
28 | final String type = split[0];
29 |
30 | if ("primary".equalsIgnoreCase(type)) {
31 | return String.format("%s/%s", Environment.getExternalStorageDirectory().getPath(), split[1]);
32 | }
33 |
34 | // TODO handle non-primary volumes
35 | }
36 | // DownloadsProvider
37 | else if (isDownloadsDocument(uri)) {
38 |
39 | final String id = DocumentsContract.getDocumentId(uri);
40 | final Uri contentUri = ContentUris.withAppendedId(
41 | Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
42 |
43 | return getDataColumn(context, contentUri, null, null);
44 | }
45 | // MediaProvider
46 | else if (isMediaDocument(uri)) {
47 | final String docId = DocumentsContract.getDocumentId(uri);
48 | final String[] split = docId.split(":");
49 | final String type = split[0];
50 |
51 | Uri contentUri = null;
52 | if ("image".equals(type)) {
53 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
54 | } else if ("video".equals(type)) {
55 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
56 | } else if ("audio".equals(type)) {
57 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
58 | }
59 |
60 | final String selection = "_id=?";
61 | final String[] selectionArgs = new String[]{split[1]};
62 |
63 | return getDataColumn(context, contentUri, selection, selectionArgs);
64 | }
65 | }
66 | // MediaStore (and general)
67 | else if ("content".equalsIgnoreCase(uri.getScheme())) {
68 |
69 | // Return the remote address
70 | if (isGooglePhotosUri(uri)) {
71 | return uri.getLastPathSegment();
72 | }
73 |
74 | return getDataColumn(context, uri, null, null);
75 | }
76 | // File
77 | else if ("file".equalsIgnoreCase(uri.getScheme())) {
78 | return uri.getPath();
79 | }
80 |
81 | return null;
82 | }
83 |
84 | /**
85 | * Get the value of the data column for this Uri. This is useful for
86 | * MediaStore Uris, and other file-based ContentProviders.
87 | *
88 | * @param context The context.
89 | * @param uri The Uri to query.
90 | * @param selection (Optional) Filter used in the query.
91 | * @param selectionArgs (Optional) Selection arguments used in the query.
92 | * @return The value of the _data column, which is typically a file path.
93 | */
94 | public static final String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
95 |
96 | Cursor cursor = null;
97 |
98 | try {
99 | cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Images.Media.DATA}
100 | , selection, selectionArgs, null);
101 | if (cursor != null && cursor.moveToFirst()) {
102 | final int index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
103 | return cursor.getString(index);
104 | }
105 | } finally {
106 | if (cursor != null) {
107 | cursor.close();
108 | }
109 | }
110 | return null;
111 | }
112 |
113 |
114 | /**
115 | * @param uri The Uri to check.
116 | * @return Whether the Uri authority is ExternalStorageProvider.
117 | */
118 | public static final boolean isExternalStorageDocument(Uri uri) {
119 | return "com.android.externalstorage.documents".equals(uri.getAuthority());
120 | }
121 |
122 | /**
123 | * @param uri The Uri to check.
124 | * @return Whether the Uri authority is DownloadsProvider.
125 | */
126 | public static final boolean isDownloadsDocument(Uri uri) {
127 | return "com.android.providers.downloads.documents".equals(uri.getAuthority());
128 | }
129 |
130 | /**
131 | * @param uri The Uri to check.
132 | * @return Whether the Uri authority is MediaProvider.
133 | */
134 | public static final boolean isMediaDocument(Uri uri) {
135 | return "com.android.providers.media.documents".equals(uri.getAuthority());
136 | }
137 |
138 | /**
139 | * @param uri The Uri to check.
140 | * @return Whether the Uri authority is Google Photos.
141 | */
142 | public static final boolean isGooglePhotosUri(Uri uri) {
143 | return "com.google.android.apps.photos.content".equals(uri.getAuthority());
144 | }
145 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AI-Powered Secure File Upload for CrossVersionWebViewUploader
2 |
3 | ## Overview
4 |
5 | This project began as a technical demo exploring how an Android `WebView` could reliably upload image files from the gallery or camera. It has since evolved into an advanced proof-of-concept, showcasing a modern file-handling pipeline that integrates **AI-powered content analysis**, **client-side encryption**, and **secure native uploads** within a hybrid Android application.
6 |
7 | This solution moves beyond solving `WebView` compatibility issues to establish a new, secure, and intelligent architecture for data ingestion and processing. It seamlessly bridges a `WebView`-based frontend with powerful native Java capabilities and a Python AI backend service.
8 |
9 | ## Core Features
10 |
11 | - **🤖 AI-Powered Content Analysis**
12 | Before any file is uploaded, its content is sent to a backend AI service for in-depth analysis. The AI model can automatically generate summaries, extract keywords, or identify content types, transforming unstructured data into structured, valuable metadata.
13 |
14 | - **🔒 Client-Side End-to-End Encryption**
15 | Security is at the core of this design. All files are encrypted locally on the user's device using the AES-256 symmetric algorithm *before* transmission. This ensures that even if data is intercepted, it remains unreadable without the secret key, guaranteeing data privacy and security.
16 |
17 | - **🚀 Native Secure Upload Handler**
18 | The upload process has been completely re-architected. Instead of passing a file URI back to the `WebView`, the native Android code takes full control. The app manages communication with the AI backend, performs encryption, and uploads the final encrypted package with its AI metadata to a dedicated server endpoint.
19 |
20 | - **🔧 Decoupled Hybrid Architecture**
21 | The project utilizes a clean client-server model:
22 | * **Android (Java) Client**: Manages user interaction, native capabilities (camera/gallery), encryption, and network communication.
23 | * **Python (Flask) Backend**: Serves as a lightweight AI microservice, handling analysis requests and receiving uploaded files.
24 |
25 | ## How It Works
26 |
27 | The new AI-driven upload workflow is as follows:
28 |
29 | 1. **Trigger**: A user clicks a file input element (``) on a webpage within the `WebView`.
30 | 2. **Native Interception**: The Android app intercepts this request and opens the native system UI for the camera or gallery.
31 | 3. **Content Reading**: After the user selects a file, the app reads its byte content at the native layer.
32 | 4. **AI Analysis**: The app sends the file content to the `/analyze` endpoint of the Python AI backend.
33 | 5. **Metadata Retrieval**: The backend returns the AI-generated analysis results (e.g., a JSON object with a summary and keywords).
34 | 6. **Local Encryption**: The app uses the `CryptoUtil` class to encrypt the original file on the device.
35 | 7. **Secure Upload**: The app POSTs the **encrypted file** and the **AI metadata** together to the secure `/upload` endpoint on the backend.
36 | 8. **User Feedback**: The app provides UI feedback (e.g., a progress bar and Toast messages) to inform the user of the process status and then concludes the `WebView`'s file selection session.
37 |
38 | ## Future Vision: A Foundational Platform for Cross-Industry Intelligence
39 |
40 | The long-term goal for this technology is to evolve beyond a single application into a versatile, domain-agnostic platform for intelligent and secure data intake. By building on its core principles, this framework can become a foundational component for digital transformation across numerous industries.
41 |
42 | #### **1. Universal Data Comprehension (Advanced AI Reading)**
43 |
44 | The platform will be enhanced with multi-modal AI capable of understanding any data format, making it universally applicable.
45 | * **Healthcare**: Ingest and structure unstructured data from patient records, lab reports (PDFs), and medical imagery (DICOM), automatically tagging them with relevant medical codes.
46 | * **Finance & Legal**: Automate the processing of invoices, contracts, and KYC (Know Your Customer) documents by extracting key entities, terms, and clauses, reducing manual review time.
47 | * **Logistics & Supply Chain**: Analyze shipping manifests, bills of lading, and delivery confirmation photos to automate tracking, verify contents, and flag discrepancies.
48 | * **Manufacturing & IoT**: Process quality control images from production lines and ingest sensor data from IoT devices to perform real-time anomaly detection and predictive maintenance.
49 |
50 | #### **2. Adaptive Security & Governance (Intelligent Encryption)**
51 |
52 | The AI's role will expand to include automated data classification, enabling the platform to serve highly regulated fields.
53 | * **Dynamic Compliance**: The AI will identify sensitive data types on the fly (e.g., PII, PHI, financial records) and dynamically apply industry-specific encryption and handling protocols (e.g., HIPAA for healthcare, GDPR for personal data, PCI DSS for financial).
54 | * **Automated Governance**: This adaptive security model ensures that as the platform is deployed in new industries, it automatically adheres to the local compliance and data residency requirements, drastically simplifying setup and reducing risk.
55 |
56 | #### **3. Federated Data Ecosystems (Big Data & Secure Sharing)**
57 |
58 | The platform will act as a secure gateway to larger data ecosystems, enabling unprecedented cross-domain collaboration.
59 | * **Privacy-Preserving Insights**: The rich, AI-generated metadata allows for powerful, large-scale analytics to be performed without ever decrypting the underlying raw data. This preserves privacy while still enabling insight generation.
60 | * **Cross-Industry Intelligence**: Imagine a future where anonymized data from different sectors can be correlated. Logistics data on shipping delays could be securely correlated with financial data on market impacts, or public health data could be correlated with supply chain information during a crisis. This platform would serve as the secure, intelligent fabric connecting these disparate data sets, unlocking new levels of predictive and analytical power for a smarter, more connected world.
61 |
--------------------------------------------------------------------------------
/src/main/java/com/alextam/uploadimage/AiProcessor.java:
--------------------------------------------------------------------------------
1 | // file: AiProcessor.java
2 | package com.alextam.uploadimage;
3 |
4 | import android.content.Context;
5 | import android.net.Uri;
6 | import android.os.Handler;
7 | import android.os.Looper;
8 | import android.util.Base64;
9 | import android.util.Log;
10 |
11 | import org.json.JSONObject;
12 |
13 | import java.io.ByteArrayOutputStream;
14 | import java.io.InputStream;
15 |
16 | import okhttp3.Call;
17 | import okhttp3.Callback;
18 | import okhttp3.MediaType;
19 | import okhttp3.OkHttpClient;
20 | import okhttp3.Request;
21 | import okhttp3.RequestBody;
22 | import okhttp3.Response;
23 | import java.io.IOException;
24 |
25 | /**
26 | * This file is one of the logics that is reponsible to interace with Python back end.
27 | *
28 | **/
29 | public class AiProcessor {
30 |
31 | private static final String TAG = "AiProcessor";
32 | // IMPORTANT: Replace with your computer's IP address on the local network.
33 | private static final String BASE_URL = "http://192.168.1.100:5000";
34 | private static final String ANALYZE_URL = BASE_URL + "/analyze";
35 | private static final String UPLOAD_URL = BASE_URL + "/upload";
36 |
37 | private static final OkHttpClient client = new OkHttpClient();
38 | private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
39 |
40 | public interface AiProcessorCallback {
41 | void onSuccess(String message);
42 | void onFailure(String errorMessage);
43 | }
44 |
45 | public static void processAndUploadFile(Context context, Uri fileUri, String fileName, AiProcessorCallback callback) {
46 | new Thread(() -> {
47 | try {
48 | // Step 1: Read file content into a byte array from Uri.
49 | byte[] fileBytes = readFileBytes(context, fileUri);
50 | if (fileBytes == null) {
51 | postResult(() -> callback.onFailure("Failed to read file."));
52 | return;
53 | }
54 |
55 | // Step 2: Send file content to AI for analysis.
56 | analyzeFile(fileBytes, (aiMetadata) -> {
57 | if (aiMetadata == null) {
58 | postResult(() -> callback.onFailure("AI analysis failed."));
59 | return;
60 | }
61 |
62 | try {
63 | // Step 3: Encrypt the file locally.
64 | Log.d(TAG, "Encrypting file...");
65 | byte[] encryptedBytes = CryptoUtil.encrypt(fileBytes);
66 | Log.d(TAG, "Encryption successful.");
67 |
68 | // Step 4: Upload the encrypted file and AI metadata.
69 | uploadFile(encryptedBytes, fileName, aiMetadata, callback);
70 |
71 | } catch (Exception e) {
72 | Log.e(TAG, "Encryption failed.", e);
73 | postResult(() -> callback.onFailure("File encryption failed: " + e.getMessage()));
74 | }
75 | });
76 |
77 | } catch (Exception e) {
78 | Log.e(TAG, "File processing failed.", e);
79 | postResult(() -> callback.onFailure("File processing failed: " + e.getMessage()));
80 | }
81 | }).start();
82 | }
83 |
84 | private static byte[] readFileBytes(Context context, Uri uri) throws IOException {
85 | InputStream inputStream = context.getContentResolver().openInputStream(uri);
86 | ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
87 | int bufferSize = 1024;
88 | byte[] buffer = new byte[bufferSize];
89 |
90 | int len;
91 | while ((len = inputStream.read(buffer)) != -1) {
92 | byteBuffer.write(buffer, 0, len);
93 | }
94 | return byteBuffer.toByteArray();
95 | }
96 |
97 | private static void analyzeFile(byte[] fileBytes, AnalysisCallback callback) {
98 | try {
99 | Log.d(TAG, "Sending file for AI analysis...");
100 | String fileContentB64 = Base64.encodeToString(fileBytes, Base64.NO_WRAP);
101 | JSONObject json = new JSONObject();
102 | json.put("file_content_b64", fileContentB64);
103 |
104 | RequestBody body = RequestBody.create(json.toString(), JSON);
105 | Request request = new Request.Builder().url(ANALYZE_URL).post(body).build();
106 |
107 | client.newCall(request).enqueue(new Callback() {
108 | @Override
109 | public void onFailure(Call call, IOException e) {
110 | Log.e(TAG, "AI analysis request failed.", e);
111 | callback.onComplete(null);
112 | }
113 |
114 | @Override
115 | public void onResponse(Call call, Response response) throws IOException {
116 | if (response.isSuccessful()) {
117 | try {
118 | String responseBody = response.body().string();
119 | Log.d(TAG, "AI analysis successful. Response: " + responseBody);
120 | callback.onComplete(new JSONObject(responseBody));
121 | } catch (Exception e) {
122 | Log.e(TAG, "Failed to parse AI response.", e);
123 | callback.onComplete(null);
124 | }
125 | } else {
126 | Log.e(TAG, "AI analysis failed with code: " + response.code());
127 | callback.onComplete(null);
128 | }
129 | }
130 | });
131 | } catch (Exception e) {
132 | Log.e(TAG, "Failed to build analysis request.", e);
133 | callback.onComplete(null);
134 | }
135 | }
136 |
137 | private static void uploadFile(byte[] encryptedBytes, String fileName, JSONObject aiMetadata, AiProcessorCallback callback) {
138 | try {
139 | Log.d(TAG, "Uploading encrypted file...");
140 | String encryptedFileB64 = Base64.encodeToString(encryptedBytes, Base64.NO_WRAP);
141 | JSONObject json = new JSONObject();
142 | json.put("filename", fileName);
143 | json.put("encrypted_file_b64", encryptedFileB64);
144 | json.put("ai_metadata", aiMetadata);
145 |
146 | RequestBody body = RequestBody.create(json.toString(), JSON);
147 | Request request = new Request.Builder().url(UPLOAD_URL).post(body).build();
148 |
149 | client.newCall(request).enqueue(new Callback() {
150 | @Override
151 | public void onFailure(Call call, IOException e) {
152 | Log.e(TAG, "Upload request failed.", e);
153 | postResult(() -> callback.onFailure("Upload failed: " + e.getMessage()));
154 | }
155 |
156 | @Override
157 | public void onResponse(Call call, Response response) throws IOException {
158 | if (response.isSuccessful()) {
159 | Log.d(TAG, "Upload successful.");
160 | postResult(() -> callback.onSuccess("File uploaded securely with AI analysis."));
161 | } else {
162 | Log.e(TAG, "Upload failed with code: " + response.code());
163 | postResult(() -> callback.onFailure("Upload failed with server error: " + response.code()));
164 | }
165 | }
166 | });
167 |
168 | } catch (Exception e) {
169 | Log.e(TAG, "Failed to build upload request.", e);
170 | postResult(() -> callback.onFailure("Upload failed: " + e.getMessage()));
171 | }
172 | }
173 |
174 | private interface AnalysisCallback {
175 | void onComplete(JSONObject aiMetadata);
176 | }
177 |
178 | private static void postResult(Runnable action) {
179 | new Handler(Looper.getMainLooper()).post(action);
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/uploadimage.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | generateDebugSources
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/src/main/java/com/alextam/uploadimage/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.alextam.uploadimage;
2 |
3 | import android.Manifest;
4 | import android.app.Activity;
5 | import android.app.AlertDialog;
6 | import android.content.DialogInterface;
7 | import android.content.Intent;
8 | import android.content.pm.PackageManager;
9 | import android.database.Cursor;
10 | import android.net.Uri;
11 | import android.net.http.SslError;
12 | import android.os.Build;
13 | import android.os.Bundle;
14 | import android.provider.OpenableColumns;
15 | import android.text.TextUtils;
16 | import android.util.Log;
17 | import android.view.View;
18 | import android.webkit.CookieManager;
19 | import android.webkit.CookieSyncManager;
20 | import android.webkit.SslErrorHandler;
21 | import android.webkit.ValueCallback;
22 | import android.webkit.WebChromeClient;
23 | import android.webkit.WebSettings;
24 | import android.webkit.WebView;
25 | import android.webkit.WebViewClient;
26 | import android.widget.ProgressBar;
27 | import android.widget.Toast;
28 |
29 | import java.io.File;
30 | import java.util.ArrayList;
31 | import java.util.List;
32 |
33 |
34 | /**
35 | * WebView invokes the camera to take pictures, upload pictures, or select pictures from the album for uploading.
36 | * This version is updated to include AI-driven secure file processing and uploading.
37 | *
38 | * @author AlexTam
39 | * created at 2016/10/14 9:58
40 | */
41 | public class MainActivity extends Activity
42 | implements MyWebChomeClient.OpenFileChooserCallBack {
43 |
44 | private static final String TAG = "MainActivity";
45 | private WebView mWebView;
46 | private ProgressBar mProgressBar; // Add a progress bar to the layout
47 |
48 | private static final int REQUEST_CODE_PICK_IMAGE = 0;
49 | private static final int REQUEST_CODE_IMAGE_CAPTURE = 1;
50 |
51 | private Intent mSourceIntent;
52 | private ValueCallback mUploadMsg;
53 | public ValueCallback mUploadMsgForAndroid5;
54 |
55 | // permission Code
56 | private static final int P_CODE_PERMISSIONS = 101;
57 |
58 |
59 | @Override
60 | public void onCreate(Bundle savedInstanceState) {
61 | super.onCreate(savedInstanceState);
62 | setContentView(R.layout.activity_main);
63 |
64 | // Make sure you have a ProgressBar with id 'progressBar' in your activity_main.xml
65 | mProgressBar = findViewById(R.id.progressBar);
66 |
67 | requestPermissionsAndroidM();
68 |
69 | mWebView = (WebView) findViewById(R.id.webview);
70 |
71 | WebSettings webSettings = mWebView.getSettings();
72 | webSettings.setJavaScriptEnabled(true);
73 | webSettings.setAllowFileAccess(true);
74 | webSettings.setAllowContentAccess(true);
75 | webSettings.setBuiltInZoomControls(false);
76 |
77 | mWebView.setWebChromeClient(new MyWebChomeClient(MainActivity.this));
78 |
79 | mWebView.setWebViewClient(new WebViewClient() {
80 | @Override
81 | public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
82 | handler.proceed();
83 | }
84 |
85 | @Override
86 | public boolean shouldOverrideUrlLoading(WebView view, String url) {
87 | view.loadUrl(url);
88 | return true;
89 | }
90 |
91 | @Override
92 | public void onPageFinished(WebView view, String url) {
93 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
94 | CookieSyncManager.getInstance().sync();
95 | } else {
96 | CookieManager.getInstance().flush();
97 | }
98 | }
99 | });
100 |
101 | fixDirPath();
102 |
103 | // target your url here.
104 | mWebView.loadUrl("https://upload.test.com/");
105 | }
106 |
107 | @Override
108 | public void onActivityResult(int requestCode, int resultCode, Intent data) {
109 | if (resultCode != Activity.RESULT_OK) {
110 | // User cancelled the file selection, so restore the callback to WebView
111 | restoreUploadMsg();
112 | return;
113 | }
114 |
115 | switch (requestCode) {
116 | case REQUEST_CODE_IMAGE_CAPTURE:
117 | case REQUEST_CODE_PICK_IMAGE: {
118 | try {
119 | // This part is now different. We get the URI but don't give it back to WebView.
120 | // Instead, we pass it to our AiProcessor.
121 |
122 | Uri resultUri = null;
123 | if (data != null && data.getData() != null) {
124 | resultUri = data.getData();
125 | } else if (mSourceIntent != null) {
126 | // For camera capture, the URI is in the source intent
127 | String sourcePath = ImageUtil.retrievePath(this, mSourceIntent, data);
128 | if (sourcePath != null) {
129 | resultUri = Uri.fromFile(new File(sourcePath));
130 | }
131 | }
132 |
133 | if (resultUri != null) {
134 | handleFileUpload(resultUri);
135 | } else {
136 | Toast.makeText(this, "Failed to get file URI.", Toast.LENGTH_SHORT).show();
137 | restoreUploadMsg();
138 | }
139 |
140 | } catch (Exception e) {
141 | e.printStackTrace();
142 | Toast.makeText(this, "An error occurred.", Toast.LENGTH_SHORT).show();
143 | restoreUploadMsg();
144 | }
145 | break;
146 | }
147 | }
148 | }
149 |
150 | private void handleFileUpload(Uri fileUri) {
151 | // Show a progress bar to the user
152 | mProgressBar.setVisibility(View.VISIBLE);
153 |
154 | // Get the original filename from the URI
155 | String fileName = getFileName(fileUri);
156 |
157 | // The original callbacks to WebView are now cancelled because we are handling the upload natively.
158 | // We call restoreUploadMsg() inside the AiProcessor's callback.
159 | if (mUploadMsg == null && mUploadMsgForAndroid5 == null) {
160 | mProgressBar.setVisibility(View.GONE);
161 | return;
162 | }
163 |
164 | AiProcessor.processAndUploadFile(this, fileUri, fileName, new AiProcessor.AiProcessorCallback() {
165 | @Override
166 | public void onSuccess(String message) {
167 | // On success, show a message and restore the WebView callback with null.
168 | Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show();
169 | mProgressBar.setVisibility(View.GONE);
170 | restoreUploadMsg(); // This tells the WebView the process is complete.
171 | }
172 |
173 | @Override
174 | public void onFailure(String errorMessage) {
175 | // On failure, show an error and restore the WebView callback with null.
176 | Toast.makeText(MainActivity.this, errorMessage, Toast.LENGTH_LONG).show();
177 | mProgressBar.setVisibility(View.GONE);
178 | restoreUploadMsg(); // This tells the WebView the process is complete.
179 | }
180 | });
181 | }
182 |
183 | /**
184 | * Helper method to get filename from a content URI.
185 | * @param uri The URI to query.
186 | * @return The file name.
187 | */
188 | private String getFileName(Uri uri) {
189 | String result = null;
190 | if (uri.getScheme().equals("content")) {
191 | try (Cursor cursor = getContentResolver().query(uri, null, null, null, null)) {
192 | if (cursor != null && cursor.moveToFirst()) {
193 | int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
194 | if (index != -1) {
195 | result = cursor.getString(index);
196 | }
197 | }
198 | }
199 | }
200 | if (result == null) {
201 | result = uri.getPath();
202 | int cut = result.lastIndexOf('/');
203 | if (cut != -1) {
204 | result = result.substring(cut + 1);
205 | }
206 | }
207 | return result;
208 | }
209 |
210 |
211 | @Override
212 | public void openFileChooserCallBack(ValueCallback uploadMsg, String acceptType) {
213 | mUploadMsg = uploadMsg;
214 | showOptions();
215 | }
216 |
217 | @Override
218 | public boolean openFileChooserCallBackAndroid5
219 | (WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
220 | mUploadMsgForAndroid5 = filePathCallback;
221 | showOptions();
222 |
223 | return true;
224 | }
225 |
226 | public void showOptions() {
227 | AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
228 | alertDialog.setOnCancelListener(new DialogOnCancelListener());
229 |
230 | alertDialog.setTitle("请选择操作");
231 | // gallery, camera.
232 | String[] options = {"相册", "拍照"};
233 |
234 | alertDialog.setItems(options, new DialogInterface.OnClickListener() {
235 | @Override
236 | public void onClick(DialogInterface dialog, int which) {
237 | if (which == 0) {
238 | if (PermissionUtil.isOverMarshmallow()) {
239 | if (!PermissionUtil.isPermissionValid(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
240 | Toast.makeText(MainActivity.this,
241 | "请去\"设置\"中开启本应用的图片媒体访问权限",
242 | Toast.LENGTH_SHORT).show();
243 |
244 | restoreUploadMsg();
245 | requestPermissionsAndroidM();
246 | return;
247 | }
248 |
249 | }
250 |
251 | try {
252 | mSourceIntent = ImageUtil.choosePicture();
253 | startActivityForResult(mSourceIntent, REQUEST_CODE_PICK_IMAGE);
254 | } catch (Exception e) {
255 | e.printStackTrace();
256 | Toast.makeText(MainActivity.this,
257 | "请去\"设置\"中开启本应用的图片媒体访问权限",
258 | Toast.LENGTH_SHORT).show();
259 | restoreUploadMsg();
260 | }
261 |
262 | } else {
263 | if (PermissionUtil.isOverMarshmallow()) {
264 | if (!PermissionUtil.isPermissionValid(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
265 | Toast.makeText(MainActivity.this,
266 | "请去\"设置\"中开启本应用的图片媒体访问权限",
267 | Toast.LENGTH_SHORT).show();
268 |
269 | restoreUploadMsg();
270 | requestPermissionsAndroidM();
271 | return;
272 | }
273 |
274 | if (!PermissionUtil.isPermissionValid(MainActivity.this, Manifest.permission.CAMERA)) {
275 | Toast.makeText(MainActivity.this,
276 | "请去\"设置\"中开启本应用的相机权限",
277 | Toast.LENGTH_SHORT).show();
278 |
279 | restoreUploadMsg();
280 | requestPermissionsAndroidM();
281 | return;
282 | }
283 | }
284 |
285 | try {
286 | mSourceIntent = ImageUtil.takeBigPicture();
287 | startActivityForResult(mSourceIntent, REQUEST_CODE_IMAGE_CAPTURE);
288 |
289 | } catch (Exception e) {
290 | e.printStackTrace();
291 | Toast.makeText(MainActivity.this,
292 | "请去\"设置\"中开启本应用的相机和图片媒体访问权限",
293 | Toast.LENGTH_SHORT).show();
294 |
295 | restoreUploadMsg();
296 | }
297 | }
298 | }
299 | }
300 | );
301 |
302 | alertDialog.show();
303 | }
304 |
305 | private void fixDirPath() {
306 | String path = ImageUtil.getDirPath();
307 | File file = new File(path);
308 | if (!file.exists()) {
309 | file.mkdirs();
310 | }
311 | }
312 |
313 | private class DialogOnCancelListener implements DialogInterface.OnCancelListener {
314 | @Override
315 | public void onCancel(DialogInterface dialogInterface) {
316 | restoreUploadMsg();
317 | }
318 | }
319 |
320 | private void restoreUploadMsg() {
321 | if (mUploadMsg != null) {
322 | mUploadMsg.onReceiveValue(null);
323 | mUploadMsg = null;
324 |
325 | } else if (mUploadMsgForAndroid5 != null) {
326 | mUploadMsgForAndroid5.onReceiveValue(null);
327 | mUploadMsgForAndroid5 = null;
328 | }
329 | }
330 |
331 | @Override
332 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
333 | switch (requestCode) {
334 | case P_CODE_PERMISSIONS:
335 | requestResult(permissions, grantResults);
336 | // Don't restore upload message here anymore, as the flow might continue
337 | // restoreUploadMsg();
338 | break;
339 |
340 | default:
341 | super.onRequestPermissionsResult(requestCode, permissions, grantResults);
342 | }
343 | }
344 |
345 | private void requestPermissionsAndroidM() {
346 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
347 | List needPermissionList = new ArrayList<>();
348 | needPermissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
349 | needPermissionList.add(Manifest.permission.READ_EXTERNAL_STORAGE);
350 | needPermissionList.add(Manifest.permission.CAMERA);
351 |
352 | PermissionUtil.requestPermissions(MainActivity.this, P_CODE_PERMISSIONS, needPermissionList);
353 |
354 | } else {
355 | return;
356 | }
357 | }
358 |
359 | public void requestResult(String[] permissions, int[] grantResults) {
360 | ArrayList needPermissions = new ArrayList();
361 |
362 | for (int i = 0; i < grantResults.length; i++) {
363 | if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
364 | if (PermissionUtil.isOverMarshmallow()) {
365 |
366 | needPermissions.add(permissions[i]);
367 | }
368 | }
369 | }
370 |
371 | if (needPermissions.size() > 0) {
372 | StringBuilder permissionsMsg = new StringBuilder();
373 |
374 | for (int i = 0; i < needPermissions.size(); i++) {
375 | String strPermissons = needPermissions.get(i);
376 |
377 | if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(strPermissons)) {
378 | permissionsMsg.append("," + getString(R.string.permission_storage));
379 |
380 | } else if (Manifest.permission.READ_EXTERNAL_STORAGE.equals(strPermissons)) {
381 | permissionsMsg.append("," + getString(R.string.permission_storage));
382 |
383 | } else if (Manifest.permission.CAMERA.equals(strPermissons)) {
384 | permissionsMsg.append("," + getString(R.string.permission_camera));
385 |
386 | }
387 | }
388 |
389 | // Avoid NullPointerException if no R.string resources are found, by checking the length
390 | if (permissionsMsg.length() > 0) {
391 | String strMessage = "请允许使用\"" + permissionsMsg.substring(1).toString() + "\"权限, 以正常使用APP的所有功能.";
392 | Toast.makeText(MainActivity.this, strMessage, Toast.LENGTH_SHORT).show();
393 | }
394 |
395 | } else {
396 | return;
397 | }
398 | }
399 |
400 | }
401 |
--------------------------------------------------------------------------------