2 * Copyright 2020-2022 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
28 import android.widget.LinearLayout
30 import com.google.android.material.snackbar.Snackbar
32 import com.stoutner.privacybrowser.R
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
39 import java.lang.Exception
41 // Declare the class constants.
42 private const val SUCCESS = "Success"
44 object SaveAboutVersionImageCoroutine {
45 fun saveImage(activity: Activity, fileUri: Uri, aboutVersionLinearLayout: LinearLayout) {
46 // Save the image using a coroutine.
47 CoroutineScope(Dispatchers.Main).launch {
48 // Create a saving image snackbar.
49 val savingImageSnackbar: Snackbar
51 // Process the image on the IO thread.
52 withContext(Dispatchers.IO) {
53 // Instantiate a file name string.
54 val fileNameString: String
56 // Query the exact file name if the API >= 26.
57 if (Build.VERSION.SDK_INT >= 26) { // The API >= 26.
58 // Get a cursor from the content resolver.
59 val contentResolverCursor = activity.contentResolver.query(fileUri, null, null, null)
61 // Get the file display name if the content resolve cursor is not null.
62 if (contentResolverCursor != null) { // The content resolve cursor is not null.
63 // Move to the first row.
64 contentResolverCursor.moveToFirst()
66 // Get the file name from the cursor.
67 fileNameString = contentResolverCursor.getString(contentResolverCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
70 contentResolverCursor.close()
71 } else { // The content resolve cursor is null.
72 // Use the URI last path segment as the file name string.
73 fileNameString = fileUri.lastPathSegment.toString()
75 } else { // The API is < 26.
76 // Use the URI last path segment as the file name string.
77 fileNameString = fileUri.lastPathSegment.toString()
80 // Use the main thread to display a snackbar.
81 withContext(Dispatchers.Main) {
82 // Create a saving image snackbar.
83 savingImageSnackbar = Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.processing_image, fileNameString), Snackbar.LENGTH_INDEFINITE)
85 // Display the saving image snackbar.
86 savingImageSnackbar.show()
89 // Create the about version bitmap. This can be replaced by PixelCopy once the minimum API >= 26.
90 // Once the Minimum API >= 26 Bitmap.Config.RBGA_F16 can be used instead of ARGB_8888. The linear layout commands must be run on the UI thread.
91 val aboutVersionBitmap = Bitmap.createBitmap(aboutVersionLinearLayout.width, aboutVersionLinearLayout.height, Bitmap.Config.ARGB_8888)
94 val aboutVersionCanvas = Canvas(aboutVersionBitmap)
96 // Use the main thread to interact with the linear layout.
97 withContext(Dispatchers.Main) {
98 // Draw the current about version onto the bitmap.
99 aboutVersionLinearLayout.draw(aboutVersionCanvas)
102 // Create an about version PNG byte array output stream.
103 val aboutVersionByteArrayOutputStream = ByteArrayOutputStream()
105 // Convert the bitmap to a PNG. `0` is for lossless compression (the only option for a PNG). This compression takes a long time.
106 // Once the minimum API >= 30 this could be replaced with WEBP_LOSSLESS.
107 aboutVersionBitmap.compress(Bitmap.CompressFormat.PNG, 0, aboutVersionByteArrayOutputStream)
109 // Create a file creation disposition string.
110 var fileCreationDisposition = SUCCESS
112 // Write the image inside a try block to capture any write exceptions.
114 // Open an output stream.
115 val outputStream = activity.contentResolver.openOutputStream(fileUri)!!
117 // Write the webpage image to the image file.
118 aboutVersionByteArrayOutputStream.writeTo(outputStream)
120 // Close the output stream.
122 } catch (exception: Exception) {
123 // Store the error in the file creation disposition string.
124 fileCreationDisposition = exception.toString()
127 // Use the main thread to update the snackbars.
128 withContext(Dispatchers.Main) {
129 // Dismiss the saving image snackbar.
130 savingImageSnackbar.dismiss()
132 // Display a file creation disposition snackbar.
133 if (fileCreationDisposition == SUCCESS) {
134 // Create a file saved snackbar.
135 Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.saved, fileNameString), Snackbar.LENGTH_SHORT).show()
137 Snackbar.make(aboutVersionLinearLayout, activity.getString(R.string.error_saving_file, fileNameString, fileCreationDisposition), Snackbar.LENGTH_INDEFINITE).show()