@@ -14,16 +14,14 @@ import android.os.{Build, Environment}
1414import android .provider .{DocumentsContract , MediaStore }
1515import androidx .documentfile .provider .DocumentFile
1616import com .btcontract .walletfiat .WalletApp
17- import scodec .bits .{BitVector , ByteVector }
17+ import com .btcontract .walletfiat .WalletApp .customBackupLocation
18+ import scodec .bits .ByteVector
1819import immortan .crypto .Tools
1920import immortan .wire .ExtCodecs
2021import scodec .Attempt .{Failure , Successful }
2122
2223import scala .util .Try
2324import java .io .{BufferedInputStream , File , FileInputStream }
24- import scala .collection .immutable .HashMap
25- import scala .collection .JavaConverters ._
26-
2725object LocalBackup { me =>
2826 final val BACKUP_NAME = " encrypted.channels"
2927 final val GRAPH_NAME = " graph.snapshot"
@@ -42,60 +40,137 @@ object LocalBackup { me =>
4240 }
4341
4442 final val LOCAL_BACKUP_REQUEST_NUMBER = 105
45- def askPermission (activity : AppCompatActivity ): Unit = ActivityCompat .requestPermissions(activity, Array (android.Manifest .permission.WRITE_EXTERNAL_STORAGE ), LOCAL_BACKUP_REQUEST_NUMBER )
46- def isAllowed (context : Context ): Boolean = ContextCompat .checkSelfPermission(context, android.Manifest .permission.WRITE_EXTERNAL_STORAGE ) == PackageManager .PERMISSION_GRANTED
43+ def askPermission (activity : AppCompatActivity ): Unit = ActivityCompat .requestPermissions(activity, Array (android.Manifest .permission.WRITE_EXTERNAL_STORAGE , android.Manifest .permission.READ_EXTERNAL_STORAGE ), LOCAL_BACKUP_REQUEST_NUMBER )
44+ def isAllowed (context : Context ): Boolean = {
45+ (ContextCompat .checkSelfPermission(context, android.Manifest .permission.WRITE_EXTERNAL_STORAGE ) == PackageManager .PERMISSION_GRANTED &&
46+ ContextCompat .checkSelfPermission(context, android.Manifest .permission.READ_EXTERNAL_STORAGE ) == PackageManager .PERMISSION_GRANTED ) || customBackupLocation.nonEmpty
47+ }
48+
4749 // Note that the function returns directory in the internal storage, then we copy backups to external dir
4850 def downloadsDir (context : Context ): File = context.getExternalFilesDir(DIRECTORY_DOWNLOADS )
4951
52+ // This is directory in external storage
5053 private val DOWNLOAD_DIR = Environment .getExternalStoragePublicDirectory(Environment .DIRECTORY_DOWNLOADS )
54+ private def downloadDirFileUri (context : Context , fileName : String ): Uri = {
55+ val file = new File (DOWNLOAD_DIR .getAbsolutePath + " /" + fileName)
56+ FileProvider .getUriForFile(context, s " ${context.getPackageName}" , file)
57+ }
5158
52- // val finalUri : Uri? = copyFileToDownloads(context, downloadedFile)
59+ // A way to get direct access to file inside download dir
60+ def findFileDirectlyInDownloads (context : Context , fileName : String ): Option [Uri ] = {
61+ val downloadDir = new File (DOWNLOAD_DIR .getAbsolutePath)
62+ val files = downloadDir.listFiles()
63+ val authority = s " ${context.getPackageName}"
64+ println(files.mkString(" \n " ))
65+ files.find(_.getName == fileName).map(FileProvider .getUriForFile(context, authority, _))
66+ }
5367
54- def copyFileToDirectory (context : Context , directory : Option [Uri ], downloadedFile : File ): Uri = {
55- val resolver = context.getContentResolver
56- val downloadedUri : Uri = directory match {
68+ // Helper to print exception to logs if any
69+ def printExceptions [T ](body : => T ): T = {
70+ val result = Try {body}
71+ result match {
72+ case util.Failure (exception) => {
73+ println(exception.getMessage)
74+ exception.printStackTrace()
75+ }
76+ case _ => ()
77+ }
78+ result.get
79+ }
80+
81+ // Depending on the Android version choose the best place to locate backup in the external storage.
82+ // Takes into account:
83+ // * directory - is custom directory selected by user to store backup at
84+ // * version - older versions will use direct File API, newer will prefer SAF if possible.
85+ // * existing of old backup - older one will be overwritten
86+ def selectDestinationUri (context : Context , resolver : ContentResolver , directory : Option [Uri ], downloadedFile : File ): Uri = {
87+ val fileName = downloadedFile.getName
88+ directory match {
5789 case None => {
5890 if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
59-
60- val contentValues = new ContentValues ()
61- contentValues.put(MediaStore .MediaColumns .IS_PENDING , true )
62- contentValues.put(MediaStore .MediaColumns .DISPLAY_NAME , downloadedFile.getName)
63- contentValues.put(MediaStore .MediaColumns .MIME_TYPE , resolver.getType(android.net.Uri .fromFile(downloadedFile)))
64- contentValues.put(MediaStore .MediaColumns .SIZE , String .valueOf(downloadedFile.length()))
65-
66- resolver.insert(MediaStore .Downloads .EXTERNAL_CONTENT_URI , contentValues)
91+ val existedFile = findFileDirectlyInDownloads(context, fileName)
92+ existedFile match {
93+ case Some (uri) => {
94+ println(" LocalBackup: found the backup file at " + uri.toString)
95+ val contentValues = new ContentValues ()
96+ contentValues.put(MediaStore .MediaColumns .IS_PENDING , true )
97+ contentValues.put(MediaStore .MediaColumns .SIZE , String .valueOf(downloadedFile.length()))
98+ contentValues.put(MediaStore .MediaColumns .MIME_TYPE , resolver.getType(android.net.Uri .fromFile(downloadedFile)))
99+ resolver.update(uri, contentValues, null , null )
100+ uri
101+ }
102+ case _ => {
103+ println(" LocalBackup: no backup file at downloads" )
104+ downloadDirFileUri(context, fileName)
105+ }
106+ }
67107 } else {
68- val authority = s " ${context.getPackageName}.provider "
69- val destinyFile = new File (directory.map(uri => new File (uri.getPath)).getOrElse(DOWNLOAD_DIR ), downloadedFile.getName )
108+ val authority = s " ${context.getPackageName}"
109+ val destinyFile = new File (directory.map(uri => new File (uri.getPath)).getOrElse(DOWNLOAD_DIR ), fileName )
70110 FileProvider .getUriForFile(context, authority, destinyFile)
71111 }
72112 }
73113 case Some (uri) => {
74- val triedUri = Try {
75- println(" Got custom directory " + uri)
76- val dirFile : DocumentFile = DocumentFile .fromTreeUri(context, uri)
77- println(" Can create files in directory: " ++ dirFile.canWrite.toString)
78- dirFile.listFiles.find(_.getName.equals(downloadedFile.getName)) match {
79- case Some (existingFile) =>
80- println(" We found file to rewrite: " ++ existingFile.getUri.toString)
81- existingFile.getUri
82-
83- case None =>
84- println(" Creating new file" )
85- DocumentsContract .createDocument(resolver, dirFile.getUri, " application/valet" , downloadedFile.getName)
86- }
114+ println(" LocalBackup: Got custom directory " + uri)
115+ val dirFile : DocumentFile = DocumentFile .fromTreeUri(context, uri)
116+ println(" LocalBackup: Can create files in directory: " ++ dirFile.canWrite.toString)
117+ dirFile.listFiles.find(_.getName.equals(fileName)) match {
118+ case Some (existingFile) =>
119+ println(" LocalBackup: We found file to rewrite: " ++ existingFile.getUri.toString)
120+ existingFile.getUri
121+
122+ case None =>
123+ println(" LocalBackup: Creating new file" )
124+ DocumentsContract .createDocument(resolver, dirFile.getUri, " application/valet" , fileName)
87125 }
88- triedUri match {
89- case util.Failure (exception) => {
90- println(exception.getMessage)
91- exception.printStackTrace()
126+ }
127+ }
128+ }
129+
130+ def copyFileToDirectory (context : Context , directory : Option [Uri ], downloadedFile : File ): Uri = printExceptions {
131+ val resolver = context.getContentResolver
132+ val fileName = downloadedFile.getName
133+ val downloadedUri : Uri = directory match {
134+ case None => {
135+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
136+ val existedFile = findFileDirectlyInDownloads(context, fileName)
137+ existedFile match {
138+ case Some (uri) => {
139+ println(" LocalBackup: found the backup file at " + uri.toString)
140+ val contentValues = new ContentValues ()
141+ contentValues.put(MediaStore .MediaColumns .IS_PENDING , true )
142+ contentValues.put(MediaStore .MediaColumns .SIZE , String .valueOf(downloadedFile.length()))
143+ contentValues.put(MediaStore .MediaColumns .MIME_TYPE , resolver.getType(android.net.Uri .fromFile(downloadedFile)))
144+ resolver.update(uri, contentValues, null , null )
145+ uri
146+ }
147+ case _ => {
148+ println(" LocalBackup: no backup file at downloads" )
149+ downloadDirFileUri(context, fileName)
150+ }
92151 }
93- case _ => ()
152+ } else {
153+ val authority = s " ${context.getPackageName}"
154+ val destinyFile = new File (directory.map(uri => new File (uri.getPath)).getOrElse(DOWNLOAD_DIR ), fileName)
155+ FileProvider .getUriForFile(context, authority, destinyFile)
156+ }
157+ }
158+ case Some (uri) => {
159+ println(" LocalBackup: Got custom directory " + uri)
160+ val dirFile : DocumentFile = DocumentFile .fromTreeUri(context, uri)
161+ println(" LocalBackup: Can create files in directory: " ++ dirFile.canWrite.toString)
162+ dirFile.listFiles.find(_.getName.equals(fileName)) match {
163+ case Some (existingFile) =>
164+ println(" LocalBackup: We found file to rewrite: " ++ existingFile.getUri.toString)
165+ existingFile.getUri
166+
167+ case None =>
168+ println(" LocalBackup: Creating new file" )
169+ DocumentsContract .createDocument(resolver, dirFile.getUri, " application/valet" , fileName)
94170 }
95- triedUri.get
96171 }
97172 }
98- println(" Will write backup to: " ++ downloadedUri.toString)
173+ println(" LocalBackup: Will write backup to: " ++ downloadedUri.toString)
99174
100175 val outputStream = resolver.openOutputStream(downloadedUri, " wt" )
101176 val brr = Array .ofDim[Byte ](1024 )
0 commit comments