2 * Copyright0 2019-2023 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
6 * Privacy Browser Android is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser Android is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser Android. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.coroutines
22 import android.app.Activity
23 import android.graphics.Bitmap
24 import android.graphics.Canvas
25 import android.net.Uri
26 import android.os.Build
27 import android.provider.OpenableColumns
29 import com.google.android.material.snackbar.Snackbar
31 import com.stoutner.privacybrowser.R
32 import com.stoutner.privacybrowser.views.NestedScrollWebView
34 import kotlinx.coroutines.CoroutineScope
35 import kotlinx.coroutines.Dispatchers
36 import kotlinx.coroutines.launch
37 import kotlinx.coroutines.withContext
39 import java.io.ByteArrayOutputStream
41 class SaveWebpageImageCoroutine {
42 fun save(activity: Activity, fileUri: Uri, nestedScrollWebView: NestedScrollWebView) {
43 // Use a coroutine to save the webpage image.
44 CoroutineScope(Dispatchers.Main).launch {
45 // Create a file name string.
46 val fileNameString: String
48 // Query the exact file name if the API >= 26.
49 if (Build.VERSION.SDK_INT >= 26) {
50 // Get a cursor from the content resolver.
51 val contentResolverCursor = activity.contentResolver.query(fileUri, null, null, null)
53 // Move to the first row.
54 contentResolverCursor!!.moveToFirst()
56 // Get the file name from the cursor.
57 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
60 contentResolverCursor.close()
62 // Use the file URI last path segment as the file name string.
63 fileNameString = fileUri.lastPathSegment!!
66 // Create a saving image snackbar.
67 val savingImageSnackbar = Snackbar.make(nestedScrollWebView, activity.getString(R.string.processing_image, fileNameString), Snackbar.LENGTH_INDEFINITE)
69 // Display the saving image snackbar.
70 savingImageSnackbar.show()
72 // Create a webpage bitmap. Once the Minimum API >= 26 Bitmap.Config.RBGA_F16 can be used instead of ARGB_8888. The nested scroll WebView commands must be run on the UI thread.
73 val webpageBitmap = Bitmap.createBitmap(nestedScrollWebView.getHorizontalScrollRange(), nestedScrollWebView.getVerticalScrollRange(), Bitmap.Config.ARGB_8888)
76 val webpageCanvas = Canvas(webpageBitmap)
78 // Draw the current webpage onto the bitmap. The nested scroll WebView commands must be run on the UI thread.
79 nestedScrollWebView.draw(webpageCanvas)
81 // Compress the image on the IO thread.
82 withContext(Dispatchers.IO) {
83 // Create a webpage PNG byte array output stream.
84 val webpageByteArrayOutputStream = ByteArrayOutputStream()
86 // Convert the bitmap to a PNG. `0` is for lossless compression (the only option for a PNG). This compression takes a long time.
87 // Once the minimum API >= 30 this could be replaced with WEBP_LOSSLESS.
88 webpageBitmap.compress(Bitmap.CompressFormat.PNG, 0, webpageByteArrayOutputStream)
91 // Create an image file output stream.
92 val imageFileOutputStream = activity.contentResolver.openOutputStream(fileUri)!!
94 // Write the webpage image to the image file.
95 webpageByteArrayOutputStream.writeTo(imageFileOutputStream)
97 // Close the output stream.
98 imageFileOutputStream.close()
101 withContext(Dispatchers.Main) {
102 // Dismiss the saving image snackbar.
103 savingImageSnackbar.dismiss()
105 // Display the image saved snackbar.
106 Snackbar.make(nestedScrollWebView, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
108 } catch (exception: Exception) {
110 withContext(Dispatchers.Main) {
111 // Dismiss the saving image snackbar.
112 savingImageSnackbar.dismiss()
114 // Display the file saving error.
115 Snackbar.make(nestedScrollWebView, activity.getString(R.string.error_saving_file, fileNameString, exception), Snackbar.LENGTH_INDEFINITE).show()