added range picker on playlist
This commit is contained in:
parent
3c99f3fdc2
commit
7a8341a3c3
11 changed files with 375 additions and 164 deletions
|
|
@ -76,12 +76,12 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
|
||||||
// CHECKBOX ----------------------------------
|
// CHECKBOX ----------------------------------
|
||||||
val check = card.findViewById<CheckBox>(R.id.checkBox)
|
val check = card.findViewById<CheckBox>(R.id.checkBox)
|
||||||
check.isChecked = checkedItems.contains(item.url)
|
check.isChecked = checkedItems.contains(item.url)
|
||||||
check.setOnCheckedChangeListener { buttonView, isChecked ->
|
check.setOnClickListener {
|
||||||
checkCard(isChecked, item.url)
|
checkCard(check.isChecked, item.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
card.setOnClickListener {
|
card.setOnClickListener {
|
||||||
check.isChecked = !check.isChecked
|
check.performClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,6 +119,22 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkRange(start: Int, end: Int){
|
||||||
|
checkedItems.clear()
|
||||||
|
if (start == end ){
|
||||||
|
val item = getItem(start)
|
||||||
|
checkedItems.add(item!!.url)
|
||||||
|
notifyItemChanged(start)
|
||||||
|
}else{
|
||||||
|
for (i in start..end){
|
||||||
|
val item = getItem(i)
|
||||||
|
checkedItems.add(item!!.url)
|
||||||
|
notifyItemChanged(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
fun getCheckedItems() : List<String>{
|
fun getCheckedItems() : List<String>{
|
||||||
return checkedItems
|
return checkedItems
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,14 @@ import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.widget.doAfterTextChanged
|
||||||
|
import androidx.core.widget.doOnTextChanged
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
|
@ -23,8 +27,10 @@ import com.deniscerri.ytdlnis.receiver.ShareActivity
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>, private val type: DownloadViewModel.Type) : BottomSheetDialogFragment(), PlaylistAdapter.OnItemClickListener {
|
class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>, private val type: DownloadViewModel.Type) : BottomSheetDialogFragment(), PlaylistAdapter.OnItemClickListener {
|
||||||
|
|
@ -34,7 +40,8 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
|
||||||
private lateinit var recyclerView: RecyclerView
|
private lateinit var recyclerView: RecyclerView
|
||||||
private lateinit var behavior: BottomSheetBehavior<View>
|
private lateinit var behavior: BottomSheetBehavior<View>
|
||||||
private lateinit var selectedText: TextView
|
private lateinit var selectedText: TextView
|
||||||
|
private lateinit var fromTextInput: TextInputLayout
|
||||||
|
private lateinit var toTextInput: TextInputLayout
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
@ -75,41 +82,90 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
|
||||||
selectedText = view.findViewById<Button>(R.id.selected)
|
selectedText = view.findViewById<Button>(R.id.selected)
|
||||||
selectedText.text = "0 ${resources.getString(R.string.selected)}"
|
selectedText.text = "0 ${resources.getString(R.string.selected)}"
|
||||||
|
|
||||||
|
fromTextInput = view.findViewById(R.id.from_textinput)
|
||||||
|
toTextInput = view.findViewById(R.id.to_textinput)
|
||||||
|
|
||||||
|
|
||||||
|
fromTextInput.editText!!.doAfterTextChanged { _text ->
|
||||||
|
reset()
|
||||||
|
val start = _text.toString()
|
||||||
|
val end = toTextInput.editText!!.text.toString()
|
||||||
|
|
||||||
|
if (checkRanges(start, end)) {
|
||||||
|
if (start < end){
|
||||||
|
var startNr = Integer.parseInt(start)
|
||||||
|
startNr--
|
||||||
|
var endNr = Integer.parseInt(end)
|
||||||
|
endNr--
|
||||||
|
if (startNr <= 0) startNr = 0
|
||||||
|
if (endNr > items.size) endNr = items.size
|
||||||
|
listAdapter.checkRange(startNr, endNr)
|
||||||
|
selectedText.text = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toTextInput.editText!!.doAfterTextChanged { _text ->
|
||||||
|
reset()
|
||||||
|
|
||||||
|
val start = fromTextInput.editText!!.text.toString()
|
||||||
|
val end = _text.toString()
|
||||||
|
|
||||||
|
if (checkRanges(start, end)) {
|
||||||
|
if (start < end){
|
||||||
|
var startNr = Integer.parseInt(start)
|
||||||
|
startNr--
|
||||||
|
var endNr = Integer.parseInt(end)
|
||||||
|
endNr--
|
||||||
|
if (startNr <= 0) startNr = 0
|
||||||
|
if (endNr > items.size) endNr = items.size
|
||||||
|
listAdapter.checkRange(startNr, endNr)
|
||||||
|
selectedText.text = "${listAdapter.getCheckedItems().size} ${resources.getString(R.string.selected)}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val checkAll = view.findViewById<FloatingActionButton>(R.id.check_all)
|
val checkAll = view.findViewById<FloatingActionButton>(R.id.check_all)
|
||||||
checkAll!!.setOnClickListener {
|
checkAll!!.setOnClickListener {
|
||||||
if (listAdapter.getCheckedItems().size != items.size){
|
if (listAdapter.getCheckedItems().size != items.size){
|
||||||
|
fromTextInput.editText!!.setText("1")
|
||||||
|
toTextInput.editText!!.setText(items.size.toString())
|
||||||
listAdapter.checkAll()
|
listAdapter.checkAll()
|
||||||
selectedText.text = resources.getString(R.string.all_items_selected)
|
selectedText.text = resources.getString(R.string.all_items_selected)
|
||||||
}else{
|
}else{
|
||||||
listAdapter.clearCheckeditems()
|
reset()
|
||||||
selectedText.text = "0 ${resources.getString(R.string.selected)}"
|
fromTextInput.isEnabled = true
|
||||||
|
toTextInput.isEnabled = true
|
||||||
|
fromTextInput.editText!!.setText("")
|
||||||
|
toTextInput.editText!!.setText("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val ok = view.findViewById<Button>(R.id.bottomsheet_ok)
|
val ok = view.findViewById<Button>(R.id.bottomsheet_ok)
|
||||||
ok!!.setOnClickListener {
|
ok!!.setOnClickListener {
|
||||||
lifecycleScope.launch(Dispatchers.IO){
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val checkedItems = listAdapter.getCheckedItems()
|
val checkedItems = listAdapter.getCheckedItems()
|
||||||
val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) }
|
val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) }
|
||||||
val downloadItems = mutableListOf<DownloadItem>()
|
if (checkedResultItems.size == 1){
|
||||||
checkedResultItems.forEach { c ->
|
val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url)
|
||||||
c!!.id = 0
|
|
||||||
val i = downloadViewModel.createDownloadItemFromResult(c,type)
|
|
||||||
if (type == DownloadViewModel.Type.command){
|
|
||||||
i.format = downloadViewModel.getLatestCommandTemplateAsFormat()
|
|
||||||
}
|
|
||||||
downloadItems.add(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (downloadItems.size == 1){
|
|
||||||
val resultItem = resultViewModel.getItemByURL(items[0]!!.url)
|
|
||||||
val bottomSheet = DownloadBottomSheetDialog(resultItem, type)
|
val bottomSheet = DownloadBottomSheetDialog(resultItem, type)
|
||||||
bottomSheet.show(parentFragmentManager, "downloadSingleSheet")
|
bottomSheet.show(parentFragmentManager, "downloadSingleSheet")
|
||||||
}else{
|
}else{
|
||||||
|
val downloadItems = mutableListOf<DownloadItem>()
|
||||||
|
checkedResultItems.forEach { c ->
|
||||||
|
c!!.id = 0
|
||||||
|
val i = downloadViewModel.createDownloadItemFromResult(c,type)
|
||||||
|
if (type == DownloadViewModel.Type.command){
|
||||||
|
i.format = downloadViewModel.getLatestCommandTemplateAsFormat()
|
||||||
|
}
|
||||||
|
downloadItems.add(i)
|
||||||
|
}
|
||||||
|
|
||||||
val bottomSheet = DownloadMultipleBottomSheetDialog(downloadItems)
|
val bottomSheet = DownloadMultipleBottomSheetDialog(downloadItems)
|
||||||
bottomSheet.show(parentFragmentManager, "downloadMultipleSheet")
|
bottomSheet.show(parentFragmentManager, "downloadMultipleSheet")
|
||||||
}
|
}
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +189,15 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkRanges(start: String, end: String) : Boolean {
|
||||||
|
return start.isNotBlank() && end.isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reset(){
|
||||||
|
listAdapter.clearCheckeditems()
|
||||||
|
selectedText.text = "0 ${resources.getString(R.string.selected)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) {
|
override fun onCardSelect(itemURL: String, isChecked: Boolean, checkedItems: List<String>) {
|
||||||
if (checkedItems.size == items.size){
|
if (checkedItems.size == items.size){
|
||||||
|
|
@ -140,6 +205,13 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List<ResultItem?>,
|
||||||
}else{
|
}else{
|
||||||
selectedText.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
|
selectedText.text = "${checkedItems.size} ${resources.getString(R.string.selected)}"
|
||||||
}
|
}
|
||||||
|
if (checkedItems.isEmpty()){
|
||||||
|
fromTextInput.isEnabled = true
|
||||||
|
toTextInput.isEnabled = true
|
||||||
|
}else{
|
||||||
|
fromTextInput.isEnabled = false
|
||||||
|
toTextInput.isEnabled = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,43 @@
|
||||||
package com.deniscerri.ytdlnis.ui.more
|
package com.deniscerri.ytdlnis.ui.more
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Notification
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Looper
|
|
||||||
import android.text.format.DateFormat
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.widget.*
|
import android.widget.EditText
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ScrollView
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.work.*
|
import androidx.work.Data
|
||||||
import com.deniscerri.ytdlnis.App
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
import androidx.work.WorkInfo
|
||||||
|
import androidx.work.WorkManager
|
||||||
import com.deniscerri.ytdlnis.R
|
import com.deniscerri.ytdlnis.R
|
||||||
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
|
|
||||||
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
|
import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel
|
||||||
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
|
|
||||||
import com.deniscerri.ytdlnis.util.FileUtil
|
|
||||||
import com.deniscerri.ytdlnis.util.NotificationUtil
|
import com.deniscerri.ytdlnis.util.NotificationUtil
|
||||||
import com.deniscerri.ytdlnis.work.DownloadWorker
|
import com.deniscerri.ytdlnis.work.TerminalDownloadWorker
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.google.android.material.bottomappbar.BottomAppBar
|
import com.google.android.material.bottomappbar.BottomAppBar
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.button.MaterialButton
|
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.chip.ChipGroup
|
import com.google.android.material.chip.ChipGroup
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
|
||||||
import com.yausername.youtubedl_android.YoutubeDL
|
import com.yausername.youtubedl_android.YoutubeDL
|
||||||
import com.yausername.youtubedl_android.YoutubeDLRequest
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
import io.reactivex.disposables.Disposable
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import kotlin.properties.Delegates
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
|
|
||||||
class TerminalActivity : AppCompatActivity() {
|
class TerminalActivity : AppCompatActivity() {
|
||||||
|
|
@ -67,11 +50,15 @@ class TerminalActivity : AppCompatActivity() {
|
||||||
private lateinit var bottomAppBar: BottomAppBar
|
private lateinit var bottomAppBar: BottomAppBar
|
||||||
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
|
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
|
||||||
private lateinit var sharedPreferences: SharedPreferences
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
|
private var downloadID by Delegates.notNull<Int>()
|
||||||
var context: Context? = null
|
var context: Context? = null
|
||||||
|
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_terminal)
|
setContentView(R.layout.activity_terminal)
|
||||||
|
|
||||||
|
downloadID = System.currentTimeMillis().toInt() % 100000
|
||||||
|
|
||||||
context = baseContext
|
context = baseContext
|
||||||
scrollView = findViewById(R.id.custom_command_scrollview)
|
scrollView = findViewById(R.id.custom_command_scrollview)
|
||||||
topAppBar = findViewById(R.id.custom_command_toolbar)
|
topAppBar = findViewById(R.id.custom_command_toolbar)
|
||||||
|
|
@ -112,6 +99,28 @@ class TerminalActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
notificationUtil = NotificationUtil(this)
|
notificationUtil = NotificationUtil(this)
|
||||||
handleIntent(intent)
|
handleIntent(intent)
|
||||||
|
|
||||||
|
WorkManager.getInstance(this)
|
||||||
|
.getWorkInfosForUniqueWorkLiveData(downloadID.toString())
|
||||||
|
.observe(this){ list ->
|
||||||
|
list.forEach {work ->
|
||||||
|
if (work.state == WorkInfo.State.SUCCEEDED || work.state == WorkInfo.State.FAILED || work.state == WorkInfo.State.CANCELLED) {
|
||||||
|
input!!.visibility = View.VISIBLE
|
||||||
|
input!!.requestFocus()
|
||||||
|
hideCancelFab()
|
||||||
|
}
|
||||||
|
val line = work.progress.getString("output") ?: return@observe
|
||||||
|
val id = work.progress.getInt("id", 0)
|
||||||
|
if(id == 0) return@observe
|
||||||
|
runOnUiThread {
|
||||||
|
try {
|
||||||
|
output!!.text = "${output!!.text}\n$line"
|
||||||
|
output!!.scrollTo(0, output!!.height)
|
||||||
|
scrollView!!.fullScroll(View.FOCUS_DOWN)
|
||||||
|
}catch (ignored: Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
|
@ -204,121 +213,31 @@ class TerminalActivity : AppCompatActivity() {
|
||||||
val cmd = if (command!!.contains("yt-dlp")) command.replace("yt-dlp", "")
|
val cmd = if (command!!.contains("yt-dlp")) command.replace("yt-dlp", "")
|
||||||
else command
|
else command
|
||||||
|
|
||||||
downloadID = System.currentTimeMillis().toInt()
|
val workRequest = OneTimeWorkRequestBuilder<TerminalDownloadWorker>()
|
||||||
|
.setInputData(
|
||||||
val theIntent = Intent(this, TerminalActivity::class.java)
|
Data.Builder()
|
||||||
val pendingIntent = PendingIntent.getActivity(this, 0, theIntent, PendingIntent.FLAG_IMMUTABLE)
|
.putInt("id", downloadID)
|
||||||
|
.putString("command", cmd)
|
||||||
val commandNotification: Notification =
|
.build()
|
||||||
notificationUtil.createDownloadServiceNotification(
|
|
||||||
pendingIntent,
|
|
||||||
getString(R.string.terminal),
|
|
||||||
downloadID,
|
|
||||||
NotificationUtil.COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID
|
|
||||||
)
|
)
|
||||||
with(NotificationManagerCompat.from(this)){
|
.addTag("terminal")
|
||||||
if (ActivityCompat.checkSelfPermission(
|
.build()
|
||||||
context!!,
|
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
notify(downloadID, commandNotification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val request = YoutubeDLRequest(emptyList())
|
|
||||||
|
|
||||||
val tempFolder = StringBuilder(context!!.cacheDir.absolutePath + """/${System.currentTimeMillis()}##terminal""")
|
WorkManager.getInstance(this).beginUniqueWork(
|
||||||
val tempFileDir = File(tempFolder.toString())
|
downloadID.toString(),
|
||||||
tempFileDir.delete()
|
ExistingWorkPolicy.KEEP,
|
||||||
tempFileDir.mkdir()
|
workRequest
|
||||||
|
).enqueue()
|
||||||
request.addOption(
|
|
||||||
"--config-locations",
|
|
||||||
File(cacheDir, "config${System.currentTimeMillis()}.txt").apply {
|
|
||||||
writeText(cmd)
|
|
||||||
}.absolutePath
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
request.addOption("-P", tempFileDir.absolutePath)
|
|
||||||
|
|
||||||
showCancelFab()
|
|
||||||
val disposable: Disposable = Observable.fromCallable {
|
|
||||||
try{
|
|
||||||
YoutubeDL.getInstance().execute(request, downloadID.toString()){ progress, _, line ->
|
|
||||||
Log.e(TAG, line)
|
|
||||||
runOnUiThread {
|
|
||||||
output!!.text = "${output!!.text}\n$line"
|
|
||||||
output!!.scrollTo(0, output!!.height)
|
|
||||||
scrollView!!.fullScroll(View.FOCUS_DOWN)
|
|
||||||
}
|
|
||||||
|
|
||||||
val title: String = getString(R.string.terminal)
|
|
||||||
notificationUtil.updateDownloadNotification(
|
|
||||||
downloadID,
|
|
||||||
line, progress.toInt(), 0, title,
|
|
||||||
NotificationUtil.COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}catch (e: Exception){
|
|
||||||
e.printStackTrace()
|
|
||||||
runOnUiThread {
|
|
||||||
output!!.text = "${output!!.text}\n${e.message}"
|
|
||||||
output!!.scrollTo(0, output!!.height)
|
|
||||||
scrollView!!.fullScroll(View.FOCUS_DOWN)
|
|
||||||
|
|
||||||
input!!.visibility = View.VISIBLE
|
|
||||||
input!!.requestFocus()
|
|
||||||
|
|
||||||
hideCancelFab()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.newThread())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({
|
|
||||||
input!!.setText("yt-dlp ")
|
|
||||||
input!!.visibility = View.VISIBLE
|
|
||||||
input!!.requestFocus()
|
|
||||||
|
|
||||||
try{
|
|
||||||
val fileUtil = FileUtil()
|
|
||||||
fileUtil.moveFile(tempFileDir.absoluteFile, this, sharedPreferences.getString("command_path", getString(R.string.command))!!) {}
|
|
||||||
}catch (e: Exception){
|
|
||||||
output!!.text = "${output!!.text}\n${e.message}"
|
|
||||||
output!!.scrollTo(0, output!!.height)
|
|
||||||
scrollView!!.fullScroll(View.FOCUS_DOWN)
|
|
||||||
}
|
|
||||||
|
|
||||||
hideCancelFab()
|
|
||||||
notificationUtil.cancelDownloadNotification(downloadID)
|
|
||||||
}) { e ->
|
|
||||||
e.printStackTrace()
|
|
||||||
output!!.text = "${output!!.text}\n${e.message}"
|
|
||||||
output!!.scrollTo(0, output!!.height)
|
|
||||||
scrollView!!.fullScroll(View.FOCUS_DOWN)
|
|
||||||
input!!.setText("yt-dlp ")
|
|
||||||
input!!.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
hideCancelFab();
|
|
||||||
|
|
||||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
|
||||||
Log.e(DownloadWorker.TAG, context?.getString(R.string.failed_download), e)
|
|
||||||
notificationUtil.cancelDownloadNotification(downloadID)
|
|
||||||
}
|
|
||||||
compositeDisposable.add(disposable)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelDownload() {
|
private fun cancelDownload() {
|
||||||
YoutubeDL.getInstance().destroyProcessById(downloadID.toString())
|
YoutubeDL.getInstance().destroyProcessById(downloadID.toString())
|
||||||
WorkManager.getInstance(this).cancelUniqueWork(downloadID.toString())
|
WorkManager.getInstance(this).cancelUniqueWork(downloadID.toString())
|
||||||
notificationUtil.cancelDownloadNotification(downloadID)
|
notificationUtil.cancelDownloadNotification(downloadID.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "CustomCommandActivity"
|
private const val TAG = "CustomCommandActivity"
|
||||||
private var downloadID = System.currentTimeMillis().toInt()
|
|
||||||
private val compositeDisposable = CompositeDisposable()
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ class DownloadLogListActivity : AppCompatActivity(), DownloadLogsAdapter.OnItemC
|
||||||
fileList = mutableListOf()
|
fileList = mutableListOf()
|
||||||
try{
|
try{
|
||||||
fileList.addAll(logFolder.listFiles()!!)
|
fileList.addAll(logFolder.listFiles()!!)
|
||||||
fileList.sortByDescending { it.name.split(" -")[0].toInt() }
|
fileList.sortByDescending { it.lastModified()}
|
||||||
}catch (e: Exception){
|
}catch (e: Exception){
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,10 @@ class FileUtil() {
|
||||||
val titleRegex = Regex("[^A-Za-z\\d ]")
|
val titleRegex = Regex("[^A-Za-z\\d ]")
|
||||||
return File(context.filesDir.absolutePath + """/logs/${item.id} - ${titleRegex.replace(item.title, "")}##${item.type}##${item.format.format_id}.log""")
|
return File(context.filesDir.absolutePath + """/logs/${item.id} - ${titleRegex.replace(item.title, "")}##${item.type}##${item.format.format_id}.log""")
|
||||||
}
|
}
|
||||||
|
fun getLogFileForTerminal(context: Context, command: String) : File {
|
||||||
|
val titleRegex = Regex("[^A-Za-z\\d ]")
|
||||||
|
return File(context.filesDir.absolutePath + """/logs/Terminal - ${titleRegex.replace(command.take(30), "")}##terminal.log""")
|
||||||
|
}
|
||||||
|
|
||||||
fun convertFileSize(s: Long): String{
|
fun convertFileSize(s: Long): String{
|
||||||
if (s <= 0) return "?"
|
if (s <= 0) return "?"
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ class DownloadWorker(
|
||||||
val type = downloadItem.type
|
val type = downloadItem.type
|
||||||
val downloadLocation = downloadItem.downloadPath
|
val downloadLocation = downloadItem.downloadPath
|
||||||
|
|
||||||
val tempFileDir = File(context.cacheDir.absolutePath + downloadItem.id)
|
val tempFileDir = File(context.cacheDir.absolutePath + "/" + downloadItem.id)
|
||||||
tempFileDir.delete()
|
tempFileDir.delete()
|
||||||
tempFileDir.mkdir()
|
tempFileDir.mkdir()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
package com.deniscerri.ytdlnis.work
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.work.ForegroundInfo
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.workDataOf
|
||||||
|
import com.deniscerri.ytdlnis.MainActivity
|
||||||
|
import com.deniscerri.ytdlnis.R
|
||||||
|
import com.deniscerri.ytdlnis.util.FileUtil
|
||||||
|
import com.deniscerri.ytdlnis.util.NotificationUtil
|
||||||
|
import com.yausername.youtubedl_android.YoutubeDL
|
||||||
|
import com.yausername.youtubedl_android.YoutubeDLRequest
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
class TerminalDownloadWorker(
|
||||||
|
private val context: Context,
|
||||||
|
workerParams: WorkerParameters
|
||||||
|
) : Worker(context, workerParams) {
|
||||||
|
override fun doWork(): Result {
|
||||||
|
val itemId = inputData.getInt("id", 0)
|
||||||
|
val command = inputData.getString("command")
|
||||||
|
if (itemId == 0) return Result.failure()
|
||||||
|
if (command!!.isEmpty()) return Result.failure()
|
||||||
|
|
||||||
|
val notificationUtil = NotificationUtil(context)
|
||||||
|
val fileUtil = FileUtil()
|
||||||
|
val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, command.take(20), itemId, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID)
|
||||||
|
val foregroundInfo = ForegroundInfo(itemId, notification)
|
||||||
|
setForegroundAsync(foregroundInfo)
|
||||||
|
|
||||||
|
val request = YoutubeDLRequest(emptyList())
|
||||||
|
val sharedPreferences = context.getSharedPreferences("root_preferences",
|
||||||
|
Service.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
|
||||||
|
val downloadLocation = sharedPreferences.getString("command_path", context.getString(R.string.command_path))
|
||||||
|
val tempFileDir = File(context.cacheDir.absolutePath + "/" + itemId)
|
||||||
|
tempFileDir.delete()
|
||||||
|
tempFileDir.mkdir()
|
||||||
|
|
||||||
|
request.addOption(
|
||||||
|
"--config-locations",
|
||||||
|
File(context.cacheDir, "config${System.currentTimeMillis()}.txt").apply {
|
||||||
|
writeText(command)
|
||||||
|
}.absolutePath
|
||||||
|
)
|
||||||
|
|
||||||
|
request.addOption("-P", tempFileDir.absolutePath)
|
||||||
|
|
||||||
|
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
|
||||||
|
val logFolder = File(context.filesDir.absolutePath + "/logs")
|
||||||
|
val logFile = fileUtil.getLogFileForTerminal(context, command)
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
if (logDownloads){
|
||||||
|
logFolder.mkdirs()
|
||||||
|
logFile.createNewFile()
|
||||||
|
logFile.writeText("Downloading:\n" +
|
||||||
|
"Terminal Download\n" +
|
||||||
|
"Command: ${command}\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
YoutubeDL.getInstance().execute(request, itemId.toString()){ progress, _, line ->
|
||||||
|
setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line, "id" to itemId, "log" to logDownloads))
|
||||||
|
val title: String = command.take(20)
|
||||||
|
notificationUtil.updateDownloadNotification(
|
||||||
|
itemId,
|
||||||
|
line, progress.toInt(), 0, title,
|
||||||
|
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
|
||||||
|
)
|
||||||
|
if (logDownloads && logFile.exists()){
|
||||||
|
logFile.appendText("${line}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
//move file from internal to set download directory
|
||||||
|
var finalPath : String?
|
||||||
|
try {
|
||||||
|
finalPath = moveFile(tempFileDir.absoluteFile, downloadLocation!!){ progress ->
|
||||||
|
setProgressAsync(workDataOf("progress" to progress))
|
||||||
|
}
|
||||||
|
setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $finalPath", "id" to itemId, "log" to logDownloads))
|
||||||
|
}catch (e: Exception){
|
||||||
|
finalPath = context.getString(R.string.unfound_file)
|
||||||
|
e.printStackTrace()
|
||||||
|
handler.postDelayed({
|
||||||
|
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
//put download in history
|
||||||
|
// val incognito = sharedPreferences.getBoolean("incognito", false)
|
||||||
|
// if (!incognito) {
|
||||||
|
// val unixtime = System.currentTimeMillis() / 1000
|
||||||
|
// val file = File(finalPath!!)
|
||||||
|
// downloadItem.format.filesize = if (file.exists()) file.length() else 0L
|
||||||
|
// val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPath, downloadItem.website, downloadItem.format)
|
||||||
|
// runBlocking {
|
||||||
|
// historyDao.insert(historyItem)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// runBlocking {
|
||||||
|
// dao.delete(downloadItem.id)
|
||||||
|
// }
|
||||||
|
}.onFailure {
|
||||||
|
if (it is YoutubeDL.CanceledException) {
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logDownloads && logFile.exists()){
|
||||||
|
logFile.appendText("${it.message}\n")
|
||||||
|
}
|
||||||
|
setProgressAsync(workDataOf("progress" to -1, "output" to it.message, "id" to itemId, "log" to logDownloads))
|
||||||
|
|
||||||
|
tempFileDir.delete()
|
||||||
|
|
||||||
|
Log.e(TAG, context.getString(R.string.failed_download), it)
|
||||||
|
notificationUtil.updateDownloadNotification(
|
||||||
|
itemId,
|
||||||
|
context.getString(R.string.failed_download), 0, 0, command.take(20),
|
||||||
|
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
|
||||||
|
)
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
|
||||||
|
}
|
||||||
|
@Throws(Exception::class)
|
||||||
|
private fun moveFile(originDir: File, downLocation: String, progress: (progress: Int) -> Unit) : String{
|
||||||
|
val fileUtil = FileUtil()
|
||||||
|
val path = fileUtil.moveFile(originDir, context, downLocation){ p ->
|
||||||
|
progress(p)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "DownloadWorker"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -12,4 +12,4 @@
|
||||||
android:fillColor="@color/icon_fg"
|
android:fillColor="@color/icon_fg"
|
||||||
android:strokeWidth="1"
|
android:strokeWidth="1"
|
||||||
android:fillType="evenOdd"/>
|
android:fillType="evenOdd"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
@ -57,11 +57,12 @@
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="10dp"
|
android:layout_marginHorizontal="10dp"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/cancelButton"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/rangeSlider">
|
app:layout_constraintTop_toBottomOf="@+id/rangeSlider">
|
||||||
|
|
||||||
|
|
@ -69,8 +70,8 @@
|
||||||
android:id="@+id/from_textinput"
|
android:id="@+id/from_textinput"
|
||||||
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:hint="@string/start"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content">
|
android:hint="@string/start">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
@ -94,8 +95,8 @@
|
||||||
android:id="@+id/to_textinput"
|
android:id="@+id/to_textinput"
|
||||||
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:hint="@string/end"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/end"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/colon">
|
app:layout_constraintStart_toEndOf="@+id/colon">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,18 @@
|
||||||
android:id="@+id/playlist_card_constraintLayout"
|
android:id="@+id/playlist_card_constraintLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_marginBottom="10dp"
|
android:layout_marginBottom="10dp"
|
||||||
|
android:checkable="true"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/download_card_view"
|
android:id="@+id/download_card_view"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_marginStart="10dp"
|
android:padding="10dp"
|
||||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
|
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:checkable="true"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
app:cardPreventCornerOverlap="true"
|
app:cardPreventCornerOverlap="true"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/checkBox"
|
app:layout_constraintEnd_toStartOf="@+id/checkBox"
|
||||||
|
|
|
||||||
|
|
@ -77,17 +77,60 @@
|
||||||
android:autoLink="all"
|
android:autoLink="all"
|
||||||
android:text="@string/ok"
|
android:text="@string/ok"
|
||||||
app:icon="@drawable/ic_check"
|
app:icon="@drawable/ic_check"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/bottomsheet_ok"
|
||||||
|
app:layout_constraintVertical_bias="0.0">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/from_textinput"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
||||||
|
android:layout_width="70dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
android:hint="@string/start">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/to_textinput"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.FilledBox"
|
||||||
|
android:layout_width="70dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/end"
|
||||||
|
app:errorEnabled="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/colon">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/downloadMultipleRecyclerview"
|
android:id="@+id/downloadMultipleRecyclerview"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:paddingTop="10dp"
|
|
||||||
tools:listitem="@layout/playlist_item"
|
tools:listitem="@layout/playlist_item"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue