2 * Copyright 2020-2022, 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
27 import android.widget.LinearLayout
29 import com.google.android.material.snackbar.Snackbar
31 import com.stoutner.privacybrowser.R
32 import kotlinx.coroutines.CoroutineScope
33 import kotlinx.coroutines.Dispatchers
34 import kotlinx.coroutines.launch
35 import kotlinx.coroutines.withContext
37 import java.io.ByteArrayOutputStream
38 import java.lang.Exception
40 // Declare the class constants.
41 private const val SUCCESS = "Success"
43 object SaveAboutVersionImageCoroutine {
44 fun saveImage(activity: Activity, fileUri: Uri, aboutVersionLinearLayout: LinearLayout) {
45 // Save the image using a coroutine.
46 CoroutineScope(Dispatchers.Main).launch {
47 // Create a saving image snackbar.
48 val savingImageSnackbar: Snackbar
50 // Process the image on the IO thread.
51 withContext(Dispatchers.IO) {
52 // Instantiate a file name string.
53 val fileNameString: String
55 // Get a cursor from the content resolver.
56 val contentResolverCursor = activity.contentResolver.query(fileUri, null, null, null)
58 // Get the file display name if the content resolve cursor is not null.
59 if (contentResolverCursor != null) { // The content resolve cursor is not null.
60 // Move to the first row.
61 contentResolverCursor.moveToFirst()
63 // Get the file name from the cursor.
64 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
67 contentResolverCursor.close()
68 } else { // The content resolve cursor is null.
69 // Use the URI last path segment as the file name string.
70 fileNameString = fileUri.lastPathSegment.toString()
73 // Use the main thread to display a snackbar.
74 withContext(Dispatchers.Main) {
75 // Create a saving image snackbar.
76 savingImageSnackbar = Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.processing_image, fileNameString), Snackbar.LENGTH_INDEFINITE)
78 // Display the saving image snackbar.
79 savingImageSnackbar.show()
82 // Create an empty bitmap with the dimensions of the linear layout. Once the minimum API >= 33 Bitmap.Config.RGBA_1010102 can be used instead of RBGA_F16.
83 val aboutVersionBitmap = Bitmap.createBitmap(aboutVersionLinearLayout.width, aboutVersionLinearLayout.height, Bitmap.Config.RGBA_F16)
86 val aboutVersionCanvas = Canvas(aboutVersionBitmap)
88 // Use the main thread to interact with the linear layout.
89 withContext(Dispatchers.Main) {
90 // Draw the current about version onto the bitmap. It might be possible to do this with PixelCopy, but I am not sure that would be any better.
91 aboutVersionLinearLayout.draw(aboutVersionCanvas)
94 // Create an about version PNG byte array output stream.
95 val aboutVersionByteArrayOutputStream = ByteArrayOutputStream()
97 // Convert the bitmap to a PNG. `0` is for lossless compression (the only option for a PNG). This compression takes a long time.
98 // Once the minimum API >= 30 this can be replaced with WEBP_LOSSLESS.
99 aboutVersionBitmap.compress(Bitmap.CompressFormat.PNG, 0, aboutVersionByteArrayOutputStream)
101 // Create a file creation disposition string.
102 var fileCreationDisposition = SUCCESS
104 // Write the image inside a try block to capture any write exceptions.
106 // Open an output stream.
107 val outputStream = activity.contentResolver.openOutputStream(fileUri)!!
109 // Write the webpage image to the image file.
110 aboutVersionByteArrayOutputStream.writeTo(outputStream)
112 // Close the output stream.
114 } catch (exception: Exception) {
115 // Store the error in the file creation disposition string.
116 fileCreationDisposition = exception.toString()
119 // Use the main thread to update the snackbars.
120 withContext(Dispatchers.Main) {
121 // Dismiss the saving image snackbar.
122 savingImageSnackbar.dismiss()
124 // Display a file creation disposition snackbar.
125 if (fileCreationDisposition == SUCCESS) {
126 // Create a file saved snackbar.
127 Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
129 Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.error_saving_file, fileNameString, fileCreationDisposition), Snackbar.LENGTH_INDEFINITE).show()