NewPipe/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.kt

1040 lines
48 KiB
Kotlin

package org.schabi.newpipe.download
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.IBinder
import android.provider.Settings
import android.text.Editable
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.RadioGroup
import android.widget.SeekBar
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.menu.ActionMenuItemView
import androidx.appcompat.widget.Toolbar
import androidx.collection.SparseArrayCompat
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.DialogFragment
import androidx.preference.PreferenceManager
import com.nononsenseapps.filepicker.Utils
import icepick.Icepick
import icepick.State
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.functions.Consumer
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.DownloadDialogBinding
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
import org.schabi.newpipe.error.ErrorUtil.Companion.showSnackbar
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.MediaFormat
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.localization.Localization
import org.schabi.newpipe.extractor.stream.AudioStream
import org.schabi.newpipe.extractor.stream.DeliveryMethod
import org.schabi.newpipe.extractor.stream.Stream
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.SubtitlesStream
import org.schabi.newpipe.extractor.stream.VideoStream
import org.schabi.newpipe.settings.NewPipeSettings
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
import org.schabi.newpipe.streams.io.StoredDirectoryHelper
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.AudioTrackAdapter
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper
import org.schabi.newpipe.util.FilePickerActivityHelper
import org.schabi.newpipe.util.FilenameUtils
import org.schabi.newpipe.util.ListHelper
import org.schabi.newpipe.util.PermissionHelper
import org.schabi.newpipe.util.SecondaryStreamHelper
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener
import org.schabi.newpipe.util.StreamItemAdapter
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper
import org.schabi.newpipe.util.ThemeHelper
import us.shandian.giga.get.MissionRecoveryInfo
import us.shandian.giga.postprocessing.Postprocessing
import us.shandian.giga.service.DownloadManager
import us.shandian.giga.service.DownloadManagerService
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder
import us.shandian.giga.service.MissionState
import java.io.File
import java.io.IOException
import java.util.Locale
import java.util.Objects
import java.util.Optional
import java.util.function.Function
class DownloadDialog : DialogFragment, RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
@State
var currentInfo: StreamInfo? = null
@State
var wrappedVideoStreams: StreamInfoWrapper<VideoStream?>? = null
@State
var wrappedSubtitleStreams: StreamInfoWrapper<SubtitlesStream?>? = null
@State
var wrappedAudioTracks: AudioTracksWrapper? = null
@State
var selectedAudioTrackIndex: Int = 0
@State
var selectedVideoIndex: Int = 0 // set in the constructor
@State
var selectedAudioIndex: Int = 0 // default to the first item
@State
var selectedSubtitleIndex: Int = 0 // default to the first item
private var mainStorageAudio: StoredDirectoryHelper? = null
private var mainStorageVideo: StoredDirectoryHelper? = null
private var downloadManager: DownloadManager? = null
private var okButton: ActionMenuItemView? = null
private var context: Context? = null
private var askForSavePath: Boolean = false
private var audioTrackAdapter: AudioTrackAdapter? = null
private var audioStreamsAdapter: StreamItemAdapter<AudioStream?, Stream>? = null
private var videoStreamsAdapter: StreamItemAdapter<VideoStream?, AudioStream?>? = null
private var subtitleStreamsAdapter: StreamItemAdapter<SubtitlesStream?, Stream>? = null
private val disposables: CompositeDisposable = CompositeDisposable()
private var dialogBinding: DownloadDialogBinding? = null
private var prefs: SharedPreferences? = null
// Variables for file name and MIME type when picking new folder because it's not set yet
private var filenameTmp: String? = null
private var mimeTmp: String? = null
private val requestDownloadSaveAsLauncher: ActivityResultLauncher<Intent> = registerForActivityResult<Intent, ActivityResult>(
StartActivityForResult(), ActivityResultCallback<ActivityResult>({ result: ActivityResult -> requestDownloadSaveAsResult(result) }))
private val requestDownloadPickAudioFolderLauncher: ActivityResultLauncher<Intent> = registerForActivityResult<Intent, ActivityResult>(
StartActivityForResult(), ActivityResultCallback<ActivityResult>({ result: ActivityResult -> requestDownloadPickAudioFolderResult(result) }))
private val requestDownloadPickVideoFolderLauncher: ActivityResultLauncher<Intent> = registerForActivityResult<Intent, ActivityResult>(
StartActivityForResult(), ActivityResultCallback<ActivityResult>({ result: ActivityResult -> requestDownloadPickVideoFolderResult(result) }))
/*//////////////////////////////////////////////////////////////////////////
// Instance creation
////////////////////////////////////////////////////////////////////////// */
constructor()
/**
* Create a new download dialog with the video, audio and subtitle streams from the provided
* stream info. Video streams and video-only streams will be put into a single list menu,
* sorted according to their resolution and the default video resolution will be selected.
*
* @param context the context to use just to obtain preferences and strings (will not be stored)
* @param info the info from which to obtain downloadable streams and other info (e.g. title)
*/
constructor(context: Context, info: StreamInfo) {
currentInfo = info
val audioStreams: List<AudioStream?> = ListHelper.getStreamsOfSpecifiedDelivery(info.getAudioStreams(), DeliveryMethod.PROGRESSIVE_HTTP)
val groupedAudioStreams: List<List<AudioStream?>?>? = ListHelper.getGroupedAudioStreams(context, audioStreams)
wrappedAudioTracks = AudioTracksWrapper((groupedAudioStreams)!!, context)
selectedAudioTrackIndex = ListHelper.getDefaultAudioTrackGroup(context, groupedAudioStreams)
// TODO: Adapt this code when the downloader support other types of stream deliveries
val videoStreams: List<VideoStream?> = ListHelper.getSortedStreamVideosList(
context,
ListHelper.getStreamsOfSpecifiedDelivery(info.getVideoStreams(), DeliveryMethod.PROGRESSIVE_HTTP),
ListHelper.getStreamsOfSpecifiedDelivery(info.getVideoOnlyStreams(), DeliveryMethod.PROGRESSIVE_HTTP),
false, // If there are multiple languages available, prefer streams without audio
// to allow language selection
wrappedAudioTracks!!.size() > 1
)
wrappedVideoStreams = StreamInfoWrapper(videoStreams, context)
wrappedSubtitleStreams = StreamInfoWrapper(
ListHelper.getStreamsOfSpecifiedDelivery(info.getSubtitles(), DeliveryMethod.PROGRESSIVE_HTTP), context)
selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams)
}
/*//////////////////////////////////////////////////////////////////////////
// Android lifecycle
////////////////////////////////////////////////////////////////////////// */
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (DEBUG) {
Log.d(TAG, ("onCreate() called with: "
+ "savedInstanceState = [" + savedInstanceState + "]"))
}
if (!PermissionHelper.checkStoragePermissions(getActivity(),
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
dismiss()
return
}
// context will remain null if dismiss() was called above, allowing to check whether the
// dialog is being dismissed in onViewCreated()
context = getContext()
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context))
Icepick.restoreInstanceState(this, savedInstanceState)
audioTrackAdapter = AudioTrackAdapter(wrappedAudioTracks)
subtitleStreamsAdapter = StreamItemAdapter(wrappedSubtitleStreams)
updateSecondaryStreams()
val intent: Intent = Intent(context, DownloadManagerService::class.java)
context!!.startService(intent)
context!!.bindService(intent, object : ServiceConnection {
public override fun onServiceConnected(cname: ComponentName, service: IBinder) {
val mgr: DownloadManagerBinder = service as DownloadManagerBinder
mainStorageAudio = mgr.getMainStorageAudio()
mainStorageVideo = mgr.getMainStorageVideo()
downloadManager = mgr.getDownloadManager()
askForSavePath = mgr.askForSavePath()
okButton!!.setEnabled(true)
context!!.unbindService(this)
}
public override fun onServiceDisconnected(name: ComponentName) {
// nothing to do
}
}, Context.BIND_AUTO_CREATE)
}
/**
* Update the displayed video streams based on the selected audio track.
*/
private fun updateSecondaryStreams() {
val audioStreams: StreamInfoWrapper<AudioStream?>? = getWrappedAudioStreams()
val secondaryStreams: SparseArrayCompat<SecondaryStreamHelper<AudioStream?>?> = SparseArrayCompat(4)
val videoStreams: List<VideoStream?>? = wrappedVideoStreams.getStreamsList()
wrappedVideoStreams!!.resetInfo()
for (i in videoStreams!!.indices) {
if (!videoStreams.get(i)!!.isVideoOnly()) {
continue
}
val audioStream: AudioStream? = SecondaryStreamHelper.Companion.getAudioStreamFor(
(context)!!, audioStreams.getStreamsList(), (videoStreams.get(i))!!)
if (audioStream != null) {
secondaryStreams.append(i, SecondaryStreamHelper(audioStreams, audioStream))
} else if (DEBUG) {
val mediaFormat: MediaFormat? = videoStreams.get(i)!!.getFormat()
if (mediaFormat != null) {
Log.w(TAG, ("No audio stream candidates for video format "
+ mediaFormat.name))
} else {
Log.w(TAG, "No audio stream candidates for unknown video format")
}
}
}
videoStreamsAdapter = StreamItemAdapter(wrappedVideoStreams, secondaryStreams)
audioStreamsAdapter = StreamItemAdapter(audioStreams)
}
public override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
if (DEBUG) {
Log.d(TAG, ("onCreateView() called with: "
+ "inflater = [" + inflater + "], container = [" + container + "], "
+ "savedInstanceState = [" + savedInstanceState + "]"))
}
return inflater.inflate(R.layout.download_dialog, container)
}
public override fun onViewCreated(view: View,
savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dialogBinding = DownloadDialogBinding.bind(view)
if (context == null) {
return // the dialog is being dismissed, see the call to dismiss() in onCreate()
}
dialogBinding!!.fileName.setText(FilenameUtils.createFilename(getContext(),
currentInfo!!.getName()))
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(),
getWrappedAudioStreams().getStreamsList())
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll())
dialogBinding!!.qualitySpinner.setOnItemSelectedListener(this)
dialogBinding!!.audioStreamSpinner.setOnItemSelectedListener(this)
dialogBinding!!.audioTrackSpinner.setOnItemSelectedListener(this)
dialogBinding!!.videoAudioGroup.setOnCheckedChangeListener(this)
initToolbar(dialogBinding!!.toolbarLayout.toolbar)
setupDownloadOptions()
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val threads: Int = prefs.getInt(getString(R.string.default_download_threads), 3)
dialogBinding!!.threadsCount.setText(threads.toString())
dialogBinding!!.threads.setProgress(threads - 1)
dialogBinding!!.threads.setOnSeekBarChangeListener(object : SimpleOnSeekBarChangeListener() {
public override fun onProgressChanged(seekbar: SeekBar,
progress: Int,
fromUser: Boolean) {
val newProgress: Int = progress + 1
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
.apply()
dialogBinding!!.threadsCount.setText(newProgress.toString())
}
})
fetchStreamsSize()
}
private fun initToolbar(toolbar: Toolbar) {
if (DEBUG) {
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]")
}
toolbar.setTitle(R.string.download_dialog_title)
toolbar.setNavigationIcon(R.drawable.ic_arrow_back)
toolbar.inflateMenu(R.menu.dialog_url)
toolbar.setNavigationOnClickListener(View.OnClickListener({ v: View? -> dismiss() }))
toolbar.setNavigationContentDescription(R.string.cancel)
okButton = toolbar.findViewById(R.id.okay)
okButton.setEnabled(false) // disable until the download service connection is done
toolbar.setOnMenuItemClickListener(Toolbar.OnMenuItemClickListener({ item: MenuItem ->
if (item.getItemId() == R.id.okay) {
prepareSelectedDownload()
return@setOnMenuItemClickListener true
}
false
}))
}
public override fun onDestroy() {
super.onDestroy()
disposables.clear()
}
public override fun onDestroyView() {
dialogBinding = null
super.onDestroyView()
}
public override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Icepick.saveInstanceState(this, outState)
}
/*//////////////////////////////////////////////////////////////////////////
// Video, audio and subtitle spinners
////////////////////////////////////////////////////////////////////////// */
private fun fetchStreamsSize() {
disposables.clear()
disposables.add(StreamInfoWrapper.Companion.fetchMoreInfoForWrapper<VideoStream?>(wrappedVideoStreams)
.subscribe(Consumer<Boolean>({ result: Boolean? ->
if ((dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()
== R.id.video_button)) {
setupVideoSpinner()
}
}), Consumer<Throwable>({ throwable: Throwable? ->
showSnackbar((context)!!,
ErrorInfo((throwable)!!, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading video stream size",
currentInfo!!.getServiceId()))
})))
disposables.add(StreamInfoWrapper.Companion.fetchMoreInfoForWrapper<AudioStream?>(getWrappedAudioStreams())
.subscribe(Consumer<Boolean>({ result: Boolean? ->
if ((dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()
== R.id.audio_button)) {
setupAudioSpinner()
}
}), Consumer<Throwable>({ throwable: Throwable? ->
showSnackbar((context)!!,
ErrorInfo((throwable)!!, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading audio stream size",
currentInfo!!.getServiceId()))
})))
disposables.add(StreamInfoWrapper.Companion.fetchMoreInfoForWrapper<SubtitlesStream?>(wrappedSubtitleStreams)
.subscribe(Consumer<Boolean>({ result: Boolean? ->
if ((dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()
== R.id.subtitle_button)) {
setupSubtitleSpinner()
}
}), Consumer<Throwable>({ throwable: Throwable? ->
showSnackbar((context)!!,
ErrorInfo((throwable)!!, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading subtitle stream size",
currentInfo!!.getServiceId()))
})))
}
private fun setupAudioTrackSpinner() {
if (getContext() == null) {
return
}
dialogBinding!!.audioTrackSpinner.setAdapter(audioTrackAdapter)
dialogBinding!!.audioTrackSpinner.setSelection(selectedAudioTrackIndex)
}
private fun setupAudioSpinner() {
if (getContext() == null) {
return
}
dialogBinding!!.qualitySpinner.setVisibility(View.GONE)
setRadioButtonsState(true)
dialogBinding!!.audioStreamSpinner.setAdapter(audioStreamsAdapter)
dialogBinding!!.audioStreamSpinner.setSelection(selectedAudioIndex)
dialogBinding!!.audioStreamSpinner.setVisibility(View.VISIBLE)
dialogBinding!!.audioTrackSpinner.setVisibility(
if (wrappedAudioTracks!!.size() > 1) View.VISIBLE else View.GONE)
dialogBinding!!.audioTrackPresentInVideoText.setVisibility(View.GONE)
}
private fun setupVideoSpinner() {
if (getContext() == null) {
return
}
dialogBinding!!.qualitySpinner.setAdapter(videoStreamsAdapter)
dialogBinding!!.qualitySpinner.setSelection(selectedVideoIndex)
dialogBinding!!.qualitySpinner.setVisibility(View.VISIBLE)
setRadioButtonsState(true)
dialogBinding!!.audioStreamSpinner.setVisibility(View.GONE)
onVideoStreamSelected()
}
private fun onVideoStreamSelected() {
val isVideoOnly: Boolean = videoStreamsAdapter!!.getItem(selectedVideoIndex)!!.isVideoOnly()
dialogBinding!!.audioTrackSpinner.setVisibility(
if (isVideoOnly && wrappedAudioTracks!!.size() > 1) View.VISIBLE else View.GONE)
dialogBinding!!.audioTrackPresentInVideoText.setVisibility(
if (!isVideoOnly && wrappedAudioTracks!!.size() > 1) View.VISIBLE else View.GONE)
}
private fun setupSubtitleSpinner() {
if (getContext() == null) {
return
}
dialogBinding!!.qualitySpinner.setAdapter(subtitleStreamsAdapter)
dialogBinding!!.qualitySpinner.setSelection(selectedSubtitleIndex)
dialogBinding!!.qualitySpinner.setVisibility(View.VISIBLE)
setRadioButtonsState(true)
dialogBinding!!.audioStreamSpinner.setVisibility(View.GONE)
dialogBinding!!.audioTrackSpinner.setVisibility(View.GONE)
dialogBinding!!.audioTrackPresentInVideoText.setVisibility(View.GONE)
}
/*//////////////////////////////////////////////////////////////////////////
// Activity results
////////////////////////////////////////////////////////////////////////// */
private fun requestDownloadPickAudioFolderResult(result: ActivityResult) {
requestDownloadPickFolderResult(
result, getString(R.string.download_path_audio_key), DownloadManager.Companion.TAG_AUDIO)
}
private fun requestDownloadPickVideoFolderResult(result: ActivityResult) {
requestDownloadPickFolderResult(
result, getString(R.string.download_path_video_key), DownloadManager.Companion.TAG_VIDEO)
}
private fun requestDownloadSaveAsResult(result: ActivityResult) {
if (result.getResultCode() != Activity.RESULT_OK) {
return
}
if (result.getData() == null || result.getData()!!.getData() == null) {
showFailedDialog(R.string.general_error)
return
}
if (FilePickerActivityHelper.Companion.isOwnFileUri((context)!!, result.getData()!!.getData()!!)) {
val file: File = Utils.getFileForUri(result.getData()!!.getData()!!)
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
StoredFileHelper.Companion.DEFAULT_MIME)
return
}
val docFile: DocumentFile? = DocumentFile.fromSingleUri((context)!!,
result.getData()!!.getData()!!)
if (docFile == null) {
showFailedDialog(R.string.general_error)
return
}
// check if the selected file was previously used
checkSelectedDownload(null, result.getData()!!.getData(), docFile.getName(),
docFile.getType())
}
private fun requestDownloadPickFolderResult(result: ActivityResult,
key: String,
tag: String) {
if (result.getResultCode() != Activity.RESULT_OK) {
return
}
if (result.getData() == null || result.getData()!!.getData() == null) {
showFailedDialog(R.string.general_error)
return
}
var uri: Uri? = result.getData()!!.getData()
if (FilePickerActivityHelper.Companion.isOwnFileUri((context)!!, (uri)!!)) {
uri = Uri.fromFile(Utils.getFileForUri((uri)))
} else {
context!!.grantUriPermission(context!!.getPackageName(), uri,
StoredDirectoryHelper.Companion.PERMISSION_FLAGS)
}
PreferenceManager.getDefaultSharedPreferences((context)!!).edit().putString(key,
uri.toString()).apply()
try {
val mainStorage: StoredDirectoryHelper = StoredDirectoryHelper((context)!!, (uri)!!, tag)
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
filenameTmp, mimeTmp)
} catch (e: IOException) {
showFailedDialog(R.string.general_error)
}
}
/*//////////////////////////////////////////////////////////////////////////
// Listeners
////////////////////////////////////////////////////////////////////////// */
public override fun onCheckedChanged(group: RadioGroup, @IdRes checkedId: Int) {
if (DEBUG) {
Log.d(TAG, ("onCheckedChanged() called with: "
+ "group = [" + group + "], checkedId = [" + checkedId + "]"))
}
var flag: Boolean = true
when (checkedId) {
R.id.audio_button -> setupAudioSpinner()
R.id.video_button -> setupVideoSpinner()
R.id.subtitle_button -> {
setupSubtitleSpinner()
flag = false
}
}
dialogBinding!!.threads.setEnabled(flag)
}
public override fun onItemSelected(parent: AdapterView<*>,
view: View,
position: Int,
id: Long) {
if (DEBUG) {
Log.d(TAG, ("onItemSelected() called with: "
+ "parent = [" + parent + "], view = [" + view + "], "
+ "position = [" + position + "], id = [" + id + "]"))
}
when (parent.getId()) {
R.id.quality_spinner -> {
when (dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()) {
R.id.video_button -> {
selectedVideoIndex = position
onVideoStreamSelected()
}
R.id.subtitle_button -> selectedSubtitleIndex = position
}
onItemSelectedSetFileName()
}
R.id.audio_track_spinner -> {
val trackChanged: Boolean = selectedAudioTrackIndex != position
selectedAudioTrackIndex = position
if (trackChanged) {
updateSecondaryStreams()
fetchStreamsSize()
}
}
R.id.audio_stream_spinner -> selectedAudioIndex = position
}
}
private fun onItemSelectedSetFileName() {
val fileName: String? = FilenameUtils.createFilename(getContext(), currentInfo!!.getName())
val prevFileName: String = Optional.ofNullable(dialogBinding!!.fileName.getText())
.map(Function({ obj: Editable -> obj.toString() }))
.orElse("")
if ((prevFileName.isEmpty()
|| (prevFileName == fileName) || prevFileName.startsWith(getString(R.string.caption_file_name, fileName, "")))) {
// only update the file name field if it was not edited by the user
when (dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()) {
R.id.audio_button, R.id.video_button -> if (!(prevFileName == fileName)) {
// since the user might have switched between audio and video, the correct
// text might already be in place, so avoid resetting the cursor position
dialogBinding!!.fileName.setText(fileName)
}
R.id.subtitle_button -> {
val setSubtitleLanguageCode: String = subtitleStreamsAdapter
.getItem(selectedSubtitleIndex)!!.getLanguageTag()
// this will reset the cursor position, which is bad UX, but it can't be avoided
dialogBinding!!.fileName.setText(getString(
R.string.caption_file_name, fileName, setSubtitleLanguageCode))
}
}
}
}
public override fun onNothingSelected(parent: AdapterView<*>?) {}
/*//////////////////////////////////////////////////////////////////////////
// Download
////////////////////////////////////////////////////////////////////////// */
protected fun setupDownloadOptions() {
setRadioButtonsState(false)
setupAudioTrackSpinner()
val isVideoStreamsAvailable: Boolean = videoStreamsAdapter!!.getCount() > 0
val isAudioStreamsAvailable: Boolean = audioStreamsAdapter!!.getCount() > 0
val isSubtitleStreamsAvailable: Boolean = subtitleStreamsAdapter!!.getCount() > 0
dialogBinding!!.audioButton.setVisibility(if (isAudioStreamsAvailable) View.VISIBLE else View.GONE)
dialogBinding!!.videoButton.setVisibility(if (isVideoStreamsAvailable) View.VISIBLE else View.GONE)
dialogBinding!!.subtitleButton.setVisibility(if (isSubtitleStreamsAvailable) View.VISIBLE else View.GONE)
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val defaultMedia: String? = prefs.getString(getString(R.string.last_used_download_type),
getString(R.string.last_download_type_video_key))
if ((isVideoStreamsAvailable
&& ((defaultMedia == getString(R.string.last_download_type_video_key))))) {
dialogBinding!!.videoButton.setChecked(true)
setupVideoSpinner()
} else if ((isAudioStreamsAvailable
&& ((defaultMedia == getString(R.string.last_download_type_audio_key))))) {
dialogBinding!!.audioButton.setChecked(true)
setupAudioSpinner()
} else if ((isSubtitleStreamsAvailable
&& ((defaultMedia == getString(R.string.last_download_type_subtitle_key))))) {
dialogBinding!!.subtitleButton.setChecked(true)
setupSubtitleSpinner()
} else if (isVideoStreamsAvailable) {
dialogBinding!!.videoButton.setChecked(true)
setupVideoSpinner()
} else if (isAudioStreamsAvailable) {
dialogBinding!!.audioButton.setChecked(true)
setupAudioSpinner()
} else if (isSubtitleStreamsAvailable) {
dialogBinding!!.subtitleButton.setChecked(true)
setupSubtitleSpinner()
} else {
Toast.makeText(getContext(), R.string.no_streams_available_download,
Toast.LENGTH_SHORT).show()
dismiss()
}
}
private fun setRadioButtonsState(enabled: Boolean) {
dialogBinding!!.audioButton.setEnabled(enabled)
dialogBinding!!.videoButton.setEnabled(enabled)
dialogBinding!!.subtitleButton.setEnabled(enabled)
}
private fun getWrappedAudioStreams(): StreamInfoWrapper<AudioStream?>? {
if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks!!.size()) {
return StreamInfoWrapper.Companion.empty<AudioStream?>()
}
return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex)
}
private fun getSubtitleIndexBy(streams: List<SubtitlesStream?>): Int {
val preferredLocalization: Localization = NewPipe.getPreferredLocalization()
var candidate: Int = 0
for (i in streams.indices) {
val streamLocale: Locale = streams.get(i)!!.getLocale()
val languageEquals: Boolean = (streamLocale.getLanguage() != null
) && (preferredLocalization.getLanguageCode() != null
) && (streamLocale.getLanguage()
== Locale(preferredLocalization.getLanguageCode()).getLanguage())
val countryEquals: Boolean = (streamLocale.getCountry() != null
&& (streamLocale.getCountry() == preferredLocalization.getCountryCode()))
if (languageEquals) {
if (countryEquals) {
return i
}
candidate = i
}
}
return candidate
}
private fun getNameEditText(): String {
val str: String = Objects.requireNonNull(dialogBinding!!.fileName.getText()).toString()
.trim({ it <= ' ' })
return FilenameUtils.createFilename(context, if (str.isEmpty()) currentInfo!!.getName() else str)
}
private fun showFailedDialog(@StringRes msg: Int) {
org.schabi.newpipe.util.Localization.assureCorrectAppLanguage(requireContext())
AlertDialog.Builder((context)!!)
.setTitle(R.string.general_error)
.setMessage(msg)
.setNegativeButton(getString(R.string.ok), null)
.show()
}
private fun launchDirectoryPicker(launcher: ActivityResultLauncher<Intent>) {
NoFileManagerSafeGuard.launchSafe<Intent>(launcher, StoredDirectoryHelper.Companion.getPicker(context), TAG,
context)
}
private fun prepareSelectedDownload() {
val mainStorage: StoredDirectoryHelper?
val format: MediaFormat?
val selectedMediaType: String
val size: Long
// first, build the filename and get the output folder (if possible)
// later, run a very very very large file checking logic
filenameTmp = getNameEditText() + "."
when (dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()) {
R.id.audio_button -> {
selectedMediaType = getString(R.string.last_download_type_audio_key)
mainStorage = mainStorageAudio
format = audioStreamsAdapter!!.getItem(selectedAudioIndex)!!.getFormat()
size = getWrappedAudioStreams()!!.getSizeInBytes(selectedAudioIndex)
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg"
filenameTmp += "opus"
} else if (format != null) {
mimeTmp = format.mimeType
filenameTmp += format.getSuffix()
}
}
R.id.video_button -> {
selectedMediaType = getString(R.string.last_download_type_video_key)
mainStorage = mainStorageVideo
format = videoStreamsAdapter!!.getItem(selectedVideoIndex)!!.getFormat()
size = wrappedVideoStreams!!.getSizeInBytes(selectedVideoIndex)
if (format != null) {
mimeTmp = format.mimeType
filenameTmp += format.getSuffix()
}
}
R.id.subtitle_button -> {
selectedMediaType = getString(R.string.last_download_type_subtitle_key)
mainStorage = mainStorageVideo // subtitle & video files go together
format = subtitleStreamsAdapter!!.getItem(selectedSubtitleIndex)!!.getFormat()
size = wrappedSubtitleStreams!!.getSizeInBytes(selectedSubtitleIndex)
if (format != null) {
mimeTmp = format.mimeType
}
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.getSuffix()
} else if (format != null) {
filenameTmp += format.getSuffix()
}
}
else -> throw RuntimeException("No stream selected")
}
if (!askForSavePath && ((mainStorage == null
) || (mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
) || mainStorage.isInvalidSafStorage())) {
// Pick new download folder if one of:
// - Download folder is not set
// - Download folder uses SAF while SAF is disabled
// - Download folder doesn't use SAF while SAF is enabled
// - Download folder uses SAF but the user manually revoked access to it
Toast.makeText(context, getString(R.string.no_dir_yet),
Toast.LENGTH_LONG).show()
if (dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
launchDirectoryPicker(requestDownloadPickAudioFolderLauncher)
} else {
launchDirectoryPicker(requestDownloadPickVideoFolderLauncher)
}
return
}
if (askForSavePath) {
val initialPath: Uri?
if (NewPipeSettings.useStorageAccessFramework(context)) {
initialPath = null
} else {
val initialSavePath: File
if (dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC)
} else {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES)
}
initialPath = Uri.parse(initialSavePath.getAbsolutePath())
}
NoFileManagerSafeGuard.launchSafe<Intent>(requestDownloadSaveAsLauncher,
StoredFileHelper.Companion.getNewPicker((context)!!, filenameTmp, (mimeTmp)!!, initialPath), TAG,
context)
return
}
// Check for free memory space (for api 24 and up)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val freeSpace: Long = mainStorage!!.getFreeMemory()
if (freeSpace <= size) {
Toast.makeText(context, getString(R.string.error_insufficient_storage), Toast.LENGTH_LONG).show()
// move the user to storage setting tab
val storageSettingsIntent: Intent = Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS)
if (storageSettingsIntent.resolveActivity(context!!.getPackageManager()) != null) {
startActivity(storageSettingsIntent)
}
return
}
}
// check for existing file with the same name
checkSelectedDownload(mainStorage, mainStorage!!.findFile(filenameTmp), filenameTmp,
mimeTmp)
// remember the last media type downloaded by the user
prefs!!.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
.apply()
}
private fun checkSelectedDownload(mainStorage: StoredDirectoryHelper?,
targetFile: Uri?,
filename: String?,
mime: String?) {
var storage: StoredFileHelper?
try {
if (mainStorage == null) {
// using SAF on older android version
storage = StoredFileHelper(context, null, (targetFile)!!, "")
} else if (targetFile == null) {
// the file does not exist, but it is probably used in a pending download
storage = StoredFileHelper(mainStorage.getUri(), filename, mime,
mainStorage.getTag())
} else {
// the target filename is already use, attempt to use it
storage = StoredFileHelper(context, mainStorage.getUri(), targetFile,
mainStorage.getTag())
}
} catch (e: Exception) {
createNotification(requireContext(),
ErrorInfo(e, UserAction.DOWNLOAD_FAILED, "Getting storage"))
return
}
// get state of potential mission referring to the same file
val state: MissionState? = downloadManager!!.checkForExistingMission(storage)
@StringRes val msgBtn: Int
@StringRes val msgBody: Int
when (state) {
MissionState.Finished -> {
msgBtn = R.string.overwrite
msgBody = R.string.overwrite_finished_warning
}
MissionState.Pending -> {
msgBtn = R.string.overwrite
msgBody = R.string.download_already_pending
}
MissionState.PendingRunning -> {
msgBtn = R.string.generate_unique_name
msgBody = R.string.download_already_running
}
MissionState.None -> {
if (mainStorage == null) {
// This part is called if:
// * using SAF on older android version
// * save path not defined
// * if the file exists overwrite it, is not necessary ask
if (!storage.existsAsFile() && !storage.create()) {
showFailedDialog(R.string.error_file_creation)
return
}
continueSelectedDownload(storage)
return
} else if (targetFile == null) {
// This part is called if:
// * the filename is not used in a pending/finished download
// * the file does not exists, create
if (!mainStorage.mkdirs()) {
showFailedDialog(R.string.error_path_creation)
return
}
storage = mainStorage.createFile(filename, mime)
if (storage == null || !storage.canWrite()) {
showFailedDialog(R.string.error_file_creation)
return
}
continueSelectedDownload(storage)
return
}
msgBtn = R.string.overwrite
msgBody = R.string.overwrite_unrelated_warning
}
else -> return // unreachable
}
val askDialog: AlertDialog.Builder = AlertDialog.Builder((context)!!)
.setTitle(R.string.download_dialog_title)
.setMessage(msgBody)
.setNegativeButton(R.string.cancel, null)
val finalStorage: StoredFileHelper = storage
if (mainStorage == null) {
// This part is called if:
// * using SAF on older android version
// * save path not defined
when (state) {
MissionState.Pending, MissionState.Finished -> askDialog.setPositiveButton(msgBtn, DialogInterface.OnClickListener({ dialog: DialogInterface, which: Int ->
dialog.dismiss()
downloadManager!!.forgetMission(finalStorage)
continueSelectedDownload(finalStorage)
}))
}
askDialog.show()
return
}
askDialog.setPositiveButton(msgBtn, DialogInterface.OnClickListener({ dialog: DialogInterface, which: Int ->
dialog.dismiss()
var storageNew: StoredFileHelper?
when (state) {
MissionState.Finished, MissionState.Pending -> {
downloadManager!!.forgetMission(finalStorage)
if (targetFile == null) {
storageNew = mainStorage.createFile(filename, mime)
} else {
try {
// try take (or steal) the file
storageNew = StoredFileHelper(context, mainStorage.getUri(),
targetFile, mainStorage.getTag())
} catch (e: IOException) {
Log.e(TAG, ("Failed to take (or steal) the file in "
+ targetFile.toString()))
storageNew = null
}
}
if (storageNew != null && storageNew.canWrite()) {
continueSelectedDownload(storageNew)
} else {
showFailedDialog(R.string.error_file_creation)
}
}
MissionState.None -> {
if (targetFile == null) {
storageNew = mainStorage.createFile(filename, mime)
} else {
try {
storageNew = StoredFileHelper(context, mainStorage.getUri(),
targetFile, mainStorage.getTag())
} catch (e: IOException) {
Log.e(TAG, ("Failed to take (or steal) the file in "
+ targetFile.toString()))
storageNew = null
}
}
if (storageNew != null && storageNew.canWrite()) {
continueSelectedDownload(storageNew)
} else {
showFailedDialog(R.string.error_file_creation)
}
}
MissionState.PendingRunning -> {
storageNew = mainStorage.createUniqueFile((filename)!!, mime)
if (storageNew == null) {
showFailedDialog(R.string.error_file_creation)
} else {
continueSelectedDownload(storageNew)
}
}
}
}))
askDialog.show()
}
private fun continueSelectedDownload(storage: StoredFileHelper) {
if (!storage.canWrite()) {
showFailedDialog(R.string.permission_denied)
return
}
// check if the selected file has to be overwritten, by simply checking its length
try {
if (storage.length() > 0) {
storage.truncate()
}
} catch (e: IOException) {
Log.e(TAG, "Failed to truncate the file: " + storage.getUri().toString(), e)
showFailedDialog(R.string.overwrite_failed)
return
}
val selectedStream: Stream?
var secondaryStream: Stream? = null
val kind: Char
var threads: Int = dialogBinding!!.threads.getProgress() + 1
val urls: Array<String>
val recoveryInfo: List<MissionRecoveryInfo>
var psName: String? = null
var psArgs: Array<String>? = null
var nearLength: Long = 0
when (dialogBinding!!.videoAudioGroup.getCheckedRadioButtonId()) {
R.id.audio_button -> {
kind = 'a'
selectedStream = audioStreamsAdapter!!.getItem(selectedAudioIndex)
if (selectedStream!!.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.Companion.ALGORITHM_M4A_NO_DASH
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
psName = Postprocessing.Companion.ALGORITHM_OGG_FROM_WEBM_DEMUXER
}
}
R.id.video_button -> {
kind = 'v'
selectedStream = videoStreamsAdapter!!.getItem(selectedVideoIndex)
val secondary: SecondaryStreamHelper<AudioStream?>? = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream))
if (secondary != null) {
secondaryStream = secondary.getStream()
if (selectedStream!!.getFormat() == MediaFormat.MPEG_4) {
psName = Postprocessing.Companion.ALGORITHM_MP4_FROM_DASH_MUXER
} else {
psName = Postprocessing.Companion.ALGORITHM_WEBM_MUXER
}
val videoSize: Long = wrappedVideoStreams!!.getSizeInBytes(
selectedStream as VideoStream?)
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondary.getSizeInBytes() + videoSize
}
}
}
R.id.subtitle_button -> {
threads = 1 // use unique thread for subtitles due small file size
kind = 's'
selectedStream = subtitleStreamsAdapter!!.getItem(selectedSubtitleIndex)
if (selectedStream!!.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.Companion.ALGORITHM_TTML_CONVERTER
psArgs = arrayOf(
selectedStream.getFormat()!!.getSuffix(),
"false" // ignore empty frames
)
}
}
else -> return
}
if (secondaryStream == null) {
urls = arrayOf(
selectedStream!!.getContent()
)
recoveryInfo = java.util.List.of(MissionRecoveryInfo((selectedStream)))
} else {
if (secondaryStream.getDeliveryMethod() != DeliveryMethod.PROGRESSIVE_HTTP) {
throw IllegalArgumentException(("Unsupported stream delivery format"
+ secondaryStream.getDeliveryMethod()))
}
urls = arrayOf(
selectedStream!!.getContent(), secondaryStream.getContent()
)
recoveryInfo = java.util.List.of(
MissionRecoveryInfo((selectedStream)),
MissionRecoveryInfo(secondaryStream)
)
}
DownloadManagerService.Companion.startMission(context, urls, storage, kind, threads,
currentInfo!!.getUrl(), psName, psArgs, nearLength, ArrayList<MissionRecoveryInfo>(recoveryInfo))
Toast.makeText(context, getString(R.string.download_has_started),
Toast.LENGTH_SHORT).show()
dismiss()
}
companion object {
private val TAG: String = "DialogFragment"
private val DEBUG: Boolean = MainActivity.Companion.DEBUG
}
}