From 2f63f8550a2a54291a4dc02b8bc48aeb0d69be62 Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Wed, 20 Aug 2025 17:08:17 +0900 Subject: [PATCH] ... --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 323 +++++++++--------- .../extensions/my_extension/messaging.js | 50 ++- .../bums/lunatic/launcher/LauncherActivity.kt | 85 ++--- ...uetoothManager.kt => ForeGroundService.kt} | 109 ++++-- .../launcher/helpers/ServiceWatchdogWorker.kt | 36 ++ .../bums/lunatic/launcher/home/GeckoWeb.kt | 196 +++++++++-- .../bums/lunatic/launcher/home/RssHome.kt | 2 + .../lunatic/launcher/receiver/NLService.kt | 142 ++------ .../lunatic/launcher/view/ZoomImageView.kt | 57 ++++ app/src/main/res/drawable/dl_vid.png | Bin 0 -> 530 bytes app/src/main/res/layout/launcher_activity.xml | 42 ++- .../main/res/layout/layout_rss_summary.xml | 4 +- app/src/main/res/layout/progress_dialog.xml | 29 ++ 14 files changed, 686 insertions(+), 393 deletions(-) rename app/src/main/kotlin/bums/lunatic/launcher/helpers/{BluetoothManager.kt => ForeGroundService.kt} (75%) create mode 100644 app/src/main/kotlin/bums/lunatic/launcher/helpers/ServiceWatchdogWorker.kt create mode 100644 app/src/main/kotlin/bums/lunatic/launcher/view/ZoomImageView.kt create mode 100644 app/src/main/res/drawable/dl_vid.png create mode 100644 app/src/main/res/layout/progress_dialog.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e7e98741..ced53b4f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -120,7 +120,9 @@ dependencies { // implementation("org.opencv:opencv-android:4.11.0") // build.gradle에 추가 // implementation ("com.github.aeonSolutions:FloatingActionButtonMenuDrag:1.1") - + implementation("io.github.junkfood02.youtubedl-android:library:0.17.4") + implementation("io.github.junkfood02.youtubedl-android:ffmpeg:0.17.4") + implementation("io.github.junkfood02.youtubedl-android:aria2c:0.17.4") implementation ("androidx.media:media:1.7.0") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 445e4c3f..c1636ba8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,17 +57,17 @@ - - - - - - - - - - - + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + @@ -212,52 +213,52 @@ android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false"/> - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/extensions/my_extension/messaging.js b/app/src/main/assets/extensions/my_extension/messaging.js index 41dc2b02..fb7a2565 100644 --- a/app/src/main/assets/extensions/my_extension/messaging.js +++ b/app/src/main/assets/extensions/my_extension/messaging.js @@ -253,9 +253,16 @@ document.addEventListener('DOMContentLoaded', function () { if (port) { sendMessage({type: "MSG", msg: "connect prot"}); time1 = setTimeout(autoScrollAndSave(false), 3500) + } }) +document.addEventListener('touchstart', function(e) { + console.log('터치 시작'); +}); +document.addEventListener('touchend', function(e) { + autoScrollAndSave() +}); function scrollToLazyImg(fastMode) { (function(autoScrollAndSave){ @@ -346,12 +353,16 @@ function isNewerThanOneDay(dateStr) { function autoScrollAndSave(senContents) { // 도메인에 맞는 handler 실행 const matchedRule = domainRules.find(rule => rule.test(location.href)); - if (matchedRule) { - matchedRule.handler(); - } + try { + if (matchedRule) { + matchedRule.handler(); + } + }catch (e) { } + + try { // 공통 광고 요소 제거는 항상 실행 handleCommon(); - window.scrollTo({ top: 2, behavior: 'smooth' }); +}catch (e) { } if (mainContentsEl == null) { mainContentsEl = document.body.outerHTML } @@ -545,7 +556,11 @@ function handleCommon() { gotoNext() } - window.scrollTo({ top: 2, behavior: 'smooth' }); + if (window.scrollY < 5) { + console.log("window.scrollY >>> " + window.scrollY) + window.scrollTo({ top: 5, behavior: 'smooth' }); + } + } function handleToreentZota() { if (location.href.search("torrentzota") > -1 && document.querySelectorAll('a')) { @@ -759,13 +774,26 @@ function handleDoctorsnews() { } function handleDcinside() { - document.querySelectorAll( - '[id^="view_btn_area"], [class^="trend-rank"], [class^="view-btm-con"], [class^="md-tit-box"], [class^="gall-detail-lst"], [class^="outside-search-box"], [class^="footer ftlong"], [class^="adv-group"], li[style^="cursor:default;"], [id^="div_adnmore_area"]' - ).forEach(e => e.remove()); - document.querySelectorAll('div[class^="imgwrap"]').forEach(function (e) { - try {e.style.backgroundColor = 'red';} catch (e) {} - }) + try { + document.querySelectorAll( + '[id^="view_btn_area"], [class^="trend-rank"], [class^="view-btm-con"], [class^="md-tit-box"], [class^="gall-detail-lst"], [class^="outside-search-box"], [class^="footer ftlong"], [class^="adv-group"], li[style^="cursor:default;"], [id^="div_adnmore_area"]' + ).forEach(e => e.remove()); + }catch (e) { + + } + try { + document.querySelectorAll('div[class^="imgwrap"]').forEach(function (e) { + try {e.style.backgroundColor = 'red';} catch (e) {} + }) + } catch (e) { + + } + try { + document.querySelectorAll('div[class^="imgwrap"]')[0].click() + }catch (e) { + + } mainContentsEl = document.querySelector('div[class="container"]'); } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt index 37d5e65a..003161e6 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/LauncherActivity.kt @@ -22,13 +22,11 @@ package bums.lunatic.launcher import android.annotation.SuppressLint import android.app.SearchManager -import android.appwidget.AppWidgetManager import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.res.Configuration import android.graphics.Color -import android.graphics.Rect import android.net.Uri import android.os.Build import android.os.Bundle @@ -49,89 +47,50 @@ import android.view.PointerIcon import android.view.View import android.view.WindowInsets import android.view.WindowManager -import android.widget.Button -import android.widget.HorizontalScrollView -import android.widget.LinearLayout import androidx.activity.OnBackPressedCallback import androidx.annotation.RequiresApi -import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.core.view.updatePadding -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager import bums.lunatic.launcher.LauncherActivity.Companion.lActivity import bums.lunatic.launcher.apps.AppDrawer -import bums.lunatic.launcher.tokiz.Novels import bums.lunatic.launcher.common.CommonActivity import bums.lunatic.launcher.databinding.LauncherActivityBinding -import bums.lunatic.launcher.feeds.WidgetHost -import bums.lunatic.launcher.helpers.BluetoothManager import bums.lunatic.launcher.helpers.Constants.Companion.KEY_STATUS_BAR import bums.lunatic.launcher.helpers.Constants.Companion.PREFS_SETTINGS -import bums.lunatic.launcher.helpers.Constants.Companion.widgetHostId +import bums.lunatic.launcher.helpers.ForeGroundService import bums.lunatic.launcher.helpers.HeadsetActionButtonReceiver -import bums.lunatic.launcher.helpers.PrefHelper.putString -import bums.lunatic.launcher.helpers.PrefLong -import bums.lunatic.launcher.home.GeckoWeb import bums.lunatic.launcher.home.RssHome import bums.lunatic.launcher.home.RssViewBuilder import bums.lunatic.launcher.model.RssData import bums.lunatic.launcher.model.RssDataType +import bums.lunatic.launcher.receiver.NLService +import bums.lunatic.launcher.settings.SettingsActivity import bums.lunatic.launcher.tokiz.Comics import bums.lunatic.launcher.tokiz.Magnet +import bums.lunatic.launcher.tokiz.Novels import bums.lunatic.launcher.tokiz.Perplexity import bums.lunatic.launcher.tokiz.Twitter import bums.lunatic.launcher.tokiz.Webtoons import bums.lunatic.launcher.tokiz.Zota import bums.lunatic.launcher.utils.Blog -import bums.lunatic.launcher.utils.FeedParseManager -import bums.lunatic.launcher.utils.getJ -import bums.lunatic.launcher.workers.AppInfoGetter -import bums.lunatic.launcher.workers.ArcaGetter -import bums.lunatic.launcher.workers.CalendarGetter -import bums.lunatic.launcher.workers.ClienGetter -import bums.lunatic.launcher.workers.ContactInfoGetter -import bums.lunatic.launcher.workers.DCGetter -import bums.lunatic.launcher.workers.DotaxGetter -import bums.lunatic.launcher.workers.DotaxGetter.Companion.COMIC2_WORK_TAG -import bums.lunatic.launcher.workers.FmKoreaGetter -import bums.lunatic.launcher.workers.FmKoreaGetter.Companion.FM_WORK_TAG -import bums.lunatic.launcher.workers.LocationGetter -import bums.lunatic.launcher.workers.NewsFeedsGetter -import bums.lunatic.launcher.workers.NewsFeedsGetter.Companion.FEDDS_WORK_TAG -import bums.lunatic.launcher.workers.RecentCallGetter -import bums.lunatic.launcher.workers.RecentSmsGetter -import bums.lunatic.launcher.workers.RecentSmsGetter.Companion.SMS_WORK_TAG -import bums.lunatic.launcher.workers.RedditGetter -import bums.lunatic.launcher.workers.RedditGetter.Companion.REDDIT_WORK_TAG -import bums.lunatic.launcher.workers.RuliWebGetter -import bums.lunatic.launcher.workers.TheQooGetter import bums.lunatic.launcher.workers.WorkersDb -import bums.lunatic.launcher.workers.YoutubeGetter -import bums.lunatic.launcher.workers.YoutubeGetter.Companion.YT_WORK_TAG import com.google.android.material.color.DynamicColors +import com.yausername.ffmpeg.FFmpeg +import com.yausername.youtubedl_android.YoutubeDL +import com.yausername.youtubedl_android.YoutubeDLException import io.realm.kotlin.ext.query -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kr.lunaticbum.utils.ui.DisplayUtil import org.json.JSONObject import org.mozilla.geckoview.ExperimentDelegate import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntimeSettings -import java.util.Base64 import java.util.Calendar import java.util.Date -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import kotlin.jvm.java open class LauncherActivity : CommonActivity() { @@ -209,7 +168,8 @@ open class LauncherActivity : CommonActivity() { Blog.LOGE("onConfigurationChanged newConfig?.screenWidthDp >> ${newConfig?.screenWidthDp}") Blog.LOGE("onConfigurationChanged newConfig?.screenHeightDp >> ${newConfig?.screenHeightDp}") isOpendFold = (newConfig.screenWidthDp * 1.1f) > newConfig.screenHeightDp - + val nullCursor = PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL) + binding.root.setPointerIcon(nullCursor) } // override fun onKeyClick(keyCode: Int): Boolean { // when (keyCode) { @@ -533,15 +493,24 @@ open class LauncherActivity : CommonActivity() { @SuppressLint("NewApi", "MissingPermission") override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() + super.onCreate(savedInstanceState) + try { + YoutubeDL.getInstance().init(this) + FFmpeg.getInstance().init(this); + } catch (e: YoutubeDLException) { + Blog.LOGE("failed to initialize youtubedl-android", e) + } + val intent = Intent(this, ForeGroundService::class.java) + this.startForegroundService(intent) + + val nlService = Intent(this, NLService::class.java) + this.startService(nlService) + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE); lActivity = this - DynamicColors.applyToActivityIfAvailable(this) - - settingsPrefs = getSharedPreferences(PREFS_SETTINGS, 0) -// AppCompatDelegate.setDefaultNightMode(settingsPrefs.getInt(KEY_APPLICATION_THEME, MODE_NIGHT_FOLLOW_SYSTEM)) - super.onCreate(savedInstanceState) + binding = LauncherActivityBinding.inflate(layoutInflater) setContentView(binding.root) @@ -553,8 +522,8 @@ open class LauncherActivity : CommonActivity() { updateLocationService() - val intent = Intent(this, BluetoothManager::class.java) - ContextCompat.startForegroundService(this, intent) + + showContents(binding.feeds.id) binding.floatingActionMenu.setOnMenuButtonClickListener { v-> Blog.LOGE("v >> ${v}") @@ -622,6 +591,10 @@ open class LauncherActivity : CommonActivity() { .replace(R.id.fragment_container, Magnet()) .commit() } + R.id.setting ->{ + startActivity(Intent(this, SettingsActivity::class.java)) + } + else -> {} } binding.floatingActionMenu.close(false) diff --git a/app/src/main/kotlin/bums/lunatic/launcher/helpers/BluetoothManager.kt b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt similarity index 75% rename from app/src/main/kotlin/bums/lunatic/launcher/helpers/BluetoothManager.kt rename to app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt index a555d592..87e2a2fb 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/helpers/BluetoothManager.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ForeGroundService.kt @@ -2,9 +2,9 @@ package bums.lunatic.launcher.helpers import android.Manifest import android.annotation.SuppressLint -import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager +import android.app.PendingIntent import android.app.Service import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice @@ -17,12 +17,13 @@ import android.os.Build import android.os.IBinder import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager +import bums.lunatic.launcher.LauncherActivity import bums.lunatic.launcher.LauncherActivity.Companion.lActivity import bums.lunatic.launcher.R -import bums.lunatic.launcher.home.GeckoWeb import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.workers.ArcaGetter import bums.lunatic.launcher.workers.ClienGetter @@ -51,8 +52,11 @@ import okhttp3.ResponseBody import java.util.concurrent.TimeUnit -class BluetoothManager : Service() { - +class ForeGroundService : Service() { + companion object { + val ACTION_SENDMSG = "ACTION_SEND_TO_LOVE" + val EXTRA_MSGKEY = "SEND_MSG" + } enum class BLUETOOTH_STATE(val statestr: String) { ENABLED("enabledBlutooth"), DISABLED("disableBlutooth"), @@ -67,44 +71,71 @@ class BluetoothManager : Service() { super.onCreate() Blog.LOGE("onCreate") mWorkManager = WorkManager.getInstance(this) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForeground(NOTIF_ID, createNotification(this)) - } val filter = IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED) registerReceiver(bluetoothreceiver, filter) refreshFeeds() -// GeckoWeb(applicationContext).apply { -// loadUrl("https://arca.live/b/live") -// } } - override fun onBind(intent: Intent?): IBinder? { - Blog.LOGE("intent >>> ${intent}") - return null + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Blog.LOGE("onStartCommand >>> ${intent}") + if (ACTION_SENDMSG.equals(intent?.action)) { + intent?.getStringExtra(EXTRA_MSGKEY)?.let { + sendToI(it) + } + } + startForeGround() + return START_STICKY } - private val CHANNEL_ID = "ble_service_channel" - private val CHANNEL_NAME = "BLE 서비스" - fun createNotification(context: Context): Notification { + fun startForeGround() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, - CHANNEL_NAME, - NotificationManager.IMPORTANCE_HIGH // 중요도 낮게 (필요시 변경) + "BLE 서비스 채널", + NotificationManager.IMPORTANCE_HIGH ) - val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(channel) } - return NotificationCompat.Builder(context, CHANNEL_ID) + val intent = Intent(this, LauncherActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + + val pendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + startForeground(NOTIF_ID, NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("BLE 서비스") .setContentText("실행중입니다.") .setPriority(NotificationCompat.PRIORITY_MAX) .setSmallIcon(R.drawable.ic_b) + .setContentIntent(pendingIntent) + .addAction(android.R.drawable.ic_btn_speak_now,"퇴근", makeSendMsgAction(0,"돼지 퇴근했다요~!")) + .addAction(android.R.drawable.ic_btn_speak_now,"버스 탐", makeSendMsgAction(1,"돼지 버스 탔다요~!")) + .addAction(android.R.drawable.ic_btn_speak_now,"버스 내림", makeSendMsgAction(2,"돼지 버스 내린다요~!")) .setOngoing(true) // 사용자가 알림을 스와이프로 지울 수 없게 만듦 - .build() + .build()) } + fun makeSendMsgAction(code : Int, msg : String) : PendingIntent { + val actionIntent = Intent(this, ForeGroundService::class.java).apply { + action = ACTION_SENDMSG + putExtra(EXTRA_MSGKEY, msg) // 전달할 데이터 + } + return PendingIntent.getService(this, code, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) + } + + override fun onBind(intent: Intent?): IBinder? { + Blog.LOGE("onBind intent >>> ${intent}") + return null + } + private val CHANNEL_ID = "ble_service_channel" + private val CHANNEL_NAME = "BLE 서비스" + //페어링된 디바이스 정보 가져오기 fun getPairedDevices() { @@ -192,6 +223,14 @@ class BluetoothManager : Service() { PeriodicWorkRequestBuilder(PrefLong.locationTimePeriod.get(), TimeUnit.MINUTES) .addTag(LocationGetter.TAG) .build()) + mWorkManager?.cancelAllWorkByTag(ServiceWatchdogWorker.TAG) + mWorkManager?.enqueueUniquePeriodicWork( + ServiceWatchdogWorker.TAG, + ExistingPeriodicWorkPolicy.REPLACE, + PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES) + .addTag(ServiceWatchdogWorker.TAG) + .build()) + } @@ -202,6 +241,21 @@ class BluetoothManager : Service() { return mWorkManager } + fun sendToI(msg: String) { + if (PrefString.telegramSendTarget.get().length > 5) { + CoroutineScope(Dispatchers.IO).launch { + OkHttpClient.Builder() + .connectionPool(ConnectionPool(5, 60, TimeUnit.SECONDS)) + .build().newCall(Request.Builder().url("https://api.telegram.org/bot7934509464:AAE_xUbICxMdywLGnxo7BkeIqA1nVza4P9w/sendMessage?chat_id=${PrefString.telegramSendTarget.get()}&text=${msg}") + .addHeader("Content-Type", "application/json").get().build()).execute()?.let { response -> + if (response.isSuccessful()) { + val body: ResponseBody? = response.body() + if (body != null) { } + } else Blog.LOGE("sendToI telegram Error Occurred") + } + } + } + } fun sendToI(boolean: Boolean) { if (PrefString.telegramSendTarget.get().length > 5) { CoroutineScope(Dispatchers.IO).launch { @@ -215,7 +269,7 @@ class BluetoothManager : Service() { val response: Response = OkHttpClient.Builder() .connectionPool(ConnectionPool(5, 60, TimeUnit.SECONDS)) .build().newCall(Request.Builder().url(url) - .addHeader("Content-Type", "application/json").get().build()).execute() + .addHeader("Content-Type", "application/json").get().build()).execute() if (response.isSuccessful()) { // 응답 받아서 처리 val body: ResponseBody? = response.body() @@ -251,6 +305,13 @@ class BluetoothManager : Service() { return BLUETOOTH_STATE.NOT_SUPPORT.statestr } + override fun stopService(name: Intent?): Boolean { + Blog.LOGE("stopService ${name}") + val intent = Intent(this, ForeGroundService::class.java) + ContextCompat.startForegroundService(this, intent) + return super.stopService(name) + } + //add Receive action private fun addFilterAction(): IntentFilter { val stateFilter = IntentFilter() diff --git a/app/src/main/kotlin/bums/lunatic/launcher/helpers/ServiceWatchdogWorker.kt b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ServiceWatchdogWorker.kt new file mode 100644 index 00000000..64e17ff8 --- /dev/null +++ b/app/src/main/kotlin/bums/lunatic/launcher/helpers/ServiceWatchdogWorker.kt @@ -0,0 +1,36 @@ +package bums.lunatic.launcher.helpers + +import android.app.ActivityManager +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.work.Worker +import androidx.work.WorkerParameters + +class ServiceWatchdogWorker( + private val context: Context, + workerParams: WorkerParameters +) : Worker(context, workerParams) { + + companion object{ + val TAG = "ServiceWatchdogWorker" + } + + override fun doWork(): Result { + val isServiceRunning = isServiceRunning(ForeGroundService::class.java) + if (!isServiceRunning) { + val intent = Intent(context, ForeGroundService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + return Result.success() + } + fun isServiceRunning(serviceClass: Class<*>): Boolean { + val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + return manager.getRunningServices(Int.MAX_VALUE) + .any { it.service.className == serviceClass.name } + } +} diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt index 5d5f6c99..58564a38 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/GeckoWeb.kt @@ -40,6 +40,7 @@ import androidx.core.net.toUri import androidx.core.view.isVisible import bums.lunatic.launcher.LauncherActivity.Companion.getRuntime import bums.lunatic.launcher.R +import bums.lunatic.launcher.model.others.Button import bums.lunatic.launcher.tokiz.data.model.PortMessage import bums.lunatic.launcher.tokiz.view.BWebview import bums.lunatic.launcher.utils.Blog @@ -47,6 +48,9 @@ import bums.lunatic.launcher.utils.CommonUtils import bums.lunatic.launcher.workers.WorkersDb import com.google.gson.Gson import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter +import com.yausername.youtubedl_android.YoutubeDL +import com.yausername.youtubedl_android.YoutubeDLRequest +import com.yausername.youtubedl_android.YoutubeDLResponse import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -54,6 +58,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kr.lunaticbum.utils.service.ServiceUtil.getSystemService +import kr.lunaticbum.utils.service.ServiceUtil.layoutInflater import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONException @@ -61,6 +66,7 @@ import org.json.JSONObject import org.jsoup.Jsoup import org.jsoup.nodes.Document import org.mozilla.gecko.util.ThreadUtils +import org.mozilla.gecko.util.ThreadUtils.runOnUiThread import org.mozilla.geckoview.ExperimentDelegate import org.mozilla.geckoview.GeckoResult import org.mozilla.geckoview.GeckoSession @@ -76,6 +82,7 @@ import java.io.FileOutputStream import java.io.InputStream import java.text.SimpleDateFormat import java.util.Date +import java.util.UUID class GeckoWeb : BWebview { constructor(context: Context?) : super(context) { @@ -84,7 +91,7 @@ class GeckoWeb : BWebview { var decoViews = arrayListOf() override fun setVisibility(visibility: Int) { super.setVisibility(visibility) - decoViews.filter { it != null && it.id > -1 }.forEach { it.visibility = visibility } + decoViews.filter { it != null && it.id > -1 && it.id != R.id.dl_video }.forEach { it.visibility = visibility } } interface OnSave { fun saved() @@ -448,6 +455,153 @@ class GeckoWeb : BWebview { } } } + + fun checkIfDownloadable(url: String) { + CoroutineScope(Dispatchers.Main).launch { + runOnUiThread { + decoViews.filter { it.id == R.id.dl_video }.firstOrNull()?.let { + it.setOnClickListener {} + it.visibility = View.GONE + }}} + Blog.LOGE("checkIfDownloadable ${url}") + CoroutineScope(Dispatchers.IO).launch { + try { + val videoInfo = YoutubeDL.getInstance().getInfo(url) + // videoInfo 가 null 아니고, 필요한 키(예: title, url 등)가 있으면 다운로드 가능 + Blog.LOGE("checkIfDownloadable ${url}\n videoInfo : ${videoInfo}") + var canVideoDown = videoInfo != null && !videoInfo.title.isNullOrEmpty() + CoroutineScope(Dispatchers.Main).launch { + runOnUiThread { + decoViews.filter { it.id == R.id.dl_video }.firstOrNull()?.let { + it.setOnClickListener { + videoDlownLoad(url) + } + it.visibility = if (canVideoDown){View.VISIBLE} else{View.GONE} + } + } + } + + } catch (e: Exception) { + Blog.LOGE("checkIfDownloadable ${url} ${e}") + CoroutineScope(Dispatchers.Main).launch { + runOnUiThread { + decoViews.filter { it.id == R.id.dl_video }.firstOrNull()?.let { + it.setOnClickListener {} + it.visibility = View.GONE + }}} + } + } + } + fun replaceDcUrl(origin: String): String { + var result = origin + for (i in 0..19) { + result = result.replace(String.format("dcimg%d.", i), "dcimg2.") + } + return result + } + + fun copyToClipboard(text: String?) { + if (text == null) return + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Media URL", text) + clipboard.setPrimaryClip(clip) + Toast.makeText(context, "주소가 복사되었습니다.", Toast.LENGTH_SHORT).show() + } + + + suspend fun getFormatList(url: String): List = withContext(Dispatchers.IO) { + val command = YoutubeDLRequest(lastedUrl!!) + command.addOption("--list-formats", url) + val output = YoutubeDL.getInstance().execute(command) + // output.stdout에 포맷 리스트가 문자열로 들어있음 + // 줄 단위로 분리 후 리턴 + Blog.LOGE("output.out >>> ${output.out}") + return@withContext output.out.split("\n").filter { it.isNotBlank() } + } + + fun showFormatSelectionDialog(formats: List, onFormatSelected: (String) -> Unit) { + val builder = AlertDialog.Builder(context) + builder.setTitle("포맷 선택") + builder.setItems(formats.toTypedArray()) { _, which -> + onFormatSelected(formats[which]) + } + builder.setNegativeButton("취소", null) + builder.show() + } + + + + lateinit var progressDialog: AlertDialog + fun showProgressDialog() { + val dialogView = layoutInflater.inflate(R.layout.progress_dialog, null) + val progressBar = dialogView.findViewById(R.id.progressBar) + val textProgress = dialogView.findViewById(R.id.textProgress) + val btn = dialogView.findViewById(R.id.dl_cancel) + progressDialog = AlertDialog.Builder(context) + .setTitle("다운로드 중...") + .setView(dialogView) + .setCancelable(false) + .create() + progressDialog.show() + + // UI 업데이트 함수 예 (나중에 실행) + fun updateProgress(progress: Int, est : Long, str : String) { + runOnUiThread { + progressBar.progress = progress + textProgress.text = "$progress%" + } + } + } + fun dismissProgressDialog() { + progressDialog.dismiss() + } + + suspend fun downloadVideo(processId : String,url: String, updateProgress: (Float, Long, String) -> Unit) = withContext(Dispatchers.IO) { + val youtubeDLDir = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "youtubedl-android" + ) + val command = YoutubeDLRequest(url) + command.addOption("-o", youtubeDLDir.getAbsolutePath() + "/%(title)s.%(ext)s"); + var process = YoutubeDL.getInstance().execute(command,processId) { progress, est , str -> + updateProgress(progress, est, str) + } + return@withContext process + } + + fun videoDlownLoad(videoUrl : String) { + CoroutineScope(Dispatchers.Main).launch { + try { + showProgressDialog() + var res: YoutubeDLResponse? = null + val processId = UUID.randomUUID().toString() + res = downloadVideo(processId, videoUrl) { progress , time , str-> + runOnUiThread { + val pb = + progressDialog.findViewById(R.id.progressBar) + val tv = + progressDialog.findViewById(R.id.textProgress) + pb?.progress = progress.toInt() + val btn = progressDialog.findViewById(R.id.dl_cancel) + tv?.text = "$progress%\n$str" + btn?.setOnClickListener { + progressDialog?.dismiss() + YoutubeDL.getInstance().destroyProcessById(processId); + } + } + } + dismissProgressDialog() + Toast.makeText(context, "다운로드 완료", Toast.LENGTH_SHORT) + .show() + } catch (e: Exception) { + e.printStackTrace() + progressDialog?.dismiss() + Toast.makeText(context, "오류: ${e.message}", Toast.LENGTH_LONG) + .show() + } + } + } + var dialog : Dialog? = null fun getFilterF() = String(java.util.Base64.getMimeDecoder().decode("aHR0cHM6Ly9pamF2dG9ycmVudC5jb20=".toByteArray())) var currentTitle = "" @@ -474,21 +628,7 @@ class GeckoWeb : BWebview { super.onFirstContentfulPaint(session) } - fun replaceDcUrl(origin: String): String { - var result = origin - for (i in 0..19) { - result = result.replace(String.format("dcimg%d.", i), "dcimg2.") - } - return result - } - fun copyToClipboard(text: String?) { - if (text == null) return - val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Media URL", text) - clipboard.setPrimaryClip(clip) - Toast.makeText(context, "주소가 복사되었습니다.", Toast.LENGTH_SHORT).show() - } override fun onExternalResponse(session: GeckoSession, response: WebResponse) { Blog.LOGE("response >>> ${response.uri} ") @@ -504,6 +644,7 @@ class GeckoWeb : BWebview { val client = OkHttpClient() val request = Request.Builder() .url(url) + .addHeader("Referer", lastedUrl) .addHeader("User-Agent", "Mozilla/5.0") // 필요시 Referer, 쿠키 등 헤더 추가 .build() @@ -531,6 +672,7 @@ class GeckoWeb : BWebview { super.onExternalResponse(session, response) } + override fun onContextMenu( session: GeckoSession, screenX: Int, @@ -539,18 +681,11 @@ class GeckoWeb : BWebview { ) { if (element.baseUri?.contains("youtube") == true) { - copyToClipboard(lastedUrl) - loadUrl("https://ko.savefrom.net/227lt/#url=${lastedUrl}") -// copyToClipboard(lastedUrl) -// Dialog(context)?.let { dialog -> -// val popupWebView = GeckoWeb(context).apply { -// loadUrl(lastedUrl!!.replace("https://","https://ss")) -// this.dialog = dialog -// } -// dialog.setCanceledOnTouchOutside(true) -// dialog.setContentView(popupWebView) -// dialog.show() -// } + lastedUrl?.let { videoUrl -> + lastedUrl?.let { + videoDlownLoad(it) + } + } } else { Blog.LOGE("onContextMenu:: x = ${x}, y = ${y} , element = ${Gson().toJson(element)}") if (element.type == GeckoSession.ContentDelegate.ContextElement.TYPE_IMAGE) { @@ -656,8 +791,8 @@ class GeckoWeb : BWebview { ) } == true || ((it.host?.contains("x.com") ?: false) == true) || - ((it.host?.contains("www.instagram.com") ?: false) == true) - ) { + ((it.host?.contains("www.instagram.com") ?: false) == true) + ) { loadUrl(uri) } else if(uri.contains("googlevideo.com")) { CommonUtils.downloadFileWithOkHttp(context, Uri.parse(lastedUrl),uri) @@ -723,6 +858,7 @@ class GeckoWeb : BWebview { } else { lastedUrl = url } + checkIfDownloadable(url) } @@ -735,6 +871,8 @@ class GeckoWeb : BWebview { it.tag = currentTitle it.text = url } + }else if (it.id == R.id.reload) { + it.setOnClickListener { session.reload() } } } } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt b/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt index 37a3925d..0f118deb 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/home/RssHome.kt @@ -625,6 +625,8 @@ internal class RssHome : Fragment() { (activity as? LauncherActivity)?.let { activity -> binding.geckoWeb.decoViews.add(activity.findViewById(R.id.current_address)) binding.geckoWeb.decoViews.add(activity.findViewById(R.id.back)) + binding.geckoWeb.decoViews.add(activity.findViewById(R.id.reload)) + binding.geckoWeb.decoViews.add(activity.findViewById(R.id.dl_video)) } diff --git a/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt b/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt index 07212a82..49f0ffdb 100644 --- a/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt +++ b/app/src/main/kotlin/bums/lunatic/launcher/receiver/NLService.kt @@ -1,5 +1,6 @@ package bums.lunatic.launcher.receiver +import android.app.Notification import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context @@ -13,9 +14,11 @@ import android.service.notification.NotificationListenerService import android.service.notification.StatusBarNotification import androidx.annotation.RequiresApi import androidx.core.content.getSystemService +import bums.lunatic.launcher.helpers.ForeGroundService import bums.lunatic.launcher.model.CurrentPlayItem import bums.lunatic.launcher.model.NotificationItem import bums.lunatic.launcher.utils.BitmapConverter +import bums.lunatic.launcher.utils.Blog import bums.lunatic.launcher.workers.WorkersDb import io.realm.kotlin.UpdatePolicy import io.realm.kotlin.ext.query @@ -29,7 +32,6 @@ class NLService : NotificationListenerService() { super.onCreate() nlservicereciver = NLServiceReceiver() val filter = IntentFilter() -// filter.addAction("com.kpbird.nlsexample.NOTIFICATION_LISTENER_SERVICE_EXAMPLE") registerReceiver(nlservicereciver, filter) } @@ -38,130 +40,54 @@ class NLService : NotificationListenerService() { unregisterReceiver(nlservicereciver) } - val skips = arrayListOf("com.wssyncmldm") + @RequiresApi(Build.VERSION_CODES.S) override fun onNotificationPosted(sbn: StatusBarNotification) { -// BLog.LOGE("NLService********** onNotificationPosted") -// BLog.LOGE("NLServiceID :" + sbn.id + "\t${sbn.notification.tickerText}\t" + sbn.packageName) -// sbn.notification.extras.keySet().forEach { -// BLog.LOGE("NLService********** keySet >> ${it} ${sbn.notification.extras.get(it)}") -// } - try { - if (sbn.id != 0 && (sbn.packageName.contains(".") || sbn.packageName.contains("android")) && sbn.packageName.length > 0) { - NotificationItem().apply { - notiId = sbn.id - pkgName = sbn.packageName - title = sbn.notification?.extras?.getString("android.title") ?: "" - subtext = sbn.notification?.extras?.getString("android.subText") ?: "" - selfDisplayName = sbn.notification?.extras?.getString("android.selfDisplayName") ?: "" - tikerMsg = sbn.notification?.tickerText?.toString() ?: "" - postTime = sbn.postTime - var uniq = title ?: subtext ?: selfDisplayName ?: tikerMsg ?: "" - uniq_id = "${sbn.id}_${sbn.packageName}_${if (uniq.length > 3) uniq.substring(0,3) else uniq}" -// BLog.LOGE("NLService********** enqueue TelegramBotGetter ${true == "bumssavor".equals(title)}") -// BLog.LOGE("NLService********** enqueue TelegramBotGetter ${(true == "org.telegram.messenger".equals(pkgName))}") -// BLog.LOGE("NLService********** enqueue TelegramBotGetter ${sbn.notification?.extras?.getString("android.text")?.startsWith("/") == true}") - }.apply { - if (skips.contains(pkgName)) { + Blog.LOGE("onNotificationPosted ${sbn}") + val notification = sbn.notification + val extras = notification.extras + when (sbn.packageName){ + "com.kakao.talk" -> { - } else { -// WorkersDb.insertNoti(this) -// BLog.LOGE("NLService********** onNotificationPosted ${Gson().toJson(this)}") - } - } - } - } catch (e : Exception) { - e.printStackTrace() - } - - - try { - if (sbn.packageName.contains("youtube")) { - val m = getSystemService()!! - val component = ComponentName(this, NLService::class.java) - val sessions = m.getActiveSessions(component) - sessions.forEach { session -> - WorkersDb.getRealm().writeBlocking { -// Blog.LOGE("session.playbackState >>> ${session.playbackState}") - if (session.playbackState != null) { - if (session.playbackState?.isActive == true && session.playbackState?.state?.equals( - STATE_PLAYING - ) == true - ) { - session.playbackState?.state - val result = query().find() - var current: CurrentPlayItem? = null - if (result.size > 0) { - current = result.first() - } else { - current = CurrentPlayItem() - copyToRealm(current, UpdatePolicy.ALL) - } - if (session?.metadata?.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART) == true) { - current.albumArt = BitmapConverter.BitmapToString( - session.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) - ) - } else { - current.albumArt = "" - } - current.title = - session?.metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) - current.artists = - session?.metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) - } else { - delete(query().find()) - } - } - } } - }}catch (e : Exception) { - e.printStackTrace() } -// val i = Intent("com.kpbird.nlsexample.NOTIFICATION_LISTENER_EXAMPLE") -// i.putExtra("notification_event", "onNotificationPosted :" + sbn.packageName + "\n") -// sendBroadcast(i) + val title = extras.getString(Notification.EXTRA_TITLE) ?: "" + val text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: "" + val bigText = extras.getCharSequence(Notification.EXTRA_BIG_TEXT)?.toString() ?: "" + val extraInfo = extras.getCharSequence(Notification.EXTRA_INFO_TEXT)?.toString() ?: "" + val subText = extras.getCharSequence(Notification.EXTRA_SUB_TEXT)?.toString() ?: "" + val conversationTitle = extras.getCharSequence(Notification.EXTRA_CONVERSATION_TITLE)?.toString() ?: "" + val summaryText = extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT)?.toString() ?: "" + val verificationText = extras.getCharSequence(Notification.EXTRA_VERIFICATION_TEXT)?.toString() ?: "" + + Blog.LOGE("title >> ${title} text >> ${text} bigText >> ${bigText} extraInfo >> ${extraInfo} subText >> ${subText} conversationTitle >> ${conversationTitle} summaryText >> ${summaryText} verificationText >> ${verificationText}") + } override fun onNotificationRemoved(sbn: StatusBarNotification) { -//// BLog.LOGE("NLService********** onNOtificationRemoved") -//// BLog.LOGE("NLService ID :" + sbn.id + "\t" + sbn.notification.tickerText + "\t" + sbn.packageName) -// var uniq_id = "${sbn.id}_${sbn.packageName}" -// try { -// WorkersDb.getRealm()?.apply { -// this.writeBlocking { -//// delete(query().query("pkgName == $0", sbn.packageName).find()) -// } -// } -// }catch (e : Exception){e.printStackTrace()} -//// val i = Intent("com.kpbird.nlsexample.NOTIFICATION_LISTENER_EXAMPLE") -//// i.putExtra("notification_event", "onNotificationRemoved :" + sbn.packageName + "\n") -//// sendBroadcast(i) + Blog.LOGE("onNotificationPosted ${sbn}") + if (sbn.packageName == "bums.lunatic.launcher" && sbn.id == 830721) { + // 포그라운드 알림이 사라짐 감지 + Blog.LOGE("NotificationListener", "포그라운드 알림 제거 감지") + + // 서비스 재시작 시도 + val intent = Intent(this, ForeGroundService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + } } internal inner class NLServiceReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent) { -// BLog.LOGE("NLService intent >>> ${intent.action}") if (intent.getStringExtra("command") == "clearall") { this@NLService.cancelAllNotifications() } else if (intent.getStringExtra("command") == "list") { -// val i1 = Intent("com.kpbird.nlsexample.NOTIFICATION_LISTENER_EXAMPLE") -// i1.putExtra("notification_event", "=====================") -// sendBroadcast(i1) - var i = 1 - for (sbn in this@NLService.activeNotifications) { -// BLog.LOGE("NLService sbn >>> ${sbn.packageName} , ${Gson().toJson(sbn.notification.extras.keySet())}") -// val i2 = Intent("com.kpbird.nlsexample.NOTIFICATION_LISTENER_EXAMPLE") -// i2.putExtra("notification_event", i.toString() + " " + sbn.packageName + "\n") -// sendBroadcast(i2) -// i++ - } -// val i3 = Intent("com.kpbird.nlsexample.NOTIFICATION_LISTENER_EXAMPLE") -// i3.putExtra("notification_event", "===== Notification List ====") -// sendBroadcast(i3) + } } } - - } \ No newline at end of file diff --git a/app/src/main/kotlin/bums/lunatic/launcher/view/ZoomImageView.kt b/app/src/main/kotlin/bums/lunatic/launcher/view/ZoomImageView.kt new file mode 100644 index 00000000..db223da1 --- /dev/null +++ b/app/src/main/kotlin/bums/lunatic/launcher/view/ZoomImageView.kt @@ -0,0 +1,57 @@ +package bums.lunatic.launcher.view + +import android.content.Context +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import androidx.appcompat.widget.AppCompatImageView +class ZoomImageView(context: Context, attrs: AttributeSet) : AppCompatImageView(context, attrs), + ScaleGestureDetector.OnScaleGestureListener, GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener { + + private var scaleFactor = 1.0f + private val scaleGestureDetector = ScaleGestureDetector(context, this) + private val gestureDetector = GestureDetector(context, this) + + override fun onTouchEvent(event: MotionEvent): Boolean { + scaleGestureDetector.onTouchEvent(event) + gestureDetector.onTouchEvent(event) + return true + } + + // ScaleGestureDetector callbacks + override fun onScale(detector: ScaleGestureDetector): Boolean { + scaleFactor *= detector.scaleFactor + scaleFactor = scaleFactor.coerceIn(1.0f, 4.0f) + scaleX = scaleFactor + scaleY = scaleFactor + return true + } + + override fun onScaleBegin(detector: ScaleGestureDetector) = true + override fun onScaleEnd(detector: ScaleGestureDetector) {} + + // GestureDetector callbacks (클릭, 터치 등) + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + performClick() // 클릭 이벤트 발생 + return true + } + + override fun performClick(): Boolean { + super.performClick() + // 추가로 클릭 시 동작할 코드 넣기 + if (hasOnClickListeners()) { + callOnClick() + } + return true + } + + override fun onDown(e: MotionEvent) = true + override fun onShowPress(e: MotionEvent) {} + override fun onSingleTapUp(e: MotionEvent) = true + override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float) = false + override fun onLongPress(e: MotionEvent) {} + override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float) = false + override fun onDoubleTap(e: MotionEvent) = true + override fun onDoubleTapEvent(e: MotionEvent) = true +} diff --git a/app/src/main/res/drawable/dl_vid.png b/app/src/main/res/drawable/dl_vid.png new file mode 100644 index 0000000000000000000000000000000000000000..c614885814abcf192a11fb92e8e27ae3f2232085 GIT binary patch literal 530 zcmV+t0`2{YP)G6&1uxKELR+Xi zw}RK{##*~l6s#-3!pOi|JOW9lY0^w^K6qp@e3N|N%p@-}fq;O3fPfOP2@HTKFb7OU zF&7Pepyew582GMGv`L8>(01v0AH^GGqM2o2pvcd_ArMg=A|M9FO4nUhgQ+4~@xCBd zx+Ymf{lHrHr@)UyNAcEOA0-~_ROtC;(N*WROojC(q0PxaNA<*s;#E;^@)+<5#zot^ z5Z6DX$8%?%8pvZg^ft1KpH2OkS9OVW0tQ6*OlnG4LdW$GRGr zyQJ$3?8$dant@n2d0@&;WV^txv>gIHb*-M%8R1y&vKUGGQCS8K674@z$~N|MTz?0+ z0k+FBfa>L}u$*BXJ>N%d6r{ z@{~Q+ay{@Ny6mx*227qr=F6f!w}IL%>gS3Sd9eTe;z&$ U3po5zG5`Po07*qoM6N<$f^B{0;s5{u literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/launcher_activity.xml b/app/src/main/res/layout/launcher_activity.xml index d6646ed5..01f48345 100644 --- a/app/src/main/res/layout/launcher_activity.xml +++ b/app/src/main/res/layout/launcher_activity.xml @@ -36,12 +36,27 @@ android:layout_width="40dp" tools:ignore="ContentDescription" android:layout_height="40dp" /> + + + diff --git a/app/src/main/res/layout/layout_rss_summary.xml b/app/src/main/res/layout/layout_rss_summary.xml index 2a62acb2..827c3e41 100644 --- a/app/src/main/res/layout/layout_rss_summary.xml +++ b/app/src/main/res/layout/layout_rss_summary.xml @@ -49,7 +49,7 @@ android:textColor="@color/white" android:layout_width="match_parent" android:layout_height="wrap_content"/> - - + + + + + +