2 * Copyright0 2019-2024 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.provider.OpenableColumns
28 import com.google.android.material.snackbar.Snackbar
30 import com.stoutner.privacybrowser.R
31 import com.stoutner.privacybrowser.views.NestedScrollWebView
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.Dispatchers
35 import kotlinx.coroutines.launch
36 import kotlinx.coroutines.withContext
38 import java.io.ByteArrayOutputStream
40 class SaveWebpageImageCoroutine {
41 fun save(activity: Activity, fileUri: Uri, nestedScrollWebView: NestedScrollWebView) {
42 // Use a coroutine to save the webpage image.
43 CoroutineScope(Dispatchers.Main).launch {
44 // Get a cursor from the content resolver.
45 val contentResolverCursor = activity.contentResolver.query(fileUri, null, null, null)
47 // Move to the first row.
48 contentResolverCursor!!.moveToFirst()
50 // Get the file name from the cursor.
51 val fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
54 contentResolverCursor.close()
56 // Create a saving image snackbar.
57 val savingImageSnackbar = Snackbar.make(nestedScrollWebView, activity.getString(R.string.processing_image, fileNameString), Snackbar.LENGTH_INDEFINITE)
59 // Display the saving image snackbar.
60 savingImageSnackbar.show()
62 // Create a webpage bitmap. Once the Minimum API >= 33 Bitmap.Config.RGBA_1010102 can be used instead of ARGB_8888.
63 // RGBA_F16 can't be used because it produces black output for the part of page not currently visible on the screen.
64 val webpageBitmap = Bitmap.createBitmap(nestedScrollWebView.getHorizontalScrollRange(), nestedScrollWebView.getVerticalScrollRange(), Bitmap.Config.ARGB_8888)
67 val webpageCanvas = Canvas(webpageBitmap)
69 // Draw the current webpage onto the bitmap. The nested scroll WebView commands must be run on the UI thread.
70 nestedScrollWebView.draw(webpageCanvas)
72 // Compress the image on the IO thread.
73 withContext(Dispatchers.IO) {
74 // Create a webpage PNG byte array output stream.
75 val webpageByteArrayOutputStream = ByteArrayOutputStream()
77 // Convert the bitmap to a PNG. `0` is for lossless compression (the only option for a PNG). This compression takes a long time.
78 // Once the minimum API >= 30 this could be replaced with WEBP_LOSSLESS.
79 webpageBitmap.compress(Bitmap.CompressFormat.PNG, 0, webpageByteArrayOutputStream)
82 // Create an image file output stream.
83 val imageFileOutputStream = activity.contentResolver.openOutputStream(fileUri)!!
85 // Write the webpage image to the image file.
86 webpageByteArrayOutputStream.writeTo(imageFileOutputStream)
88 // Close the output stream.
89 imageFileOutputStream.close()
92 withContext(Dispatchers.Main) {
93 // Dismiss the saving image snackbar.
94 savingImageSnackbar.dismiss()
96 // Display the image saved snackbar.
97 Snackbar.make(nestedScrollWebView, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
99 } catch (exception: Exception) {
101 withContext(Dispatchers.Main) {
102 // Dismiss the saving image snackbar.
103 savingImageSnackbar.dismiss()
105 // Display the file saving error.
106 Snackbar.make(nestedScrollWebView, activity.getString(R.string.error_saving_file, fileNameString, exception), Snackbar.LENGTH_INDEFINITE).show()