Merge pull request #1243 from Bnyro/sandbox

Sandboxed downloads
This commit is contained in:
Bnyro 2022-09-09 18:05:12 +02:00 committed by GitHub
commit 147d2151a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 524 additions and 256 deletions

View File

@ -5,12 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
@ -42,6 +37,10 @@
android:name=".activities.CommunityActivity"
android:label="@string/settings" />
<activity
android:name=".activities.OfflinePlayerActivity"
android:label="@string/player" />
<activity
android:name=".activities.MainActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"

View File

@ -2,10 +2,12 @@ package com.github.libretube.activities
import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import com.github.libretube.R
import com.github.libretube.databinding.ActivityNointernetBinding
import com.github.libretube.extensions.BaseActivity
import com.github.libretube.extensions.getStyledSnackBar
import com.github.libretube.fragments.DownloadsFragment
import com.github.libretube.util.NetworkHelper
import com.github.libretube.util.ThemeHelper
@ -28,11 +30,31 @@ class NoInternetActivity : BaseActivity() {
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
}
setContentView(binding.root)
}
override fun onBackPressed() {
finishAffinity()
super.onBackPressed()
binding.downloads.setOnClickListener {
supportFragmentManager.beginTransaction()
.replace(R.id.noInternet_container, DownloadsFragment())
.addToBackStack(null)
.commit()
}
setContentView(binding.root)
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
supportFragmentManager.fragments.forEach {
if (it is DownloadsFragment) {
supportFragmentManager.beginTransaction()
.remove(it)
.commit()
return
}
}
finishAffinity()
}
}
)
}
}

View File

@ -0,0 +1,160 @@
package com.github.libretube.activities
import android.content.pm.ActivityInfo
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.ActivityOfflinePlayerBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.extensions.BaseActivity
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.source.MergingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.upstream.FileDataSource
import java.io.File
class OfflinePlayerActivity : BaseActivity() {
private lateinit var binding: ActivityOfflinePlayerBinding
private lateinit var fileName: String
private lateinit var player: ExoPlayer
private lateinit var playerView: StyledPlayerView
private lateinit var playerBinding: ExoStyledPlayerControlViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
hideSystemBars()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
super.onCreate(savedInstanceState)
fileName = intent?.getStringExtra(IntentData.fileName)!!
binding = ActivityOfflinePlayerBinding.inflate(layoutInflater)
setContentView(binding.root)
initializePlayer()
playVideo()
}
private fun initializePlayer() {
player = ExoPlayer.Builder(this)
.build()
playerView = binding.player
playerView.player = player
playerBinding = binding.player.binding
playerBinding.fullscreen.visibility = View.GONE
playerBinding.closeImageButton.setOnClickListener {
finish()
}
binding.player.initialize(
supportFragmentManager,
null,
binding.doubleTapOverlay.binding,
null
)
}
private fun playVideo() {
val videoDownloadDir = File(
getExternalFilesDir(null),
"video"
)
val videoFile = File(
videoDownloadDir,
fileName
)
val audioDownloadDir = File(
getExternalFilesDir(null),
"audio"
)
val audioFile = File(
audioDownloadDir,
fileName
)
val videoUri = if (videoFile.exists()) Uri.fromFile(videoFile) else null
val audioUri = if (audioFile.exists()) Uri.fromFile(audioFile) else null
setMediaSource(
videoUri,
audioUri
)
player.prepare()
player.play()
}
private fun setMediaSource(videoUri: Uri?, audioUri: Uri?) {
when {
videoUri != null && audioUri != null -> {
val videoSource = ProgressiveMediaSource.Factory(FileDataSource.Factory())
.createMediaSource(
MediaItem.fromUri(videoUri)
)
val audioSource = ProgressiveMediaSource.Factory(FileDataSource.Factory())
.createMediaSource(
MediaItem.fromUri(audioUri)
)
val mediaSource = MergingMediaSource(
audioSource,
videoSource
)
player.setMediaSource(mediaSource)
}
videoUri != null -> player.setMediaItem(
MediaItem.fromUri(videoUri)
)
audioUri != null -> player.setMediaItem(
MediaItem.fromUri(audioUri)
)
}
}
private fun hideSystemBars() {
window?.decorView?.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
window.statusBarColor = Color.TRANSPARENT
window.setFlags(
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
)
val windowInsetsController =
WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
windowInsetsController.hide(WindowInsetsCompat.Type.statusBars())
supportActionBar?.hide()
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
}
override fun onDestroy() {
player.release()
super.onDestroy()
}
}

View File

@ -0,0 +1,79 @@
package com.github.libretube.adapters
import android.annotation.SuppressLint
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R
import com.github.libretube.activities.OfflinePlayerActivity
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.DownloadedMediaRowBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
class DownloadsAdapter(
private val files: MutableList<File>
) : RecyclerView.Adapter<DownloadsViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsViewHolder {
val binding = DownloadedMediaRowBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return DownloadsViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: DownloadsViewHolder, position: Int) {
val file = files[position]
holder.binding.apply {
fileName.text = file.name
fileSize.text = "${file.length() / (1024 * 1024)} MiB"
root.setOnClickListener {
val intent = Intent(root.context, OfflinePlayerActivity::class.java).also {
it.putExtra(IntentData.fileName, file.name)
}
root.context.startActivity(intent)
}
root.setOnLongClickListener {
MaterialAlertDialogBuilder(root.context)
.setItems(
arrayOf(
root.context.getString(R.string.delete)
)
) { _, index ->
when (index) {
0 -> {
val downloadDir = File(
root.context.getExternalFilesDir(null),
"video"
)
File(
downloadDir,
file.name
).delete()
files.removeAt(position)
notifyItemRemoved(position)
}
}
}
.setNegativeButton(R.string.cancel, null)
.show()
true
}
}
}
override fun getItemCount(): Int {
return files.size
}
}
class DownloadsViewHolder(
val binding: DownloadedMediaRowBinding
) : RecyclerView.ViewHolder(binding.root)

View File

@ -1,4 +1,4 @@
package com.github.libretube.obj
package com.github.libretube.constants
/**
* object for saving the download type
@ -6,6 +6,6 @@ package com.github.libretube.obj
object DownloadType {
const val AUDIO = 0
const val VIDEO = 1
const val MUX = 2
const val AUDIO_VIDEO = 2
const val NONE = 3
}

View File

@ -7,4 +7,5 @@ object IntentData {
const val playlistId = "playlistId"
const val timeStamp = "timeStamp"
const val position = "position"
const val fileName = "fileName"
}

View File

@ -16,7 +16,6 @@ import com.github.libretube.databinding.DialogDownloadBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.obj.Streams
import com.github.libretube.services.DownloadService
import com.github.libretube.util.PermissionHelper
import com.github.libretube.util.ThemeHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import retrofit2.HttpException
@ -32,8 +31,6 @@ class DownloadDialog(
fetchAvailableSources()
PermissionHelper.requestReadWrite(requireActivity())
binding.title.text = ThemeHelper.getStyledAppName(requireContext())
binding.audioRadio.setOnClickListener {
@ -71,18 +68,18 @@ class DownloadDialog(
private fun initDownloadOptions(streams: Streams) {
val vidName = arrayListOf<String>()
val vidUrl = arrayListOf<String>()
val videoUrl = arrayListOf<String>()
// add empty selection
vidName.add(getString(R.string.no_video))
vidUrl.add("")
videoUrl.add("")
// add all available video streams
for (vid in streams.videoStreams!!) {
if (vid.url != null) {
val name = vid.quality + " " + vid.format
vidName.add(name)
vidUrl.add(vid.url!!)
videoUrl.add(vid.url!!)
}
}
@ -111,6 +108,7 @@ class DownloadDialog(
videoArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.videoSpinner.adapter = videoArrayAdapter
if (binding.videoSpinner.size >= 1) binding.videoSpinner.setSelection(1)
if (binding.audioSpinner.size >= 1) binding.audioSpinner.setSelection(1)
// initialize the audio sources
val audioArrayAdapter = ArrayAdapter(
@ -123,15 +121,12 @@ class DownloadDialog(
if (binding.audioSpinner.size >= 1) binding.audioSpinner.setSelection(1)
binding.download.setOnClickListener {
val selectedAudioUrl =
if (binding.audioRadio.isChecked) audioUrl[binding.audioSpinner.selectedItemPosition] else ""
val selectedVideoUrl =
if (binding.videoRadio.isChecked) vidUrl[binding.videoSpinner.selectedItemPosition] else ""
val intent = Intent(context, DownloadService::class.java)
intent.putExtra("videoName", streams.title)
intent.putExtra("videoUrl", selectedVideoUrl)
intent.putExtra("audioUrl", selectedAudioUrl)
intent.putExtra("videoUrl", videoUrl[binding.videoSpinner.selectedItemPosition])
intent.putExtra("audioUrl", audioUrl[binding.audioSpinner.selectedItemPosition])
context?.startService(intent)
dismiss()
}

View File

@ -10,7 +10,6 @@ import androidx.fragment.app.DialogFragment
import com.github.libretube.R
import com.github.libretube.services.UpdateService
import com.github.libretube.update.UpdateInfo
import com.github.libretube.util.PermissionHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class UpdateDialog(
@ -26,7 +25,6 @@ class UpdateDialog(
val downloadUrl = getDownloadUrl(updateInfo)
Log.i("downloadUrl", downloadUrl.toString())
if (downloadUrl != null) {
PermissionHelper.requestReadWrite(requireActivity())
val intent = Intent(context, UpdateService::class.java)
intent.putExtra("downloadUrl", downloadUrl)
context?.startService(intent)

View File

@ -0,0 +1,38 @@
package com.github.libretube.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.adapters.DownloadsAdapter
import com.github.libretube.databinding.FragmentDownloadsBinding
import com.github.libretube.extensions.BaseFragment
import java.io.File
class DownloadsFragment : BaseFragment() {
private lateinit var binding: FragmentDownloadsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentDownloadsBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val downloadDir = File(
context?.getExternalFilesDir(null),
"video"
)
binding.downloads.layoutManager = LinearLayoutManager(context)
binding.downloads.adapter = DownloadsAdapter(
downloadDir.listFiles()?.toMutableList() ?: mutableListOf()
)
}
}

View File

@ -30,12 +30,6 @@ class LibraryFragment : BaseFragment() {
private lateinit var binding: FragmentLibraryBinding
private val playerViewModel: PlayerViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -67,6 +61,10 @@ class LibraryFragment : BaseFragment() {
}
}
binding.downloads.setOnClickListener {
findNavController().navigate(R.id.downloadsFragment)
}
if (token != "") {
binding.boogh.setImageResource(R.drawable.ic_list)
binding.textLike.text = getString(R.string.emptyList)

View File

@ -23,7 +23,6 @@ import com.github.libretube.dialogs.LoginDialog
import com.github.libretube.dialogs.LogoutDialog
import com.github.libretube.extensions.await
import com.github.libretube.util.ImportHelper
import com.github.libretube.util.PermissionHelper
import com.github.libretube.util.PreferenceHelper
import com.github.libretube.views.MaterialPreferenceFragment
@ -147,15 +146,7 @@ class InstanceSettings : MaterialPreferenceFragment() {
val importSubscriptions = findPreference<Preference>(PreferenceKeys.IMPORT_SUBS)
importSubscriptions?.setOnPreferenceClickListener {
// check StorageAccess
val accessGranted =
PermissionHelper.isStoragePermissionGranted(requireActivity())
// import subscriptions
if (accessGranted) {
getContent.launch("*/*")
} // request permissions if not granted
else {
PermissionHelper.requestReadWrite(requireActivity())
}
getContent.launch("*/*")
true
}

View File

@ -1,7 +1,6 @@
package com.github.libretube.services
import android.app.DownloadManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
@ -9,10 +8,6 @@ import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Environment.DIRECTORY_DOWNLOADS
import android.os.Environment.DIRECTORY_MOVIES
import android.os.Environment.DIRECTORY_MUSIC
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
@ -21,28 +16,23 @@ import com.github.libretube.Globals
import com.github.libretube.R
import com.github.libretube.constants.DOWNLOAD_CHANNEL_ID
import com.github.libretube.constants.DOWNLOAD_FAILURE_NOTIFICATION_ID
import com.github.libretube.constants.DOWNLOAD_PENDING_NOTIFICATION_ID
import com.github.libretube.constants.DOWNLOAD_SUCCESS_NOTIFICATION_ID
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.constants.DownloadType
import com.github.libretube.extensions.TAG
import com.github.libretube.obj.DownloadType
import com.github.libretube.util.PreferenceHelper
import java.io.File
class DownloadService : Service() {
private lateinit var notification: NotificationCompat.Builder
private var downloadId: Long = -1
private lateinit var videoName: String
private lateinit var videoUrl: String
private lateinit var audioUrl: String
private var downloadType: Int = 3
private lateinit var audioDir: File
private lateinit var videoDir: File
private lateinit var libretubeDir: File
private lateinit var tempDir: File
private lateinit var videoDownloadDir: File
private lateinit var audioDownloadDir: File
private var videoDownloadId: Long? = null
private var audioDownloadId: Long? = null
override fun onCreate() {
super.onCreate()
Globals.IS_DOWNLOAD_RUNNING = true
@ -53,15 +43,14 @@ class DownloadService : Service() {
videoUrl = intent.getStringExtra("videoUrl")!!
audioUrl = intent.getStringExtra("audioUrl")!!
downloadType = if (audioUrl != "") {
DownloadType.AUDIO
} else if (videoUrl != "") {
DownloadType.VIDEO
} else {
DownloadType.NONE
downloadType = when {
videoUrl != "" && audioUrl != "" -> DownloadType.AUDIO_VIDEO
audioUrl != "" -> DownloadType.AUDIO
videoUrl != "" -> DownloadType.VIDEO
else -> DownloadType.NONE
}
if (downloadType != DownloadType.NONE) {
downloadNotification(intent)
downloadManager()
} else {
onDestroy()
@ -75,37 +64,19 @@ class DownloadService : Service() {
}
private fun downloadManager() {
// create folder for temporary files
tempDir = File(
applicationContext.getExternalFilesDir(DIRECTORY_DOWNLOADS),
".tmp"
videoDownloadDir = File(
this.getExternalFilesDir(null),
"video"
)
if (!tempDir.exists()) {
tempDir.mkdirs()
Log.e(TAG(), "Directory make")
} else {
tempDir.deleteRecursively()
tempDir.mkdirs()
Log.e(TAG(), "Directory already have")
}
val downloadLocationPref = PreferenceHelper.getString(PreferenceKeys.DOWNLOAD_LOCATION, "")
val folderName = PreferenceHelper.getString(PreferenceKeys.DOWNLOAD_FOLDER, "LibreTube")
if (!videoDownloadDir.exists()) videoDownloadDir.mkdirs()
val location = when (downloadLocationPref) {
"downloads" -> Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)
"music" -> Environment.getExternalStoragePublicDirectory(DIRECTORY_MUSIC)
"movies" -> Environment.getExternalStoragePublicDirectory(DIRECTORY_MOVIES)
"sdcard" -> Environment.getExternalStorageDirectory()
else -> Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS)
}
libretubeDir = File(
location,
folderName
audioDownloadDir = File(
this.getExternalFilesDir(null),
"audio"
)
if (!libretubeDir.exists()) libretubeDir.mkdirs()
Log.i(TAG(), libretubeDir.toString())
if (!audioDownloadDir.exists()) audioDownloadDir.mkdirs()
// start download
try {
@ -113,25 +84,25 @@ class DownloadService : Service() {
onDownloadComplete,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
when (downloadType) {
DownloadType.VIDEO -> {
videoDir = File(libretubeDir, videoName)
downloadId = downloadManagerRequest(
getString(R.string.video),
getString(R.string.downloading),
videoUrl,
videoDir
if (downloadType in listOf(DownloadType.VIDEO, DownloadType.AUDIO_VIDEO)) {
videoDownloadId = downloadManagerRequest(
getString(R.string.video),
getString(R.string.downloading),
videoUrl,
Uri.fromFile(
File(videoDownloadDir, videoName)
)
}
DownloadType.AUDIO -> {
audioDir = File(libretubeDir, videoName)
downloadId = downloadManagerRequest(
getString(R.string.audio),
getString(R.string.downloading),
audioUrl,
audioDir
)
}
if (downloadType in listOf(DownloadType.AUDIO, DownloadType.AUDIO_VIDEO)) {
audioDownloadId = downloadManagerRequest(
getString(R.string.audio),
getString(R.string.downloading),
audioUrl,
Uri.fromFile(
File(audioDownloadDir, videoName)
)
}
)
}
} catch (e: IllegalArgumentException) {
Log.e(TAG(), "download error $e")
@ -144,19 +115,15 @@ class DownloadService : Service() {
// Fetching the download id received with the broadcast
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
// Checking if the received broadcast is for our enqueued download by matching download id
if (downloadId == id) {
if (downloadType == DownloadType.MUX) {
downloadManagerRequest(
getString(R.string.audio),
getString(R.string.downloading),
audioUrl,
audioDir
)
} else {
downloadSucceededNotification()
onDestroy()
}
when (id) {
videoDownloadId -> videoDownloadId = null
audioDownloadId -> audioDownloadId = null
}
if (audioDownloadId != null || videoDownloadId != null) return
downloadSucceededNotification()
onDestroy()
}
}
@ -164,48 +131,28 @@ class DownloadService : Service() {
title: String,
descriptionText: String,
url: String,
fileDir: File
destination: Uri
): Long {
val request: DownloadManager.Request =
DownloadManager.Request(Uri.parse(url))
.setTitle(title) // Title of the Download Notification
.setDescription(descriptionText) // Description of the Download Notification
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) // Visibility of the download Notification
.setDestinationUri(Uri.fromFile(fileDir))
.setDestinationUri(destination)
.setAllowedOverMetered(true) // Set if download is allowed on Mobile network
.setAllowedOverRoaming(true) //
.setAllowedOverRoaming(true)
val downloadManager: DownloadManager =
applicationContext.getSystemService(DOWNLOAD_SERVICE) as DownloadManager
return downloadManager.enqueue(request)
}
private fun downloadNotification(intent: Intent) {
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE)
} else {
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}
// Creating a notification and setting its various attributes
notification =
NotificationCompat.Builder(this@DownloadService, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_download)
.setContentTitle("LibreTube")
.setContentText(getString(R.string.downloading))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setProgress(100, 0, true)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
startForeground(DOWNLOAD_PENDING_NOTIFICATION_ID, notification.build())
}
private fun downloadFailedNotification() {
val builder = NotificationCompat.Builder(this@DownloadService, DOWNLOAD_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_download)
.setContentTitle(resources.getString(R.string.downloadfailed))
.setContentText(getString(R.string.fail))
.setPriority(NotificationCompat.PRIORITY_HIGH)
with(NotificationManagerCompat.from(this@DownloadService)) {
// notificationId is a unique int for each notification that you must define
notify(DOWNLOAD_FAILURE_NOTIFICATION_ID, builder.build())
@ -219,6 +166,7 @@ class DownloadService : Service() {
.setContentTitle(resources.getString(R.string.success))
.setContentText(getString(R.string.downloadsucceeded))
.setPriority(NotificationCompat.PRIORITY_HIGH)
with(NotificationManagerCompat.from(this@DownloadService)) {
// notificationId is a unique int for each notification that you must define
notify(DOWNLOAD_SUCCESS_NOTIFICATION_ID, builder.build())
@ -232,7 +180,6 @@ class DownloadService : Service() {
}
Globals.IS_DOWNLOAD_RUNNING = false
Log.d(TAG(), "dl finished!")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)

View File

@ -1,70 +0,0 @@
package com.github.libretube.util
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import androidx.core.app.ActivityCompat
object PermissionHelper {
/**
* request storage permissions if not granted yet
*/
fun requestReadWrite(activity: Activity): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
ActivityCompat.requestPermissions(
activity,
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.MANAGE_EXTERNAL_STORAGE
),
1
) // permission request code is just an int
return false
}
} else {
if (ActivityCompat.checkSelfPermission(
activity,
Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(
activity,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
1
)
return false
}
}
return true
}
fun isStoragePermissionGranted(activity: Activity): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (activity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED
) {
true
} else {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
1
)
false
}
} else {
// permission is automatically granted on sdk < 23 upon installation
true
}
}
}

View File

@ -85,9 +85,9 @@ internal class CustomExoPlayerView(
fun initialize(
childFragmentManager: FragmentManager,
playerViewInterface: OnlinePlayerOptionsInterface,
playerViewInterface: OnlinePlayerOptionsInterface?,
doubleTapOverlayBinding: DoubleTapOverlayBinding,
trackSelector: TrackSelector
trackSelector: TrackSelector?
) {
this.childFragmentManager = childFragmentManager
this.onlinePlayerOptionsInterface = playerViewInterface

View File

@ -1,49 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/noInternet_settingsImageView"
android:layout_width="33dp"
android:layout_height="33dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="end"
android:layout_margin="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="3dp"
android:src="@drawable/ic_settings"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:src="@drawable/ic_settings" />
<ImageView
android:id="@+id/noInternet_imageView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="-50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_no_wifi" />
<TextView
android:id="@+id/noInternet_textView"
<LinearLayout
android:id="@+id/middle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/noInternet"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/noInternet_imageView" />
android:layout_gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/retry_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/retry"
android:textColor="?android:attr/colorBackground"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/noInternet_textView" />
<ImageView
android:id="@+id/noInternet_imageView"
android:layout_width="200dp"
android:layout_height="200dp"
app:srcCompat="@drawable/ic_no_wifi" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/noInternet_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/noInternet" />
<com.google.android.material.button.MaterialButton
android:id="@+id/retry_button"
style="@style/ThemeOverlay.Material3.Button.ElevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:text="@string/retry"
android:textColor="?android:attr/colorBackground"
app:elevation="20dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/downloads"
style="@style/ThemeOverlay.Material3.Button.ElevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="5dp"
android:text="@string/downloads"
android:textColor="?android:attr/colorBackground"
app:elevation="20dp" />
</LinearLayout>
<FrameLayout
android:id="@+id/noInternet_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="20dp"
android:paddingTop="10dp" />
</FrameLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.github.libretube.views.CustomExoPlayerView
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
app:show_buffering="when_playing">
<com.github.libretube.views.DoubleTapOverlay
android:id="@+id/doubleTapOverlay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center" />
</com.github.libretube.views.CustomExoPlayerView>
</LinearLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:orientation="horizontal"
android:paddingHorizontal="10dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/fileName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp" />
<TextView
android:id="@+id/fileSize"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloads"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@ -73,6 +73,32 @@
</LinearLayout>
<LinearLayout
android:id="@+id/downloads"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:background="?attr/selectableItemBackground"
android:orientation="horizontal"
android:padding="10dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="10dp"
android:src="@drawable/ic_download" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:text="@string/downloads"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -34,15 +34,20 @@
android:id="@+id/channelFragment"
android:name="com.github.libretube.fragments.ChannelFragment"
android:label="channel"
tools:layout="@layout/fragment_channel"></fragment>
tools:layout="@layout/fragment_channel" />
<fragment
android:id="@+id/playlistFragment"
android:name="com.github.libretube.fragments.PlaylistFragment"
android:label="fragment_playlist"
tools:layout="@layout/fragment_playlist"></fragment>
tools:layout="@layout/fragment_playlist" />
<fragment
android:id="@+id/watchHistoryFragment"
android:name="com.github.libretube.fragments.WatchHistoryFragment"
android:label="@string/watch_history"
tools:layout="@layout/fragment_watch_history" />
<fragment
android:id="@+id/downloadsFragment"
android:name="com.github.libretube.fragments.DownloadsFragment"
android:label="@string/downloads"
tools:layout="@layout/fragment_downloads" />
</navigation>

View File

@ -314,4 +314,5 @@
<string name="legacy_subscriptions">Legacy subscriptions view</string>
<string name="device_info">Device Info</string>
<string name="audio_video_summary">Quality and format</string>
<string name="delete">Delete</string>
</resources>

View File

@ -2,6 +2,7 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!--
<PreferenceCategory app:title="@string/downloads">
<ListPreference
@ -31,6 +32,7 @@
app:title="@string/share_with_time" />
</PreferenceCategory>
-->
<PreferenceCategory app:title="@string/advanced">