...
This commit is contained in:
parent
b73fbf2160
commit
614e244be7
@ -359,7 +359,10 @@ function toast(msg) {
|
||||
port.postMessage(JSON.stringify({type:"MSG",msg:msg}));
|
||||
}
|
||||
|
||||
var mainContentsEl = null
|
||||
var mainContentsEl = null;
|
||||
let lastState = null; // 이전 상태 저장 (-1, 0, 1)
|
||||
let throttleTimer = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
const currentUrl = location.href;
|
||||
@ -382,6 +385,40 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
// toast("connect port on " + location.href);
|
||||
time1 = setTimeout(autoScrollAndSave(false), 3500);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
window.onscroll = function() {
|
||||
if (throttleTimer) return; // 이미 타이머가 동작 중이면 무시
|
||||
|
||||
throttleTimer = setTimeout(() => {
|
||||
const scrollY = window.scrollY;
|
||||
const innerHeight = window.innerHeight;
|
||||
const offsetHeight = document.body.offsetHeight;
|
||||
|
||||
let currentState;
|
||||
|
||||
if (scrollY <= 2) {
|
||||
currentState = -1; // 최상단
|
||||
} else if ((innerHeight + scrollY) >= offsetHeight - 10) {
|
||||
currentState = 1; // 최하단
|
||||
} else {
|
||||
currentState = 0; // 중간
|
||||
}
|
||||
|
||||
// 💡 상태가 바뀌었을 때만 앱으로 전송
|
||||
if (currentState !== lastState) {
|
||||
lastState = currentState;
|
||||
sendMessage({
|
||||
type: "SCROLL_STATE",
|
||||
value: currentState
|
||||
});
|
||||
}
|
||||
|
||||
throttleTimer = null;
|
||||
}, 150); // 0.15초 간격으로 체크 (사용성에 따라 조절 가능)
|
||||
};
|
||||
})
|
||||
|
||||
const keywords = ["youtube", "mojeek"];
|
||||
|
||||
@ -218,8 +218,10 @@ open class LauncherActivity : CommonActivity() {
|
||||
|
||||
fun onSwipeRight() {
|
||||
startActivity(Intent(this, NeoRssActivity::class.java).apply {
|
||||
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
// 이미 실행 중인 액티비티가 있다면 스택의 맨 위로 올리고 재사용합니다.
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
// 기존에 있던 SINGLE_TOP과 함께 사용하면 중복 생성을 더 확실히 방지합니다.
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
})
|
||||
}
|
||||
|
||||
@ -267,10 +269,14 @@ open class LauncherActivity : CommonActivity() {
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
try {
|
||||
supportFragmentManager.findFragmentByTag(AppDrawerBottomSheet.TAG)?.let {
|
||||
try { if (it is AppDrawerBottomSheet) it.dismissAllowingStateLoss() } catch (e : Exception) {}
|
||||
}
|
||||
|
||||
val fragment = supportFragmentManager.findFragmentByTag(AppDrawerBottomSheet.TAG)
|
||||
Blog.LOGE("fragment >>> $fragment")
|
||||
if (fragment is AppDrawerBottomSheet) {
|
||||
fragment.dismiss()
|
||||
try { fragment.dismiss() } catch (e : Exception) {}
|
||||
}
|
||||
if (intent?.action?.equals(Intent.ACTION_MAIN) == true && intent.categories.contains(
|
||||
Intent.CATEGORY_HOME)) {
|
||||
@ -357,9 +363,17 @@ open class LauncherActivity : CommonActivity() {
|
||||
|
||||
|
||||
fun showAppDrawer() {
|
||||
// 이미 화면에 떠 있는지 확인
|
||||
val existingFragment = supportFragmentManager.findFragmentByTag(AppDrawerBottomSheet.TAG)
|
||||
if (existingFragment != null && existingFragment.isAdded) {
|
||||
return // 이미 존재하면 새로 띄우지 않음
|
||||
}
|
||||
|
||||
val bottomSheet = AppDrawerBottomSheet.newInstance()
|
||||
bottomSheet.show(supportFragmentManager, AppDrawerBottomSheet.TAG)
|
||||
}
|
||||
|
||||
|
||||
private lateinit var homeGestureDetector: androidx.core.view.GestureDetectorCompat
|
||||
|
||||
@SuppressLint("NewApi", "MissingPermission", "ClickableViewAccessibility")
|
||||
|
||||
@ -3,6 +3,7 @@ package bums.lunatic.launcher.apps
|
||||
import android.app.Dialog
|
||||
import android.app.SearchManager
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
@ -129,11 +130,23 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
|
||||
val displayList = categoryMap.keys.toList()
|
||||
|
||||
// 2. 어댑터 설정 (기본 안드로이드 레이아웃 사용)
|
||||
val adapter = ArrayAdapter(
|
||||
val adapter = object : ArrayAdapter<String>(
|
||||
requireContext(),
|
||||
R.layout.spinner_item_dark,
|
||||
displayList
|
||||
)
|
||||
){
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
view.setBackgroundColor(Color.parseColor("#000000")) // 펼쳐진 리스트의 배경
|
||||
return view
|
||||
}
|
||||
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = super.getDropDownView(position, convertView, parent)
|
||||
view.setBackgroundColor(Color.parseColor("#000000")) // 펼쳐진 리스트의 배경
|
||||
return view
|
||||
}
|
||||
}
|
||||
adapter.setDropDownViewResource(R.layout.spinner_item_dark)
|
||||
binding.categorySpinner.adapter = adapter
|
||||
|
||||
@ -147,6 +160,8 @@ class AppDrawerBottomSheet : BottomSheetDialogFragment() {
|
||||
fetchApps(binding.searchInput.text.toString())
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ import bums.lunatic.launcher.utils.Blog
|
||||
import bums.lunatic.launcher.workers.TaskAggregator
|
||||
import com.yausername.youtubedl_android.YoutubeDL
|
||||
import com.yausername.youtubedl_android.YoutubeDLRequest
|
||||
import io.realm.kotlin.internal.interop.sync.CancellableTimer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -45,7 +46,11 @@ import okhttp3.ResponseBody
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import android.bluetooth.BluetoothClass
|
||||
import android.media.AudioManager
|
||||
import android.os.SystemClock
|
||||
import android.view.KeyEvent
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
class AggregatedSystemWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||
override suspend fun doWork(): Result {
|
||||
@ -63,6 +68,19 @@ class AggregatedNewsWorker(private val context: Context, params: WorkerParameter
|
||||
}
|
||||
}
|
||||
|
||||
class VibrationWorker(private val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
||||
override suspend fun doWork(): Result {
|
||||
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as android.os.Vibrator
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(android.os.VibrationEffect.createOneShot(1000, android.os.VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
} else {
|
||||
vibrator.vibrate(1000)
|
||||
}
|
||||
// 필요 시 여기서 상단 알림(Notification)을 한 번 더 띄워줄 수 있습니다.
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
class ForeGroundService : Service() {
|
||||
companion object {
|
||||
val ACTION_SENDMSG = "ACTION_SEND_TO_LOVE"
|
||||
@ -70,6 +88,7 @@ class ForeGroundService : Service() {
|
||||
|
||||
val ACTION_VIDEO_DOWNLOAD = "ACTION_YTURL_DOWNLOAD"
|
||||
val EXTRA_TARGET_URL = "ACTION_SEND_TO_LOVE"
|
||||
val ACTION_SIT_DOWN = "ACTION_SIT_DOWN"
|
||||
val targetUrls = arrayListOf<String>()
|
||||
}
|
||||
|
||||
@ -89,9 +108,11 @@ class ForeGroundService : Service() {
|
||||
val filter = IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||
registerReceiver(bluetoothreceiver, filter)
|
||||
refreshFeeds()
|
||||
startForeGround()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Blog.LOGE("intent?.action >> ${intent?.action}")
|
||||
when(intent?.action) {
|
||||
ACTION_SENDMSG -> {
|
||||
intent?.getStringExtra(EXTRA_MSGKEY)?.let {
|
||||
@ -105,9 +126,27 @@ class ForeGroundService : Service() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ACTION_SIT_DOWN -> {
|
||||
val sitDownRequest = OneTimeWorkRequestBuilder<VibrationWorker>()
|
||||
.setInitialDelay(45, TimeUnit.MINUTES) // 45분 지연
|
||||
.build()
|
||||
|
||||
startForeGround()
|
||||
WorkManager.getInstance(this).enqueueUniqueWork(
|
||||
"SitDownVibration",
|
||||
ExistingWorkPolicy.REPLACE, // 새로 누를 때마다 타이머 초기화 (원치 않으면 KEEP)
|
||||
sitDownRequest
|
||||
)
|
||||
// 사용자 확인을 위해 알림 텍스트 변경 가능
|
||||
startForeGround(0, 0, "45분 뒤에 진동 알림이 예약되었습니다.")
|
||||
}
|
||||
else -> {
|
||||
|
||||
}
|
||||
}.apply {
|
||||
if(intent?.action?.equals(ACTION_SIT_DOWN) == false) {
|
||||
startForeGround()
|
||||
}
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
@ -161,7 +200,8 @@ class ForeGroundService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun startForeGround(max : Int = 0 , progress : Int = 0, str : String = "실행중입니다.") {
|
||||
fun startForeGround(max : Int = 0 , progress : Int = 0, str : String? = "실행중입니다.") {
|
||||
Blog.LOGE(str?:"")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
@ -189,11 +229,20 @@ class ForeGroundService : Service() {
|
||||
.setSmallIcon(R.drawable.ic_b)
|
||||
.setContentIntent(pendingIntent)
|
||||
.addAction(android.R.drawable.ic_btn_speak_now,"퇴근", makeSendMsgAction(0,"돼지 퇴근했다요~!"))
|
||||
.addAction(android.R.drawable.ic_menu_directions, "앉음", makeSitDownAction())
|
||||
.setOngoing(true) // 사용자가 알림을 스와이프로 지울 수 없게 만듦
|
||||
.setProgress(max, progress, false)
|
||||
.build())
|
||||
}
|
||||
|
||||
fun makeSitDownAction(): PendingIntent {
|
||||
val intent = Intent(this, ForeGroundService::class.java).apply {
|
||||
action = ACTION_SIT_DOWN
|
||||
}
|
||||
return PendingIntent.getService(this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
|
||||
fun makeSendMsgAction(code : Int, msg : String) : PendingIntent {
|
||||
val actionIntent = Intent(this, ForeGroundService::class.java).apply {
|
||||
action = ACTION_SENDMSG
|
||||
@ -356,6 +405,66 @@ class ForeGroundService : Service() {
|
||||
if (context == null) return
|
||||
if (ActivityCompat.checkSelfPermission(context!!, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return
|
||||
getPairedDevices()
|
||||
|
||||
if (action == BluetoothDevice.ACTION_ACL_CONNECTED) {
|
||||
val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
|
||||
|
||||
// 연결된 기기가 오디오(이어폰, 스피커, 차량 블루투스)인지 확인
|
||||
val majorClass = device?.bluetoothClass?.majorDeviceClass
|
||||
if (majorClass == BluetoothClass.Device.Major.AUDIO_VIDEO) {
|
||||
Blog.LOGE("Audio Bluetooth Connected: ${device?.name ?: "Unknown"}")
|
||||
if ("BUMz pod".equals(device?.name)) {
|
||||
launchAppleMusic(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchAppleMusic(context: Context) {
|
||||
try {
|
||||
val appleMusicPackage = "com.apple.android.music"
|
||||
val launchIntent = context.packageManager.getLaunchIntentForPackage(appleMusicPackage)
|
||||
|
||||
if (launchIntent != null) {
|
||||
// 서비스(백그라운드)에서 화면을 띄우려면 NEW_TASK 플래그가 필수입니다.
|
||||
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
context.startActivity(launchIntent)
|
||||
Blog.LOGE("애플뮤직 자동 실행 완료")
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
delay(1500) // 1.5초 대기 (기기 속도에 따라 1000~2000 사이로 조절 가능)
|
||||
|
||||
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val eventTime = SystemClock.uptimeMillis()
|
||||
|
||||
// 재생 버튼 누름 (ACTION_DOWN)
|
||||
val downEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY, 0)
|
||||
audioManager.dispatchMediaKeyEvent(downEvent)
|
||||
|
||||
// 재생 버튼 뗌 (ACTION_UP)
|
||||
val upEvent = KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY, 0)
|
||||
audioManager.dispatchMediaKeyEvent(upEvent)
|
||||
|
||||
Blog.LOGE("애플뮤직 미디어 재생(Play) 명령 전송 완료 🎵")
|
||||
delay(300) // 시스템이 재생 명령을 처리할 수 있도록 아주 잠깐 대기
|
||||
|
||||
// 💡 3. 홈 화면(런처)으로 즉시 강제 복귀
|
||||
val homeIntent = Intent(Intent.ACTION_MAIN).apply {
|
||||
addCategory(Intent.CATEGORY_HOME)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
context.startActivity(homeIntent)
|
||||
Blog.LOGE("런처(홈) 화면으로 복귀 완료")
|
||||
}
|
||||
|
||||
} else {
|
||||
Blog.LOGE("애플뮤직 앱이 기기에 설치되어 있지 않습니다.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Blog.LOGE("애플뮤직 실행 중 오류 발생: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -4,14 +4,11 @@ import CustomVideoNodeRenderer
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.util.AttributeSet
|
||||
@ -21,7 +18,6 @@ import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.PointerIcon
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
@ -30,7 +26,6 @@ import android.widget.RadioGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContentProviderCompat.requireContext
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import bums.lunatic.launcher.BookmarkUploader
|
||||
@ -56,30 +51,28 @@ import com.yausername.youtubedl_android.YoutubeDLRequest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.Jsoup
|
||||
import org.mozilla.gecko.util.ThreadUtils
|
||||
import org.mozilla.geckoview.GeckoResult
|
||||
import org.mozilla.geckoview.GeckoSession
|
||||
import org.mozilla.geckoview.GeckoSession.CompositorScrollDelegate
|
||||
import org.mozilla.geckoview.GeckoSessionSettings
|
||||
import org.mozilla.geckoview.GeckoView
|
||||
import org.mozilla.geckoview.MediaSession
|
||||
import org.mozilla.geckoview.PanZoomController
|
||||
import org.mozilla.geckoview.ScreenLength
|
||||
import org.mozilla.geckoview.WebExtension
|
||||
import org.mozilla.geckoview.WebExtension.MessageDelegate
|
||||
import org.mozilla.geckoview.WebExtension.PortDelegate
|
||||
import org.mozilla.geckoview.WebExtensionController.AddonManagerDelegate
|
||||
import org.mozilla.geckoview.WebRequestError
|
||||
import org.mozilla.geckoview.WebResponse
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import kotlin.jvm.java
|
||||
|
||||
// BWebview와 GeckoWeb을 통합한 클래스
|
||||
open class GeckoWeb @JvmOverloads constructor(
|
||||
@ -91,35 +84,29 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
// 1. 세션 상태를 저장할 SharedPreferences 키
|
||||
private val PREF_SESSION_STATE = "gecko_session_state"
|
||||
|
||||
// 2. 현재 세션 상태 저장 메서드
|
||||
var sessionTag: String = "default" // 프레그먼트에서 설정할 고유 키
|
||||
|
||||
fun saveCurrentSessionState() {
|
||||
|
||||
lastSessionState?.let { state ->
|
||||
// SessionState.toString()은 내부적으로 JSON 문자열을 반환합니다.
|
||||
val stateJson = state.toString()
|
||||
|
||||
// 💡 키 이름에 sessionTag를 포함시켜 분리 저장
|
||||
context.getSharedPreferences("GeckoPrefs", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString("gecko_session_state", stateJson)
|
||||
.putString("gecko_session_state_$sessionTag", stateJson)
|
||||
.apply()
|
||||
|
||||
Log.d("GeckoWeb", "Session State Saved: $stateJson")
|
||||
Log.d("GeckoWeb", "Saved State for $sessionTag")
|
||||
}
|
||||
}
|
||||
|
||||
// // 3. 저장된 세션 상태 복구 메서드
|
||||
fun restoreSessionState(session: GeckoSession) {
|
||||
fun restoreSessionState() {
|
||||
// 💡 저장할 때와 동일한 태그로 불러오기
|
||||
val stateJson = context.getSharedPreferences("GeckoPrefs", Context.MODE_PRIVATE)
|
||||
.getString("gecko_session_state", null)
|
||||
.getString("gecko_session_state_$sessionTag", null)
|
||||
|
||||
if (!stateJson.isNullOrEmpty()) {
|
||||
// 문자열에서 SessionState 객체 생성
|
||||
val state = GeckoSession.SessionState.fromString(stateJson)
|
||||
Blog.LOGE("Restored state >>> ${state}")
|
||||
if (state != null) {
|
||||
|
||||
session.restoreState(state)
|
||||
Log.d("GeckoWeb", "Session State Restored")
|
||||
session?.restoreState(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -339,6 +326,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
R.id.reload -> view.setOnClickListener { session.reload() }
|
||||
}
|
||||
}
|
||||
// scrollState = 0
|
||||
}
|
||||
|
||||
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
|
||||
@ -433,7 +421,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
}
|
||||
buildWeb()
|
||||
}
|
||||
|
||||
var lastScrollY = 0.0f
|
||||
private fun buildWeb() {
|
||||
getRuntime()?.let { runtime ->
|
||||
val sessionSettings = GeckoSessionSettings.Builder()
|
||||
@ -446,7 +434,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
.build()
|
||||
val session = GeckoSession(sessionSettings)
|
||||
|
||||
restoreSessionState(session)
|
||||
|
||||
session.open(runtime)
|
||||
this.setSession(session)
|
||||
|
||||
@ -457,6 +445,15 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
session.mediaDelegate = mediaDelegate
|
||||
session.promptDelegate = promptDelegate
|
||||
session.mediaSessionDelegate = mediaSessionDelegate
|
||||
session.compositorScrollDelegate = object : CompositorScrollDelegate {
|
||||
override fun onScrollChanged(
|
||||
session: GeckoSession,
|
||||
update: GeckoSession.ScrollPositionUpdate
|
||||
) {
|
||||
super.onScrollChanged(session, update)
|
||||
lastScrollY = update.scrollY
|
||||
}
|
||||
}
|
||||
runtime.settings.loginAutofillEnabled = true
|
||||
|
||||
runtime.webExtensionController.setAddonManagerDelegate(addonManagerDelegate)
|
||||
@ -476,8 +473,38 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
}
|
||||
// --- 4. Helper Methods ---
|
||||
|
||||
fun checkScrollEndUsingMatrix(): Boolean {
|
||||
val matrix = android.graphics.Matrix()
|
||||
// 현재 페이지 좌표 -> 화면 좌표 변환 행렬 가져오기
|
||||
session?.getPageToScreenMatrix(matrix)
|
||||
|
||||
// 웹페이지의 콘텐츠 높이를 짐작하기 위한 로직
|
||||
// (GeckoView에서 scrollHeight를 직접 가져오기 어려우므로
|
||||
// CompositorScrollDelegate에서 업데이트된 정보를 활용하거나
|
||||
// 아래와 같이 Matrix의 역산 값을 활용합니다.)
|
||||
|
||||
val values = FloatArray(9)
|
||||
matrix.getValues(values)
|
||||
|
||||
// values[Matrix.MTRANS_Y]는 현재 페이지의 Top이 화면상 어디에 있는지(y축 오프셋)를 나타냅니다.
|
||||
// values[Matrix.MSCALE_Y]는 현재 줌 배율입니다.
|
||||
val currentTopInScreen = values[android.graphics.Matrix.MTRANS_Y]
|
||||
val currentZoom = values[android.graphics.Matrix.MSCALE_Y]
|
||||
|
||||
// 💡 논리:
|
||||
// 손가락 스크롤이나 볼륨키 스크롤 모두 이 Matrix 값을 실시간으로 변화시킵니다.
|
||||
// 특정 시점에 '스크롤 다운'을 명령했는데 이 Matrix의 변환 값(MTRANS_Y)이
|
||||
// 더 이상 변하지 않는다면 그것이 물리적인 끝입니다.
|
||||
|
||||
return false // 실제 구현 시에는 이전 MTRANS_Y 값과 비교 로직 추가
|
||||
}
|
||||
|
||||
private fun handlePortMessage(msg: PortMessage) {
|
||||
when (msg.type) {
|
||||
"SCROLL_STATE" -> {
|
||||
Blog.LOGE("${msg.type} : ${msg.value}")
|
||||
scrollState = msg.value?.toInt() ?: 0
|
||||
}
|
||||
"allImagesFound" -> lastedUrl?.let { startBookmarkSaveProcessForMultipleImages(it, msg.urls) }
|
||||
"SINGLE_IMAGE_DATA" -> if (msg.imgSrc != null && msg.base64Data != null) {
|
||||
Base64ImageCache.put(msg.imgSrc!!, msg.base64Data!!)
|
||||
@ -517,7 +544,7 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
}
|
||||
return super.dispatchKeyEvent(ev)
|
||||
}
|
||||
|
||||
var scrollState = 0
|
||||
// Message Sending Helpers
|
||||
fun sendScrollDown(isUp: Boolean) = sendJsonMsg("scrollDown", "isUpDown" to if(isUp) 1 else -1)
|
||||
fun sendSearch(keyword: String) = sendJsonMsg("search", "keyword" to keyword)
|
||||
@ -529,6 +556,29 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
params.forEach { json.put(it.first, it.second) }
|
||||
mPort?.postMessage(json)
|
||||
}
|
||||
var HH = 0.3
|
||||
fun pageDown() {
|
||||
val session = this.session ?: return
|
||||
val pzc = session.panZoomController
|
||||
|
||||
// 💡 현재 눈에 보이는 GeckoView의 높이를 가져옵니다.
|
||||
val viewHeight = this.height.toDouble().times(HH)
|
||||
|
||||
// 세로 방향(y)으로 뷰 높이만큼 아래로 스크롤 (픽셀 단위)
|
||||
// 세 번째 인자는 애니메이션 여부입니다.
|
||||
pzc.scrollBy(ScreenLength.fromPixels(0.0), ScreenLength.fromPixels(viewHeight),
|
||||
PanZoomController.SCROLL_BEHAVIOR_SMOOTH)
|
||||
}
|
||||
|
||||
fun pageUp() {
|
||||
val session = this.session ?: return
|
||||
val pzc = session.panZoomController
|
||||
val viewHeight = this.height.toDouble().times(HH)
|
||||
|
||||
// 위로 스크롤
|
||||
pzc.scrollBy(ScreenLength.fromPixels(0.0), ScreenLength.fromPixels(-viewHeight),
|
||||
PanZoomController.SCROLL_BEHAVIOR_SMOOTH)
|
||||
}
|
||||
|
||||
// File/Download Helpers
|
||||
private fun downloadFile(url: String) {
|
||||
@ -631,4 +681,15 @@ open class GeckoWeb @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun View.post(action: () -> Unit) = this.post(Runnable(action))
|
||||
fun onPause() {
|
||||
saveCurrentSessionState()
|
||||
session?.stop()
|
||||
session?.setActive(false)
|
||||
Blog.LOGE("called onstop $lastedUrl")
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
restoreSessionState()
|
||||
session?.setActive(true)
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,7 @@ import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import bums.lunatic.launcher.LunaticLauncher
|
||||
import bums.lunatic.launcher.R
|
||||
import bums.lunatic.launcher.common.CommonActivity
|
||||
@ -61,6 +62,10 @@ import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||
import org.mozilla.geckoview.GeckoRuntimeSettings.ALLOW_ALL
|
||||
import java.io.File
|
||||
|
||||
interface KeyEventHandler {
|
||||
// 이벤트를 소비(consume)했으면 true, 아니면 false 반환
|
||||
fun onKeyEvent(event: KeyEvent): Boolean
|
||||
}
|
||||
|
||||
open class NeoRssActivity : CommonActivity() {
|
||||
|
||||
@ -95,6 +100,10 @@ open class NeoRssActivity : CommonActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
}
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
@ -154,33 +163,11 @@ open class NeoRssActivity : CommonActivity() {
|
||||
return true
|
||||
}
|
||||
else {
|
||||
val currentFragment = supportFragmentManager.findFragmentById(R.id.fragment_container)
|
||||
Blog.LOGE("currentFragment >>> ${currentFragment} ${currentFragment is TokiFragment && currentFragment.isbooktoki()}")
|
||||
if (currentFragment is TokiFragment && currentFragment.isbooktoki()) {
|
||||
if(MotionEvent.ACTION_UP.equals(ev?.action ?: MotionEvent.ACTION_CANCEL) == true) {
|
||||
return when (ev.keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
if(currentFragment is TokiFragment){ currentFragment.actionNextEvent() }
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
if(currentFragment is TokiFragment){ currentFragment.actionPrevEvent() }
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
return when (ev.keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
true
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
val currentFragment = fragmentMap.values.find { it.isAdded && (!it.isHidden || it.isVisible) }
|
||||
if (currentFragment is KeyEventHandler) {
|
||||
if (currentFragment.onKeyEvent(ev)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
else { return super.dispatchKeyEvent(ev) }
|
||||
@ -373,7 +360,7 @@ open class NeoRssActivity : CommonActivity() {
|
||||
startActivity(shareIntent)
|
||||
}
|
||||
}
|
||||
showContents(R.id.feeds)
|
||||
showContents(R.id.btn_info)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@ -390,78 +377,64 @@ open class NeoRssActivity : CommonActivity() {
|
||||
private var lastTouchY = 0f
|
||||
|
||||
|
||||
private val fragmentMap = mutableMapOf<Int, Fragment>()
|
||||
|
||||
fun showContents(id : Int) {
|
||||
binding.fragmentLayer.visibility = View.VISIBLE
|
||||
binding.fragmentContainer.visibility = View.VISIBLE
|
||||
binding.controllPanel.visibility = View.VISIBLE
|
||||
binding.floatingActionMenu.visibility = View.VISIBLE
|
||||
when(id) {
|
||||
R.id.feeds -> {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, RssHome())
|
||||
.commit()
|
||||
}
|
||||
R.id.books ->{
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, TokiFragment.newInstanceNovels())
|
||||
.commit()
|
||||
}
|
||||
|
||||
R.id.webtoons ->{
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, TokiFragment.newInstanceWebtoons())
|
||||
.commit()
|
||||
}
|
||||
R.id.comics ->{
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, TokiFragment.newInstanceComics())
|
||||
.commit()
|
||||
}
|
||||
R.id.youtube ->{
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, TokiFragment.newInstanceYouTube())
|
||||
.commit()
|
||||
}
|
||||
R.id.perplexity ->{
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, TokiFragment.newInstancePerplexity())
|
||||
.commit()
|
||||
}
|
||||
R.id.zzalbang ->{
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, BookmarkPagerFragment())
|
||||
.commit()
|
||||
}
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
|
||||
R.id.btn_x ->{
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, TokiFragment.newInstanceX())
|
||||
.commit()
|
||||
fragmentMap.values.forEach { fragment ->
|
||||
if (fragment.isAdded) {
|
||||
transaction.hide(fragment)
|
||||
}
|
||||
|
||||
R.id.btn_torrent ->{
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, TorrentListFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
R.id.close ->{
|
||||
supportFragmentManager.findFragmentById(R.id.fragment_container)?.let {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.remove(it)
|
||||
.commit()
|
||||
binding.fragmentLayer.visibility = View.GONE
|
||||
binding.fragmentContainer.visibility = View.GONE
|
||||
binding.controllPanel.visibility = View.GONE
|
||||
binding.floatingActionMenu.visibility = View.GONE
|
||||
}
|
||||
// sRuntime?.shutdown()
|
||||
// sRuntime = null
|
||||
finish()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
// 2. 요청된 ID에 해당하는 프래그먼트가 이미 생성되었는지 확인
|
||||
var targetFragment = fragmentMap[id]
|
||||
|
||||
if (targetFragment == null) {
|
||||
targetFragment = supportFragmentManager.findFragmentByTag("TAG_$id")
|
||||
|
||||
// 시스템이 복구해 놓은 게 있다면, 맵에 다시 등록(싱크 맞추기)
|
||||
if (targetFragment != null) {
|
||||
fragmentMap[id] = targetFragment
|
||||
}
|
||||
}
|
||||
|
||||
if (targetFragment == null) {
|
||||
// 처음 호출되는 메뉴라면 인스턴스 생성 및 추가
|
||||
targetFragment = when(id) {
|
||||
R.id.feeds -> RssHome()
|
||||
R.id.books -> TokiFragment.newInstanceNovels()
|
||||
R.id.webtoons -> TokiFragment.newInstanceWebtoons()
|
||||
R.id.comics -> TokiFragment.newInstanceComics()
|
||||
R.id.youtube -> TokiFragment.newInstanceYouTube()
|
||||
R.id.perplexity -> TokiFragment.newInstancePerplexity()
|
||||
R.id.zzalbang -> BookmarkPagerFragment()
|
||||
R.id.btn_x -> TokiFragment.newInstanceX()
|
||||
R.id.btn_i -> TokiFragment.newInstanceI()
|
||||
R.id.btn_torrent -> TorrentListFragment()
|
||||
R.id.btn_info -> SystemStatusFragment()
|
||||
R.id.close -> {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
targetFragment?.let {
|
||||
fragmentMap[id] = it
|
||||
transaction.add(R.id.fragment_container, it, "TAG_$id")
|
||||
}
|
||||
} else {
|
||||
// 이미 생성된 프래그먼트가 있다면 다시 보여줌
|
||||
transaction.show(targetFragment)
|
||||
}
|
||||
|
||||
transaction.commit()
|
||||
binding.floatingActionMenu.close(false)
|
||||
}
|
||||
|
||||
@ -524,14 +497,14 @@ open class NeoRssActivity : CommonActivity() {
|
||||
currentFragment.doNextPage()
|
||||
}
|
||||
}
|
||||
// is YouTube -> {
|
||||
// currentFragment.back()
|
||||
// }
|
||||
is TokiFragment -> {
|
||||
currentFragment.back()
|
||||
}
|
||||
// is Novels -> {
|
||||
// currentFragment.actionNextEvent(false)
|
||||
// }
|
||||
else -> {
|
||||
showContents(R.id.close)
|
||||
// showContents(R.id.close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.PointerIcon
|
||||
@ -82,7 +83,7 @@ import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
|
||||
internal class RssHome : Fragment() {
|
||||
internal class RssHome : Fragment() , KeyEventHandler {
|
||||
|
||||
lateinit var binding: LauncherHomeBinding
|
||||
private lateinit var fragManager: FragmentManager
|
||||
@ -213,6 +214,36 @@ internal class RssHome : Fragment() {
|
||||
home = this
|
||||
}
|
||||
|
||||
override fun onKeyEvent(event: KeyEvent): Boolean {
|
||||
|
||||
val isUp = event.action == MotionEvent.ACTION_UP
|
||||
return when (event.keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
if (isUp) {
|
||||
if (binding.geckoWeb.scrollState > 0) {
|
||||
// 사용자가 손으로 끝까지 내렸든, 볼륨키로 내렸든 상관없이 이곳에 도달함
|
||||
doNextPage()
|
||||
} else {
|
||||
binding.geckoWeb.pageDown()
|
||||
}
|
||||
}
|
||||
true
|
||||
|
||||
}
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
if (isUp) {
|
||||
if (binding.geckoWeb.scrollState < 0) {
|
||||
binding.geckoWeb.session?.goBack()
|
||||
} else {
|
||||
binding.geckoWeb.pageUp()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
var targetList = arrayListOf<String>()
|
||||
val dateViewClick = View.OnClickListener { v ->
|
||||
Blog.LOGE("click view >> ${v}")
|
||||
@ -576,7 +607,7 @@ internal class RssHome : Fragment() {
|
||||
binding.geckoWeb.decoViews.add(activity.findViewById<ImageButton>(R.id.reload))
|
||||
binding.geckoWeb.decoViews.add(activity.findViewById<ImageButton>(R.id.dl_video))
|
||||
}
|
||||
|
||||
binding.geckoWeb.restoreSessionState()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@ -584,6 +615,20 @@ internal class RssHome : Fragment() {
|
||||
binding.geckoWeb?.saveMd(true)
|
||||
}
|
||||
|
||||
// TokiFragment 또는 GeckoView를 사용하는 프래그먼트 내부
|
||||
override fun onHiddenChanged(hidden: Boolean) {
|
||||
super.onHiddenChanged(hidden)
|
||||
if (hidden) {
|
||||
// 💡 화면에서 사라질 때: 타이머 중지 및 애니메이션 중지
|
||||
binding.geckoWeb?.onPause()
|
||||
// 일반 WebView라면: webView.onPause() 및 webView.pauseTimers()
|
||||
} else {
|
||||
// 💡 다시 나타날 때: 다시 시작
|
||||
// binding.geckoWeb?.onResume()
|
||||
// 일반 WebView라면: webView.onResume() 및 webView.resumeTimers()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
fun doNextPage() {
|
||||
|
||||
@ -0,0 +1,582 @@
|
||||
package bums.lunatic.launcher.home
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.AppOpsManager
|
||||
import android.app.NotificationManager
|
||||
import android.app.usage.NetworkStatsManager
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Color
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.TrafficStats
|
||||
import android.net.Uri
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.BatteryManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.StatFs
|
||||
import android.os.SystemClock
|
||||
import android.provider.Settings
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import bums.lunatic.launcher.databinding.FragmentSystemStatusBinding
|
||||
import bums.lunatic.launcher.utils.Blog
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.Calendar
|
||||
|
||||
class SystemStatusFragment : Fragment() {
|
||||
|
||||
private var _binding: FragmentSystemStatusBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private var monitorJob: Job? = null
|
||||
|
||||
private var lastRxBytes: Long = 0
|
||||
private var lastTxBytes: Long = 0
|
||||
private var lastUpdateTime: Long = 0
|
||||
|
||||
private lateinit var audioManager: AudioManager
|
||||
|
||||
// 앱 데이터 사용량 계산용 데이터 클래스
|
||||
data class AppUsage(val uid: Int, var name: String = "", var wifiBytes: Long = 0L, var mobileBytes: Long = 0L) {
|
||||
val totalBytes get() = wifiBytes + mobileBytes
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentSystemStatusBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
audioManager = requireContext().getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
setupClickListeners()
|
||||
setupQuickControls()
|
||||
|
||||
resetNetworkCounters()
|
||||
startMonitoring()
|
||||
updateMonthlyDataUsage()
|
||||
}
|
||||
|
||||
private fun setupClickListeners() {
|
||||
binding.textMonthlyUsage.setOnClickListener {
|
||||
if (!hasUsageStatsPermission()) {
|
||||
startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
|
||||
} else {
|
||||
updateMonthlyDataUsage()
|
||||
}
|
||||
}
|
||||
|
||||
binding.textStorage.setOnClickListener {
|
||||
try {
|
||||
startActivity(Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS))
|
||||
} catch (e: Exception) {
|
||||
startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
binding.progressStorage.setOnClickListener { binding.textStorage.performClick() }
|
||||
|
||||
binding.textRam.setOnClickListener {
|
||||
try {
|
||||
val samsungIntent = Intent().apply {
|
||||
setClassName("com.samsung.android.lool", "com.samsung.android.sm.ui.ram.RamActivity")
|
||||
}
|
||||
startActivity(samsungIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
try {
|
||||
startActivity(Intent(Settings.ACTION_APPLICATION_SETTINGS))
|
||||
Toast.makeText(requireContext(), "앱 설정 화면으로 이동합니다.", Toast.LENGTH_SHORT).show()
|
||||
} catch (ex: Exception) {
|
||||
startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.progressRam.setOnClickListener { binding.textRam.performClick() }
|
||||
|
||||
binding.textBattery.setOnClickListener {
|
||||
try {
|
||||
startActivity(Intent(Intent.ACTION_POWER_USAGE_SUMMARY))
|
||||
} catch (e: Exception) {
|
||||
startActivity(Intent(Settings.ACTION_SETTINGS))
|
||||
}
|
||||
}
|
||||
binding.progressBattery.setOnClickListener { binding.textBattery.performClick() }
|
||||
}
|
||||
|
||||
private fun setupQuickControls() {
|
||||
binding.btnQuickWifi.setOnClickListener {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startActivity(Intent(Settings.Panel.ACTION_WIFI))
|
||||
} else {
|
||||
startActivity(Intent(Settings.ACTION_WIFI_SETTINGS))
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnQuickBt.setOnClickListener {
|
||||
startActivity(Intent(Settings.ACTION_BLUETOOTH_SETTINGS))
|
||||
}
|
||||
|
||||
binding.btnQuickRinger.setOnClickListener {
|
||||
val nm = requireContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !nm.isNotificationPolicyAccessGranted) {
|
||||
Toast.makeText(requireContext(), "무음 모드 제어를 위해 권한을 허용해주세요.", Toast.LENGTH_LONG).show()
|
||||
startActivity(Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS))
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
when (audioManager.ringerMode) {
|
||||
AudioManager.RINGER_MODE_NORMAL -> audioManager.ringerMode = AudioManager.RINGER_MODE_VIBRATE
|
||||
AudioManager.RINGER_MODE_VIBRATE -> audioManager.ringerMode = AudioManager.RINGER_MODE_SILENT
|
||||
AudioManager.RINGER_MODE_SILENT -> audioManager.ringerMode = AudioManager.RINGER_MODE_NORMAL
|
||||
}
|
||||
updateQuickControlUI()
|
||||
}
|
||||
|
||||
binding.seekVolume.max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||
binding.seekVolume.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
if (fromUser) audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, progress, 0)
|
||||
}
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
||||
})
|
||||
|
||||
binding.seekBrightness.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
if (!fromUser) return
|
||||
if (Settings.System.canWrite(requireContext())) {
|
||||
val cr = requireContext().contentResolver
|
||||
Settings.System.putInt(cr, Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL)
|
||||
Settings.System.putInt(cr, Settings.System.SCREEN_BRIGHTNESS, progress)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "밝기 조절을 위해 '시스템 설정 수정' 권한이 필요합니다.", Toast.LENGTH_LONG).show()
|
||||
val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply {
|
||||
data = Uri.parse("package:${requireContext().packageName}")
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
||||
})
|
||||
|
||||
updateQuickControlUI()
|
||||
}
|
||||
|
||||
private fun updateQuickControlUI() {
|
||||
val ringerText = when (audioManager.ringerMode) {
|
||||
AudioManager.RINGER_MODE_NORMAL -> "🔔 소리"
|
||||
AudioManager.RINGER_MODE_VIBRATE -> "📳 진동"
|
||||
AudioManager.RINGER_MODE_SILENT -> "🔕 무음"
|
||||
else -> "소리"
|
||||
}
|
||||
binding.btnQuickRinger.text = ringerText
|
||||
binding.seekVolume.progress = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
|
||||
try {
|
||||
val brightness = Settings.System.getInt(requireContext().contentResolver, Settings.System.SCREEN_BRIGHTNESS)
|
||||
binding.seekBrightness.progress = brightness
|
||||
} catch (e: Settings.SettingNotFoundException) { }
|
||||
}
|
||||
|
||||
override fun onHiddenChanged(hidden: Boolean) {
|
||||
super.onHiddenChanged(hidden)
|
||||
if (hidden) {
|
||||
stopMonitoring()
|
||||
} else {
|
||||
resetNetworkCounters()
|
||||
startMonitoring()
|
||||
updateMonthlyDataUsage()
|
||||
updateQuickControlUI()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
stopMonitoring()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!isHidden) {
|
||||
resetNetworkCounters()
|
||||
startMonitoring()
|
||||
updateMonthlyDataUsage()
|
||||
updateQuickControlUI()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetNetworkCounters() {
|
||||
lastRxBytes = TrafficStats.getTotalRxBytes()
|
||||
lastTxBytes = TrafficStats.getTotalTxBytes()
|
||||
lastUpdateTime = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
private fun startMonitoring() {
|
||||
if (monitorJob?.isActive == true) return
|
||||
|
||||
monitorJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
while (isActive) {
|
||||
updateSystemInfo()
|
||||
updateNetworkSpeed()
|
||||
if (!binding.seekVolume.isPressed && !binding.seekBrightness.isPressed) {
|
||||
updateQuickControlUI()
|
||||
}
|
||||
delay(2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopMonitoring() {
|
||||
monitorJob?.cancel()
|
||||
monitorJob = null
|
||||
}
|
||||
|
||||
private fun updateSystemInfo() {
|
||||
val context = requireContext()
|
||||
|
||||
// 1. RAM
|
||||
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
val memoryInfo = ActivityManager.MemoryInfo()
|
||||
activityManager.getMemoryInfo(memoryInfo)
|
||||
|
||||
val totalRam = memoryInfo.totalMem / (1024 * 1024)
|
||||
val availRam = memoryInfo.availMem / (1024 * 1024)
|
||||
val usedRam = totalRam - availRam
|
||||
val ramPercent = if (totalRam > 0) (usedRam.toFloat() / totalRam.toFloat() * 100).toInt() else 0
|
||||
|
||||
binding.textRam.text = "RAM: ${usedRam}MB / ${totalRam}MB ($ramPercent%)"
|
||||
binding.progressRam.progress = ramPercent
|
||||
|
||||
// 2. Storage
|
||||
val statFs = StatFs(Environment.getDataDirectory().path)
|
||||
val totalStorage = statFs.totalBytes / (1024 * 1024 * 1024)
|
||||
val availStorage = statFs.availableBytes / (1024 * 1024 * 1024)
|
||||
val usedStorage = totalStorage - availStorage
|
||||
val storagePercent = if (totalStorage > 0) (usedStorage.toFloat() / totalStorage.toFloat() * 100).toInt() else 0
|
||||
|
||||
binding.textStorage.text = "Storage: ${usedStorage}GB / ${totalStorage}GB ($storagePercent%)"
|
||||
binding.progressStorage.progress = storagePercent
|
||||
|
||||
// 3. Battery
|
||||
val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
|
||||
context.registerReceiver(null, ifilter)
|
||||
}
|
||||
|
||||
batteryStatus?.let { intent ->
|
||||
val level: Int = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
||||
val scale: Int = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
|
||||
val batteryPct = if (scale > 0) level * 100 / scale.toFloat() else 0f
|
||||
|
||||
val tempInt = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0)
|
||||
val temperature = tempInt / 10f
|
||||
|
||||
val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
||||
val isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
|
||||
val chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1)
|
||||
|
||||
val chargingText = when {
|
||||
status == BatteryManager.BATTERY_STATUS_FULL -> "충전 완료 (100%)"
|
||||
isCharging && chargePlug == BatteryManager.BATTERY_PLUGGED_AC -> "충전 중 (AC 어댑터)"
|
||||
isCharging && chargePlug == BatteryManager.BATTERY_PLUGGED_USB -> "충전 중 (USB 연결)"
|
||||
isCharging && chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS -> "충전 중 (무선 충전)"
|
||||
isCharging -> "충전 중"
|
||||
else -> "방전 중"
|
||||
}
|
||||
binding.textChargingStatus.text = "Status: $chargingText"
|
||||
|
||||
binding.textBattery.text = "Battery: ${batteryPct.toInt()}%"
|
||||
binding.progressBattery.progress = batteryPct.toInt()
|
||||
|
||||
if (temperature >= 40.0f) {
|
||||
binding.textTemperature.setTextColor(Color.parseColor("#F44336"))
|
||||
} else {
|
||||
binding.textTemperature.setTextColor(Color.parseColor("#FF9800"))
|
||||
}
|
||||
binding.textTemperature.text = "Temp: ${temperature}°C"
|
||||
}
|
||||
|
||||
// 4. Connectivity
|
||||
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val activeNetwork = cm.activeNetwork
|
||||
val caps = cm.getNetworkCapabilities(activeNetwork)
|
||||
|
||||
var networkName = "오프라인"
|
||||
if (caps != null) {
|
||||
when {
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
|
||||
networkName = "Wi-Fi"
|
||||
try {
|
||||
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
var ssid = wifiManager.connectionInfo.ssid
|
||||
if (ssid.startsWith("\"") && ssid.endsWith("\"")) {
|
||||
ssid = ssid.substring(1, ssid.length - 1)
|
||||
}
|
||||
if (ssid != "<unknown ssid>" && ssid.isNotBlank()) {
|
||||
networkName = "Wi-Fi ($ssid)"
|
||||
}
|
||||
} catch (e: SecurityException) { }
|
||||
}
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> networkName = "Mobile Data"
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> networkName = "Ethernet"
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> networkName = "VPN"
|
||||
}
|
||||
}
|
||||
binding.textNetworkState.text = "Net: $networkName"
|
||||
|
||||
// 5. Bluetooth
|
||||
var btState = "OFF"
|
||||
var connectedDevicesStr = ""
|
||||
try {
|
||||
val btManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
val btAdapter = btManager.adapter
|
||||
if (btAdapter?.isEnabled == true) {
|
||||
btState = "ON"
|
||||
val connectedNames = mutableSetOf<String>()
|
||||
|
||||
val audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
||||
audioDevices.forEach { device ->
|
||||
if (device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
|
||||
device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
|
||||
device.type == AudioDeviceInfo.TYPE_BLE_HEADSET ||
|
||||
device.type == AudioDeviceInfo.TYPE_BLE_SPEAKER) {
|
||||
|
||||
if (!device.productName.isNullOrBlank()) {
|
||||
connectedNames.add(device.productName.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
btManager.getConnectedDevices(BluetoothProfile.GATT).forEach { device ->
|
||||
if (!device.name.isNullOrBlank()) {
|
||||
connectedNames.add(device.name)
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) { }
|
||||
|
||||
if (connectedNames.isNotEmpty()) {
|
||||
connectedDevicesStr = " (${connectedNames.joinToString(", ")})"
|
||||
}
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
btState = "권한 필요"
|
||||
}
|
||||
|
||||
binding.textBluetoothState.text = "BT: $btState$connectedDevicesStr"
|
||||
|
||||
// 6. Uptime
|
||||
val uptimeMillis = SystemClock.elapsedRealtime()
|
||||
val hours = (uptimeMillis / (1000 * 60 * 60)).toInt()
|
||||
val minutes = ((uptimeMillis / (1000 * 60)) % 60).toInt()
|
||||
val seconds = ((uptimeMillis / 1000) % 60).toInt()
|
||||
|
||||
binding.textUptime.text = String.format("System Uptime: %02d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
|
||||
private fun updateNetworkSpeed() {
|
||||
val currentRxBytes = TrafficStats.getTotalRxBytes()
|
||||
val currentTxBytes = TrafficStats.getTotalTxBytes()
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
if (currentRxBytes == TrafficStats.UNSUPPORTED.toLong() || lastUpdateTime == 0L) return
|
||||
|
||||
val timeDiff = (currentTime - lastUpdateTime) / 1000f
|
||||
if (timeDiff > 0) {
|
||||
val rxSpeed = (currentRxBytes - lastRxBytes) / timeDiff
|
||||
val txSpeed = (currentTxBytes - lastTxBytes) / timeDiff
|
||||
|
||||
binding.textDownload.text = "↓ DL: ${formatSpeed(rxSpeed)}"
|
||||
binding.textUpload.text = "↑ UL: ${formatSpeed(txSpeed)}"
|
||||
}
|
||||
|
||||
lastRxBytes = currentRxBytes
|
||||
lastTxBytes = currentTxBytes
|
||||
lastUpdateTime = currentTime
|
||||
}
|
||||
|
||||
// 💡 앱별 다중 색상 데이터 사용량 뷰 생성 로직
|
||||
private fun updateMonthlyDataUsage() {
|
||||
if (!hasUsageStatsPermission()) {
|
||||
binding.textMonthlyUsage.text = "Monthly Usage: 권한 필요 (여기를 클릭하여 허용)"
|
||||
binding.layoutTopApps.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val networkStatsManager = requireContext().getSystemService(Context.NETWORK_STATS_SERVICE) as NetworkStatsManager
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.set(Calendar.DAY_OF_MONTH, 1)
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 0)
|
||||
val startTime = calendar.timeInMillis
|
||||
val endTime = System.currentTimeMillis()
|
||||
|
||||
val wifiBucket = networkStatsManager.querySummaryForDevice(ConnectivityManager.TYPE_WIFI, null, startTime, endTime)
|
||||
val mobileBucket = networkStatsManager.querySummaryForDevice(ConnectivityManager.TYPE_MOBILE, null, startTime, endTime)
|
||||
|
||||
val wifiTotal = wifiBucket.rxBytes + wifiBucket.txBytes
|
||||
val mobileTotal = mobileBucket.rxBytes + mobileBucket.txBytes
|
||||
|
||||
val appUsageMap = mutableMapOf<Int, AppUsage>()
|
||||
fun aggregateStats(networkType: Int, isWifi: Boolean) {
|
||||
try {
|
||||
val networkStats = networkStatsManager.querySummary(networkType, null, startTime, endTime)
|
||||
val bucket = android.app.usage.NetworkStats.Bucket()
|
||||
while (networkStats.hasNextBucket()) {
|
||||
networkStats.getNextBucket(bucket)
|
||||
val uid = bucket.uid
|
||||
val totalBytes = bucket.rxBytes + bucket.txBytes
|
||||
|
||||
val appUsage = appUsageMap.getOrPut(uid) { AppUsage(uid) }
|
||||
if (isWifi) appUsage.wifiBytes += totalBytes
|
||||
else appUsage.mobileBytes += totalBytes
|
||||
}
|
||||
networkStats.close()
|
||||
} catch (e: Exception) { e.printStackTrace() }
|
||||
}
|
||||
|
||||
aggregateStats(ConnectivityManager.TYPE_WIFI, true)
|
||||
aggregateStats(ConnectivityManager.TYPE_MOBILE, false)
|
||||
|
||||
val packageManager = requireContext().packageManager
|
||||
val topApps = appUsageMap.values
|
||||
.filter { it.totalBytes > 0 }
|
||||
.sortedByDescending { it.totalBytes }
|
||||
.take(10)
|
||||
|
||||
topApps.forEach { app ->
|
||||
app.name = "Unknown (UID: ${app.uid})"
|
||||
try {
|
||||
val packages = packageManager.getPackagesForUid(app.uid)
|
||||
if (!packages.isNullOrEmpty()) {
|
||||
val appInfo = packageManager.getApplicationInfo(packages[0], 0)
|
||||
app.name = packageManager.getApplicationLabel(appInfo).toString()
|
||||
}
|
||||
} catch (e: Exception) { }
|
||||
}
|
||||
|
||||
val maxBytes = topApps.maxOfOrNull { it.totalBytes }?.toFloat() ?: 1f
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
// 전체 요약 (색상 아이콘 추가)
|
||||
binding.textMonthlyUsage.text = "이번 달 누적 (Wi-Fi 🟩: ${formatSize(wifiTotal)} / 모바일 🟦: ${formatSize(mobileTotal)})"
|
||||
|
||||
val container = binding.layoutTopApps
|
||||
container.removeAllViews()
|
||||
container.visibility = View.VISIBLE
|
||||
|
||||
if (topApps.isEmpty()) {
|
||||
val emptyText = TextView(context).apply {
|
||||
text = "앱별 데이터 없음"
|
||||
setTextColor(Color.parseColor("#BBBBBB"))
|
||||
}
|
||||
container.addView(emptyText)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
// 💡 동적 멀티 컬러 프로그레스바 그리기 (LinearLayout + layout_weight)
|
||||
topApps.forEachIndexed { index, app ->
|
||||
// 텍스트 라벨
|
||||
val textView = TextView(context).apply {
|
||||
text = "${index + 1}. ${app.name}\n(총: ${formatSize(app.totalBytes)} | Wi-Fi 🟩: ${formatSize(app.wifiBytes)} | 모바일 🟦: ${formatSize(app.mobileBytes)})"
|
||||
setTextColor(Color.parseColor("#CCCCCC"))
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
||||
setPadding(0, 16, 0, 4)
|
||||
}
|
||||
|
||||
// 전체 게이지 바 컨테이너
|
||||
val barContainer = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
weightSum = maxBytes
|
||||
val heightPx = (10 * resources.displayMetrics.density).toInt()
|
||||
val marginPx = (8 * resources.displayMetrics.density).toInt()
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, heightPx).apply {
|
||||
setMargins(0, 0, 0, marginPx)
|
||||
}
|
||||
setBackgroundColor(Color.parseColor("#333333")) // 빈 부분 어두운 배경
|
||||
}
|
||||
|
||||
// 1. Wi-Fi 사용량 막대 (초록색)
|
||||
if (app.wifiBytes > 0) {
|
||||
val wifiView = View(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, app.wifiBytes.toFloat())
|
||||
setBackgroundColor(Color.parseColor("#4CAF50"))
|
||||
}
|
||||
barContainer.addView(wifiView)
|
||||
}
|
||||
|
||||
// 2. 모바일 사용량 막대 (파란색)
|
||||
if (app.mobileBytes > 0) {
|
||||
val mobileView = View(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, app.mobileBytes.toFloat())
|
||||
setBackgroundColor(Color.parseColor("#2196F3"))
|
||||
}
|
||||
barContainer.addView(mobileView)
|
||||
}
|
||||
|
||||
// 3. 남은 여백 (가장 많이 쓴 앱 기준 스케일 맞추기 위함)
|
||||
val emptyWeight = maxBytes - app.totalBytes.toFloat()
|
||||
if (emptyWeight > 0) {
|
||||
val emptyView = View(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, emptyWeight)
|
||||
}
|
||||
barContainer.addView(emptyView)
|
||||
}
|
||||
|
||||
container.addView(textView)
|
||||
container.addView(barContainer)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasUsageStatsPermission(): Boolean {
|
||||
val appOps = requireContext().getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
||||
val mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS, android.os.Process.myUid(), requireContext().packageName)
|
||||
return mode == AppOpsManager.MODE_ALLOWED
|
||||
}
|
||||
|
||||
private fun formatSpeed(bytesPerSec: Float): String {
|
||||
return when {
|
||||
bytesPerSec >= 1024 * 1024 -> String.format("%.2f MB/s", bytesPerSec / (1024 * 1024))
|
||||
bytesPerSec >= 1024 -> String.format("%.1f KB/s", bytesPerSec / 1024)
|
||||
else -> "${bytesPerSec.toInt()} B/s"
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatSize(bytes: Long): String {
|
||||
return when {
|
||||
bytes >= 1024 * 1024 * 1024 -> String.format("%.2f GB", bytes / (1024f * 1024f * 1024f))
|
||||
bytes >= 1024 * 1024 -> String.format("%.1f MB", bytes / (1024f * 1024f))
|
||||
else -> "${bytes / 1024} KB"
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
stopMonitoring()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@ -39,7 +39,7 @@ class TorrentListAdapter(
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val task = tasks[position]
|
||||
holder.tvName.text = task.name
|
||||
holder.tvName.text = task.name.plus("(${task.stateText})")
|
||||
holder.progressBar.progress = task.progress.toInt()
|
||||
holder.tvProgress.text = String.format("%.1f%%", task.progress)
|
||||
|
||||
|
||||
@ -103,6 +103,7 @@ class PortMessage {
|
||||
var urls : List<String> = emptyList()
|
||||
var imgSrc: String? = null
|
||||
var base64Data: String? = null
|
||||
var value : String? = null
|
||||
}
|
||||
class BookContents {
|
||||
var chapterTitle : String? = null
|
||||
|
||||
@ -12,6 +12,7 @@ import android.text.InputType
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.PointerIcon
|
||||
@ -34,6 +35,7 @@ import bums.lunatic.launcher.R
|
||||
import bums.lunatic.launcher.databinding.BooktokiBinding
|
||||
import bums.lunatic.launcher.home.GeckoWeb
|
||||
import bums.lunatic.launcher.home.GeckoWeb.JxEvent
|
||||
import bums.lunatic.launcher.home.KeyEventHandler
|
||||
import bums.lunatic.launcher.home.NeoRssActivity
|
||||
import bums.lunatic.launcher.home.toast
|
||||
import bums.lunatic.launcher.home.tokiz.view.PagedTextLayout
|
||||
@ -64,12 +66,12 @@ import java.util.Date
|
||||
import kotlin.random.Random
|
||||
|
||||
// 기존 BaseToki 및 하위 클래스들을 통합한 단일 클래스
|
||||
class TokiFragment : Fragment(), PagedTextViewInterface {
|
||||
class TokiFragment : Fragment(), PagedTextViewInterface,KeyEventHandler {
|
||||
|
||||
// --- Configuration Properties (Arguments에서 로드) ---
|
||||
private lateinit var contentsType: String
|
||||
private var lastNumber: Int = 0
|
||||
private lateinit var webcontentsName: String
|
||||
lateinit var webcontentsName: String
|
||||
private lateinit var afterDot: String
|
||||
private var useNumberInUrl: Boolean = true
|
||||
private var enableGestures: Boolean = true
|
||||
@ -160,6 +162,17 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
||||
}
|
||||
}
|
||||
|
||||
fun newInstanceI(): TokiFragment = TokiFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(ARG_TYPE, "ijavtorrent")
|
||||
putInt(ARG_LAST_NUM, 143)
|
||||
putString(ARG_NAME, "ijavtorrent")
|
||||
putString(ARG_DOT, "com")
|
||||
putBoolean(ARG_USE_NUM_URL, false)
|
||||
putBoolean(ARG_ENABLE_GESTURE, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun newInstanceNovels(): TokiFragment = TokiFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(ARG_TYPE, "book")
|
||||
@ -236,6 +249,24 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
||||
fun isbooktoki() : Boolean {
|
||||
return webcontentsName.contains("booktoki")
|
||||
}
|
||||
|
||||
override fun onKeyEvent(event: KeyEvent): Boolean {
|
||||
if (!isbooktoki()) return false // 조건부 처리
|
||||
|
||||
val isUp = event.action == MotionEvent.ACTION_UP
|
||||
return when (event.keyCode) {
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
if (isUp) actionNextEvent()
|
||||
true
|
||||
}
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
if (isUp) actionPrevEvent()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSwipeLeft(count: Int) {
|
||||
if (!enableGestures) return
|
||||
Blog.LOGD(log = "onSwipeLeft ${count}")
|
||||
@ -283,8 +314,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
||||
if (contentsType == "youtube") {
|
||||
binding.menuWeb.session?.goBack()
|
||||
} else {
|
||||
// binding.menuWeb.session?.goBack()
|
||||
// 기존 BaseToki 주석 처리됨
|
||||
actionNextEvent(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -395,6 +425,21 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
||||
return workingUrl
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onHiddenChanged(hidden: Boolean) {
|
||||
super.onHiddenChanged(hidden)
|
||||
if (hidden) {
|
||||
// 💡 화면에서 사라질 때: 타이머 중지 및 애니메이션 중지
|
||||
binding.menuWeb?.onPause()
|
||||
// 일반 WebView라면: webView.onPause() 및 webView.pauseTimers()
|
||||
} else {
|
||||
// 💡 다시 나타날 때: 다시 시작
|
||||
// binding.menuWeb?.onResume()
|
||||
// 일반 WebView라면: webView.onResume() 및 webView.resumeTimers()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@ -403,6 +448,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
||||
Blog.LOGD(log = "onCreate ${this::class.java.name} >> savedInstanceState ${savedInstanceState}")
|
||||
binding = BooktokiBinding.inflate(inflater)
|
||||
binding.menuWeb.let {
|
||||
it.sessionTag = webcontentsName
|
||||
it.visibility = View.VISIBLE
|
||||
it.lastDomain = getLastedDoamin()
|
||||
it.setOnLongClickListener {
|
||||
@ -416,27 +462,27 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
||||
if (lastedUrl?.contains("youtube.com") == true) {
|
||||
|
||||
} else {
|
||||
binding.menuWeb.postDelayed({
|
||||
|
||||
Blog.LOGE("onPageStop $success from WebExtension ${mPort!!.name}")
|
||||
val message: JSONObject = JSONObject()
|
||||
try {
|
||||
message.put("type", "getList")
|
||||
message.put("event", "sadsadds")
|
||||
// message.put("tab", session.settings.screenId)
|
||||
} catch (ex: JSONException) {
|
||||
throw RuntimeException(ex)
|
||||
}
|
||||
|
||||
mPort!!.postMessage(message)
|
||||
|
||||
|
||||
// 타입별 분기 처리 (기존 when 절 대체)
|
||||
if (contentsType == "comics" || contentsType == "webtoon") {
|
||||
lastInfo
|
||||
}
|
||||
binding.progress.visibility = GONE
|
||||
}, 10L)
|
||||
// binding.menuWeb.postDelayed({
|
||||
//
|
||||
// Blog.LOGE("onPageStop $success from WebExtension ${mPort!!.name}")
|
||||
// val message: JSONObject = JSONObject()
|
||||
// try {
|
||||
// message.put("type", "getList")
|
||||
// message.put("event", "sadsadds")
|
||||
//// message.put("tab", session.settings.screenId)
|
||||
// } catch (ex: JSONException) {
|
||||
// throw RuntimeException(ex)
|
||||
// }
|
||||
//
|
||||
// mPort!!.postMessage(message)
|
||||
//
|
||||
//
|
||||
// // 타입별 분기 처리 (기존 when 절 대체)
|
||||
// if (contentsType == "comics" || contentsType == "webtoon") {
|
||||
// lastInfo
|
||||
// }
|
||||
// binding.progress.visibility = GONE
|
||||
// }, 10L)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -531,6 +577,7 @@ class TokiFragment : Fragment(), PagedTextViewInterface {
|
||||
it.decoViews.add(activity.findViewById<ImageButton>(R.id.reload))
|
||||
it.decoViews.add(activity.findViewById<ImageButton>(R.id.dl_video))
|
||||
}
|
||||
it.restoreSessionState()
|
||||
}
|
||||
|
||||
binding.btnList.setOnClickListener { v ->
|
||||
|
||||
@ -106,6 +106,14 @@ class FloatingActionMenu @JvmOverloads constructor(
|
||||
|
||||
private var isDragMenuDisabled = false
|
||||
|
||||
// 💡 라벨 위치 동적 변경을 위한 프로퍼티
|
||||
var labelsPosition: Int
|
||||
get() = mLabelsPosition
|
||||
set(position) {
|
||||
mLabelsPosition = position
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
interface OnMenuToggleListener {
|
||||
fun onMenuToggle(opened: Boolean)
|
||||
}
|
||||
@ -388,8 +396,14 @@ class FloatingActionMenu @JvmOverloads constructor(
|
||||
if (label != null) {
|
||||
val labelOffset =
|
||||
(mMaxButtonWidth - child.getMeasuredWidth()) / (if (mUsingMenuLabel) 1 else 2)
|
||||
val labelUsedWidth: Int =
|
||||
|
||||
// 💡 [수정] Center 옵션일 경우 라벨을 버튼 위로 겹쳐 그리므로 공간을 추가하지 않음
|
||||
val labelUsedWidth: Int = if (mLabelsPosition == LABELS_POSITION_CENTER) {
|
||||
child.getMeasuredWidth()
|
||||
} else {
|
||||
child.getMeasuredWidth() + label.calculateShadowWidth() + mLabelsMargin + labelOffset
|
||||
}
|
||||
|
||||
measureChildWithMargins(
|
||||
label,
|
||||
widthMeasureSpec,
|
||||
@ -397,9 +411,11 @@ class FloatingActionMenu @JvmOverloads constructor(
|
||||
heightMeasureSpec,
|
||||
0
|
||||
)
|
||||
usedWidth += label.getMeasuredWidth()
|
||||
maxLabelWidth =
|
||||
max(maxLabelWidth.toDouble(), (usedWidth + labelOffset).toDouble()).toInt()
|
||||
|
||||
if (mLabelsPosition != LABELS_POSITION_CENTER) {
|
||||
usedWidth += label.getMeasuredWidth()
|
||||
maxLabelWidth = max(maxLabelWidth.toDouble(), (usedWidth + labelOffset).toDouble()).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,30 +499,40 @@ class FloatingActionMenu @JvmOverloads constructor(
|
||||
|
||||
val label = fab.getTag(R.id.fab_label) as View?
|
||||
if (label != null) {
|
||||
val labelsOffset =
|
||||
(if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.getMeasuredWidth() / 2) + mLabelsMargin
|
||||
val labelXNearButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
buttonsHorizontalCenter - labelsOffset
|
||||
else
|
||||
buttonsHorizontalCenter + labelsOffset
|
||||
val labelLeft: Int
|
||||
val labelRight: Int
|
||||
val labelTop: Int
|
||||
|
||||
val labelXAwayFromButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXNearButton - label.getMeasuredWidth()
|
||||
else
|
||||
labelXNearButton + label.getMeasuredWidth()
|
||||
// 💡 [수정] Center 옵션일 경우 X,Y축 모두 버튼의 중앙으로 계산
|
||||
if (mLabelsPosition == LABELS_POSITION_CENTER) {
|
||||
labelLeft = childX + (fab.getMeasuredWidth() - label.getMeasuredWidth()) / 2
|
||||
labelRight = labelLeft + label.getMeasuredWidth()
|
||||
labelTop = childY + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
|
||||
} else {
|
||||
val labelsOffset =
|
||||
(if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.getMeasuredWidth() / 2) + mLabelsMargin
|
||||
val labelXNearButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
buttonsHorizontalCenter - labelsOffset
|
||||
else
|
||||
buttonsHorizontalCenter + labelsOffset
|
||||
|
||||
val labelLeft = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXAwayFromButton
|
||||
else
|
||||
labelXNearButton
|
||||
val labelXAwayFromButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXNearButton - label.getMeasuredWidth()
|
||||
else
|
||||
labelXNearButton + label.getMeasuredWidth()
|
||||
|
||||
val labelRight = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXNearButton
|
||||
else
|
||||
labelXAwayFromButton
|
||||
labelLeft = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXAwayFromButton
|
||||
else
|
||||
labelXNearButton
|
||||
|
||||
val labelTop = childY - mLabelsVerticalOffset + (fab.getMeasuredHeight()
|
||||
- label.getMeasuredHeight()) / 2
|
||||
labelRight = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXNearButton
|
||||
else
|
||||
labelXAwayFromButton
|
||||
|
||||
labelTop = childY - mLabelsVerticalOffset + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
|
||||
}
|
||||
|
||||
label.layout(labelLeft, labelTop, labelRight, labelTop + label.getMeasuredHeight())
|
||||
|
||||
@ -667,7 +693,6 @@ class FloatingActionMenu @JvmOverloads constructor(
|
||||
fab = child as FloatingActionButton
|
||||
if (fab === mMenuButton) continue
|
||||
|
||||
// only child buttons and imageToggle on menuBtn.
|
||||
child.layout(
|
||||
childX, childY, childX + child.getMeasuredWidth(),
|
||||
childY + child.getMeasuredHeight()
|
||||
@ -681,30 +706,40 @@ class FloatingActionMenu @JvmOverloads constructor(
|
||||
if (child !== mImageToggle) label = fab.getTag(R.id.fab_label) as View?
|
||||
|
||||
if (label != null) {
|
||||
val labelsOffset =
|
||||
(if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.getMeasuredWidth() / 2) + mLabelsMargin
|
||||
val labelXNearButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
l + buttonsHorizontalCenter - labelsOffset
|
||||
else
|
||||
l + buttonsHorizontalCenter + labelsOffset
|
||||
val labelLeft: Int
|
||||
val labelRight: Int
|
||||
val labelTop: Int
|
||||
|
||||
val labelXAwayFromButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXNearButton - label.getMeasuredWidth()
|
||||
else
|
||||
labelXNearButton + label.getMeasuredWidth()
|
||||
// 💡 [수정] Center 옵션일 경우 드래그 중에도 중앙으로 정렬
|
||||
if (mLabelsPosition == LABELS_POSITION_CENTER) {
|
||||
labelLeft = childX + (fab.getMeasuredWidth() - label.getMeasuredWidth()) / 2
|
||||
labelRight = labelLeft + label.getMeasuredWidth()
|
||||
labelTop = childY + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
|
||||
} else {
|
||||
val labelsOffset =
|
||||
(if (mUsingMenuLabel) mMaxButtonWidth / 2 else fab.getMeasuredWidth() / 2) + mLabelsMargin
|
||||
val labelXNearButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
l + buttonsHorizontalCenter - labelsOffset
|
||||
else
|
||||
l + buttonsHorizontalCenter + labelsOffset
|
||||
|
||||
val labelLeft = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXAwayFromButton
|
||||
else
|
||||
labelXNearButton
|
||||
val labelXAwayFromButton = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXNearButton - label.getMeasuredWidth()
|
||||
else
|
||||
labelXNearButton + label.getMeasuredWidth()
|
||||
|
||||
val labelRight = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXNearButton
|
||||
else
|
||||
labelXAwayFromButton
|
||||
labelLeft = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXAwayFromButton
|
||||
else
|
||||
labelXNearButton
|
||||
|
||||
val labelTop = childY - mLabelsVerticalOffset + (fab.getMeasuredHeight()
|
||||
- label.getMeasuredHeight()) / 2
|
||||
labelRight = if (mLabelsPosition == LABELS_POSITION_LEFT)
|
||||
labelXNearButton
|
||||
else
|
||||
labelXAwayFromButton
|
||||
|
||||
labelTop = childY - mLabelsVerticalOffset + (fab.getMeasuredHeight() - label.getMeasuredHeight()) / 2
|
||||
}
|
||||
|
||||
label.layout(
|
||||
labelLeft,
|
||||
@ -739,31 +774,43 @@ class FloatingActionMenu @JvmOverloads constructor(
|
||||
label.setShowShadow(false)
|
||||
label.setUsingStyle(true)
|
||||
} else {
|
||||
label.setColors(mLabelsColorNormal, mLabelsColorPressed, mLabelsColorRipple)
|
||||
label.setShowShadow(mLabelsShowShadow)
|
||||
label.setCornerRadius(mLabelsCornerRadius)
|
||||
if (mLabelsEllipsize > 0) {
|
||||
setLabelEllipsize(label)
|
||||
// 💡 [수정] Center 옵션일 경우 배경 및 테두리 효과를 제거하여 내장 텍스트처럼 보이게 연출
|
||||
if (mLabelsPosition == LABELS_POSITION_CENTER) {
|
||||
label.setColors(Color.TRANSPARENT, Color.TRANSPARENT, Color.TRANSPARENT)
|
||||
label.setShowShadow(false)
|
||||
label.setCornerRadius(0)
|
||||
label.updateBackground()
|
||||
|
||||
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, mLabelsTextSize)
|
||||
label.setTextColor(mLabelsTextColor)
|
||||
label.setPadding(0, 0, 0, 0)
|
||||
} else {
|
||||
label.setColors(mLabelsColorNormal, mLabelsColorPressed, mLabelsColorRipple)
|
||||
label.setShowShadow(mLabelsShowShadow)
|
||||
label.setCornerRadius(mLabelsCornerRadius)
|
||||
if (mLabelsEllipsize > 0) {
|
||||
setLabelEllipsize(label)
|
||||
}
|
||||
label.setMaxLines(mLabelsMaxLines)
|
||||
label.updateBackground()
|
||||
|
||||
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, mLabelsTextSize)
|
||||
label.setTextColor(mLabelsTextColor)
|
||||
|
||||
var left = mLabelsPaddingLeft
|
||||
var top = mLabelsPaddingTop
|
||||
if (mLabelsShowShadow) {
|
||||
left += (fab.shadowRadius + abs(fab.shadowXOffset.toDouble())).toInt()
|
||||
top += (fab.shadowRadius + abs(fab.shadowYOffset.toDouble())).toInt()
|
||||
}
|
||||
|
||||
label.setPadding(
|
||||
left,
|
||||
top,
|
||||
mLabelsPaddingLeft,
|
||||
mLabelsPaddingTop
|
||||
)
|
||||
}
|
||||
label.setMaxLines(mLabelsMaxLines)
|
||||
label.updateBackground()
|
||||
|
||||
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, mLabelsTextSize)
|
||||
label.setTextColor(mLabelsTextColor)
|
||||
|
||||
var left = mLabelsPaddingLeft
|
||||
var top = mLabelsPaddingTop
|
||||
if (mLabelsShowShadow) {
|
||||
left += (fab.shadowRadius + abs(fab.shadowXOffset.toDouble())).toInt()
|
||||
top += (fab.shadowRadius + abs(fab.shadowYOffset.toDouble())).toInt()
|
||||
}
|
||||
|
||||
label.setPadding(
|
||||
left,
|
||||
top,
|
||||
mLabelsPaddingLeft,
|
||||
mLabelsPaddingTop
|
||||
)
|
||||
|
||||
if (mLabelsMaxLines < 0 || mLabelsSingleLine) {
|
||||
label.setSingleLine(mLabelsSingleLine)
|
||||
@ -1224,7 +1271,9 @@ class FloatingActionMenu @JvmOverloads constructor(
|
||||
private const val OPEN_UP = 0
|
||||
private const val OPEN_DOWN = 1
|
||||
|
||||
private const val LABELS_POSITION_LEFT = 0
|
||||
private const val LABELS_POSITION_RIGHT = 1
|
||||
// 💡 [수정] 상수 공개 선언 및 CENTER 속성(2) 추가
|
||||
const val LABELS_POSITION_LEFT = 0
|
||||
const val LABELS_POSITION_RIGHT = 1
|
||||
const val LABELS_POSITION_CENTER = 2
|
||||
}
|
||||
}
|
||||
@ -109,12 +109,16 @@ object TaskAggregator {
|
||||
|
||||
// [일괄 저장]
|
||||
if (allFeeds.isNotEmpty()) {
|
||||
WorkersDb.getRealm().write {
|
||||
// 1. 새 데이터 저장
|
||||
allFeeds.forEach { feed ->
|
||||
copyToRealm(feed, UpdatePolicy.ALL)
|
||||
try {
|
||||
WorkersDb.getRealm().write {
|
||||
// 1. 새 데이터 저장
|
||||
allFeeds.forEach { feed ->
|
||||
try { copyToRealm(feed, UpdatePolicy.ERROR) }catch (e: Exception){}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch (e: Exception){}
|
||||
|
||||
}
|
||||
|
||||
Blog.LOGE("NewsFeed Aggregation finished in ${System.currentTimeMillis() - startTime}ms. Items: ${allFeeds.size}")
|
||||
|
||||
@ -28,6 +28,7 @@ import bums.lunatic.launcher.utils.Blog
|
||||
import com.frostwire.jlibtorrent.swig.error_code
|
||||
import com.frostwire.jlibtorrent.swig.libtorrent
|
||||
import com.frostwire.jlibtorrent.swig.settings_pack
|
||||
import com.frostwire.jlibtorrent.swig.torrent_status
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable.isActive
|
||||
@ -41,8 +42,10 @@ import kotlinx.coroutines.launch
|
||||
data class TorrentTask(
|
||||
val infoHash: String,
|
||||
val name: String,
|
||||
val progress: Float, // 0.0 ~ 100.0
|
||||
val isPaused: Boolean
|
||||
val progress: Float,
|
||||
val isPaused: Boolean,
|
||||
val isQueued: Boolean, // 대기열 여부
|
||||
val stateText: String
|
||||
)
|
||||
|
||||
class TorrentService : Service() {
|
||||
@ -87,6 +90,16 @@ class TorrentService : Service() {
|
||||
} else if (hasActiveTorrents && taskCount == 0) {
|
||||
// 활성화된 적이 있었는데 0개가 되었다면 (모두 완료되어 이동되었거나 삭제됨)
|
||||
println("TorrentService: 모든 작업이 완료되어 서비스를 자동 종료합니다.")
|
||||
|
||||
try {
|
||||
if (tempDir.exists()) {
|
||||
tempDir.listFiles()?.forEach { it.deleteRecursively() }
|
||||
println("TorrentService: 임시 다운로드 폴더 정리를 완료했습니다.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
stopForeground(true) // 💡 상단바 알림 즉시 제거
|
||||
stopSelf() // 💡 서비스 스스로 종료 (메모리 해제)
|
||||
break // 무한 루프 탈출
|
||||
@ -103,6 +116,30 @@ class TorrentService : Service() {
|
||||
if (!handle.isValid) continue
|
||||
|
||||
val status = handle.status()
|
||||
val state = status.state()
|
||||
|
||||
// 1. 현재 다운로드 대기 중(Queued)인지 확인
|
||||
// 'auto_managed'이면서, 실제 'downloading' 상태가 아닌 경우를 체크합니다.
|
||||
val isQueued = status.flags().and_(libtorrent.getAuto_managed()).nonZero() &&
|
||||
(state.swig() == torrent_status.state_t.downloading_metadata.swigValue() ||
|
||||
state.swig() == torrent_status.state_t.checking_files.swigValue() ||
|
||||
state.swig() == torrent_status.state_t.checking_resume_data.swigValue())
|
||||
|
||||
// 2. 더 확실한 방법 (상태값이 'queued_for_checking' 등인 경우 포함)
|
||||
// 실제 데이터 전송이 일어나지 않고 대기 순번을 기다리는 상태인지 확인
|
||||
val isDownloading = (state.swig() == torrent_status.state_t.downloading.swigValue())
|
||||
val isFinished = status.isFinished
|
||||
val isPaused = status.flags().and_(libtorrent.getPaused()).nonZero()
|
||||
val stateText = when {
|
||||
status.flags().and_(libtorrent.getPaused()).nonZero() -> "일시정지"
|
||||
state.swig() == torrent_status.state_t.checking_files.swigValue() -> "파일 검사 중"
|
||||
state.swig() == torrent_status.state_t.downloading_metadata.swigValue() -> "메타데이터 수신 중"
|
||||
state.swig() == torrent_status.state_t.downloading.swigValue() -> "다운로드 중"
|
||||
state.swig() == torrent_status.state_t.finished.swigValue() || state.swig() == torrent_status.state_t.seeding.swigValue() -> "완료"
|
||||
else -> "대기 중" // activeDownloads 제한에 걸려 대기하는 경우 여기로 들어옵니다.
|
||||
}
|
||||
// "일시정지도 아니고, 완료도 아닌데, 현재 다운로드 중도 아니면" -> 대기열 상태로 간주
|
||||
val realIsQueued = !isPaused && !isFinished && !isDownloading
|
||||
val hashStr = status.infoHash().toString()
|
||||
var rawName = status.name()
|
||||
|
||||
@ -133,7 +170,9 @@ class TorrentService : Service() {
|
||||
infoHash = hashStr,
|
||||
name = displayName,
|
||||
progress = status.progress() * 100f,
|
||||
isPaused = status.flags().and_(com.frostwire.jlibtorrent.swig.libtorrent.getPaused()).nonZero()
|
||||
isPaused = status.flags().and_(com.frostwire.jlibtorrent.swig.libtorrent.getPaused()).nonZero(),
|
||||
isQueued = isQueued,
|
||||
stateText = stateText
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -199,12 +238,24 @@ class TorrentService : Service() {
|
||||
sp.setBoolean(settings_pack.bool_types.enable_dht.swigValue(), true) // DHT 활성화
|
||||
// sp.setBoolean(settings_pack.bool_types.enable_pex.swigValue(), true) // Peer Exchange (피어 교환)
|
||||
sp.setBoolean(settings_pack.bool_types.enable_lsd.swigValue(), true) // 로컬 네트워크 탐색
|
||||
sp.setBoolean(settings_pack.bool_types.dont_count_slow_torrents.swigValue(), true)
|
||||
// 2. '느리다'고 판단할 다운로드 속도 기준 (예: 10KB/s 미만)
|
||||
sp.setInteger(settings_pack.int_types.inactive_down_rate.swigValue(), 10 * 1024)
|
||||
|
||||
// 3. '느리다'고 판단할 업로드 속도 기준 (예: 5KB/s 미만)
|
||||
sp.setInteger(settings_pack.int_types.inactive_up_rate.swigValue(), 5 * 1024)
|
||||
|
||||
// 4. 이 느린 속도가 몇 초간 지속될 때 대기열로 미룰 것인지 판단 (예: 20초)
|
||||
sp.setInteger(settings_pack.int_types.inactivity_timeout.swigValue(), 20)
|
||||
|
||||
// [3] 얼마나 오래 느려야 대기열로 보낼지 결정 (초 단위)
|
||||
sp.setInteger(settings_pack.int_types.auto_manage_interval.swigValue(), 20)
|
||||
// [3] 모바일 맞춤형 연결 최적화
|
||||
// 안드로이드는 데스크탑처럼 수천 개를 뚫으면 공유기가 뻗거나 앱 OOM이 발생할 수 있습니다.
|
||||
sp.connectionsLimit(400) // 글로벌 최대 연결 수 (기본값 보통 200)
|
||||
sp.activeDownloads(3) // 동시에 속도를 끌어올릴 다운로드 개수 (너무 많으면 속도가 분산됨)
|
||||
sp.activeLimit(5) // 활성 상태(업/다운 포함) 유지 최대 개수
|
||||
sp.activeDownloads(4) // 동시에 속도를 끌어올릴 다운로드 개수 (너무 많으면 속도가 분산됨)
|
||||
sp.activeLimit(6) // 활성 상태(업/다운 포함) 유지 최대 개수
|
||||
sp.activeSeeds(2)
|
||||
|
||||
// 세팅을 세션에 반영
|
||||
session.applySettings(sp)
|
||||
@ -254,15 +305,14 @@ class TorrentService : Service() {
|
||||
println("TorrentService: 마그넷 파싱 에러 - ${error.message()} / URI: $magnetUri")
|
||||
return
|
||||
}
|
||||
swigParams.max_connections = 150
|
||||
swigParams.flags = swigParams.flags.or_(libtorrent.getAuto_managed())
|
||||
swigParams.max_connections = Int.MAX_VALUE
|
||||
// 다운로드/업로드 제한 해제 (기본값이 -1이긴 하나 명시적으로 박아줌)
|
||||
swigParams.download_limit = -1
|
||||
swigParams.upload_limit = -1
|
||||
|
||||
// 참고: 만약 "순차 다운로드(동영상 스트리밍용)"가 필요하다면 아래 주석을 푸세요.
|
||||
// 단, 전체 다운로드 완료 속도는 일반 다운로드보다 느려집니다.
|
||||
// swigParams.flags = swigParams.flags.or_(libtorrent.getSequential_download())
|
||||
|
||||
swigParams.setSave_path(tempDir.absolutePath)
|
||||
session.swig().async_add_torrent(swigParams)
|
||||
|
||||
@ -289,7 +339,10 @@ class TorrentService : Service() {
|
||||
|
||||
/** 제어 기능 */
|
||||
fun pauseTorrent(infoHash: String) {
|
||||
session.find(Sha1Hash(infoHash))?.pause()
|
||||
session.find(Sha1Hash(infoHash))?.let {
|
||||
it.pause()
|
||||
it.queuePositionBottom()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -125,23 +125,22 @@
|
||||
<LinearLayout
|
||||
android:id="@+id/quickSearch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="30dp"
|
||||
android:layout_height="32dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="@color/black"
|
||||
android:gravity="center_vertical"
|
||||
>
|
||||
<androidx.appcompat.widget.AppCompatSpinner
|
||||
android:paddingLeft="5dp"
|
||||
android:paddingRight="5dp"
|
||||
style="@style/SearchAccs"
|
||||
android:layout_marginLeft="6dp"
|
||||
android:layout_marginRight="6dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:paddingRight="5dp"
|
||||
android:paddingLeft="5dp"
|
||||
android:id="@+id/categorySpinner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="30dp"
|
||||
android:spinnerMode="dropdown"
|
||||
android:background="@drawable/base_bg"/>
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:spinnerMode="dropdown" />
|
||||
<TextView
|
||||
app:autoSizeTextType="uniform"
|
||||
style="@style/SearchAccs"
|
||||
|
||||
327
app/src/main/res/layout/fragment_system_status.xml
Normal file
327
app/src/main/res/layout/fragment_system_status.xml
Normal file
@ -0,0 +1,327 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:background="#E6121212">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="System Status"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="#1AFFFFFF"
|
||||
android:padding="12dp"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Quick Controls"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:weightSum="3">
|
||||
<TextView
|
||||
android:id="@+id/btnQuickWifi"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:padding="8dp"
|
||||
android:text="📶 Wi-Fi"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:id="@+id/btnQuickBt"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:padding="8dp"
|
||||
android:text="🛜 Bluetooth"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:id="@+id/btnQuickRinger"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:padding="8dp"
|
||||
android:text="🔔 소리"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="☀️"
|
||||
android:textSize="18sp"/>
|
||||
<SeekBar
|
||||
android:id="@+id/seekBrightness"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:max="255"
|
||||
android:progressTint="#FFEB3B"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🎵"
|
||||
android:textSize="18sp"/>
|
||||
<SeekBar
|
||||
android:id="@+id/seekVolume"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:progressTint="#03A9F4"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="24dp">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Memory (RAM) - 터치 시 정리"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="14sp" />
|
||||
<TextView
|
||||
android:id="@+id/textRam"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="RAM: 계산 중..."
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<ProgressBar
|
||||
android:id="@+id/progressRam"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="12dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:max="100"
|
||||
android:progress="0"
|
||||
android:progressTint="#4CAF50" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="24dp">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Internal Storage - 터치 시 관리"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="14sp" />
|
||||
<TextView
|
||||
android:id="@+id/textStorage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Storage: 계산 중..."
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<ProgressBar
|
||||
android:id="@+id/progressStorage"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="12dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:max="100"
|
||||
android:progress="0"
|
||||
android:progressTint="#2196F3" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="24dp">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Battery & Thermal"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="14sp" />
|
||||
<TextView
|
||||
android:id="@+id/textChargingStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="Status: 상태 확인 중..."
|
||||
android:textColor="#888888"
|
||||
android:textSize="12sp" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/textBattery"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Battery: 계산 중..."
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:id="@+id/textTemperature"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Temp: 0.0°C"
|
||||
android:textColor="#FF9800"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBattery"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="12dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:max="100"
|
||||
android:progress="0"
|
||||
android:progressTint="#FFC107" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="24dp">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Network & Connectivity"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textNetworkState"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Net: 확인 중..."
|
||||
android:textColor="#2196F3"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textBluetoothState"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="BT: 확인 중..."
|
||||
android:textColor="#03A9F4"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
<TextView
|
||||
android:id="@+id/textDownload"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="↓ DL: 0 KB/s"
|
||||
android:textColor="#4CAF50"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
<TextView
|
||||
android:id="@+id/textUpload"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="↑ UL: 0 KB/s"
|
||||
android:textColor="#F44336"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textMonthlyUsage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingVertical="4dp"
|
||||
android:text="Monthly Usage: 권한 필요 (여기를 클릭)"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutTopApps"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:id="@+id/textUptime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="System Uptime: 00:00:00"
|
||||
android:textColor="#888888"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@ -11,13 +11,13 @@
|
||||
<TextView
|
||||
android:id="@+id/tvName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
tools:text="Ubuntu-22.04-desktop-amd64.iso"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/white"
|
||||
android:textStyle="bold"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="middle"
|
||||
android:ellipsize="marquee"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnPauseResume"
|
||||
|
||||
@ -89,6 +89,7 @@
|
||||
app:menu_colorNormal="#80FF0000"
|
||||
app:menu_fab_size="mini"
|
||||
app:menu_icon="@drawable/ic_add"
|
||||
app:menu_labels_position="center"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_height="match_parent"
|
||||
@ -102,6 +103,7 @@
|
||||
android:onClick="floatClick"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"/>
|
||||
|
||||
<bums.lunatic.launcher.view.FloatingActionButton
|
||||
app:fab_label="booktoki"
|
||||
android:id="@+id/books"
|
||||
@ -160,6 +162,15 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"/>
|
||||
|
||||
<bums.lunatic.launcher.view.FloatingActionButton
|
||||
app:fab_label="I"
|
||||
android:id="@+id/btn_i"
|
||||
app:fab_showShadow="true"
|
||||
app:fab_size="mini"
|
||||
android:onClick="floatClick"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"/>
|
||||
|
||||
<bums.lunatic.launcher.view.FloatingActionButton
|
||||
app:fab_label="torrent"
|
||||
android:id="@+id/btn_torrent"
|
||||
@ -168,7 +179,14 @@
|
||||
android:onClick="floatClick"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"/>
|
||||
|
||||
<bums.lunatic.launcher.view.FloatingActionButton
|
||||
app:fab_label="system"
|
||||
android:id="@+id/btn_info"
|
||||
app:fab_showShadow="true"
|
||||
app:fab_size="mini"
|
||||
android:onClick="floatClick"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"/>
|
||||
<bums.lunatic.launcher.view.FloatingActionButton
|
||||
app:fab_label="close"
|
||||
android:id="@+id/close"
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/text1"
|
||||
style="@style/SearchAccs"
|
||||
android:layout_margin="10dp"
|
||||
android:padding="10dp"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/bottom_option"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:background="@color/black"
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true" />
|
||||
@ -29,6 +29,7 @@
|
||||
<enum name="normal" value="0" />
|
||||
<enum name="mini" value="1" />
|
||||
</attr>
|
||||
|
||||
<attr name="fab_showAnimation" format="reference" />
|
||||
<attr name="fab_hideAnimation" format="reference" />
|
||||
<attr name="fab_label" format="string" />
|
||||
@ -62,6 +63,7 @@
|
||||
<attr name="menu_labels_position" format="enum">
|
||||
<enum name="left" value="0" />
|
||||
<enum name="right" value="1" />
|
||||
<enum name="center" value="2" />
|
||||
</attr>
|
||||
<attr name="menu_icon" format="reference" />
|
||||
<attr name="menu_animationDelayPerItem" format="integer" />
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">35dp</item>
|
||||
<item name="android:gravity">center</item>
|
||||
<item name="android:textColor">@color/tabs_black</item>
|
||||
<item name="android:textColor">@color/bottom_option</item>
|
||||
<item name="android:visibility">visible</item>
|
||||
</style>
|
||||
<style name="asdda" parent="Widget.Material3.Button.OutlinedButton">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user