diff --git a/core/runtime/android/widgets/src/main/java/org/hapjs/widgets/view/NestedWebView.java b/core/runtime/android/widgets/src/main/java/org/hapjs/widgets/view/NestedWebView.java index 077cb2b2..9831dd08 100644 --- a/core/runtime/android/widgets/src/main/java/org/hapjs/widgets/view/NestedWebView.java +++ b/core/runtime/android/widgets/src/main/java/org/hapjs/widgets/view/NestedWebView.java @@ -11,6 +11,8 @@ import android.app.DownloadManager; import android.content.ActivityNotFoundException; import android.content.ClipData; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -58,6 +60,7 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.ArraySet; import androidx.core.app.ActivityCompat; @@ -87,11 +90,14 @@ import org.hapjs.common.utils.UriUtils; import org.hapjs.common.utils.WebViewUtils; import org.hapjs.component.Component; +import org.hapjs.common.executors.Executors; +import org.hapjs.common.net.HttpConfig; import org.hapjs.component.bridge.RenderEventCallback; import org.hapjs.component.view.ComponentHost; import org.hapjs.component.view.NestedScrollingListener; import org.hapjs.component.view.NestedScrollingView; import org.hapjs.component.view.gesture.GestureHost; +import org.hapjs.common.utils.ThemeUtils; import org.hapjs.component.view.gesture.IGesture; import org.hapjs.component.view.keyevent.KeyEventDelegate; import org.hapjs.component.view.webview.BaseWebViewClient; @@ -110,10 +116,12 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; - +import org.hapjs.runtime.HybridDialog; +import org.hapjs.runtime.HybridDialogProvider; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URL; @@ -124,6 +132,10 @@ import static org.hapjs.logging.RuntimeLogManager.VALUE_ROUTER_APP_FROM_WEB; +import okhttp3.Call; +import okhttp3.Request; +import okhttp3.ResponseBody; + public class NestedWebView extends WebView implements ComponentHost, NestedScrollingView, GestureHost { protected static final String TAG = "NestedWebView"; @@ -195,6 +207,13 @@ public class NestedWebView extends WebView private static final String JSSDK_URL = "https://quickapp/js/jssdk.hapwebview.min.js"; private static final String JSSDK_LOCAL_PATH = "jsscript/hapjssdk.min.js"; private static final String ERROR_MSG_URL_UNTRUSTED = "url untrusted"; + private static final String DIVIDER = "/"; + private static final String IMAGE = "image" + DIVIDER; + private static final String DEFAULT_FORMAT = "png"; + private static final String HYBRID = DIVIDER + "hybrid"; + private static final String CONTENT_TYPE = "Content-Type"; + private final String[] SUPPORTED_EXTENSIONS = {"apng", "gif", "jpg", "jpeg", "png", "svg", "webp"}; + private WebviewSettingProvider mWebViewSettingProvider; public NestedWebView(Context context) { @@ -397,6 +416,26 @@ protected void initWebView() { mSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); } + setOnLongClickListener(v -> { + WebView.HitTestResult result = getHitTestResult(); + if (result.getType() == WebView.HitTestResult.IMAGE_TYPE + || result.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { + + HybridDialogProvider provider = ProviderManager.getDefault().getProvider(HybridDialogProvider.NAME); + HybridDialog dialog = provider.createAlertDialog(mContext, ThemeUtils.getAlertDialogTheme()); + String title = mContext.getString(R.string.webview_save_pictures); + dialog.setTitle(title); + dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), (dialogCancelInterface, which) -> dialogCancelInterface.dismiss()); + dialog.setButton(DialogInterface.BUTTON_POSITIVE, mContext.getString(android.R.string.ok), (dialogOkInterface, which) -> { + downloadImage(result.getExtra()); + dialogOkInterface.dismiss(); + }); + dialog.show(); + return true; + } + return false; + }); + setWebViewClient( new BaseWebViewClient(BaseWebViewClient.WebSourceType.WEB) { @@ -876,7 +915,7 @@ && isSupportWebRTC()) { String host = request.getOrigin().getHost(); if (webRtcPermissions.contains(Manifest.permission.CAMERA) && webRtcPermissions.contains(Manifest.permission.RECORD_AUDIO)) { - warnMessage = getResources().getString(R.string.webrtc_warn_permission_camera_and_microphone, + warnMessage = getResources().getString(R.string.webrtc_warn_permission_camera_and_microphone, host); } else if (webRtcPermissions.contains(Manifest.permission.CAMERA)) { warnMessage = getResources().getString(R.string.webrtc_warn_permission_camera, @@ -989,13 +1028,13 @@ public void onClick(DialogInterface dialog, int which) { nameET.getText().toString().trim(); if (TextUtils.isEmpty(downloadFileName)) { Toast.makeText( - mContext, R.string.web_download_invalid_url, - Toast.LENGTH_SHORT) + mContext, R.string.web_download_invalid_url, + Toast.LENGTH_SHORT) .show(); } else if (!checkUrl(url)) { Toast.makeText( - mContext, R.string.web_download_no_file_name, - Toast.LENGTH_SHORT) + mContext, R.string.web_download_no_file_name, + Toast.LENGTH_SHORT) .show(); } else { download(url, userAgent, contentDisposition, mimetype, @@ -1577,10 +1616,10 @@ public void onRequestPermissionsResult( downloadManager.enqueue(request); } else { Toast.makeText( - act, - getResources() - .getString(R.string.web_download_no_permission), - Toast.LENGTH_SHORT) + act, + getResources() + .getString(R.string.web_download_no_permission), + Toast.LENGTH_SHORT) .show(); } hybridManager.removeLifecycleListener(this); @@ -2384,4 +2423,82 @@ public void show() { super.show(); } } + + private void downloadImage(String imageUrl) { + Request request = new Request.Builder().url(imageUrl).build(); + HttpConfig.get().getOkHttpClient().newCall(request).enqueue(new okhttp3.Callback() { + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + Log.e(TAG, "Image download failed.", e); + Executors.ui().execute(() -> Toast.makeText(mContext, getResources().getString(R.string.webview_save_failed), Toast.LENGTH_SHORT).show()); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull okhttp3.Response response) throws IOException { + if (response.isSuccessful()) { + ResponseBody body = response.body(); + if (body != null) { + if (saveImageToAlbum(body.bytes(), getExtension(response))) { + Executors.ui().execute(() -> Toast.makeText(mContext, getResources().getString(R.string.webview_save_successfully), Toast.LENGTH_SHORT).show()); + return; + } + } + } + Executors.ui().execute(() -> Toast.makeText(mContext, getResources().getString(R.string.webview_save_failed), Toast.LENGTH_SHORT).show()); + } + }); + } + + private String getExtension(okhttp3.Response response) { + String contentType = response.header(CONTENT_TYPE); + String extension = DEFAULT_FORMAT; + if (contentType != null && contentType.startsWith(IMAGE)) { + extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(contentType); + } else { + String imageUrl = response.request().url().url().toString(); + int lastDotIndex = imageUrl.lastIndexOf("."); + if (lastDotIndex != -1 && lastDotIndex < imageUrl.length() - 1) { + extension = imageUrl.substring(lastDotIndex + 1); + } + boolean isSupportExtension = false; + for (String type : SUPPORTED_EXTENSIONS) { + if (extension.equalsIgnoreCase(type)) { + isSupportExtension = true; + break; + } + } + extension = isSupportExtension ? extension : DEFAULT_FORMAT; + } + return extension; + } + + private boolean saveImageToAlbum(byte[] imageBytes, String extension) { + String fileName = System.currentTimeMillis() + "." + extension; + boolean isSaveSucceed = false; + try { + ContentValues values = new ContentValues(); + values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName); + values.put(MediaStore.Images.Media.MIME_TYPE, IMAGE + extension); + values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + HYBRID); + ContentResolver resolver = mContext.getContentResolver(); + Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + if (uri != null) { + OutputStream out = null; + try { + out = resolver.openOutputStream(uri); + if (out != null) { + out.write(imageBytes); + isSaveSucceed = true; + } + } catch (IOException e) { + Log.e(TAG, "Image Save failed.", e); + } finally { + FileUtils.closeQuietly(out); + } + } + } catch (Exception e) { + Log.e(TAG, "Image Save failed.", e); + } + return isSaveSucceed; + } } \ No newline at end of file diff --git a/core/runtime/android/widgets/src/main/res/values-zh-rCN/strings.xml b/core/runtime/android/widgets/src/main/res/values-zh-rCN/strings.xml index 9d0dc196..2ed2694d 100644 --- a/core/runtime/android/widgets/src/main/res/values-zh-rCN/strings.xml +++ b/core/runtime/android/widgets/src/main/res/values-zh-rCN/strings.xml @@ -63,4 +63,8 @@ 暂停 全屏 退出全屏 + + 保存成功 + 保存失败 + 保存图片 diff --git a/core/runtime/android/widgets/src/main/res/values/strings.xml b/core/runtime/android/widgets/src/main/res/values/strings.xml index 2e5b453c..4e8cd322 100644 --- a/core/runtime/android/widgets/src/main/res/values/strings.xml +++ b/core/runtime/android/widgets/src/main/res/values/strings.xml @@ -64,4 +64,8 @@ pause full screen exit full screen + + Save successfully + Save failed + Save Pictures