mirror of https://github.com/deniscerri/ytdlnis
merge
commit
f4c4282188
@ -1,73 +1,136 @@
|
||||
package com.deniscerri.ytdl.database.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import com.deniscerri.ytdl.core.RuntimeManager
|
||||
import com.deniscerri.ytdl.database.DBManager
|
||||
import com.deniscerri.ytdl.database.dao.TerminalDao
|
||||
import com.deniscerri.ytdl.database.models.TerminalItem
|
||||
import com.deniscerri.ytdl.util.NotificationUtil
|
||||
import com.deniscerri.ytdl.work.download.TerminalDownloadWorker
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.TypedValue
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.deniscerri.ytdl.terminal.SessionService
|
||||
import com.deniscerri.ytdl.ui.more.terminal.TerminalActivity
|
||||
import com.deniscerri.ytdl.ui.more.terminal.TerminalBackEnd
|
||||
import com.google.android.material.R
|
||||
import com.deniscerri.ytdl.ui.more.terminal.virtualkeys.VirtualKeysListener
|
||||
import com.deniscerri.ytdl.ui.more.terminal.virtualkeys.VirtualKeysView
|
||||
import com.deniscerri.ytdl.ui.more.terminal.TerminalUtils
|
||||
import com.termux.view.TerminalView
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
|
||||
class TerminalViewModel(private val application: Application) : AndroidViewModel(application) {
|
||||
private val dbManager: DBManager = DBManager.getInstance(application)
|
||||
private val dao: TerminalDao = dbManager.terminalDao
|
||||
private val notificationUtil = NotificationUtil(application)
|
||||
fun getCount() : Int{
|
||||
return dao.getActiveTerminalsCount()
|
||||
}
|
||||
private var terminalViewRef = WeakReference<TerminalView>(null)
|
||||
private var virtualKeysViewRef = WeakReference<VirtualKeysView>(null)
|
||||
|
||||
fun getTerminals() : Flow<List<TerminalItem>> {
|
||||
return dao.getActiveTerminalDownloadsFlow()
|
||||
}
|
||||
val terminalView: TerminalView? get() = terminalViewRef.get()
|
||||
val virtualKeysView: VirtualKeysView? get() = virtualKeysViewRef.get()
|
||||
|
||||
fun getTerminal(id: Long) : Flow<TerminalItem?> {
|
||||
return dao.getActiveTerminalFlow(id)
|
||||
}
|
||||
fun setTerminalView(view: TerminalView?) { terminalViewRef = WeakReference(view) }
|
||||
fun setVirtualKeysView(view: VirtualKeysView?) { virtualKeysViewRef = WeakReference(view) }
|
||||
|
||||
suspend fun insert(item: TerminalItem) : Long {
|
||||
return dao.insert(item)
|
||||
fun setFont(typeface: Typeface) {
|
||||
TerminalUtils.typeface = typeface
|
||||
terminalView?.apply {
|
||||
setTypeface(typeface)
|
||||
onScreenUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun delete(id: Long) = CoroutineScope(Dispatchers.IO).launch{
|
||||
dao.delete(id)
|
||||
}
|
||||
fun changeSession(context: Context, sessionBinder: SessionService.SessionBinder, sessionId: String) {
|
||||
val terminal = terminalView ?: return
|
||||
val activity = context as? TerminalActivity ?: return
|
||||
val client = TerminalBackEnd(terminal, activity)
|
||||
|
||||
val session = sessionBinder.getSession(sessionId)
|
||||
?: sessionBinder.createSession(sessionId, client)
|
||||
|
||||
session.updateTerminalSessionClient(client)
|
||||
terminal.setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||
|
||||
val zoom = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getFloat("terminal_zoom", 14f).coerceIn(10f, 30f)
|
||||
terminal.setTextSize(zoom.toInt())
|
||||
terminal.setTypeface(TerminalUtils.typeface)
|
||||
|
||||
terminal.attachSession(session)
|
||||
terminal.setTerminalViewClient(client)
|
||||
|
||||
terminal.post {
|
||||
val typedValue = TypedValue()
|
||||
context.theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true)
|
||||
terminal.keepScreenOn = true
|
||||
terminal.requestFocus()
|
||||
terminal.isFocusableInTouchMode = true
|
||||
|
||||
fun startTerminalDownloadWorker(item: TerminalItem) = CoroutineScope(Dispatchers.IO).launch {
|
||||
val workRequest = OneTimeWorkRequestBuilder<TerminalDownloadWorker>()
|
||||
.setInputData(
|
||||
Data.Builder()
|
||||
.putInt("id", item.id.toInt())
|
||||
.putString("command", item.command)
|
||||
.build()
|
||||
)
|
||||
.addTag("terminal")
|
||||
.addTag(item.id.toString())
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(application).beginUniqueWork(
|
||||
item.id.toString(),
|
||||
ExistingWorkPolicy.KEEP,
|
||||
workRequest
|
||||
).enqueue()
|
||||
terminal.mEmulator?.mColors?.mCurrentColors?.apply {
|
||||
set(256, typedValue.data)
|
||||
set(257, TerminalUtils.getBackgroundColor(context))
|
||||
set(258, typedValue.data)
|
||||
}
|
||||
}
|
||||
|
||||
virtualKeysView?.apply {
|
||||
virtualKeysViewClient = terminal.mTermSession?.let { VirtualKeysListener(it) }
|
||||
}
|
||||
|
||||
sessionBinder.getService().currentSession.value = sessionId
|
||||
}
|
||||
|
||||
fun cancelTerminalDownload(id: Long) = CoroutineScope(Dispatchers.IO).launch{
|
||||
RuntimeManager.getInstance().destroyProcessById(id.toString())
|
||||
WorkManager.getInstance(application).cancelUniqueWork(id.toString())
|
||||
Thread.sleep(200)
|
||||
notificationUtil.cancelDownloadNotification(id.toInt())
|
||||
delete(id)
|
||||
var sessionBinder by mutableStateOf<SessionService.SessionBinder?>(null)
|
||||
private set
|
||||
|
||||
var isBound by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private val _isBoundState = MutableStateFlow(false)
|
||||
val isBoundState: StateFlow<Boolean> = _isBoundState
|
||||
|
||||
private val _serviceConnectedEvent = Channel<Unit>(Channel.BUFFERED)
|
||||
val serviceConnectedEvent = _serviceConnectedEvent.receiveAsFlow()
|
||||
|
||||
private val serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
sessionBinder = service as SessionService.SessionBinder
|
||||
isBound = true
|
||||
_isBoundState.value = true
|
||||
_serviceConnectedEvent.trySend(Unit)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
isBound = false
|
||||
sessionBinder = null
|
||||
_isBoundState.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun startAndBindService(context: Context) {
|
||||
val intent = Intent(context, SessionService::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
|
||||
fun unbindService(context: Context) {
|
||||
if (isBound) {
|
||||
context.unbindService(serviceConnection)
|
||||
isBound = false
|
||||
sessionBinder = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.deniscerri.ytdl.terminal
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
|
||||
class RunCommandService : Service() {
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,155 @@
|
||||
package com.deniscerri.ytdl.terminal
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.ui.more.terminal.MkSession
|
||||
import com.deniscerri.ytdl.ui.more.terminal.TerminalActivity
|
||||
import com.termux.terminal.TerminalSession
|
||||
import com.termux.terminal.TerminalSessionClient
|
||||
|
||||
class SessionService : Service() {
|
||||
private val sessions = hashMapOf<String, TerminalSession>()
|
||||
val sessionList = mutableStateMapOf<String, Int>()
|
||||
var currentSession = mutableStateOf("main1")
|
||||
|
||||
inner class SessionBinder : Binder() {
|
||||
fun getService(): SessionService = this@SessionService
|
||||
|
||||
fun terminateAllSessions() {
|
||||
sessions.values.forEach { it.finishIfRunning() }
|
||||
sessions.clear()
|
||||
sessionList.clear()
|
||||
updateNotification()
|
||||
}
|
||||
|
||||
fun createSession(
|
||||
id: String,
|
||||
client: TerminalSessionClient,
|
||||
): TerminalSession {
|
||||
return MkSession.createSession(
|
||||
context = this@SessionService,
|
||||
sessionClient = client,
|
||||
).also {
|
||||
sessions[id] = it
|
||||
sessionList[id] = 1
|
||||
updateNotification()
|
||||
}
|
||||
}
|
||||
|
||||
fun getSession(id: String): TerminalSession? = sessions[id]
|
||||
|
||||
fun terminateSession(id: String) {
|
||||
sessions[id]?.apply {
|
||||
if (emulator != null) {
|
||||
finishIfRunning()
|
||||
}
|
||||
}
|
||||
sessions.remove(id)
|
||||
sessionList.remove(id)
|
||||
if (sessions.isEmpty()) {
|
||||
stopSelf()
|
||||
} else {
|
||||
updateNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val binder = SessionBinder()
|
||||
private val notificationManager by lazy {
|
||||
getSystemService(NotificationManager::class.java)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder = binder
|
||||
|
||||
override fun onDestroy() {
|
||||
sessions.values.forEach { it.finishIfRunning() }
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createNotificationChannel()
|
||||
}
|
||||
val notification = createNotification()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(1, notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.action == "ACTION_EXIT") {
|
||||
sessions.values.forEach { it.finishIfRunning() }
|
||||
stopSelf()
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
private fun createNotification(): Notification {
|
||||
val intent = Intent(this, TerminalActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
val exitIntent = Intent(this, SessionService::class.java).apply {
|
||||
action = "ACTION_EXIT"
|
||||
}
|
||||
val exitPendingIntent = PendingIntent.getService(
|
||||
this, 1, exitIntent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("YTDLnis Terminal")
|
||||
.setContentText(getNotificationContentText())
|
||||
.setSmallIcon(R.drawable.ic_terminal)
|
||||
.setContentIntent(pendingIntent)
|
||||
.addAction(
|
||||
NotificationCompat.Action.Builder(
|
||||
null,
|
||||
"EXIT",
|
||||
exitPendingIntent
|
||||
).build()
|
||||
)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
private val CHANNEL_ID = "session_service_channel"
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel() {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Session Service",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Notification for Terminal Service"
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun updateNotification() {
|
||||
val notification = createNotification()
|
||||
notificationManager.notify(1, notification)
|
||||
}
|
||||
|
||||
private fun getNotificationContentText(): String {
|
||||
val count = sessions.size
|
||||
return if (count == 1) "1 session running" else "$count sessions running"
|
||||
}
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
package com.deniscerri.ytdl.ui.adapter
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.database.models.TerminalItem
|
||||
import com.deniscerri.ytdl.util.Extensions.popup
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
|
||||
class TerminalDownloadsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<TerminalItem?, TerminalDownloadsAdapter.ViewHolder>(AsyncDifferConfig.Builder(
|
||||
DIFF_CALLBACK
|
||||
).build()) {
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
private val activity: Activity
|
||||
private val sharedPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
this.onItemClickListener = onItemClickListener
|
||||
this.activity = activity
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val cardView: MaterialCardView
|
||||
|
||||
init {
|
||||
cardView = itemView.findViewById(R.id.active_download_card_view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val cardView = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.active_terminal_card, parent, false)
|
||||
return ViewHolder(cardView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
val card = holder.cardView
|
||||
card.popup()
|
||||
|
||||
card.tag = "${item!!.id}##card"
|
||||
|
||||
// PROGRESS BAR ----------------------------------------------------
|
||||
val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress)
|
||||
progressBar.tag = "${item.id}##progress"
|
||||
progressBar.progress = 0
|
||||
progressBar.isIndeterminate = true
|
||||
|
||||
// COMMAND ----------------------------------
|
||||
val itemTitle = card.findViewById<TextView>(R.id.title)
|
||||
itemTitle.text = item.command.trim()
|
||||
|
||||
//OUTPUT
|
||||
val output = card.findViewById<TextView>(R.id.output)
|
||||
output.tag = "${item.id}##output"
|
||||
|
||||
// STOP BUTTON ----------------------------------
|
||||
val stopButton = card.findViewById<MaterialButton>(R.id.active_download_stop)
|
||||
if (stopButton.hasOnClickListeners()) stopButton.setOnClickListener(null)
|
||||
stopButton.setOnClickListener {onItemClickListener.onCancelClick(item.id)}
|
||||
|
||||
card.setOnClickListener {
|
||||
onItemClickListener.onCardClick(item)
|
||||
}
|
||||
}
|
||||
interface OnItemClickListener {
|
||||
fun onCancelClick(itemID: Long)
|
||||
fun onCardClick(item: TerminalItem)
|
||||
}
|
||||
companion object {
|
||||
private val DIFF_CALLBACK: DiffUtil.ItemCallback<TerminalItem> = object : DiffUtil.ItemCallback<TerminalItem>() {
|
||||
override fun areItemsTheSame(oldItem: TerminalItem, newItem: TerminalItem): Boolean {
|
||||
val ranged = arrayListOf(oldItem.id, newItem.id)
|
||||
return ranged[0] == ranged[1]
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: TerminalItem, newItem: TerminalItem): Boolean {
|
||||
return oldItem.command == newItem.command
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package com.deniscerri.ytdl.ui.adapter
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.util.Extensions.popup
|
||||
import com.google.android.material.button.MaterialButton
|
||||
|
||||
class TerminalSessionsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<String, TerminalSessionsAdapter.ViewHolder>(AsyncDifferConfig.Builder(
|
||||
DIFF_CALLBACK
|
||||
).build()) {
|
||||
private val onItemClickListener: OnItemClickListener
|
||||
private val activity: Activity
|
||||
private val sharedPreferences: SharedPreferences
|
||||
|
||||
init {
|
||||
this.onItemClickListener = onItemClickListener
|
||||
this.activity = activity
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val cardView = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.terminal_session_card, parent, false)
|
||||
return ViewHolder(cardView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
val card = holder.itemView
|
||||
card.popup()
|
||||
card.tag = "${item!!}##card"
|
||||
|
||||
val sessionName = card.findViewById<TextView>(R.id.session_name)
|
||||
sessionName.text = item
|
||||
|
||||
// STOP BUTTON ----------------------------------
|
||||
val stopButton = card.findViewById<MaterialButton>(R.id.deleteSession)
|
||||
stopButton.setOnClickListener {onItemClickListener.onDeleteClick(item)}
|
||||
|
||||
sessionName.setOnClickListener {
|
||||
onItemClickListener.onCardClick(item)
|
||||
}
|
||||
}
|
||||
interface OnItemClickListener {
|
||||
fun onDeleteClick(sessionId: String)
|
||||
fun onCardClick(sessionId: String)
|
||||
}
|
||||
companion object {
|
||||
private val DIFF_CALLBACK: DiffUtil.ItemCallback<String> = object : DiffUtil.ItemCallback<String>() {
|
||||
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.view.KeyEvent
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.deniscerri.ytdl.App
|
||||
import com.deniscerri.ytdl.database.viewmodel.TerminalViewModel
|
||||
|
||||
object KeyShortcutHandler {
|
||||
|
||||
fun handle(keyCode: Int, event: KeyEvent, activity: TerminalActivity): Boolean {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(App.instance)
|
||||
for (action in ShortcutAction.entries) {
|
||||
val raw = preferences.getString(action.prefKey, action.default.serialize())!!
|
||||
val binding = ShortcutBinding.deserialize(raw)
|
||||
if (binding.matches(event)) {
|
||||
return dispatch(action, activity)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun dispatch(action: ShortcutAction, activity: TerminalActivity): Boolean {
|
||||
val terminalViewModel = ViewModelProvider(activity)[TerminalViewModel::class.java]
|
||||
|
||||
return when (action) {
|
||||
ShortcutAction.PASTE -> handlePaste(terminalViewModel)
|
||||
// ShortcutAction.NEW_SESSION -> handleNewSession(activity, terminalViewModel)
|
||||
// ShortcutAction.CLOSE_SESSION -> handleCloseSession(activity, terminalViewModel)
|
||||
// ShortcutAction.SWITCH_SESSION_PREV -> handleSwitchSession(activity, terminalViewModel, forward = false)
|
||||
// ShortcutAction.SWITCH_SESSION_NEXT -> handleSwitchSession(activity, terminalViewModel, forward = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handlePaste(viewModel: TerminalViewModel): Boolean {
|
||||
val clipboard: ClipboardManager = App.instance.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = clipboard.primaryClip
|
||||
if (clip != null) {
|
||||
val clipText = clip.getItemAt(0).text.toString()
|
||||
if (clipText.trim().isNotEmpty()) {
|
||||
viewModel.terminalView?.mEmulator?.paste(clipText)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun generateUniqueSessionId(activity: TerminalActivity): String {
|
||||
val binder = activity.terminalViewModel.sessionBinder ?: return ""
|
||||
val service = binder.getService()
|
||||
val existingIds = service.sessionList.keys.toList()
|
||||
var index = 1
|
||||
var newId: String
|
||||
do {
|
||||
newId = "main$index"
|
||||
index++
|
||||
} while (newId in existingIds)
|
||||
return newId
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,215 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal
|
||||
|
||||
import android.content.Context
|
||||
import com.anggrayudi.storage.file.child
|
||||
import com.deniscerri.ytdl.BuildConfig
|
||||
import com.deniscerri.ytdl.core.RuntimeManager
|
||||
import com.termux.terminal.TerminalEmulator
|
||||
import com.termux.terminal.TerminalSession
|
||||
import com.termux.terminal.TerminalSessionClient
|
||||
import java.io.File
|
||||
|
||||
object MkSession {
|
||||
fun createSession(
|
||||
context: Context,
|
||||
sessionClient: TerminalSessionClient,
|
||||
pendingCommand: PendingCommand? = null
|
||||
): TerminalSession {
|
||||
with(context) {
|
||||
val envVariables = mutableMapOf(
|
||||
"ANDROID_ART_ROOT" to System.getenv("ANDROID_ART_ROOT"),
|
||||
"ANDROID_DATA" to System.getenv("ANDROID_DATA"),
|
||||
"ANDROID_I18N_ROOT" to System.getenv("ANDROID_I18N_ROOT"),
|
||||
"ANDROID_ROOT" to System.getenv("ANDROID_ROOT"),
|
||||
"ANDROID_RUNTIME_ROOT" to System.getenv("ANDROID_RUNTIME_ROOT"),
|
||||
"ANDROID_TZDATA_ROOT" to System.getenv("ANDROID_TZDATA_ROOT"),
|
||||
"BOOTCLASSPATH" to System.getenv("BOOTCLASSPATH"),
|
||||
"DEX2OATBOOTCLASSPATH" to System.getenv("DEX2OATBOOTCLASSPATH"),
|
||||
"EXTERNAL_STORAGE" to System.getenv("EXTERNAL_STORAGE")
|
||||
)
|
||||
|
||||
val runtimeManager = RuntimeManager.getInstance()
|
||||
runtimeManager.assertInit()
|
||||
|
||||
val runtimeVariables = runtimeManager.getEnvironmentForTerminal()
|
||||
val ldPath = runtimeVariables["LD_LIBRARY_PATH"] ?: ""
|
||||
val pythonHome = runtimeVariables["PYTHONHOME"] ?: ""
|
||||
val sslCert = runtimeVariables["SSL_CERT_FILE"] ?: ""
|
||||
val openSslConf = runtimeVariables["OPENSSL_CONF"] ?: ""
|
||||
|
||||
val linker = if (File("/system/bin/linker64").exists()) "/system/bin/linker64" else "/system/bin/linker"
|
||||
|
||||
// Build shell FUNCTIONS instead of standalone executable wrapper scripts.
|
||||
// On Android 10+ (W^X enforcement), files written at runtime under app-private
|
||||
// storage (codeCacheDir, filesDir, etc.) cannot be mmap'd PROT_EXEC, so any
|
||||
// attempt to `execve()` a wrapper script written there fails with EACCES
|
||||
// ("Permission denied"), even with correct chmod bits. A file that is only
|
||||
// *read* (sourced by the shell) never hits that restriction, so we emit shell
|
||||
// functions into a single rc file and have `sh` source it via $ENV.
|
||||
fun shellFunction(name: String, commandToExec: String): String {
|
||||
return """
|
||||
|$name() {
|
||||
| LD_LIBRARY_PATH="$ldPath" \
|
||||
| PYTHONHOME="$pythonHome" \
|
||||
| SSL_CERT_FILE="$sslCert" \
|
||||
| OPENSSL_CONF="$openSslConf" \
|
||||
| $commandToExec "${'$'}@"
|
||||
|}
|
||||
|
|
||||
""".trimMargin()
|
||||
}
|
||||
|
||||
val rcBuilder = StringBuilder()
|
||||
|
||||
// mksh doesn't interpret bash-style \w / \u escapes in PS1 — it re-evaluates
|
||||
// PS1 as a normal parameter/command substitution each time it's displayed,
|
||||
// so embed $PWD directly. Colors are plain ANSI escapes; TERM is already
|
||||
// xterm-256color so they render fine in TerminalView.
|
||||
//
|
||||
// The ANSI codes must be wrapped in \x01 / \x02 (mksh's equivalent of bash's
|
||||
// \[ \[) so the line editor treats them as zero-width. Without this, the editor
|
||||
// miscounts the prompt's visual length and the cursor drifts to the wrong line
|
||||
// after running a command.
|
||||
val esc = "\u001b"
|
||||
val nonPrintStart = "\u0001"
|
||||
val nonPrintEnd = "\u0002"
|
||||
rcBuilder.append(
|
||||
"""
|
||||
|PS1='$nonPrintStart$esc[01;32m$nonPrintEnd${'$'}PWD$nonPrintStart$esc[00m$nonPrintEnd ${'$'} '
|
||||
|alias ls='ls --color=auto' 2>/dev/null
|
||||
|export CLICOLOR=1
|
||||
|export LSCOLORS=ExGxFxdxCxDxDxBxBxExEx
|
||||
|
|
||||
""".trimMargin()
|
||||
)
|
||||
|
||||
val executables = mapOf(
|
||||
"python" to runtimeManager.pythonLocation.executable,
|
||||
"ffmpeg" to runtimeManager.ffmpegLocation.executable,
|
||||
"deno" to runtimeManager.denoLocation.executable,
|
||||
"node" to runtimeManager.nodeLocation.executable,
|
||||
"qjs" to runtimeManager.quickJsLocation.executable,
|
||||
"aria2" to runtimeManager.aria2Location.executable,
|
||||
)
|
||||
|
||||
executables.forEach { (name, file) ->
|
||||
if (file.exists()) {
|
||||
val execCommand = if (file.name.endsWith(".so")) {
|
||||
"$linker \"${file.absolutePath}\""
|
||||
} else {
|
||||
"\"${file.absolutePath}\""
|
||||
}
|
||||
rcBuilder.append(shellFunction(name, execCommand))
|
||||
}
|
||||
}
|
||||
|
||||
val pythonBin = runtimeManager.pythonLocation.executable
|
||||
val ytdlpBin = runtimeManager.ytdlpPath
|
||||
if (pythonBin.exists() && ytdlpBin != null && ytdlpBin.exists()) {
|
||||
val pythonExec = if (pythonBin.name.endsWith(".so")) {
|
||||
"$linker \"${pythonBin.absolutePath}\""
|
||||
} else {
|
||||
"\"${pythonBin.absolutePath}\""
|
||||
}
|
||||
|
||||
val ytdlpExtraArgs = StringBuilder()
|
||||
if (runtimeManager.ffmpegLocation.isAvailable) {
|
||||
ytdlpExtraArgs.append(" --ffmpeg-location \"${runtimeManager.ffmpegLocation.executable.absolutePath}\"")
|
||||
}
|
||||
if (runtimeManager.nodeLocation.isAvailable) {
|
||||
ytdlpExtraArgs.append(" --js-runtimes \"node:${runtimeManager.nodeLocation.executable.absolutePath}\"")
|
||||
}
|
||||
if (runtimeManager.denoLocation.isAvailable) {
|
||||
ytdlpExtraArgs.append(" --js-runtimes \"deno:${runtimeManager.denoLocation.executable.absolutePath}\"")
|
||||
}
|
||||
if (runtimeManager.quickJsLocation.isAvailable) {
|
||||
ytdlpExtraArgs.append(" --js-runtimes \"quickjs:${runtimeManager.quickJsLocation.executable.absolutePath}\"")
|
||||
}
|
||||
|
||||
rcBuilder.append(
|
||||
shellFunction(
|
||||
"yt-dlp",
|
||||
"$pythonExec \"${ytdlpBin.absolutePath}\"$ytdlpExtraArgs"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val currentSystemPath = System.getenv("PATH") ?: "/system/bin"
|
||||
runtimeVariables["PATH"] = currentSystemPath
|
||||
envVariables.putAll(runtimeVariables)
|
||||
|
||||
val localDir = localDir()
|
||||
|
||||
val rcFile = localDir.child("shellrc")
|
||||
rcFile.writeText(
|
||||
rcBuilder.toString() +
|
||||
// Probe support in a subshell first: if `set -o multiline` is
|
||||
// unsupported by this shell build, POSIX allows the shell to exit
|
||||
// outright on a bad `set` option even mid-script. Running the probe
|
||||
// in a subshell means a failure there can't abort sourcing of this
|
||||
// rc file in the parent (interactive) shell — everything above this
|
||||
// line (functions, aliases, exports, PS1) is already safely loaded
|
||||
// by the time we get here regardless of the outcome.
|
||||
"(set -o multiline) >/dev/null 2>&1 && set -o multiline\n"
|
||||
)
|
||||
|
||||
|
||||
val env = mutableListOf(
|
||||
"ENV=${rcFile.absolutePath}",
|
||||
"PUBLIC_HOME=${getExternalFilesDir(null)?.absolutePath}",
|
||||
"COLORTERM=truecolor",
|
||||
"TERM=xterm-256color",
|
||||
"LANG=C.UTF-8",
|
||||
"DEBUG=${BuildConfig.DEBUG}",
|
||||
"PREFIX=${filesDir.parentFile!!.path}",
|
||||
"LINKER=$linker",
|
||||
"NATIVE_LIB_DIR=${applicationInfo.nativeLibraryDir}",
|
||||
"PKG=${packageName}",
|
||||
"PKG_PATH=${applicationInfo.sourceDir}",
|
||||
)
|
||||
|
||||
env.addAll(envVariables.map { "${it.key}=${it.value}" })
|
||||
|
||||
localDir.child("stat").apply {
|
||||
if (exists().not()) {
|
||||
writeText(TerminalUtils.stat)
|
||||
}
|
||||
}
|
||||
|
||||
localDir.child("vmstat").apply {
|
||||
if (exists().not()) {
|
||||
writeText(TerminalUtils.vmstat)
|
||||
}
|
||||
}
|
||||
|
||||
pendingCommand?.env?.let {
|
||||
env.addAll(it)
|
||||
}
|
||||
|
||||
val shell = pendingCommand?.shell ?: "/system/bin/sh"
|
||||
|
||||
return TerminalSession(
|
||||
shell,
|
||||
envVariables["HOME"],
|
||||
arrayOf(),
|
||||
env.toTypedArray(),
|
||||
TerminalEmulator.DEFAULT_TERMINAL_TRANSCRIPT_ROWS,
|
||||
sessionClient,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.localDir(): File {
|
||||
return File(filesDir.parentFile, "terminal_local").also {
|
||||
if (!it.exists()) {
|
||||
it.mkdirs()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class PendingCommand(
|
||||
val shell: String,
|
||||
val workingDir: String?,
|
||||
val env: List<String>?
|
||||
)
|
||||
@ -0,0 +1,140 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal
|
||||
|
||||
import android.view.KeyEvent
|
||||
|
||||
/**
|
||||
* Represents a configurable keyboard shortcut binding.
|
||||
* Stored as a string in SharedPreferences: "modifiers|keyCode"
|
||||
* e.g. "CTRL|SHIFT|54" for Ctrl+Shift+V
|
||||
*/
|
||||
data class ShortcutBinding(
|
||||
val ctrl: Boolean = false,
|
||||
val shift: Boolean = false,
|
||||
val alt: Boolean = false,
|
||||
val keyCode: Int = 0,
|
||||
) {
|
||||
/** Check if this binding is empty (no key assigned) */
|
||||
val isEmpty: Boolean get() = keyCode == 0
|
||||
|
||||
/** Check if a KeyEvent matches this binding */
|
||||
fun matches(event: KeyEvent): Boolean {
|
||||
if (isEmpty) return false
|
||||
return event.keyCode == keyCode
|
||||
&& event.isCtrlPressed == ctrl
|
||||
&& event.isShiftPressed == shift
|
||||
&& event.isAltPressed == alt
|
||||
}
|
||||
|
||||
/** Serialize to string for SharedPreferences storage */
|
||||
fun serialize(): String {
|
||||
if (isEmpty) return ""
|
||||
val parts = mutableListOf<String>()
|
||||
if (ctrl) parts.add("CTRL")
|
||||
if (shift) parts.add("SHIFT")
|
||||
if (alt) parts.add("ALT")
|
||||
parts.add(keyCode.toString())
|
||||
return parts.joinToString("|")
|
||||
}
|
||||
|
||||
/** Human-readable display string */
|
||||
fun toDisplayString(): String {
|
||||
if (isEmpty) return "Not set"
|
||||
val parts = mutableListOf<String>()
|
||||
if (ctrl) parts.add("Ctrl")
|
||||
if (shift) parts.add("Shift")
|
||||
if (alt) parts.add("Alt")
|
||||
parts.add(KeyEvent.keyCodeToString(keyCode)
|
||||
.removePrefix("KEYCODE_")
|
||||
.replace("_", " ")
|
||||
.lowercase()
|
||||
.replaceFirstChar { it.uppercase() })
|
||||
return parts.joinToString(" + ")
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Deserialize from SharedPreferences string */
|
||||
fun deserialize(value: String): ShortcutBinding {
|
||||
if (value.isBlank()) return ShortcutBinding()
|
||||
val parts = value.split("|")
|
||||
var ctrl = false
|
||||
var shift = false
|
||||
var alt = false
|
||||
var keyCode = 0
|
||||
for (part in parts) {
|
||||
when (part) {
|
||||
"CTRL" -> ctrl = true
|
||||
"SHIFT" -> shift = true
|
||||
"ALT" -> alt = true
|
||||
else -> keyCode = part.toIntOrNull() ?: 0
|
||||
}
|
||||
}
|
||||
return ShortcutBinding(ctrl, shift, alt, keyCode)
|
||||
}
|
||||
|
||||
/** Create from a KeyEvent (for capture dialog) */
|
||||
fun fromKeyEvent(event: KeyEvent): ShortcutBinding {
|
||||
return ShortcutBinding(
|
||||
ctrl = event.isCtrlPressed,
|
||||
shift = event.isShiftPressed,
|
||||
alt = event.isAltPressed,
|
||||
keyCode = event.keyCode,
|
||||
)
|
||||
}
|
||||
|
||||
/** Keys that should not be used as shortcut targets */
|
||||
private val RESERVED_KEY_CODES = setOf(
|
||||
KeyEvent.KEYCODE_HOME,
|
||||
KeyEvent.KEYCODE_BACK,
|
||||
KeyEvent.KEYCODE_APP_SWITCH,
|
||||
KeyEvent.KEYCODE_POWER,
|
||||
KeyEvent.KEYCODE_VOLUME_UP,
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN,
|
||||
KeyEvent.KEYCODE_VOLUME_MUTE,
|
||||
KeyEvent.KEYCODE_MENU,
|
||||
)
|
||||
|
||||
/** Modifier-only key codes (should not finalize a binding) */
|
||||
val MODIFIER_KEY_CODES = setOf(
|
||||
KeyEvent.KEYCODE_CTRL_LEFT,
|
||||
KeyEvent.KEYCODE_CTRL_RIGHT,
|
||||
KeyEvent.KEYCODE_SHIFT_LEFT,
|
||||
KeyEvent.KEYCODE_SHIFT_RIGHT,
|
||||
KeyEvent.KEYCODE_ALT_LEFT,
|
||||
KeyEvent.KEYCODE_ALT_RIGHT,
|
||||
KeyEvent.KEYCODE_META_LEFT,
|
||||
KeyEvent.KEYCODE_META_RIGHT,
|
||||
)
|
||||
|
||||
fun isReservedKey(keyCode: Int): Boolean = keyCode in RESERVED_KEY_CODES
|
||||
fun isModifierKey(keyCode: Int): Boolean = keyCode in MODIFIER_KEY_CODES
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines all available shortcut actions with their default bindings and preference keys.
|
||||
*/
|
||||
enum class ShortcutAction(
|
||||
val prefKey: String,
|
||||
val default: ShortcutBinding,
|
||||
) {
|
||||
PASTE(
|
||||
prefKey = "shortcut_paste",
|
||||
default = ShortcutBinding(ctrl = true, shift = true, keyCode = KeyEvent.KEYCODE_V),
|
||||
),
|
||||
// NEW_SESSION(
|
||||
// prefKey = "shortcut_new_session",
|
||||
// default = ShortcutBinding(ctrl = true, shift = true, keyCode = KeyEvent.KEYCODE_N),
|
||||
// ),
|
||||
// CLOSE_SESSION(
|
||||
// prefKey = "shortcut_close_session",
|
||||
// default = ShortcutBinding(ctrl = true, shift = true, keyCode = KeyEvent.KEYCODE_W),
|
||||
// ),
|
||||
// SWITCH_SESSION_PREV(
|
||||
// prefKey = "shortcut_switch_prev",
|
||||
// default = ShortcutBinding(ctrl = true, shift = true, keyCode = KeyEvent.KEYCODE_DPAD_LEFT),
|
||||
// ),
|
||||
// SWITCH_SESSION_NEXT(
|
||||
// prefKey = "shortcut_switch_next",
|
||||
// default = ShortcutBinding(ctrl = true, shift = true, keyCode = KeyEvent.KEYCODE_DPAD_RIGHT),
|
||||
// ),
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.deniscerri.ytdl.App
|
||||
import com.deniscerri.ytdl.database.viewmodel.TerminalViewModel
|
||||
import com.deniscerri.ytdl.ui.more.terminal.virtualkeys.SpecialButton
|
||||
import com.termux.shared.view.KeyboardUtils
|
||||
import com.termux.terminal.TerminalEmulator
|
||||
import com.termux.terminal.TerminalSession
|
||||
import com.termux.terminal.TerminalSessionClient
|
||||
import com.termux.view.TerminalView
|
||||
import com.termux.view.TerminalViewClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class TerminalBackEnd(
|
||||
private val terminal: TerminalView,
|
||||
private val activity: TerminalActivity,
|
||||
private val onFinished: (() -> Unit)? = null
|
||||
) : TerminalViewClient, TerminalSessionClient {
|
||||
|
||||
private val terminalViewModel by lazy { ViewModelProvider(activity)[TerminalViewModel::class.java] }
|
||||
|
||||
override fun onTextChanged(changedSession: TerminalSession) {
|
||||
terminal.onScreenUpdated()
|
||||
}
|
||||
|
||||
override fun onTitleChanged(changedSession: TerminalSession) {}
|
||||
override fun onSessionFinished(finishedSession: TerminalSession) {
|
||||
onFinished?.invoke()
|
||||
}
|
||||
|
||||
override fun onCopyTextToClipboard(session: TerminalSession, text: String) {
|
||||
val clipboard: ClipboardManager = activity.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("Terminal", text))
|
||||
}
|
||||
|
||||
override fun onPasteTextFromClipboard(session: TerminalSession?) {
|
||||
val clipboard: ClipboardManager = activity.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = clipboard.primaryClip
|
||||
if (clip != null) {
|
||||
val clipText = clip.getItemAt(0).text.toString()
|
||||
if (clipText.trim().isNotEmpty() && terminal.mEmulator != null) {
|
||||
terminal.mEmulator.paste(clipText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBell(session: TerminalSession) {
|
||||
return
|
||||
}
|
||||
|
||||
override fun onColorsChanged(session: TerminalSession) {}
|
||||
override fun onTerminalCursorStateChange(state: Boolean) {}
|
||||
override fun getTerminalCursorStyle(): Int = TerminalEmulator.DEFAULT_TERMINAL_CURSOR_STYLE
|
||||
|
||||
override fun logError(tag: String?, message: String?) { Log.e(tag ?: "Terminal", message ?: "") }
|
||||
override fun logWarn(tag: String?, message: String?) { Log.w(tag ?: "Terminal", message ?: "") }
|
||||
override fun logInfo(tag: String?, message: String?) { Log.i(tag ?: "Terminal", message ?: "") }
|
||||
override fun logDebug(tag: String?, message: String?) { Log.d(tag ?: "Terminal", message ?: "") }
|
||||
override fun logVerbose(tag: String?, message: String?) { Log.v(tag ?: "Terminal", message ?: "") }
|
||||
|
||||
override fun logStackTraceWithMessage(tag: String?, message: String?, e: Exception?) {
|
||||
Log.e(tag ?: "Terminal", message ?: "", e)
|
||||
}
|
||||
|
||||
override fun logStackTrace(tag: String?, e: Exception?) {
|
||||
Log.e(tag ?: "Terminal", "Stack trace", e)
|
||||
}
|
||||
|
||||
override fun onScale(scale: Float): Float {
|
||||
val fontScale = scale.coerceIn(11f, 45f)
|
||||
terminal.setTextSize(fontScale.toInt())
|
||||
return fontScale
|
||||
}
|
||||
|
||||
private val isHardwareKeyboardConnected: Boolean
|
||||
get() = Resources.getSystem().configuration.keyboard != Configuration.KEYBOARD_NOKEYS
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent) {
|
||||
if (!(isHardwareKeyboardConnected)) {
|
||||
showSoftInput()
|
||||
}
|
||||
}
|
||||
|
||||
override fun shouldBackButtonBeMappedToEscape(): Boolean = false
|
||||
override fun shouldEnforceCharBasedInput(): Boolean = true
|
||||
override fun shouldUseCtrlSpaceWorkaround(): Boolean = true
|
||||
override fun isTerminalViewSelected(): Boolean = true
|
||||
override fun copyModeChanged(copyMode: Boolean) {}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, e: KeyEvent, session: TerminalSession): Boolean {
|
||||
return KeyShortcutHandler.handle(keyCode, e, activity)
|
||||
}
|
||||
|
||||
override fun onKeyUp(keyCode: Int, e: KeyEvent): Boolean = false
|
||||
override fun onLongPress(event: MotionEvent): Boolean = false
|
||||
|
||||
override fun readControlKey(): Boolean =
|
||||
terminalViewModel.virtualKeysView?.readSpecialButton(SpecialButton.CTRL, true) == true
|
||||
|
||||
override fun readAltKey(): Boolean =
|
||||
terminalViewModel.virtualKeysView?.readSpecialButton(SpecialButton.ALT, true) == true
|
||||
|
||||
override fun readShiftKey(): Boolean =
|
||||
terminalViewModel.virtualKeysView?.readSpecialButton(SpecialButton.SHIFT, true) == true
|
||||
|
||||
override fun readFnKey(): Boolean =
|
||||
terminalViewModel.virtualKeysView?.readSpecialButton(SpecialButton.FN, true) == true
|
||||
|
||||
override fun onCodePoint(codePoint: Int, ctrlDown: Boolean, session: TerminalSession): Boolean = false
|
||||
|
||||
override fun onEmulatorSet() {
|
||||
if (terminal.mEmulator != null) {
|
||||
terminal.setTerminalCursorBlinkerState(true, true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSoftInput() {
|
||||
terminal.requestFocus()
|
||||
KeyboardUtils.showSoftKeyboard(App.instance,terminal)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,229 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.util.TypedValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import com.anggrayudi.storage.file.child
|
||||
import com.deniscerri.ytdl.App
|
||||
import com.google.android.material.color.MaterialColors
|
||||
|
||||
object TerminalUtils {
|
||||
var typeface: Typeface = Typeface.MONOSPACE
|
||||
|
||||
fun init(context: Context) {
|
||||
val fontFile = context.filesDir.child("font.ttf")
|
||||
if (fontFile.exists() && fontFile.canRead()) {
|
||||
typeface = Typeface.createFromFile(fontFile)
|
||||
}
|
||||
}
|
||||
|
||||
fun getViewColor(context: Context): Int = MaterialColors.getColor(context, android.R.attr.textColorPrimary, Color.BLACK)
|
||||
|
||||
fun getBackgroundColor(context: Context): Int = MaterialColors.getColor(context, com.google.android.material.R.attr.colorSurface, Color.BLACK)
|
||||
|
||||
fun getComposeColor(): androidx.compose.ui.graphics.Color =
|
||||
androidx.compose.ui.graphics.Color.White
|
||||
|
||||
const val stat = """
|
||||
cpu 1957 0 2877 93280 262 342 254 87 0 0
|
||||
cpu0 31 0 226 12027 82 10 4 9 0 0
|
||||
cpu1 45 0 664 11144 21 263 233 12 0 0
|
||||
cpu2 494 0 537 11283 27 10 3 8 0 0
|
||||
cpu3 359 0 234 11723 24 26 5 7 0 0
|
||||
cpu4 295 0 268 11772 10 12 2 12 0 0
|
||||
cpu5 270 0 251 11833 15 3 1 10 0 0
|
||||
cpu6 430 0 520 11386 30 8 1 12 0 0
|
||||
cpu7 30 0 172 12108 50 8 1 13 0 0
|
||||
intr 127541 38 290 0 0 0 0 4 0 1 0 0 25329 258 0 5777 277 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 140223
|
||||
btime 1680020856
|
||||
processes 772
|
||||
procs_running 2
|
||||
procs_blocked 0
|
||||
softirq 75663 0 5903 6 25375 10774 0 243 11685 0 21677
|
||||
"""
|
||||
|
||||
val vmstat = """
|
||||
nr_free_pages 1743136
|
||||
nr_zone_inactive_anon 179281
|
||||
nr_zone_active_anon 7183
|
||||
nr_zone_inactive_file 22858
|
||||
nr_zone_active_file 51328
|
||||
nr_zone_unevictable 642
|
||||
nr_zone_write_pending 0
|
||||
nr_mlock 0
|
||||
nr_bounce 0
|
||||
nr_zspages 0
|
||||
nr_free_cma 0
|
||||
numa_hit 1259626
|
||||
numa_miss 0
|
||||
numa_foreign 0
|
||||
numa_interleave 720
|
||||
numa_local 1259626
|
||||
numa_other 0
|
||||
nr_inactive_anon 179281
|
||||
nr_active_anon 7183
|
||||
nr_inactive_file 22858
|
||||
nr_active_file 51328
|
||||
nr_unevictable 642
|
||||
nr_slab_reclaimable 8091
|
||||
nr_slab_unreclaimable 7804
|
||||
nr_isolated_anon 0
|
||||
nr_isolated_file 0
|
||||
workingset_nodes 0
|
||||
workingset_refault_anon 0
|
||||
workingset_refault_file 0
|
||||
workingset_activate_anon 0
|
||||
workingset_activate_file 0
|
||||
workingset_restore_anon 0
|
||||
workingset_restore_file 0
|
||||
workingset_nodereclaim 0
|
||||
nr_anon_pages 7723
|
||||
nr_mapped 8905
|
||||
nr_file_pages 253569
|
||||
nr_dirty 0
|
||||
nr_writeback 0
|
||||
nr_writeback_temp 0
|
||||
nr_shmem 178741
|
||||
nr_shmem_hugepages 0
|
||||
nr_shmem_pmdmapped 0
|
||||
nr_file_hugepages 0
|
||||
nr_file_pmdmapped 0
|
||||
nr_anon_transparent_hugepages 1
|
||||
nr_vmscan_write 0
|
||||
nr_vmscan_immediate_reclaim 0
|
||||
nr_dirtied 0
|
||||
nr_written 0
|
||||
nr_throttled_written 0
|
||||
nr_kernel_misc_reclaimable 0
|
||||
nr_foll_pin_acquired 0
|
||||
nr_foll_pin_released 0
|
||||
nr_kernel_stack 2780
|
||||
nr_page_table_pages 344
|
||||
nr_sec_page_table_pages 0
|
||||
nr_swapcached 0
|
||||
pgpromote_success 0
|
||||
pgpromote_candidate 0
|
||||
nr_dirty_threshold 356564
|
||||
nr_dirty_background_threshold 178064
|
||||
pgpgin 890508
|
||||
pgpgout 0
|
||||
pswpin 0
|
||||
pswpout 0
|
||||
pgalloc_dma 272
|
||||
pgalloc_dma32 261
|
||||
pgalloc_normal 1328079
|
||||
pgalloc_movable 0
|
||||
pgalloc_device 0
|
||||
allocstall_dma 0
|
||||
allocstall_dma32 0
|
||||
allocstall_normal 0
|
||||
allocstall_movable 0
|
||||
allocstall_device 0
|
||||
pgskip_dma 0
|
||||
pgskip_dma32 0
|
||||
pgskip_normal 0
|
||||
pgskip_movable 0
|
||||
pgskip_device 0
|
||||
pgfree 3077011
|
||||
pgactivate 0
|
||||
pgdeactivate 0
|
||||
pglazyfree 0
|
||||
pgfault 176973
|
||||
pgmajfault 488
|
||||
pglazyfreed 0
|
||||
pgrefill 0
|
||||
pgreuse 19230
|
||||
pgsteal_kswapd 0
|
||||
pgsteal_direct 0
|
||||
pgsteal_khugepaged 0
|
||||
pgdemote_kswapd 0
|
||||
pgdemote_direct 0
|
||||
pgdemote_khugepaged 0
|
||||
pgscan_kswapd 0
|
||||
pgscan_direct 0
|
||||
pgscan_khugepaged 0
|
||||
pgscan_direct_throttle 0
|
||||
pgscan_anon 0
|
||||
pgscan_file 0
|
||||
pgsteal_anon 0
|
||||
pgsteal_file 0
|
||||
zone_reclaim_failed 0
|
||||
pginodesteal 0
|
||||
slabs_scanned 0
|
||||
kswapd_inodesteal 0
|
||||
kswapd_low_wmark_hit_quickly 0
|
||||
kswapd_high_wmark_hit_quickly 0
|
||||
pageoutrun 0
|
||||
pgrotated 0
|
||||
drop_pagecache 0
|
||||
drop_slab 0
|
||||
oom_kill 0
|
||||
numa_pte_updates 0
|
||||
numa_huge_pte_updates 0
|
||||
numa_hint_faults 0
|
||||
numa_hint_faults_local 0
|
||||
numa_pages_migrated 0
|
||||
pgmigrate_success 0
|
||||
pgmigrate_fail 0
|
||||
thp_migration_success 0
|
||||
thp_migration_fail 0
|
||||
thp_migration_split 0
|
||||
compact_migrate_scanned 0
|
||||
compact_free_scanned 0
|
||||
compact_isolated 0
|
||||
compact_stall 0
|
||||
compact_fail 0
|
||||
compact_success 0
|
||||
compact_daemon_wake 0
|
||||
compact_daemon_migrate_scanned 0
|
||||
compact_daemon_free_scanned 0
|
||||
htlb_buddy_alloc_success 0
|
||||
htlb_buddy_alloc_fail 0
|
||||
cma_alloc_success 0
|
||||
cma_alloc_fail 0
|
||||
unevictable_pgs_culled 27002
|
||||
unevictable_pgs_scanned 0
|
||||
unevictable_pgs_rescued 744
|
||||
unevictable_pgs_mlocked 744
|
||||
unevictable_pgs_munlocked 744
|
||||
unevictable_pgs_cleared 0
|
||||
unevictable_pgs_stranded 0
|
||||
thp_fault_alloc 13
|
||||
thp_fault_fallback 0
|
||||
thp_fault_fallback_charge 0
|
||||
thp_collapse_alloc 4
|
||||
thp_collapse_alloc_failed 0
|
||||
thp_file_alloc 0
|
||||
thp_file_fallback 0
|
||||
thp_file_fallback_charge 0
|
||||
thp_file_mapped 0
|
||||
thp_split_page 0
|
||||
thp_split_page_failed 0
|
||||
thp_deferred_split_page 1
|
||||
thp_split_pmd 1
|
||||
thp_scan_exceed_none_pte 0
|
||||
thp_scan_exceed_swap_pte 0
|
||||
thp_scan_exceed_share_pte 0
|
||||
thp_split_pud 0
|
||||
thp_zero_page_alloc 0
|
||||
thp_zero_page_alloc_failed 0
|
||||
thp_swpout 0
|
||||
thp_swpout_fallback 0
|
||||
balloon_inflate 0
|
||||
balloon_deflate 0
|
||||
balloon_migrate 0
|
||||
swap_ra 0
|
||||
swap_ra_hit 0
|
||||
ksm_swpin_copy 0
|
||||
cow_ksm 0
|
||||
zswpin 0
|
||||
zswpout 0
|
||||
direct_map_level2_splits 29
|
||||
direct_map_level3_splits 0
|
||||
nr_unstable 0
|
||||
""".trimIndent()
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal.virtualkeys
|
||||
|
||||
|
||||
/** The {@link Class} that implements special buttons for {@link VirtualKeysView}. */
|
||||
class SpecialButton(
|
||||
/** The special button key. */
|
||||
val key: String
|
||||
) {
|
||||
/** Get [.key] for this [SpecialButton]. */
|
||||
|
||||
/**
|
||||
* Initialize a [SpecialButton].
|
||||
*
|
||||
* @param key The unique key name for the special button. The key is registered in [.map]
|
||||
* with which the [SpecialButton] can be retrieved via a call to [ ][.valueOf].
|
||||
*/
|
||||
init {
|
||||
map.put(key, this)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return key
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val map = HashMap<String?, SpecialButton?>()
|
||||
|
||||
var CTRL: SpecialButton = SpecialButton("CTRL")
|
||||
val ALT: SpecialButton = SpecialButton("ALT")
|
||||
val SHIFT: SpecialButton = SpecialButton("SHIFT")
|
||||
val FN: SpecialButton = SpecialButton("FN")
|
||||
|
||||
/**
|
||||
* Get the [SpecialButton] for `key`.
|
||||
*
|
||||
* @param key The unique key name for the special button.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun valueOf(key: String?): SpecialButton? {
|
||||
return map.get(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal.virtualkeys;
|
||||
|
||||
import android.widget.Button;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** The {@link Class} that maintains a state of a {@link SpecialButton} */
|
||||
public class SpecialButtonState {
|
||||
|
||||
/** If special button has been created for the {@link VirtualKeysView}. */
|
||||
boolean isCreated = false;
|
||||
/** If special button is active. */
|
||||
boolean isActive = false;
|
||||
/**
|
||||
* If special button is locked due to long hold on it and should not be deactivated if its state
|
||||
* is read.
|
||||
*/
|
||||
boolean isLocked = false;
|
||||
|
||||
List<Button> buttons = new ArrayList<>();
|
||||
|
||||
VirtualKeysView mVirtualKeysView;
|
||||
|
||||
/**
|
||||
* Initialize a {@link SpecialButtonState} to maintain state of a {@link SpecialButton}.
|
||||
*
|
||||
* @param extraKeysView The {@link VirtualKeysView} instance in which the {@link SpecialButton} is
|
||||
* to be registered.
|
||||
*/
|
||||
public SpecialButtonState(VirtualKeysView extraKeysView) {
|
||||
mVirtualKeysView = extraKeysView;
|
||||
}
|
||||
|
||||
/** Set {@link #isCreated}. */
|
||||
public void setIsCreated(boolean value) {
|
||||
isCreated = value;
|
||||
}
|
||||
|
||||
/** Set {@link #isActive}. */
|
||||
public void setIsActive(boolean value) {
|
||||
isActive = value;
|
||||
for (Button button : buttons) {
|
||||
button.setTextColor(
|
||||
value
|
||||
? mVirtualKeysView.getButtonActiveTextColor()
|
||||
: mVirtualKeysView.getButtonTextColor());
|
||||
}
|
||||
}
|
||||
|
||||
/** Set {@link #isLocked}. */
|
||||
public void setIsLocked(boolean value) {
|
||||
isLocked = value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal.virtualkeys
|
||||
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import com.termux.terminal.TerminalSession
|
||||
|
||||
class VirtualKeyClient(
|
||||
private val session: TerminalSession?,
|
||||
private val virtualKeysView: VirtualKeysView? = null
|
||||
) : VirtualKeysView.IVirtualKeysView {
|
||||
|
||||
companion object {
|
||||
private val KEY_ESCAPE_SEQUENCES = mapOf(
|
||||
"ESC" to "\u001B",
|
||||
"TAB" to "\u0009",
|
||||
"HOME" to "\u001B[H",
|
||||
"UP" to "\u001B[A",
|
||||
"DOWN" to "\u001B[B",
|
||||
"LEFT" to "\u001B[D",
|
||||
"RIGHT" to "\u001B[C",
|
||||
"PGUP" to "\u001B[5~",
|
||||
"PGDN" to "\u001B[6~",
|
||||
"END" to "\u001B[4~",
|
||||
"ENTER" to "\u000D",
|
||||
"DRAWER" to ""
|
||||
)
|
||||
}
|
||||
|
||||
override fun onVirtualKeyButtonClick(
|
||||
view: View,
|
||||
buttonInfo: VirtualKeyButton,
|
||||
button: Button
|
||||
) {
|
||||
val activeSession = session ?: return
|
||||
val rawKey = buttonInfo.key.takeIf { it.isNotEmpty() } ?: return
|
||||
|
||||
// Read active modifier states if VirtualKeysView context is available
|
||||
val isCtrlActive = virtualKeysView?.readSpecialButton(SpecialButton.CTRL, true) == true
|
||||
val isAltActive = virtualKeysView?.readSpecialButton(SpecialButton.ALT, true) == true
|
||||
|
||||
var payload = KEY_ESCAPE_SEQUENCES[rawKey] ?: rawKey
|
||||
|
||||
if (payload.isEmpty()) return
|
||||
|
||||
// Handle CTRL modifier transformations (e.g. CTRL + c -> ASCII 0x03)
|
||||
if (isCtrlActive && payload.length == 1) {
|
||||
val char = payload[0]
|
||||
if (char in 'a'..'z') {
|
||||
payload = (char.code - 'a'.code + 1).toChar().toString()
|
||||
} else if (char in 'A'..'Z') {
|
||||
payload = (char.code - 'A'.code + 1).toChar().toString()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ALT modifier transformations (prepends ESC prefix)
|
||||
if (isAltActive) {
|
||||
payload = "\u001B$payload"
|
||||
}
|
||||
|
||||
activeSession.write(payload)
|
||||
}
|
||||
|
||||
override fun performVirtualKeyButtonHapticFeedback(
|
||||
view: View,
|
||||
buttonInfo: VirtualKeyButton,
|
||||
button: Button
|
||||
): Boolean {
|
||||
// Return false so VirtualKeysView handles standard system haptic feedback
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,168 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal.virtualkeys;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class VirtualKeyButton {
|
||||
|
||||
/**
|
||||
* The key name for the name of the extra key if using a dict to define the extra key. {key: name,
|
||||
* ...}
|
||||
*/
|
||||
public static final String KEY_KEY_NAME = "key";
|
||||
|
||||
/**
|
||||
* The key name for the macro value of the extra key if using a dict to define the extra key.
|
||||
* {macro: value, ...}
|
||||
*/
|
||||
public static final String KEY_MACRO = "macro";
|
||||
|
||||
/**
|
||||
* The key name for the alternate display name of the extra key if using a dict to define the
|
||||
* extra key. {display: name, ...}
|
||||
*/
|
||||
public static final String KEY_DISPLAY_NAME = "display";
|
||||
|
||||
/**
|
||||
* The key name for the nested dict to define popup extra key info if using a dict to define the
|
||||
* extra key. {popup: {key: name, ...}, ...}
|
||||
*/
|
||||
public static final String KEY_POPUP = "popup";
|
||||
|
||||
/**
|
||||
* The key that will be sent to the terminal, either a control character, like defined in {@link
|
||||
* VirtualKeysConstants#PRIMARY_KEY_CODES_FOR_STRINGS} (LEFT, RIGHT, PGUP...) or some text.
|
||||
*/
|
||||
private final String key;
|
||||
|
||||
/** If the key is a macro, i.e. a sequence of keys separated by space. */
|
||||
private final boolean macro;
|
||||
|
||||
/** The text that will be displayed on the button. */
|
||||
private final String display;
|
||||
|
||||
/**
|
||||
* The {@link VirtualKeyButton} containing the information of the popup button (triggered by swipe
|
||||
* up).
|
||||
*/
|
||||
@Nullable private final VirtualKeyButton popup;
|
||||
|
||||
/**
|
||||
* Initialize a {@link VirtualKeyButton}.
|
||||
*
|
||||
* @param config The {@link JSONObject} containing the info to create the {@link
|
||||
* VirtualKeyButton}.
|
||||
* @param extraKeyDisplayMap The {@link VirtualKeysConstants.VirtualKeyDisplayMap} that defines
|
||||
* the display text mapping for the keys if a custom value is not defined by {@link
|
||||
* #KEY_DISPLAY_NAME}.
|
||||
* @param extraKeyAliasMap The {@link VirtualKeysConstants.VirtualKeyDisplayMap} that defines the
|
||||
* aliases for the actual key names.
|
||||
*/
|
||||
public VirtualKeyButton(
|
||||
@NonNull JSONObject config,
|
||||
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyDisplayMap,
|
||||
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyAliasMap)
|
||||
throws JSONException {
|
||||
this(config, null, extraKeyDisplayMap, extraKeyAliasMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a {@link VirtualKeyButton}.
|
||||
*
|
||||
* @param config The {@link JSONObject} containing the info to create the {@link
|
||||
* VirtualKeyButton}.
|
||||
* @param popup The {@link VirtualKeyButton} optional {@link #popup} button.
|
||||
* @param extraKeyDisplayMap The {@link VirtualKeysConstants.VirtualKeyDisplayMap} that defines
|
||||
* the display text mapping for the keys if a custom value is not defined by {@link
|
||||
* #KEY_DISPLAY_NAME}.
|
||||
* @param extraKeyAliasMap The {@link VirtualKeysConstants.VirtualKeyDisplayMap} that defines the
|
||||
* aliases for the actual key names.
|
||||
*/
|
||||
public VirtualKeyButton(
|
||||
@NonNull JSONObject config,
|
||||
@Nullable VirtualKeyButton popup,
|
||||
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyDisplayMap,
|
||||
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyAliasMap)
|
||||
throws JSONException {
|
||||
String keyFromConfig = getStringFromJson(config, KEY_KEY_NAME);
|
||||
String macroFromConfig = getStringFromJson(config, KEY_MACRO);
|
||||
String[] keys;
|
||||
if (keyFromConfig != null && macroFromConfig != null) {
|
||||
throw new JSONException(
|
||||
"Both key and macro can't be set for the same key. key: \""
|
||||
+ keyFromConfig
|
||||
+ "\", macro: \""
|
||||
+ macroFromConfig
|
||||
+ "\"");
|
||||
} else if (keyFromConfig != null) {
|
||||
keys = new String[] {keyFromConfig};
|
||||
this.macro = false;
|
||||
} else if (macroFromConfig != null) {
|
||||
keys = macroFromConfig.split(" ");
|
||||
this.macro = true;
|
||||
} else {
|
||||
throw new JSONException("All keys have to specify either key or macro");
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
keys[i] = replaceAlias(extraKeyAliasMap, keys[i]);
|
||||
}
|
||||
|
||||
this.key = TextUtils.join(" ", keys);
|
||||
|
||||
String displayFromConfig = getStringFromJson(config, KEY_DISPLAY_NAME);
|
||||
if (displayFromConfig != null) {
|
||||
this.display = displayFromConfig;
|
||||
} else {
|
||||
this.display =
|
||||
Arrays.stream(keys)
|
||||
.map(key -> extraKeyDisplayMap.get(key, key))
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
this.popup = popup;
|
||||
}
|
||||
|
||||
public String getStringFromJson(@NonNull JSONObject config, @NonNull String key) {
|
||||
try {
|
||||
return config.getString(key);
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Replace the alias with its actual key name if found in extraKeyAliasMap. */
|
||||
public static String replaceAlias(
|
||||
@NonNull VirtualKeysConstants.VirtualKeyDisplayMap extraKeyAliasMap, String key) {
|
||||
return extraKeyAliasMap.get(key, key);
|
||||
}
|
||||
|
||||
/** Get {@link #key}. */
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/** Check whether a {@link #macro} is defined or not. */
|
||||
public boolean isMacro() {
|
||||
return macro;
|
||||
}
|
||||
|
||||
/** Get {@link #display}. */
|
||||
public String getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
/** Get {@link #popup}. */
|
||||
@Nullable
|
||||
public VirtualKeyButton getPopup() {
|
||||
return popup;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,245 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal.virtualkeys;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class VirtualKeysConstants {
|
||||
|
||||
/** Aliases for the keys */
|
||||
public static final VirtualKeyDisplayMap CONTROL_CHARS_ALIASES =
|
||||
new VirtualKeyDisplayMap() {
|
||||
{
|
||||
put("ESCAPE", "ESC");
|
||||
put("CONTROL", "CTRL");
|
||||
put("SHFT", "SHIFT");
|
||||
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see
|
||||
// the
|
||||
// difference
|
||||
put("FUNCTION", "FN");
|
||||
// no alias for ALT
|
||||
|
||||
// Directions are sometimes written as first and last letter for brevety
|
||||
put("LT", "LEFT");
|
||||
put("RT", "RIGHT");
|
||||
put("DN", "DOWN");
|
||||
// put("UP", "UP"); well, "UP" is already two letters
|
||||
|
||||
put("PAGEUP", "PGUP");
|
||||
put("PAGE_UP", "PGUP");
|
||||
put("PAGE UP", "PGUP");
|
||||
put("PAGE-UP", "PGUP");
|
||||
|
||||
// no alias for HOME
|
||||
// no alias for END
|
||||
|
||||
put("PAGEDOWN", "PGDN");
|
||||
put("PAGE_DOWN", "PGDN");
|
||||
put("PAGE-DOWN", "PGDN");
|
||||
|
||||
put("DELETE", "DEL");
|
||||
put("BACKSPACE", "BKSP");
|
||||
|
||||
// easier for writing in termux.properties
|
||||
put("BACKSLASH", "\\");
|
||||
put("QUOTE", "\"");
|
||||
put("APOSTROPHE", "'");
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Defines the repetitive keys that can be passed to {@link
|
||||
* VirtualKeysView#setRepetitiveKeys(List)}.
|
||||
*/
|
||||
public static List<String> PRIMARY_REPETITIVE_KEYS =
|
||||
Arrays.asList("UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL");
|
||||
/** Defines the {@link KeyEvent} for common keys. */
|
||||
public static Map<String, Integer> PRIMARY_KEY_CODES_FOR_STRINGS =
|
||||
new HashMap<String, Integer>() {
|
||||
{
|
||||
put("SPACE", KeyEvent.KEYCODE_SPACE);
|
||||
put("ESC", KeyEvent.KEYCODE_ESCAPE);
|
||||
put("TAB", KeyEvent.KEYCODE_TAB);
|
||||
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
|
||||
put("END", KeyEvent.KEYCODE_MOVE_END);
|
||||
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
|
||||
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
|
||||
put("INS", KeyEvent.KEYCODE_INSERT);
|
||||
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
|
||||
put("BKSP", KeyEvent.KEYCODE_DEL);
|
||||
put("UP", KeyEvent.KEYCODE_DPAD_UP);
|
||||
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
|
||||
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
||||
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
||||
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
||||
put("F1", KeyEvent.KEYCODE_F1);
|
||||
put("F2", KeyEvent.KEYCODE_F2);
|
||||
put("F3", KeyEvent.KEYCODE_F3);
|
||||
put("F4", KeyEvent.KEYCODE_F4);
|
||||
put("F5", KeyEvent.KEYCODE_F5);
|
||||
put("F6", KeyEvent.KEYCODE_F6);
|
||||
put("F7", KeyEvent.KEYCODE_F7);
|
||||
put("F8", KeyEvent.KEYCODE_F8);
|
||||
put("F9", KeyEvent.KEYCODE_F9);
|
||||
put("F10", KeyEvent.KEYCODE_F10);
|
||||
put("F11", KeyEvent.KEYCODE_F11);
|
||||
put("F12", KeyEvent.KEYCODE_F12);
|
||||
}
|
||||
};
|
||||
|
||||
public static class VirtualKeyDisplayMap extends CleverMap<String, String> {}
|
||||
|
||||
/*
|
||||
* Multiple maps are available to quickly change
|
||||
* the style of the keys.
|
||||
*/
|
||||
|
||||
public static class EXTRA_KEY_DISPLAY_MAPS {
|
||||
/** Keys are displayed in a natural looking way, like "→" for "RIGHT" */
|
||||
public static final VirtualKeyDisplayMap CLASSIC_ARROWS_DISPLAY =
|
||||
new VirtualKeyDisplayMap() {
|
||||
{
|
||||
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
|
||||
put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW
|
||||
put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW
|
||||
put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW
|
||||
put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW
|
||||
}
|
||||
};
|
||||
|
||||
public static final VirtualKeyDisplayMap WELL_KNOWN_CHARACTERS_DISPLAY =
|
||||
new VirtualKeyDisplayMap() {
|
||||
{
|
||||
// well known characters // https://en.wikipedia.org/wiki/{Enter_key,
|
||||
// Tab_key,
|
||||
// Delete_key}
|
||||
put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
|
||||
put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO
|
||||
// BAR
|
||||
put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to
|
||||
// understand
|
||||
put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to
|
||||
// understand
|
||||
put("DRAWER", "☰"); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to
|
||||
// understand
|
||||
put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand
|
||||
put("PASTE", "⎘"); // U+2398
|
||||
}
|
||||
};
|
||||
|
||||
public static final VirtualKeyDisplayMap LESS_KNOWN_CHARACTERS_DISPLAY =
|
||||
new VirtualKeyDisplayMap() {
|
||||
{
|
||||
// https://en.wikipedia.org/wiki/{Home_key, End_key,
|
||||
// Page_Up_and_Page_Down_keys}
|
||||
// home key can mean "goto the beginning of line" or "goto first page"
|
||||
// depending on
|
||||
// context, hence the diagonal
|
||||
put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
|
||||
put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
|
||||
put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will
|
||||
// do the trick
|
||||
put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW
|
||||
// will do the trick
|
||||
}
|
||||
};
|
||||
|
||||
public static final VirtualKeyDisplayMap ARROW_TRIANGLE_VARIATION_DISPLAY =
|
||||
new VirtualKeyDisplayMap() {
|
||||
{
|
||||
// alternative to classic arrow keys
|
||||
put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
|
||||
put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
|
||||
put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
|
||||
put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
|
||||
}
|
||||
};
|
||||
|
||||
public static final VirtualKeyDisplayMap NOT_KNOWN_ISO_CHARACTERS =
|
||||
new VirtualKeyDisplayMap() {
|
||||
{
|
||||
// Control chars that are more clear as text //
|
||||
// https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key,
|
||||
// Esc_key}
|
||||
// put("FN", "FN"); // no ISO character exists
|
||||
put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people
|
||||
// and never printed
|
||||
// on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation,
|
||||
// and
|
||||
// "^" for terminal app and mac is often used
|
||||
put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown
|
||||
// to people and
|
||||
// only printed as the Option key "⌥" on Mac computer
|
||||
put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW"
|
||||
// is unknown to
|
||||
// people and not often printed on computers
|
||||
}
|
||||
};
|
||||
|
||||
public static final VirtualKeyDisplayMap NICER_LOOKING_DISPLAY =
|
||||
new VirtualKeyDisplayMap() {
|
||||
{
|
||||
// nicer looking for most cases
|
||||
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||
}
|
||||
};
|
||||
|
||||
/** Full Iso */
|
||||
public static final VirtualKeyDisplayMap FULL_ISO_CHAR_DISPLAY =
|
||||
new VirtualKeyDisplayMap() {
|
||||
{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
putAll(WELL_KNOWN_CHARACTERS_DISPLAY);
|
||||
putAll(LESS_KNOWN_CHARACTERS_DISPLAY); // NEW
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
putAll(NOT_KNOWN_ISO_CHARACTERS); // NEW
|
||||
}
|
||||
};
|
||||
|
||||
/** Only arrows */
|
||||
public static final VirtualKeyDisplayMap ARROWS_ONLY_CHAR_DISPLAY =
|
||||
new VirtualKeyDisplayMap() {
|
||||
{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
// putAll(wellKnownCharactersDisplay); // REMOVED
|
||||
// putAll(lessKnownCharactersDisplay); // REMOVED
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
}
|
||||
};
|
||||
|
||||
/** Classic symbols and less known symbols */
|
||||
public static final VirtualKeyDisplayMap LOTS_OF_ARROWS_CHAR_DISPLAY =
|
||||
new VirtualKeyDisplayMap() {
|
||||
{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
putAll(WELL_KNOWN_CHARACTERS_DISPLAY);
|
||||
putAll(LESS_KNOWN_CHARACTERS_DISPLAY); // NEW
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
}
|
||||
};
|
||||
|
||||
/** Some classic symbols everybody knows */
|
||||
public static final VirtualKeyDisplayMap DEFAULT_CHAR_DISPLAY =
|
||||
new VirtualKeyDisplayMap() {
|
||||
{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
putAll(WELL_KNOWN_CHARACTERS_DISPLAY);
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
// all other characters are displayed as themselves
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* HashMap that implements Python dict.get(key, default) function. Default java.util .get(key) is
|
||||
* then the same as .get(key, null);
|
||||
*/
|
||||
static class CleverMap<K, V> extends HashMap<K, V> {
|
||||
V get(K key, V defaultValue) {
|
||||
if (containsKey(key)) return get(key);
|
||||
else return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal.virtualkeys
|
||||
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import com.termux.terminal.TerminalSession
|
||||
|
||||
class VirtualKeysListener(
|
||||
private val session: TerminalSession?,
|
||||
private val virtualKeysView: VirtualKeysView? = null
|
||||
) : VirtualKeysView.IVirtualKeysView {
|
||||
|
||||
companion object {
|
||||
private val KEY_ESCAPE_SEQUENCES = mapOf(
|
||||
"UP" to "\u001B[A",
|
||||
"DOWN" to "\u001B[B",
|
||||
"LEFT" to "\u001B[D",
|
||||
"RIGHT" to "\u001B[C",
|
||||
"ENTER" to "\u000D",
|
||||
"PGUP" to "\u001B[5~",
|
||||
"PGDN" to "\u001B[6~",
|
||||
"TAB" to "\u0009",
|
||||
"HOME" to "\u001B[H",
|
||||
"END" to "\u001B[F",
|
||||
"ESC" to "\u001B",
|
||||
"DRAWER" to ""
|
||||
)
|
||||
}
|
||||
|
||||
override fun onVirtualKeyButtonClick(
|
||||
view: View,
|
||||
buttonInfo: VirtualKeyButton,
|
||||
button: Button
|
||||
) {
|
||||
val activeSession = session ?: return
|
||||
val rawKey = buttonInfo.key.takeIf { it.isNotEmpty() } ?: return
|
||||
|
||||
// Resolve special modifier states if VirtualKeysView context is available
|
||||
val isCtrlActive = virtualKeysView?.readSpecialButton(SpecialButton.CTRL, true) == true
|
||||
val isAltActive = virtualKeysView?.readSpecialButton(SpecialButton.ALT, true) == true
|
||||
val isFnActive = virtualKeysView?.readSpecialButton(SpecialButton.FN, true) == true
|
||||
|
||||
var payload = KEY_ESCAPE_SEQUENCES[rawKey] ?: rawKey
|
||||
|
||||
if (payload.isEmpty()) return
|
||||
|
||||
// Handle CTRL modifier combination for standard ASCII characters (A-Z -> Control characters)
|
||||
if (isCtrlActive && payload.length == 1) {
|
||||
val char = payload[0]
|
||||
if (char in 'a'..'z') {
|
||||
payload = (char.code - 'a'.code + 1).toChar().toString()
|
||||
} else if (char in 'A'..'Z') {
|
||||
payload = (char.code - 'A'.code + 1).toChar().toString()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ALT modifier prefix (send ESC before character sequence)
|
||||
if (isAltActive) {
|
||||
payload = "\u001B$payload"
|
||||
}
|
||||
|
||||
activeSession.write(payload)
|
||||
}
|
||||
|
||||
override fun performVirtualKeyButtonHapticFeedback(
|
||||
view: View,
|
||||
buttonInfo: VirtualKeyButton,
|
||||
button: Button
|
||||
): Boolean {
|
||||
// Return false to allow VirtualKeysView to fall back to standard system haptics
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,411 @@
|
||||
package com.deniscerri.ytdl.ui.more.terminal.virtualkeys
|
||||
|
||||
import android.R
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.util.AttributeSet
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.widget.Button
|
||||
import android.widget.GridLayout
|
||||
import android.widget.PopupWindow
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* A [View] showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
||||
* keyboard.
|
||||
*/
|
||||
class VirtualKeysView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : GridLayout(context, attrs) {
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_BUTTON_TEXT_COLOR = -0x1
|
||||
const val DEFAULT_BUTTON_ACTIVE_TEXT_COLOR = -0xbc2c9
|
||||
const val DEFAULT_BUTTON_BACKGROUND_COLOR = 0x00000000
|
||||
const val DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR = -0x808081
|
||||
|
||||
const val MIN_LONG_PRESS_DURATION = 200
|
||||
const val MAX_LONG_PRESS_DURATION = 3000
|
||||
const val FALLBACK_LONG_PRESS_DURATION = 400
|
||||
|
||||
const val MIN_LONG_PRESS__REPEAT_DELAY = 5
|
||||
const val MAX_LONG_PRESS__REPEAT_DELAY = 2000
|
||||
const val DEFAULT_LONG_PRESS_REPEAT_DELAY = 80
|
||||
|
||||
/** General util function to compute the longest column length in a matrix. */
|
||||
@JvmStatic
|
||||
fun maximumLength(matrix: Array<Array<VirtualKeyButton>>): Int {
|
||||
var m = 0
|
||||
for (row in matrix) {
|
||||
m = max(m, row.size)
|
||||
}
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
var virtualKeysViewClient: IVirtualKeysView? = null
|
||||
|
||||
private var _specialButtons: Map<SpecialButton, SpecialButtonState>? = null
|
||||
var specialButtons: Map<SpecialButton, SpecialButtonState>?
|
||||
get() = _specialButtons?.toMap()
|
||||
set(value) {
|
||||
_specialButtons = value
|
||||
specialButtonsKeys = value?.keys?.map { it.key }?.toSet()
|
||||
}
|
||||
|
||||
var specialButtonsKeys: Set<String>? = null
|
||||
private set
|
||||
|
||||
private var _repetitiveKeys: List<String>? = null
|
||||
var repetitiveKeys: List<String>?
|
||||
get() = _repetitiveKeys?.toList()
|
||||
set(value) {
|
||||
_repetitiveKeys = value
|
||||
}
|
||||
|
||||
var buttonTextColor: Int = MaterialColors.getColor(context, android.R.attr.textColorPrimary, Color.BLACK)
|
||||
var buttonActiveTextColor: Int = DEFAULT_BUTTON_ACTIVE_TEXT_COLOR
|
||||
var buttonBackgroundColor: Int = MaterialColors.getColor(context, com.google.android.material.R.attr.colorSurfaceContainer, Color.BLACK)
|
||||
var buttonActiveBackgroundColor: Int = DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR
|
||||
var isButtonTextAllCaps: Boolean = true
|
||||
|
||||
var longPressTimeout: Int = ViewConfiguration.getLongPressTimeout()
|
||||
set(value) {
|
||||
field = if (value in MIN_LONG_PRESS_DURATION..MAX_LONG_PRESS_DURATION) {
|
||||
value
|
||||
} else {
|
||||
FALLBACK_LONG_PRESS_DURATION
|
||||
}
|
||||
}
|
||||
|
||||
var longPressRepeatDelay: Int = DEFAULT_LONG_PRESS_REPEAT_DELAY
|
||||
set(value) {
|
||||
field = if (value in MIN_LONG_PRESS__REPEAT_DELAY..MAX_LONG_PRESS__REPEAT_DELAY) {
|
||||
value
|
||||
} else {
|
||||
DEFAULT_LONG_PRESS_REPEAT_DELAY
|
||||
}
|
||||
}
|
||||
|
||||
private var popupWindow: PopupWindow? = null
|
||||
private var scheduledExecutor: ScheduledExecutorService? = null
|
||||
private var handler: Handler? = null
|
||||
private var specialButtonsLongHoldRunnable: SpecialButtonsLongHoldRunnable? = null
|
||||
private var longPressCount: Int = 0
|
||||
|
||||
init {
|
||||
repetitiveKeys = VirtualKeysConstants.PRIMARY_REPETITIVE_KEYS
|
||||
specialButtons = getDefaultSpecialButtons(this)
|
||||
setButtonColors(
|
||||
buttonTextColor,
|
||||
buttonActiveTextColor,
|
||||
buttonBackgroundColor,
|
||||
buttonActiveBackgroundColor
|
||||
)
|
||||
longPressTimeout = ViewConfiguration.getLongPressTimeout()
|
||||
longPressRepeatDelay = DEFAULT_LONG_PRESS_REPEAT_DELAY
|
||||
}
|
||||
|
||||
fun setButtonColors(
|
||||
buttonTextColor: Int,
|
||||
buttonActiveTextColor: Int,
|
||||
buttonBackgroundColor: Int,
|
||||
buttonActiveBackgroundColor: Int
|
||||
) {
|
||||
this.buttonTextColor = buttonTextColor
|
||||
this.buttonActiveTextColor = buttonActiveTextColor
|
||||
this.buttonBackgroundColor = buttonBackgroundColor
|
||||
this.buttonActiveBackgroundColor = buttonActiveBackgroundColor
|
||||
}
|
||||
|
||||
fun getDefaultSpecialButtons(extraKeysView: VirtualKeysView): Map<SpecialButton, SpecialButtonState> {
|
||||
return mapOf(
|
||||
SpecialButton.CTRL to SpecialButtonState(extraKeysView),
|
||||
SpecialButton.ALT to SpecialButtonState(extraKeysView),
|
||||
SpecialButton.SHIFT to SpecialButtonState(extraKeysView),
|
||||
SpecialButton.FN to SpecialButtonState(extraKeysView)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun reload(extraKeysInfo: VirtualKeysInfo?) {
|
||||
if (extraKeysInfo == null) return
|
||||
|
||||
_specialButtons?.values?.forEach { state ->
|
||||
state.buttons = ArrayList()
|
||||
}
|
||||
|
||||
removeAllViews()
|
||||
|
||||
val buttons = extraKeysInfo.matrix ?: return
|
||||
|
||||
rowCount = buttons.size
|
||||
columnCount = maximumLength(buttons)
|
||||
|
||||
for (row in buttons.indices) {
|
||||
for (col in buttons[row].indices) {
|
||||
val buttonInfo = buttons[row][col]
|
||||
|
||||
val button: Button = if (isSpecialButton(buttonInfo)) {
|
||||
createSpecialButton(buttonInfo.key, true) ?: return
|
||||
} else {
|
||||
Button(context, null, R.attr.buttonBarButtonStyle)
|
||||
}
|
||||
|
||||
button.text = buttonInfo.display
|
||||
button.setTextColor(buttonTextColor)
|
||||
button.isAllCaps = isButtonTextAllCaps
|
||||
button.setPadding(0, 0, 0, 0)
|
||||
|
||||
button.setOnClickListener { view ->
|
||||
performVirtualKeyButtonHapticFeedback(view, buttonInfo, button)
|
||||
onAnyVirtualKeyButtonClick(view, buttonInfo, button)
|
||||
}
|
||||
|
||||
button.setOnTouchListener { view, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
view.setBackgroundColor(buttonActiveBackgroundColor)
|
||||
startScheduledExecutors(view, buttonInfo, button)
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (buttonInfo.popup != null) {
|
||||
if (popupWindow == null && event.y < 0) {
|
||||
stopScheduledExecutors()
|
||||
view.setBackgroundColor(buttonBackgroundColor)
|
||||
showPopup(view, buttonInfo.popup!!)
|
||||
}
|
||||
if (popupWindow != null && event.y > 0) {
|
||||
view.setBackgroundColor(buttonActiveBackgroundColor)
|
||||
dismissPopup()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
view.setBackgroundColor(buttonBackgroundColor)
|
||||
stopScheduledExecutors()
|
||||
true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
view.setBackgroundColor(buttonBackgroundColor)
|
||||
stopScheduledExecutors()
|
||||
if (longPressCount == 0 || popupWindow != null) {
|
||||
if (popupWindow != null) {
|
||||
dismissPopup()
|
||||
buttonInfo.popup?.let { popup ->
|
||||
onAnyVirtualKeyButtonClick(view, popup, button)
|
||||
}
|
||||
} else {
|
||||
view.performClick()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
val param = LayoutParams().apply {
|
||||
width = 0
|
||||
height = 0
|
||||
setMargins(0, 0, 0, 0)
|
||||
columnSpec = spec(col, FILL, 1f)
|
||||
rowSpec = spec(row, FILL, 1f)
|
||||
}
|
||||
button.layoutParams = param
|
||||
|
||||
addView(button)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performVirtualKeyButtonHapticFeedback(
|
||||
view: View,
|
||||
buttonInfo: VirtualKeyButton,
|
||||
button: Button
|
||||
) {
|
||||
if (virtualKeysViewClient?.performVirtualKeyButtonHapticFeedback(view, buttonInfo, button) == true) {
|
||||
return
|
||||
}
|
||||
|
||||
val hapticEnabled = Settings.System.getInt(
|
||||
context.contentResolver,
|
||||
Settings.System.HAPTIC_FEEDBACK_ENABLED,
|
||||
0
|
||||
) != 0
|
||||
|
||||
if (hapticEnabled) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
button.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
} else {
|
||||
if (Settings.Global.getInt(context.contentResolver, "zen_mode", 0) != 2) {
|
||||
button.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAnyVirtualKeyButtonClick(
|
||||
view: View,
|
||||
buttonInfo: VirtualKeyButton,
|
||||
button: Button
|
||||
) {
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
if (longPressCount > 0) return
|
||||
val specialButton = runCatching { SpecialButton.valueOf(buttonInfo.key) }.getOrNull() ?: return
|
||||
val state = _specialButtons?.get(specialButton) ?: return
|
||||
|
||||
state.setIsActive(!state.isActive)
|
||||
if (!state.isActive) state.setIsLocked(false)
|
||||
} else {
|
||||
onVirtualKeyButtonClick(view, buttonInfo, button)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onVirtualKeyButtonClick(view: View, buttonInfo: VirtualKeyButton, button: Button) {
|
||||
virtualKeysViewClient?.onVirtualKeyButtonClick(view, buttonInfo, button)
|
||||
}
|
||||
|
||||
private fun startScheduledExecutors(view: View, buttonInfo: VirtualKeyButton, button: Button) {
|
||||
stopScheduledExecutors()
|
||||
longPressCount = 0
|
||||
|
||||
if (_repetitiveKeys?.contains(buttonInfo.key) == true) {
|
||||
scheduledExecutor = Executors.newSingleThreadScheduledExecutor().apply {
|
||||
scheduleWithFixedDelay(
|
||||
{
|
||||
longPressCount++
|
||||
onVirtualKeyButtonClick(view, buttonInfo, button)
|
||||
},
|
||||
longPressTimeout.toLong(),
|
||||
longPressRepeatDelay.toLong(),
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
}
|
||||
} else if (isSpecialButton(buttonInfo)) {
|
||||
val specialButton = runCatching { SpecialButton.valueOf(buttonInfo.key) }.getOrNull() ?: return
|
||||
val state = _specialButtons?.get(specialButton) ?: return
|
||||
|
||||
if (handler == null) handler = Handler(Looper.getMainLooper())
|
||||
specialButtonsLongHoldRunnable = SpecialButtonsLongHoldRunnable(state)
|
||||
handler?.postDelayed(specialButtonsLongHoldRunnable!!, longPressTimeout.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopScheduledExecutors() {
|
||||
scheduledExecutor?.shutdownNow()
|
||||
scheduledExecutor = null
|
||||
|
||||
specialButtonsLongHoldRunnable?.let { runnable ->
|
||||
handler?.removeCallbacks(runnable)
|
||||
specialButtonsLongHoldRunnable = null
|
||||
}
|
||||
}
|
||||
|
||||
fun showPopup(view: View, extraButton: VirtualKeyButton) {
|
||||
val width = view.measuredWidth
|
||||
val height = view.measuredHeight
|
||||
|
||||
val button: Button = if (isSpecialButton(extraButton)) {
|
||||
createSpecialButton(extraButton.key, false) ?: return
|
||||
} else {
|
||||
Button(context, null, R.attr.buttonBarButtonStyle).apply {
|
||||
setTextColor(buttonTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
button.apply {
|
||||
text = extraButton.display
|
||||
isAllCaps = isButtonTextAllCaps
|
||||
setPadding(0, 0, 0, 0)
|
||||
minHeight = 0
|
||||
minWidth = 0
|
||||
minimumWidth = 0
|
||||
minimumHeight = 0
|
||||
this.width = width
|
||||
this.height = height
|
||||
setBackgroundColor(buttonActiveBackgroundColor)
|
||||
}
|
||||
|
||||
popupWindow = PopupWindow(this).apply {
|
||||
this.width = LayoutParams.WRAP_CONTENT
|
||||
this.height = LayoutParams.WRAP_CONTENT
|
||||
contentView = button
|
||||
isOutsideTouchable = true
|
||||
isFocusable = false
|
||||
showAsDropDown(view, 0, -2 * height)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissPopup() {
|
||||
popupWindow?.let { popup ->
|
||||
popup.contentView = null
|
||||
popup.dismiss()
|
||||
}
|
||||
popupWindow = null
|
||||
}
|
||||
|
||||
fun isSpecialButton(button: VirtualKeyButton): Boolean {
|
||||
return specialButtonsKeys?.contains(button.key) == true
|
||||
}
|
||||
|
||||
private fun createSpecialButton(buttonKey: String, needUpdate: Boolean): Button? {
|
||||
val specialButton = runCatching { SpecialButton.valueOf(buttonKey) }.getOrNull() ?: return null
|
||||
val state = _specialButtons?.get(specialButton) ?: return null
|
||||
|
||||
state.setIsCreated(true)
|
||||
val button = Button(context, null, R.attr.buttonBarButtonStyle)
|
||||
button.setTextColor(if (state.isActive) buttonActiveTextColor else buttonTextColor)
|
||||
|
||||
if (needUpdate) {
|
||||
state.buttons.add(button)
|
||||
}
|
||||
return button
|
||||
}
|
||||
|
||||
fun readSpecialButton(specialButton: SpecialButton, autoSetInActive: Boolean): Boolean? {
|
||||
val state = _specialButtons?.get(specialButton) ?: return null
|
||||
|
||||
if (!state.isCreated || !state.isActive) return false
|
||||
|
||||
if (autoSetInActive && !state.isLocked) {
|
||||
state.setIsActive(false)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private inner class SpecialButtonsLongHoldRunnable(
|
||||
private val state: SpecialButtonState
|
||||
) : Runnable {
|
||||
override fun run() {
|
||||
state.setIsLocked(!state.isActive)
|
||||
state.setIsActive(!state.isActive)
|
||||
longPressCount++
|
||||
}
|
||||
}
|
||||
|
||||
interface IVirtualKeysView {
|
||||
fun onVirtualKeyButtonClick(view: View, buttonInfo: VirtualKeyButton, button: Button)
|
||||
fun performVirtualKeyButtonHapticFeedback(view: View, buttonInfo: VirtualKeyButton, button: Button): Boolean
|
||||
}
|
||||
}
|
||||
@ -1,195 +0,0 @@
|
||||
package com.deniscerri.ytdl.work.download
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import com.deniscerri.ytdl.R
|
||||
import com.deniscerri.ytdl.core.RuntimeManager
|
||||
import com.deniscerri.ytdl.core.models.YTDLRequest
|
||||
import com.deniscerri.ytdl.database.DBManager
|
||||
import com.deniscerri.ytdl.database.enums.DownloadType
|
||||
import com.deniscerri.ytdl.database.models.Format
|
||||
import com.deniscerri.ytdl.database.models.LogItem
|
||||
import com.deniscerri.ytdl.database.repository.LogRepository
|
||||
import com.deniscerri.ytdl.ui.more.terminal.TerminalActivity
|
||||
import com.deniscerri.ytdl.util.FileUtil
|
||||
import com.deniscerri.ytdl.util.NotificationUtil
|
||||
import com.deniscerri.ytdl.util.WorkerEventBus
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
|
||||
|
||||
class TerminalDownloadWorker(
|
||||
private val context: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
override suspend fun doWork(): Result {
|
||||
itemId = inputData.getInt("id", 0)
|
||||
val command = inputData.getString("command")
|
||||
val dao = DBManager.getInstance(context).terminalDao
|
||||
if (itemId == 0) return Result.failure()
|
||||
if (command!!.isEmpty()) return Result.failure()
|
||||
|
||||
val dbManager = DBManager.getInstance(context)
|
||||
val logRepo = LogRepository(dbManager.logDao)
|
||||
val notificationUtil = NotificationUtil(context)
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
val intent = Intent(context, TerminalActivity::class.java)
|
||||
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, command.take(65), NotificationUtil.DOWNLOAD_TERMINAL_RUNNING_NOTIFICATION_ID)
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
setForegroundAsync(ForegroundInfo(itemId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC))
|
||||
}else{
|
||||
setForegroundAsync(ForegroundInfo(itemId, notification))
|
||||
}
|
||||
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val downloadLocation = sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
|
||||
|
||||
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
|
||||
val initialLogDetails = "Terminal Task\n" +
|
||||
"Command:\n${command.trim()}\n\n"
|
||||
val logItem = LogItem(
|
||||
0,
|
||||
"Terminal Task",
|
||||
initialLogDetails,
|
||||
Format(),
|
||||
DownloadType.command,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
var noCache = false
|
||||
runCatching {
|
||||
if (logDownloads){
|
||||
runBlocking {
|
||||
logItem.id = logRepo.insert(logItem)
|
||||
}
|
||||
}
|
||||
|
||||
val callback : (Float, Long, String) -> Unit = { progress, _, line ->
|
||||
runBlocking {
|
||||
WorkerEventBus.post(DownloadWorker.WorkerProgress(progress.toInt(), line, itemId.toLong(), logItem.id))
|
||||
}
|
||||
|
||||
val title: String = command.take(65)
|
||||
notificationUtil.updateTerminalDownloadNotification(
|
||||
itemId,
|
||||
line, progress.toInt(), title,
|
||||
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
|
||||
)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (logDownloads) logRepo.update(line, logItem.id)
|
||||
dao.updateLog(line, itemId.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
val c = command.lowercase()
|
||||
if (c.startsWith("deno")) {
|
||||
RuntimeManager.getInstance().executeDeno(command, itemId.toString(), callback = callback)
|
||||
} else if (c.startsWith("python")) {
|
||||
RuntimeManager.getInstance().executePython(command, itemId.toString(), callback = callback)
|
||||
}
|
||||
else {
|
||||
val (resp, isNoCache) = buildYTDLPRequest(command, sharedPreferences)
|
||||
noCache = isNoCache
|
||||
RuntimeManager.getInstance().execute(resp, itemId.toString(), true, callback = callback)
|
||||
}
|
||||
}.onSuccess {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if(!noCache){
|
||||
//move file from internal to set download directory
|
||||
try {
|
||||
FileUtil.moveFile(File(FileUtil.getCachePath(context) + "/TERMINAL/" + itemId),context, downloadLocation!!, false){ p ->
|
||||
WorkerEventBus.post(DownloadWorker.WorkerProgress(p, "", itemId.toLong(), logItem.id))
|
||||
}
|
||||
}catch (e: Exception){
|
||||
e.printStackTrace()
|
||||
handler.postDelayed({
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (logDownloads) logRepo.update(initialLogDetails + it.out, logItem.id, true)
|
||||
dao.updateLog(it.out, itemId.toLong())
|
||||
notificationUtil.cancelDownloadNotification(itemId)
|
||||
delay(1000)
|
||||
dao.delete(itemId.toLong())
|
||||
Result.success()
|
||||
}.onFailure {
|
||||
if (it.message != null){
|
||||
if (logDownloads) logRepo.update(it.message!!, logItem.id)
|
||||
dao.updateLog(it.message!!, itemId.toLong())
|
||||
}
|
||||
notificationUtil.cancelDownloadNotification(itemId)
|
||||
File(FileUtil.getDefaultCommandPath() + "/" + itemId).deleteRecursively()
|
||||
Log.e(TAG, context.getString(R.string.failed_download), it)
|
||||
delay(1000)
|
||||
dao.delete(itemId.toLong())
|
||||
Result.failure()
|
||||
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
|
||||
}
|
||||
|
||||
private fun buildYTDLPRequest(command: String, sharedPreferences: SharedPreferences) : Pair<YTDLRequest, Boolean> {
|
||||
val request = YTDLRequest(emptyList())
|
||||
request.addOption(
|
||||
"--config-locations",
|
||||
File(context.cacheDir.absolutePath + "/config-TERMINAL[${System.currentTimeMillis()}].txt").apply {
|
||||
writeText(command)
|
||||
}.absolutePath
|
||||
)
|
||||
|
||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
||||
FileUtil.getCookieFile(context){
|
||||
request.addOption("--cookies", it)
|
||||
}
|
||||
|
||||
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
||||
val header = sharedPreferences.getString("useragent_header", "")
|
||||
if (useHeader && !header.isNullOrBlank()){
|
||||
request.addOption("--add-header","User-Agent:${header}")
|
||||
}
|
||||
}
|
||||
|
||||
val commandPath = sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())!!
|
||||
var noCache = !sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(commandPath)).canWrite()
|
||||
|
||||
if (command.contains("-P ")) {
|
||||
noCache = true
|
||||
}else {
|
||||
if (!noCache){
|
||||
request.addOption("-P", FileUtil.getCachePath(context) + "TERMINAL/" + itemId)
|
||||
}else if (!request.hasOption("-P")){
|
||||
request.addOption("-P", FileUtil.formatPath(commandPath))
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(request, noCache)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var itemId : Int = 0
|
||||
const val TAG = "TerminalDownloadWorker"
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?android:colorAccent" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
||||
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorAccent" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M9,4v3h5v12h3L17,7h5L22,4L9,4zM3,12h3v7h3v-7h3L12,9L3,9v3z"/>
|
||||
|
||||
</vector>
|
||||
Binary file not shown.
@ -1,117 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintDimensionRatio="H,2:1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/active_download_card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardCornerRadius="20dp"
|
||||
app:cardElevation="0dp"
|
||||
app:cardMaxElevation="12dp"
|
||||
android:checkable="true"
|
||||
app:strokeWidth="0dp"
|
||||
app:cardPreventCornerOverlap="true"
|
||||
android:layout_margin="10dp">
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="match_parent"
|
||||
app:trackColor="#000"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="bottom"
|
||||
android:alpha="0.3"
|
||||
android:scaleY="200"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:shadowColor="#000"
|
||||
android:fontFamily="monospace"
|
||||
android:shadowDx="4"
|
||||
android:shadowDy="4"
|
||||
android:shadowRadius="2"
|
||||
android:textColor="#FFF"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/active_download_stop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/active_download_stop"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
app:backgroundTint="?attr/colorSurface"
|
||||
app:cornerRadius="15dp"
|
||||
app:icon="@drawable/baseline_close_24"
|
||||
app:iconSize="30dp"
|
||||
app:iconTint="?android:textColorPrimary"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintBottom_toTopOf="@+id/output"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="title,active_download_stop" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/output"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:ellipsize="end"
|
||||
android:focusable="true"
|
||||
android:fontFamily="monospace"
|
||||
android:maxLines="5"
|
||||
android:padding="10dp"
|
||||
android:shadowColor="#000"
|
||||
android:shadowDx="4"
|
||||
android:shadowDy="4"
|
||||
android:shadowRadius="2"
|
||||
android:textColor="#FFF"
|
||||
android:textSize="11sp"
|
||||
android:gravity="bottom"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/barrier" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@ -0,0 +1,49 @@
|
||||
<com.google.android.material.card.MaterialCardView android:id="@+id/active_download_card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
app:cardCornerRadius="20dp"
|
||||
app:cardElevation="0dp"
|
||||
android:checkable="true"
|
||||
app:strokeWidth="0dp"
|
||||
app:cardPreventCornerOverlap="true"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:layout_marginHorizontal="10dp"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:padding="10dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/session_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginEnd="10dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/deleteSession"
|
||||
tools:text="session1"
|
||||
/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/deleteSession"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="0dp"
|
||||
app:icon="@drawable/baseline_delete_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
Loading…
Reference in New Issue