@ -1,14 +1,16 @@
package awais.instagrabber.utils
import android.content.Context
import android.content.DialogInterface
import android.content.UriPermission
import android.net.Uri
import android.provider.DocumentsContract
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.util.Pair
import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.ContextThemeWrapper
import androidx.appcompat.widget.PopupMenu
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.documentfile.provider.DocumentFile
import androidx.work.*
import awais.instagrabber.R
@ -16,6 +18,7 @@ import awais.instagrabber.fragments.settings.PreferenceKeys
import awais.instagrabber.models.enums.MediaItemType
import awais.instagrabber.repositories.responses.Media
import awais.instagrabber.repositories.responses.stories.StoryMedia
import awais.instagrabber.utils.AppExecutors.tasksThread
import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.workers.DownloadWorker
import com.google.gson.Gson
@ -24,6 +27,7 @@ import java.io.IOException
import java.io.OutputStreamWriter
import java.util.*
import java.util.regex.Pattern
import kotlin.math.abs
object DownloadUtils {
@ -36,6 +40,8 @@ object DownloadUtils {
private const val DIR_RECORDINGS = " Sent Recordings "
private const val DIR_TEMP = " Temp "
private const val DIR_BACKUPS = " Backups "
private const val MIME_DIR = DocumentsContract . Document . MIME_TYPE_DIR
private val dirMap : MutableMap < String , DocumentFile ? > = mutableMapOf ( )
private var root : DocumentFile ? = null
@JvmStatic
@Throws ( ReselectDocumentTreeException :: class )
@ -48,18 +54,15 @@ object DownloadUtils {
}
val uri = Uri . parse ( barinstaDirUri )
if ( ! barinstaDirUri !! . startsWith ( " content://com.android.externalstorage.documents " ) ) {
// reselect the folder in selector view
throw ReselectDocumentTreeException ( uri )
}
val existingPermissions = context . contentResolver . persistedUriPermissions
if ( existingPermissions . isEmpty ( ) ) {
// reselect the folder in selector view
throw ReselectDocumentTreeException ( uri )
}
val anyMatch = existingPermissions . stream ( )
. anyMatch { uriPermission : UriPermission -> uriPermission . uri == uri }
if ( ! anyMatch ) {
// reselect the folder in selector view
throw ReselectDocumentTreeException ( uri )
}
root = DocumentFile . fromTreeUri ( context , uri )
@ -68,155 +71,152 @@ object DownloadUtils {
throw ReselectDocumentTreeException ( uri )
}
Utils . settingsHelper . putString ( PreferenceKeys . PREF_BARINSTA_DIR_URI , uri . toString ( ) )
// set up directories
val dirKeys = mapOf (
DIR_DOWNLOADS to MIME_DIR ,
DIR_CAMERA to MIME_DIR ,
DIR_EDIT to MIME_DIR ,
DIR_RECORDINGS to MIME_DIR ,
DIR_TEMP to MIME_DIR ,
DIR_BACKUPS to MIME_DIR
)
dirMap . putAll ( checkFiles ( context , root , dirKeys , true ) )
}
fun destroy ( ) {
root = null
dirMap . clear ( )
}
fun checkFiles ( context : Context ,
parent : DocumentFile ? ,
queries : Map < String , String > , // <file name, mime type>
create : Boolean
) : Map < String , DocumentFile ? > {
// first we'll find existing ones
val result : MutableMap < String , DocumentFile ? > = mutableMapOf ( )
if ( root == null || parent == null || ! parent . isDirectory ) return result . toMap ( )
val docId = DocumentsContract . getDocumentId ( parent . uri )
val docUri = DocumentsContract . buildChildDocumentsUriUsingTree ( root !! . uri , docId )
val docCursor = context . contentResolver . query (
docUri , arrayOf (
DocumentsContract . Document . COLUMN_DISPLAY_NAME ,
DocumentsContract . Document . COLUMN_DOCUMENT_ID ,
DocumentsContract . Document . COLUMN_MIME_TYPE
) , null , null , null
)
if ( docCursor == null ) return result . toMap ( )
while ( docCursor . moveToNext ( ) ) {
val q = queries . get ( docCursor . getString ( 0 ) )
if ( q == null || ! docCursor . getString ( 2 ) . equals ( q ) ) continue
val fileUri = DocumentsContract . buildDocumentUriUsingTree ( parent . uri , docCursor . getString ( 1 ) )
val dir = if ( q . equals ( MIME_DIR ) ) DocumentFile . fromTreeUri ( context , fileUri )
else DocumentFile . fromSingleUri ( context , fileUri )
result . put ( docCursor . getString ( 0 ) , dir )
if ( result . size >= queries . size ) break
}
fun getDownloadDir ( vararg dirs : String ? ) : DocumentFile ? {
if ( root == null ) {
return null
docCursor . close ( )
// next we'll create inexistent ones, if necessary
if ( create ) {
for ( k in queries ) {
if ( result . get ( k . key ) == null ) {
result . put ( k . key , if ( MIME_DIR . equals ( k . value ) ) parent . createDirectory ( k . key )
else parent . createFile ( k . value , k . key ) )
}
var subDir = root
for ( dir in dirs ) {
if ( subDir == null || isEmpty ( dir ) ) continue
val subDirFile = subDir . findFile ( dir !! )
val exists = subDirFile != null && subDirFile . exists ( )
subDir = if ( exists ) subDirFile else subDir . createDirectory ( dir )
}
return subDir
}
return result . toMap ( )
}
fun getRootDir ( dir : String ) : DocumentFile ? {
if ( root == null ) return null
return dirMap . get ( dir )
}
@JvmStatic
val downloadDir : DocumentFile ?
get ( ) = getDownloadDir ( DIR_DOWNLOADS )
get ( ) = getRoot Dir ( DIR_DOWNLOADS )
@JvmStatic
fun getCameraDir ( ) : DocumentFile ? {
return getDownloadDir ( DIR_CAMERA )
}
val cameraDir : DocumentFile ?
get ( ) = getRootDir ( DIR_CAMERA )
@JvmStatic
fun getImageEditDir ( sessionId : String ? ) : DocumentFile ? {
return getDownloadDir ( DIR_EDIT , sessionId )
fun getImageEditDir ( sessionId : String ? , context : Context ) : DocumentFile ? {
val editRoot = getRootDir ( DIR_EDIT )
if ( sessionId == null ) return editRoot
return checkFiles ( context ,
editRoot ,
mapOf ( sessionId to MIME_DIR ) ,
true ) . get ( sessionId )
}
fun getRecordingsDir ( ) : DocumentFile ? {
return getDownloadDir ( DIR_RECORDINGS )
}
@JvmStatic
val recordingsDir : DocumentFile ?
get ( ) = getRootDir ( DIR_RECORDINGS )
@JvmStatic
fun getBackupsDir ( ) : DocumentFile ? {
return getDownloadDir ( DIR_BACKUPS )
}
val backupsDir : DocumentFile ?
get ( ) = getRootDir ( DIR_BACKUPS )
// @Nullable
// private static DocumentFile getDownloadDir(@NonNull final Context context, @Nullable final String username) {
// return getDownloadDir(context, username, false);
// }
private fun getDownloadDir (
context : Context ? ,
username : String ?
context : Context ,
username : String ? ,
shouldCreate : Boolean
) : DocumentFile ? {
val userFolderPaths : List < String > = getSubPathForUserFolder ( username )
var dir = root
for ( dirName in userFolderPaths ) {
val file = dir !! . findFile ( dirName )
if ( file != null ) {
dir = file
continue
}
dir = dir . createDirectory ( dirName )
if ( dir == null ) break
}
// final String joined = android.text.TextUtils.join("/", userFolderPaths);
// final Uri userFolderUri = DocumentsContract.buildDocumentUriUsingTree(root.getUri(), joined);
// final DocumentFile userFolder = DocumentFile.fromSingleUri(context, userFolderUri);
if ( context != null && ( dir == null || ! dir . exists ( ) ) ) {
Toast . makeText ( context , R . string . error_creating_folders , Toast . LENGTH_SHORT ) . show ( )
return null
}
return dir
if ( ! Utils . settingsHelper . getBoolean ( PreferenceKeys . DOWNLOAD_USER_FOLDER ) || username . isNullOrEmpty ( ) )
return downloadDir
return checkFiles ( context ,
downloadDir ,
mapOf ( username to MIME_DIR ) ,
shouldCreate ) . get ( username )
}
private fun getSubPathForUserFolder ( username : String ? ) : MutableList < String > {
val list : MutableList < String > = ArrayList ( )
if ( ! Utils . settingsHelper . getBoolean ( PreferenceKeys . DOWNLOAD_USER_FOLDER ) ||
username . isNullOrEmpty ( ) ) {
list . add ( DIR_DOWNLOADS )
return list
}
val finalUsername = if ( username . startsWith ( " @ " ) ) username . substring ( 1 ) else username
list . add ( DIR_DOWNLOADS )
list . add ( finalUsername )
return list
}
private fun getTempDir ( ) : DocumentFile ? {
var file = root !! . findFile ( DIR_TEMP )
if ( file == null ) {
file = root !! . createDirectory ( DIR_TEMP )
}
return file
}
private val tempDir : DocumentFile ?
get ( ) = getRootDir ( DIR_TEMP )
private fun getDownloadSavePaths (
paths : MutableList < String > ,
postId : String ? ,
displayUrl : String ?
) : Pair < List < String > , String ? > ? {
return getDownloadSavePaths ( paths , postId , " " , displayUrl , " " )
) : Pair < String , String > {
return getDownloadFileName ( postId , " " , displayUrl , " " )
}
private fun getDownloadSavePaths (
paths : MutableList < String > ,
postId : String ? ,
displayUrl : String ,
username : String
) : Pair < List < String > , String ? > ? {
return getDownloadSavePaths ( paths , postId , " " , displayUrl , username )
) : Pair < String , String > {
return getDownloadFileName ( postId , " " , displayUrl , username )
}
private fun getDownloadChildSavePaths (
paths : MutableList < String > ,
postId : String ? ,
childPosition : Int ,
url : String ? ,
username : String
) : Pair < List < String > , String ? > ? {
) : Pair < String , String > {
val sliderPostfix = " _slide_ $childPosition "
return getDownloadSavePaths ( paths , postId , sliderPostfix , url , username )
return getDownloadFileName ( postId , sliderPostfix , url , username )
}
private fun getDownloadSavePaths (
paths : MutableList < String > ? ,
private fun getDownloadFileName (
postId : String ? ,
sliderPostfix : String ,
displayUrl : String ? ,
username : String
) : Pair < List < String > , String ? > ? {
if ( paths == null ) return null
) : Pair < String , String > {
val extension = getFileExtensionFromUrl ( displayUrl )
val usernamePrepend = if ( isEmpty ( username ) ) " " else username + " _ "
val fileName = usernamePrepend + postId + sliderPostfix + extension
// return new File(finalDir, fileName);
// DocumentFile file = finalDir.findFile(fileName);
// if (file == null) {
val mimeType = Utils . mimeTypeMap . getMimeTypeFromExtension (
if ( extension . startsWith ( " . " ) ) extension . substring ( 1 ) else extension
)
// file = finalDir.createFile(mimeType, fileName);
// }
paths . add ( fileName )
return Pair ( paths , mimeType )
return Pair ( fileName , mimeType !! )
}
// public static DocumentFile getTempFile() {
// return getTempFile(null, null);
// }
fun getTempFile ( fileName : String ? , extension : String ) : DocumentFile ? {
val dir = ge tT empDir( )
val dir = tempDir
var name = fileName
if ( isEmpty ( name ) ) {
name = UUID . randomUUID ( ) . toString ( )
@ -284,26 +284,23 @@ object DownloadUtils {
if ( user != null ) {
username = user . username
}
val userFolderPaths : List < String > = getSubPathForUserFolder ( username )
val userFolder = getDownloadDir ( context , username , false )
if ( userFolder == null ) return checkList
when ( media . type ) {
MediaItemType . MEDIA_TYPE_IMAGE , MediaItemType . MEDIA_TYPE_VIDEO -> {
val url =
if ( media . type == MediaItemType . MEDIA_TYPE_VIDEO ) ResponseBodyUtils . getVideoUrl (
media
) else ResponseBodyUtils . getImageUrl ( media )
val file = getDownloadSavePaths ( ArrayList ( userFolderPaths ) , media . code , url , " " )
val fileExists = file !! . first != null && checkPathExists ( file . first , context )
var usernameFileExists = false
if ( ! fileExists ) {
val usernameFile = getDownloadSavePaths (
ArrayList ( userFolderPaths ) , media . code , url , username
)
usernameFileExists = usernameFile !! . first != null && checkPathExists ( usernameFile . first , context )
}
checkList . add ( fileExists || usernameFileExists )
val fileName = getDownloadSavePaths ( media . code , url )
val fileNameWithUser = getDownloadSavePaths ( media . code , url , username )
val files = checkFiles ( context , userFolder , mapOf ( fileName , fileNameWithUser ) , false )
checkList . add ( files . size > 0 )
}
MediaItemType . MEDIA_TYPE_SLIDER -> {
val sliderItems = media . carouselMedia
val fileNames : MutableMap < String , String > = mutableMapOf ( )
val filePairs : MutableMap < String , String > = mutableMapOf ( )
var i = 0
while ( i < sliderItems !! . size ) {
val child = sliderItems [ i ]
@ -311,20 +308,17 @@ object DownloadUtils {
if ( child . type == MediaItemType . MEDIA_TYPE_VIDEO ) ResponseBodyUtils . getVideoUrl (
child
) else ResponseBodyUtils . getImageUrl ( child )
val file = getDownloadChildSavePaths (
ArrayList ( userFolderPaths ) , media . code , i + 1 , url , " "
)
val fileExists = file !! . first != null && checkPathExists ( file . first , context )
var usernameFileExists = false
if ( ! fileExists ) {
val usernameFile = getDownloadChildSavePaths (
ArrayList ( userFolderPaths ) , media . code , i + 1 , url , username
)
usernameFileExists = usernameFile !! . first != null && checkPathExists ( usernameFile . first , context )
}
checkList . add ( fileExists || usernameFileExists )
val fileName = getDownloadChildSavePaths ( media . code , i + 1 , url , " " )
val fileNameWithUser = getDownloadChildSavePaths ( media . code , i + 1 , url , username )
fileNames . put ( fileName . first , fileName . second )
fileNames . put ( fileNameWithUser . first , fileNameWithUser . second )
filePairs . put ( fileName . first , fileNameWithUser . first )
i ++
}
val files = checkFiles ( context , userFolder , fileNames , false )
for ( p in filePairs ) {
checkList . add ( files . get ( p . key ) != null || files . get ( p . value ) != null )
}
}
else -> {
}
@ -332,89 +326,52 @@ object DownloadUtils {
return checkList
}
private fun checkPathExists ( paths : List < String > , context : Context ) : Boolean {
if ( root == null ) return false
val uri = root !! . uri
var found = false
var docId = DocumentsContract . getTreeDocumentId ( uri )
for ( path in paths ) {
val docUri = DocumentsContract . buildChildDocumentsUriUsingTree ( uri , docId )
val docCursor = context . contentResolver . query (
docUri , arrayOf (
DocumentsContract . Document . COLUMN_DISPLAY_NAME ,
DocumentsContract . Document . COLUMN_DOCUMENT_ID
) , null , null , null
)
if ( docCursor == null ) return false
while ( docCursor . moveToNext ( ) && ! found ) {
if ( path . equals ( docCursor . getString ( 0 ) ) ) {
docId = docCursor . getString ( 1 )
found = true
}
}
docCursor . close ( )
if ( ! found ) return false
found = false
}
return true
}
@JvmStatic
fun showDownloadDialog (
context : Context ,
feedModel : Media ,
childPosition : Int
childPosition : Int ,
popupLocation : View ?
) {
if ( childPosition >= 0 ) {
val clickListener =
DialogInterface . OnClickListener { dialog : DialogInterface , which : Int ->
when ( which ) {
0 -> download ( context , feedModel , childPosition )
1 -> download ( context , feedModel )
DialogInterface . BUTTON_NEGATIVE -> dialog . dismiss ( )
else -> dialog . dismiss ( )
}
}
val items = arrayOf (
context . getString ( R . string . post_viewer_download_current ) ,
context . getString ( R . string . post_viewer_download_album )
)
AlertDialog . Builder ( context )
. setTitle ( R . string . post_viewer_download_dialog_title )
. setItems ( items , clickListener )
. setNegativeButton ( R . string . cancel , null )
. show ( )
if ( childPosition == - 1 || popupLocation == null ) {
download ( context , feedModel )
return
}
val themeWrapper = ContextThemeWrapper ( context , R . style . popupMenuStyle )
val popupMenu = PopupMenu ( themeWrapper , popupLocation )
val menu = popupMenu . menu
menu . add ( 0 , R . id . download_current , 0 , R . string . post_viewer_download_current )
menu . add ( 0 , R . id . download_all , 1 , R . string . post_viewer_download_album )
popupMenu . setOnMenuItemClickListener { item : MenuItem ->
val itemId = item . itemId
if ( itemId == R . id . download_current ) {
download ( context , feedModel , childPosition )
} else if ( itemId == R . id . download_all ) {
download ( context , feedModel )
}
false
}
popupMenu . show ( )
}
@JvmStatic
fun download (
context : Context ,
storyModel : StoryMedia
) {
val downloadDir = getDownloadDir ( context , storyModel . user ?. username ) ?: return
val downloadDir = getDownloadDir ( context , storyModel . user ?. username , true ) ?: return
val url =
if ( storyModel . mediaT ype == MediaItemType . MEDIA_TYPE_VIDEO ) ResponseBodyUtils . getVideoUrl ( storyModel )
if ( storyModel . t ype == MediaItemType . MEDIA_TYPE_VIDEO ) ResponseBodyUtils . getVideoUrl ( storyModel )
else ResponseBodyUtils . getImageUrl ( storyModel )
val extension = getFileExtensionFromUrl ( url )
val baseFileName = ( storyModel . id + " _ "
+ storyModel . takenAt + extension )
val mimeType = Utils . mimeTypeMap . getMimeTypeFromExtension ( extension )
val baseFileName = storyModel . id + " _ " + storyModel . takenAt + extension
val usernamePrepend =
if ( Utils . settingsHelper . getBoolean ( PreferenceKeys . DOWNLOAD_PREPEND_USER_NAME )
&& storyModel . user ?. username != null
) storyModel . user . username + " _ " else " "
val fileName = usernamePrepend + baseFileName
var saveFile = downloadDir . findFile ( fileName )
if ( saveFile == null ) {
val mimeType = Utils . mimeTypeMap . getMimeTypeFromExtension (
if ( extension . startsWith ( " . " ) ) extension . substring ( 1 ) else extension
)
?: return
saveFile = downloadDir . createFile ( mimeType , fileName )
}
// final File saveFile = new File(downloadDir, fileName);
var saveFile = checkFiles ( context , downloadDir , mapOf ( fileName to mimeType !! ) , true ) . get ( fileName )
download ( context , url , saveFile )
}
@ -428,12 +385,28 @@ object DownloadUtils {
download ( context , listOf ( feedModel ) , position )
}
// this must be used for bulk download, but ONLY bulk download
@JvmStatic
fun download (
context : Context ,
feedModels : List < Media >
) {
val builder = NotificationCompat . Builder ( context , Constants . DOWNLOAD_CHANNEL_ID )
. setCategory ( NotificationCompat . CATEGORY_PROGRESS )
. setSmallIcon ( R . drawable . ic_download )
. setOngoing ( true )
. setProgress ( 1 , 0 , true )
. setAutoCancel ( false )
. setOnlyAlertOnce ( true )
. setContentTitle ( context . getString ( R . string . downloader_preparing ) )
val notification = builder . build ( )
val nid = abs ( UUID . randomUUID ( ) . hashCode ( ) )
val nManager = NotificationManagerCompat . from ( context . applicationContext )
nManager . notify ( nid , notification )
tasksThread . execute {
download ( context , feedModels , - 1 )
nManager . cancel ( nid )
}
}
private fun download (
@ -441,11 +414,12 @@ object DownloadUtils {
feedModels : List < Media > ,
childPositionIfSingle : Int
) {
val map : MutableMap < String , DocumentFile > = HashMap ( )
val map : MutableMap < String , Pair < String , String > > = HashMap ( )
val fileMap : MutableMap < String , DocumentFile ? > = HashMap ( )
for ( media in feedModels ) {
val mediaUser = media . user
val username = mediaUser ?. username ?: " "
val userFolderPaths = getSubPathForUserFolder ( usernam e)
val dir = getDownloadDir ( context , username , tru e)
when ( media . type ) {
MediaItemType . MEDIA_TYPE_IMAGE , MediaItemType . MEDIA_TYPE_VIDEO -> {
val url = getUrlOfType ( media )
@ -459,9 +433,8 @@ object DownloadUtils {
fileName = mediaUser . username + " _ " + fileName
}
}
val pair = getDownloadSavePaths ( userFolderPaths , fileName , url )
val file = createFile ( pair !! ) ?: continue
map [ url !! ] = file
val pair = getDownloadSavePaths ( fileName , url )
map [ url !! ] = pair
}
MediaItemType . MEDIA_TYPE_VOICE -> {
val url = getUrlOfType ( media )
@ -469,9 +442,8 @@ object DownloadUtils {
if ( mediaUser != null ) {
fileName = mediaUser . username + " _ " + fileName
}
val pair = getDownloadSavePaths ( userFolderPaths , fileName , url )
val file = createFile ( pair !! ) ?: continue
map [ url !! ] = file
val pair = getDownloadSavePaths ( fileName , url )
map [ url !! ] = pair
}
MediaItemType . MEDIA_TYPE_SLIDER -> {
val sliderItems = media . carouselMedia
@ -485,43 +457,18 @@ object DownloadUtils {
val url = getUrlOfType ( child )
val usernamePrepend =
if ( Utils . settingsHelper . getBoolean ( PreferenceKeys . DOWNLOAD_PREPEND_USER_NAME ) && mediaUser != null ) mediaUser . username else " "
val pair = getDownloadChildSavePaths (
ArrayList ( userFolderPaths ) , media . code , i + 1 , url , usernamePrepend
)
val file = createFile ( pair !! )
if ( file == null ) {
val pair = getDownloadChildSavePaths ( media . code , i + 1 , url , usernamePrepend )
map [ url !! ] = pair
i ++
continue
}
map [ url !! ] = file
i ++
}
}
}
}
if ( map . isEmpty ( ) ) return
download ( context , map )
}
private fun createFile ( pair : Pair < List < String > , String ? > ) : DocumentFile ? {
if ( root == null ) return null
if ( pair . first == null || pair . second == null ) return null
var dir = root
val first = pair . first
for ( i in first . indices ) {
val name = first [ i ]
val file = dir !! . findFile ( name )
if ( file != null ) {
dir = file
continue
}
dir = if ( i == first . size - 1 ) dir . createFile (
pair . second !! ,
name
) else dir . createDirectory ( name )
if ( dir == null ) break
fileMap . putAll ( checkFiles ( context , dir , map . values . toMap ( ) , true ) )
}
return dir
if ( map . isEmpty ( ) || fileMap . isEmpty ( ) ) return
val resultMap : MutableMap < String , DocumentFile ? > = mutableMapOf ( )
map . mapValuesTo ( resultMap ) { fileMap . get ( it . value . first ) }
download ( context , resultMap )
}
private fun getUrlOfType ( media : Media ) : String ? {
@ -559,7 +506,7 @@ object DownloadUtils {
download ( context , Collections . singletonMap ( url !! , filePath ) )
}
private fun download ( context : Context ? , urlFilePathMap : Map < String , DocumentFile > ) {
private fun download ( context : Context ? , urlFilePathMap : Map < String , DocumentFile ? > ) {
if ( context == null ) return
val constraints = Constraints . Builder ( )
. setRequiredNetworkType ( NetworkType . CONNECTED )