625 lines
22 KiB
Kotlin
625 lines
22 KiB
Kotlin
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 androidx.documentfile.provider.DocumentFile
|
|
import androidx.work.*
|
|
import awais.instagrabber.R
|
|
import awais.instagrabber.fragments.settings.PreferenceKeys
|
|
import awais.instagrabber.models.StoryModel
|
|
import awais.instagrabber.models.enums.MediaItemType
|
|
import awais.instagrabber.repositories.responses.Media
|
|
import awais.instagrabber.utils.TextUtils.isEmpty
|
|
import awais.instagrabber.workers.DownloadWorker
|
|
import com.google.gson.Gson
|
|
import java.io.BufferedWriter
|
|
import java.io.IOException
|
|
import java.io.OutputStreamWriter
|
|
import java.util.*
|
|
import java.util.regex.Pattern
|
|
|
|
|
|
object DownloadUtils {
|
|
private val TAG = DownloadUtils::class.java.simpleName
|
|
|
|
// private static final String DIR_BARINSTA = "Barinsta";
|
|
private const val DIR_DOWNLOADS = "Downloads"
|
|
private const val DIR_CAMERA = "Camera"
|
|
private const val DIR_EDIT = "Edit"
|
|
private const val DIR_RECORDINGS = "Sent Recordings"
|
|
private const val DIR_TEMP = "Temp"
|
|
private const val DIR_BACKUPS = "Backups"
|
|
private var root: DocumentFile? = null
|
|
@JvmStatic
|
|
@Throws(ReselectDocumentTreeException::class)
|
|
fun init(
|
|
context: Context,
|
|
barinstaDirUri: String?
|
|
) {
|
|
if (isEmpty(barinstaDirUri)) {
|
|
throw ReselectDocumentTreeException("folder path is null or empty")
|
|
}
|
|
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)
|
|
if (root == null || !root!!.exists() || root!!.lastModified() == 0L) {
|
|
root = null
|
|
throw ReselectDocumentTreeException(uri)
|
|
}
|
|
Utils.settingsHelper.putString(PreferenceKeys.PREF_BARINSTA_DIR_URI, uri.toString())
|
|
}
|
|
|
|
fun destroy() {
|
|
root = null
|
|
}
|
|
|
|
fun getDownloadDir(vararg dirs: String?): DocumentFile? {
|
|
if (root == null) {
|
|
return null
|
|
}
|
|
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
|
|
}
|
|
|
|
@JvmStatic
|
|
val downloadDir: DocumentFile?
|
|
get() = getDownloadDir(DIR_DOWNLOADS)
|
|
|
|
@JvmStatic
|
|
fun getCameraDir(): DocumentFile? {
|
|
return getDownloadDir(DIR_CAMERA)
|
|
}
|
|
|
|
@JvmStatic
|
|
fun getImageEditDir(sessionId: String?): DocumentFile? {
|
|
return getDownloadDir(DIR_EDIT, sessionId)
|
|
}
|
|
|
|
fun getRecordingsDir(): DocumentFile? {
|
|
return getDownloadDir(DIR_RECORDINGS)
|
|
}
|
|
|
|
@JvmStatic
|
|
fun getBackupsDir(): DocumentFile? {
|
|
return getDownloadDir(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?
|
|
): 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
|
|
}
|
|
|
|
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 fun getDownloadSavePaths(
|
|
paths: MutableList<String>,
|
|
postId: String?,
|
|
displayUrl: String?
|
|
): Pair<List<String>, String?>? {
|
|
return getDownloadSavePaths(paths, postId, "", displayUrl, "")
|
|
}
|
|
|
|
private fun getDownloadSavePaths(
|
|
paths: MutableList<String>,
|
|
postId: String?,
|
|
displayUrl: String,
|
|
username: String
|
|
): Pair<List<String>, String?>? {
|
|
return getDownloadSavePaths(paths, postId, "", displayUrl, username)
|
|
}
|
|
|
|
private fun getDownloadChildSavePaths(
|
|
paths: MutableList<String>,
|
|
postId: String?,
|
|
childPosition: Int,
|
|
url: String?,
|
|
username: String
|
|
): Pair<List<String>, String?>? {
|
|
val sliderPostfix = "_slide_$childPosition"
|
|
return getDownloadSavePaths(paths, postId, sliderPostfix, url, username)
|
|
}
|
|
|
|
private fun getDownloadSavePaths(
|
|
paths: MutableList<String>?,
|
|
postId: String?,
|
|
sliderPostfix: String,
|
|
displayUrl: String?,
|
|
username: String
|
|
): Pair<List<String>, String?>? {
|
|
if (paths == null) return null
|
|
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)
|
|
}
|
|
|
|
// public static DocumentFile getTempFile() {
|
|
// return getTempFile(null, null);
|
|
// }
|
|
fun getTempFile(fileName: String?, extension: String): DocumentFile? {
|
|
val dir = getTempDir()
|
|
var name = fileName
|
|
if (isEmpty(name)) {
|
|
name = UUID.randomUUID().toString()
|
|
}
|
|
var mimeType: String? = "application/octet-stream"
|
|
if (!isEmpty(extension)) {
|
|
name += ".$extension"
|
|
val mimeType1 = Utils.mimeTypeMap.getMimeTypeFromExtension(extension)
|
|
if (mimeType1 != null) {
|
|
mimeType = mimeType1
|
|
}
|
|
}
|
|
var file = dir!!.findFile(name!!)
|
|
if (file == null) {
|
|
file = dir.createFile(mimeType!!, name)
|
|
}
|
|
return file
|
|
}
|
|
|
|
/**
|
|
* Copied from [MimeTypeMap.getFileExtensionFromUrl])
|
|
*
|
|
*
|
|
* Returns the file extension or an empty string if there is no
|
|
* extension. This method is a convenience method for obtaining the
|
|
* extension of a url and has undefined results for other Strings.
|
|
*
|
|
* @param url URL
|
|
* @return The file extension of the given url.
|
|
*/
|
|
@JvmStatic
|
|
fun getFileExtensionFromUrl(url: String?): String {
|
|
var url = url
|
|
if (!isEmpty(url)) {
|
|
val fragment = url!!.lastIndexOf('#')
|
|
if (fragment > 0) {
|
|
url = url.substring(0, fragment)
|
|
}
|
|
val query = url.lastIndexOf('?')
|
|
if (query > 0) {
|
|
url = url.substring(0, query)
|
|
}
|
|
val filenamePos = url.lastIndexOf('/')
|
|
val filename = if (0 <= filenamePos) url.substring(filenamePos + 1) else url
|
|
|
|
// if the filename contains special characters, we don't
|
|
// consider it valid for our matching purposes:
|
|
if (!filename.isEmpty() &&
|
|
Pattern.matches("[a-zA-Z_0-9.\\-()%]+", filename)
|
|
) {
|
|
val dotPos = filename.lastIndexOf('.')
|
|
if (0 <= dotPos) {
|
|
return filename.substring(dotPos)
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
@JvmStatic
|
|
fun checkDownloaded(media: Media, context: Context): List<Boolean> {
|
|
val checkList: MutableList<Boolean> = LinkedList()
|
|
val user = media.user
|
|
var username = "username"
|
|
if (user != null) {
|
|
username = user.username
|
|
}
|
|
val userFolderPaths: List<String> = getSubPathForUserFolder(username)
|
|
when (media.mediaType) {
|
|
MediaItemType.MEDIA_TYPE_IMAGE, MediaItemType.MEDIA_TYPE_VIDEO -> {
|
|
val url =
|
|
if (media.mediaType == 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)
|
|
}
|
|
MediaItemType.MEDIA_TYPE_SLIDER -> {
|
|
val sliderItems = media.carouselMedia
|
|
var i = 0
|
|
while (i < sliderItems!!.size) {
|
|
val child = sliderItems[i]
|
|
val url =
|
|
if (child.mediaType == 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)
|
|
i++
|
|
}
|
|
}
|
|
else -> {
|
|
}
|
|
}
|
|
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
|
|
) {
|
|
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()
|
|
return
|
|
}
|
|
download(context, feedModel)
|
|
}
|
|
|
|
@JvmStatic
|
|
fun download(
|
|
context: Context,
|
|
storyModel: StoryModel
|
|
) {
|
|
val downloadDir = getDownloadDir(context, storyModel.username) ?: return
|
|
val url =
|
|
if (storyModel.itemType == MediaItemType.MEDIA_TYPE_VIDEO) storyModel.videoUrl else storyModel.storyUrl
|
|
val extension = getFileExtensionFromUrl(url)
|
|
val baseFileName = (storyModel.storyMediaId + "_"
|
|
+ storyModel.timestamp + extension)
|
|
val usernamePrepend =
|
|
if (Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME)
|
|
&& storyModel.username != null
|
|
) storyModel.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);
|
|
download(context, url, saveFile)
|
|
}
|
|
|
|
@JvmOverloads
|
|
@JvmStatic
|
|
fun download(
|
|
context: Context,
|
|
feedModel: Media,
|
|
position: Int = -1
|
|
) {
|
|
download(context, listOf(feedModel), position)
|
|
}
|
|
|
|
@JvmStatic
|
|
fun download(
|
|
context: Context,
|
|
feedModels: List<Media>
|
|
) {
|
|
download(context, feedModels, -1)
|
|
}
|
|
|
|
private fun download(
|
|
context: Context,
|
|
feedModels: List<Media>,
|
|
childPositionIfSingle: Int
|
|
) {
|
|
val map: MutableMap<String, DocumentFile> = HashMap()
|
|
for (media in feedModels) {
|
|
val mediaUser = media.user
|
|
val username = mediaUser?.username ?: ""
|
|
val userFolderPaths = getSubPathForUserFolder(username)
|
|
when (media.mediaType) {
|
|
MediaItemType.MEDIA_TYPE_IMAGE, MediaItemType.MEDIA_TYPE_VIDEO -> {
|
|
val url = getUrlOfType(media)
|
|
var fileName = media.id
|
|
if (mediaUser != null && isEmpty(media.code)) {
|
|
fileName = mediaUser.username + "_" + fileName
|
|
}
|
|
if (!isEmpty(media.code)) {
|
|
fileName = media.code
|
|
if (Utils.settingsHelper.getBoolean(PreferenceKeys.DOWNLOAD_PREPEND_USER_NAME) && mediaUser != null) {
|
|
fileName = mediaUser.username + "_" + fileName
|
|
}
|
|
}
|
|
val pair = getDownloadSavePaths(userFolderPaths, fileName, url)
|
|
val file = createFile(pair!!) ?: continue
|
|
map[url!!] = file
|
|
}
|
|
MediaItemType.MEDIA_TYPE_VOICE -> {
|
|
val url = getUrlOfType(media)
|
|
var fileName = media.id
|
|
if (mediaUser != null) {
|
|
fileName = mediaUser.username + "_" + fileName
|
|
}
|
|
val pair = getDownloadSavePaths(userFolderPaths, fileName, url)
|
|
val file = createFile(pair!!) ?: continue
|
|
map[url!!] = file
|
|
}
|
|
MediaItemType.MEDIA_TYPE_SLIDER -> {
|
|
val sliderItems = media.carouselMedia
|
|
var i = 0
|
|
while (i < sliderItems!!.size) {
|
|
if (childPositionIfSingle >= 0 && feedModels.size == 1 && i != childPositionIfSingle) {
|
|
i++
|
|
continue
|
|
}
|
|
val child = sliderItems[i]
|
|
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) {
|
|
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
|
|
}
|
|
return dir
|
|
}
|
|
|
|
private fun getUrlOfType(media: Media): String? {
|
|
when (media.mediaType) {
|
|
MediaItemType.MEDIA_TYPE_IMAGE -> {
|
|
return ResponseBodyUtils.getImageUrl(media)
|
|
}
|
|
MediaItemType.MEDIA_TYPE_VIDEO -> {
|
|
val videoVersions = media.videoVersions
|
|
var url: String? = null
|
|
if (videoVersions != null && !videoVersions.isEmpty()) {
|
|
url = videoVersions[0].url
|
|
}
|
|
return url
|
|
}
|
|
MediaItemType.MEDIA_TYPE_VOICE -> {
|
|
val audio = media.audio
|
|
var url: String? = null
|
|
if (audio != null) {
|
|
url = audio.audioSrc
|
|
}
|
|
return url
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
@JvmStatic
|
|
fun download(
|
|
context: Context?,
|
|
url: String?,
|
|
filePath: DocumentFile?
|
|
) {
|
|
if (context == null || filePath == null) return
|
|
download(context, Collections.singletonMap(url!!, filePath))
|
|
}
|
|
|
|
private fun download(context: Context?, urlFilePathMap: Map<String, DocumentFile>) {
|
|
if (context == null) return
|
|
val constraints = Constraints.Builder()
|
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
.build()
|
|
val request = DownloadWorker.DownloadRequest.builder()
|
|
.setUrlToFilePathMap(urlFilePathMap)
|
|
.build()
|
|
val requestJson = Gson().toJson(request)
|
|
val tempFile = getTempFile(null, "json")
|
|
if (tempFile == null) {
|
|
Log.e(TAG, "download: temp file is null")
|
|
return
|
|
}
|
|
val uri = tempFile.uri
|
|
val contentResolver = context.contentResolver ?: return
|
|
try {
|
|
BufferedWriter(OutputStreamWriter(contentResolver.openOutputStream(uri))).use { writer ->
|
|
writer.write(
|
|
requestJson
|
|
)
|
|
}
|
|
} catch (e: IOException) {
|
|
Log.e(TAG, "download: Error writing request to file", e)
|
|
tempFile.delete()
|
|
return
|
|
}
|
|
val downloadWorkRequest: WorkRequest =
|
|
OneTimeWorkRequest.Builder(DownloadWorker::class.java)
|
|
.setInputData(
|
|
Data.Builder()
|
|
.putString(
|
|
DownloadWorker.KEY_DOWNLOAD_REQUEST_JSON,
|
|
tempFile.uri.toString()
|
|
)
|
|
.build()
|
|
)
|
|
.setConstraints(constraints)
|
|
.addTag("download")
|
|
.build()
|
|
WorkManager.getInstance(context)
|
|
.enqueue(downloadWorkRequest)
|
|
}
|
|
|
|
@JvmStatic
|
|
fun getRootDirUri(): Uri? {
|
|
return if (root != null) root!!.uri else null
|
|
}
|
|
|
|
class ReselectDocumentTreeException : Exception {
|
|
val initialUri: Uri?
|
|
|
|
constructor() {
|
|
initialUri = null
|
|
}
|
|
|
|
constructor(message: String?) : super(message) {
|
|
initialUri = null
|
|
}
|
|
|
|
constructor(initialUri: Uri?) {
|
|
this.initialUri = initialUri
|
|
}
|
|
}
|
|
} |