From dd8866098d26a82f151a2157f658f7bbf3212153 Mon Sep 17 00:00:00 2001 From: deniscerri <64997243+deniscerri@users.noreply.github.com> Date: Sat, 11 Apr 2026 14:07:37 +0200 Subject: [PATCH] add request install packages for github flavor only --- app/src/github/AndroidManifest.xml | 4 + .../ytdl/core/packages/PackageBase.kt | 110 +++++------------- .../downloading/DownloadSettingsModule.kt | 4 +- .../settings/updating/PackagesFragment.kt | 29 +++-- .../com/deniscerri/ytdl/util/Extensions.kt | 19 +++ .../java/com/deniscerri/ytdl/util/FileUtil.kt | 4 + .../java/com/deniscerri/ytdl/util/UiUtil.kt | 32 +++-- .../com/deniscerri/ytdl/util/UpdateUtil.kt | 102 +++++----------- 8 files changed, 128 insertions(+), 176 deletions(-) create mode 100644 app/src/github/AndroidManifest.xml diff --git a/app/src/github/AndroidManifest.xml b/app/src/github/AndroidManifest.xml new file mode 100644 index 00000000..325f3729 --- /dev/null +++ b/app/src/github/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/core/packages/PackageBase.kt b/app/src/main/java/com/deniscerri/ytdl/core/packages/PackageBase.kt index d52f6bf1..8edd6ab7 100644 --- a/app/src/main/java/com/deniscerri/ytdl/core/packages/PackageBase.kt +++ b/app/src/main/java/com/deniscerri/ytdl/core/packages/PackageBase.kt @@ -1,28 +1,18 @@ package com.deniscerri.ytdl.core.packages import android.annotation.SuppressLint -import android.app.DownloadManager -import android.content.BroadcastReceiver import android.content.Context -import android.content.Intent -import android.content.IntentFilter import android.content.pm.PackageManager import android.os.Build import android.os.Environment -import android.util.Log -import androidx.activity.result.ActivityResultLauncher import androidx.core.content.edit -import androidx.core.net.toUri -import androidx.documentfile.provider.DocumentFile import androidx.preference.PreferenceManager -import com.deniscerri.ytdl.R import com.deniscerri.ytdl.core.RuntimeManager import com.deniscerri.ytdl.core.ZipUtils import com.deniscerri.ytdl.database.models.GithubReleaseAsset import com.deniscerri.ytdl.util.FileUtil import com.google.gson.annotations.SerializedName import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import kotlinx.serialization.SerialName @@ -197,86 +187,44 @@ abstract class PackageBase { ) } - private var downloadReleaseId = -1L - private val onDownloadReleaseComplete = object : BroadcastReceiver() { - @SuppressLint("Range") - override fun onReceive(context: Context?, intent: Intent) { - if (context == null) return; - val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L) - val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - - if (id == downloadReleaseId) { - context.unregisterReceiver(this) - - val query = DownloadManager.Query().setFilterById(id) - val cursor = downloadManager.query(query) - - if (cursor.moveToFirst()) { - val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) - if (status == DownloadManager.STATUS_SUCCESSFUL) { - val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)) - FileUtil.openFileIntent(context, localUri) - } - } - cursor.close() - } - } - } - @SuppressLint("UnspecifiedRegisterReceiverFlag", "Range") - suspend fun downloadReleaseApk(context: Context, release: PackageRelease, onProgress: (Long) -> Unit) : Result { + suspend fun downloadReleaseApk(release: PackageRelease, onProgress: (Long) -> Unit) : Result { return withContext(Dispatchers.IO) { try { - val releaseVersion = release.assets.first() - - val uri = releaseVersion.browser_download_url.toUri() - Environment - .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - .mkdirs() - val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - downloadReleaseId = downloadManager.enqueue( - DownloadManager.Request(uri) - .setAllowedNetworkTypes( - DownloadManager.Request.NETWORK_WIFI or - DownloadManager.Request.NETWORK_MOBILE - ) - .setAllowedOverRoaming(true) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - .setTitle(releaseVersion.name) - .setDescription(context.getString(R.string.downloading_update)) - .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, releaseVersion.name) - ) - - var downloading = true - while (downloading) { - val query = DownloadManager.Query().setFilterById(downloadReleaseId) - val cursor = downloadManager.query(query) - - if (cursor.moveToFirst()) { - val bytesDownloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) - val totalBytes = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) - val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) - - if (status == DownloadManager.STATUS_SUCCESSFUL) { - downloading = false - } + File(FileUtil.getDefaultApksPath()).mkdirs() + val tempApk = File(FileUtil.getDefaultApksPath(), "${packageFolderName}_${release.version.replace(".", "")}.apk") + + val request = Request.Builder() + .url(release.assets.first().browser_download_url) + .build() - if (totalBytes > 0) { - val progress = (bytesDownloaded * 100L / totalBytes) + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdirs() + val response = sharedClient.newCall(request).execute() + if (!response.isSuccessful) { + return@withContext Result.failure(Throwable(response.body.string())) + } + + val body = response.body + val totalBytes = body.contentLength() + var bytesDownloaded = 0L + + body.byteStream().use { inputStream -> + tempApk.outputStream().use { outputStream -> + val buffer = ByteArray(8192) + var bytesRead: Int + + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + if (!isActive) throw CancellationException("Download cancelled by user") + + outputStream.write(buffer, 0, bytesRead) + bytesDownloaded += bytesRead + val progress = ((bytesDownloaded * 100) / totalBytes) onProgress(progress) } } - cursor.close() - delay(500) } - val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver(onDownloadReleaseComplete, intentFilter, Context.RECEIVER_EXPORTED) - } else { - context.registerReceiver(onDownloadReleaseComplete, intentFilter) - } - Result.success(Unit) + Result.success(tempApk) } catch (e: Exception) { Result.failure(e) } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/downloading/DownloadSettingsModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/downloading/DownloadSettingsModule.kt index 30be9f27..03f5a6ec 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/downloading/DownloadSettingsModule.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/downloading/DownloadSettingsModule.kt @@ -116,7 +116,7 @@ object DownloadSettingsModule : SettingModule { if (!scheduler.canSchedule() && Build.VERSION.SDK_INT >= 31){ Intent().also { intent -> intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM - context.startActivity(intent) + host.getHostContext().startActivity(intent) } allowChange = false } @@ -135,7 +135,7 @@ object DownloadSettingsModule : SettingModule { if (!scheduler.canSchedule() && Build.VERSION.SDK_INT >= 31){ Intent().also { intent -> intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM - context.startActivity(intent) + host.getHostContext().startActivity(intent) } allowChange = false }else{ diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PackagesFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PackagesFragment.kt index 1c492715..14075f65 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PackagesFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/PackagesFragment.kt @@ -1,12 +1,12 @@ package com.deniscerri.ytdl.ui.more.settings.updating import android.annotation.SuppressLint -import android.app.Activity import android.app.AlertDialog import android.content.DialogInterface import android.content.Intent import android.content.SharedPreferences import android.net.Uri +import android.os.Build import android.os.Bundle import android.text.method.LinkMovementMethod import android.util.DisplayMetrics @@ -20,7 +20,6 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.FileProvider import androidx.core.view.isVisible -import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager @@ -51,10 +50,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import androidx.core.net.toUri -import com.anggrayudi.storage.file.toRawFile import com.deniscerri.ytdl.core.packages.Deno -import java.io.File +import com.deniscerri.ytdl.util.Extensions.hasPermission class PackagesFragment : Fragment(), PackagesAdapter.OnItemClickListener, PackageReleaseAdapter.OnItemClickListener { @@ -231,7 +228,7 @@ class PackagesFragment : Fragment(), PackagesAdapter.OnItemClickListener, Packag tmpDownloadJob = lifecycleScope.launch { val instance = tmpItem!!.getInstance() - val fileResp = instance.downloadReleaseApk(requireContext(), item) { progress -> + val fileResp = instance.downloadReleaseApk(item) { progress -> lifecycleScope.launch { withContext(Dispatchers.Main) { positiveButton.text = "$progress%" @@ -248,11 +245,27 @@ class PackagesFragment : Fragment(), PackagesAdapter.OnItemClickListener, Packag } } - fileResp.onSuccess { + fileResp.onSuccess { file -> lifecycleScope.launch { withContext(Dispatchers.Main) { view.dismiss() - Snackbar.make(requireView(), getString(R.string.install_downloaded_file), Snackbar.LENGTH_LONG).show() + + val canRequestPackageInstalls = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + android.Manifest.permission.REQUEST_INSTALL_PACKAGES.hasPermission(requireContext()) + } else { + true + } + + if (canRequestPackageInstalls) { + val contentUri = FileProvider.getUriForFile(requireContext(), requireContext().packageName + ".fileprovider", file) + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(contentUri, "application/vnd.android.package-archive") + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + installLauncher.launch(intent) + } else { + Snackbar.make(requireView(), getString(R.string.install_downloaded_file), Snackbar.LENGTH_LONG).show() + } } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt index 300e6d3c..f6445375 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt @@ -4,6 +4,7 @@ import android.animation.ValueAnimator import android.annotation.SuppressLint import android.app.Dialog import android.content.Context +import android.content.pm.PackageManager import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Canvas @@ -16,6 +17,7 @@ import android.graphics.drawable.shapes.OvalShape import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION import android.net.Uri +import android.os.Build import android.text.Editable import android.text.Spanned import android.text.TextWatcher @@ -697,4 +699,21 @@ object Extensions { null } } + + fun String.hasPermission(context: Context) : Boolean { + val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.getPackageInfo( + context.packageName, + PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()) + ) + } else { + @Suppress("DEPRECATION") + context.packageManager.getPackageInfo( + context.packageName, + PackageManager.GET_PERMISSIONS + ) + } + + return packageInfo.requestedPermissions?.contains(this) ?: false + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt index 392ddb7c..616d9aaf 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt @@ -363,6 +363,10 @@ object FileUtil { return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/Command" } + fun getDefaultApksPath() : String { + return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis/Apks" + } + fun getDefaultApplicationPath() : String { return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath + File.separator + "YTDLnis" } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index 079760b2..6210808a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -5,18 +5,14 @@ import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.annotation.SuppressLint import android.app.Activity -import android.app.DownloadManager -import android.content.BroadcastReceiver import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.DialogInterface import android.content.Intent -import android.content.IntentFilter import android.content.SharedPreferences import android.net.Uri import android.os.Build -import android.os.Environment import android.text.Editable import android.text.TextWatcher import android.text.format.DateFormat @@ -51,7 +47,6 @@ import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doOnTextChanged import androidx.fragment.app.FragmentManager -import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager @@ -72,6 +67,7 @@ import com.deniscerri.ytdl.ui.downloadcard.VideoCutListener import com.deniscerri.ytdl.util.Extensions.createBadge import com.deniscerri.ytdl.util.Extensions.enableTextHighlight import com.deniscerri.ytdl.util.Extensions.getMediaDuration +import com.deniscerri.ytdl.util.Extensions.hasPermission import com.deniscerri.ytdl.util.Extensions.toStringDuration import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeUtils @@ -2745,7 +2741,7 @@ object UiUtil { val lifecycleScope = lifecycleOwner.lifecycleScope tmpDownloadJob = lifecycleScope.launch { - val fileResp = updateUtil.downloadReleaseApk(context, v) { progress -> + val fileResp = updateUtil.downloadReleaseApk(v) { progress -> lifecycleScope.launch { withContext(Dispatchers.Main) { positiveButton.text = "$progress%" @@ -2766,15 +2762,27 @@ object UiUtil { } } - fileResp.onSuccess { + fileResp.onSuccess { file -> lifecycleScope.launch { withContext(Dispatchers.Main) { + val canRequestPackageInstalls = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + android.Manifest.permission.REQUEST_INSTALL_PACKAGES.hasPermission(context) + } else { + true + } + + if (canRequestPackageInstalls) { + val contentUri = FileProvider.getUriForFile(context, context.packageName + ".fileprovider", file) + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(contentUri, "application/vnd.android.package-archive") + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + context.startActivity(intent) + } else { + Snackbar.make(context.findViewById(R.id.frame_layout), context.getString(R.string.install_downloaded_file), Snackbar.LENGTH_LONG).show() + } + view.dismiss() - Snackbar.make( - context.findViewById(R.id.frame_layout), - context.getString(R.string.install_downloaded_file), - Snackbar.LENGTH_LONG - ).show() } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt index 5d758818..d43ea11a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt @@ -1,14 +1,8 @@ package com.deniscerri.ytdl.util import android.annotation.SuppressLint -import android.app.DownloadManager -import android.content.BroadcastReceiver import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.net.Uri import android.os.Build -import android.os.Environment import android.util.Log import androidx.preference.PreferenceManager import com.deniscerri.ytdl.BuildConfig @@ -16,12 +10,10 @@ import com.deniscerri.ytdl.R import com.deniscerri.ytdl.core.RuntimeManager import com.deniscerri.ytdl.core.models.YTDLRequest import com.deniscerri.ytdl.core.packages.PackageBase.Companion.sharedClient -import com.deniscerri.ytdl.core.packages.PackageBase.PackageRelease import com.deniscerri.ytdl.database.models.GithubRelease import com.google.gson.Gson import com.google.gson.reflect.TypeToken import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import okhttp3.Request @@ -30,7 +22,6 @@ import java.io.InputStreamReader import java.net.HttpURLConnection import java.net.URL import kotlin.coroutines.cancellation.CancellationException -import androidx.core.net.toUri class UpdateUtil(var context: Context) { @@ -167,81 +158,46 @@ class UpdateUtil(var context: Context) { } @SuppressLint("Range", "UnspecifiedRegisterReceiverFlag") - suspend fun downloadReleaseApk(context: Context, release: GithubRelease, onProgress: (Long) -> Unit) : Result { + suspend fun downloadReleaseApk(release: GithubRelease, onProgress: (Long) -> Unit) : Result { return withContext(Dispatchers.IO) { try { - val releaseVersion = release.assets.firstOrNull { it.name.contains(Build.SUPPORTED_ABIS[0]) } - - val uri = releaseVersion!!.browser_download_url.toUri() - Environment - .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - .mkdirs() - val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - val downloadID = downloadManager.enqueue( - DownloadManager.Request(uri) - .setAllowedNetworkTypes( - DownloadManager.Request.NETWORK_WIFI or - DownloadManager.Request.NETWORK_MOBILE - ) - .setAllowedOverRoaming(true) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - .setTitle(releaseVersion.name) - .setDescription(context.getString(R.string.downloading_update)) - .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, releaseVersion.name) - ) - - var downloading = true - while (downloading) { - val query = DownloadManager.Query().setFilterById(downloadID) - val cursor = downloadManager.query(query) - - if (cursor.moveToFirst()) { - val bytesDownloaded = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) - val totalBytes = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) - val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) - - if (status == DownloadManager.STATUS_SUCCESSFUL) { - downloading = false - } - - if (totalBytes > 0) { - val progress = (bytesDownloaded * 100L / totalBytes) - onProgress(progress) - } - } - cursor.close() - delay(500) + val releaseVersion = release.assets.firstOrNull { it.name.contains(Build.SUPPORTED_ABIS[0]) }!! + File(FileUtil.getDefaultApksPath()).mkdirs() + val tempApk = File(FileUtil.getDefaultApksPath(), "${releaseVersion.browser_download_url.split("/").last()}") + + //download + val request = Request.Builder() + .url(releaseVersion.browser_download_url) + .build() + + val response = sharedClient.newCall(request).execute() + if (!response.isSuccessful) { + return@withContext Result.failure(Throwable(response.body.string())) } - val onDownloadComplete = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent) { - val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L) + val body = response.body + val totalBytes = body.contentLength() + var bytesDownloaded = 0L + + body.byteStream().use { inputStream -> + tempApk.outputStream().use { outputStream -> + val buffer = ByteArray(8192) // 8KB buffer + var bytesRead: Int - if (id == downloadID) { - context?.unregisterReceiver(this) + while (inputStream.read(buffer).also { bytesRead = it } != -1) { + if (!isActive) throw CancellationException("Download cancelled by user") - val query = DownloadManager.Query().setFilterById(id) - val cursor = downloadManager.query(query) + outputStream.write(buffer, 0, bytesRead) + bytesDownloaded += bytesRead - if (cursor.moveToFirst()) { - val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) - if (status == DownloadManager.STATUS_SUCCESSFUL) { - val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)) - FileUtil.openFileIntent(context!!, localUri) - } - } - cursor.close() + // Calculate percentage + val progress = ((bytesDownloaded * 100) / totalBytes) + onProgress(progress) } } } - val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - context.registerReceiver(onDownloadComplete, intentFilter, Context.RECEIVER_EXPORTED) - } else { - context.registerReceiver(onDownloadComplete, intentFilter) - } - Result.success(Unit) + Result.success(tempApk) } catch (e: Exception) { Result.failure(e) }