diff --git a/app/build.gradle b/app/build.gradle index 51572c2..d4bb971 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,7 +48,7 @@ android { applicationVariants.configureEach { variant -> variant.outputs.configureEach { - outputFileName = "Android自动化_${defaultConfig.versionName}.apk" + outputFileName = "AndroidAuto_${defaultConfig.versionName}.apk" } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2ed07a9..db3eb72 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,10 @@ + xmlns:tools="http://schemas.android.com/tools" > + + + tools:targetApi="31" > + + android:theme="@style/Theme.AndroidAuto.NoActionBar" > @@ -32,6 +35,11 @@ android:value="" /> + + + diff --git a/app/src/main/java/com/lygttpod/android/auto/ForegroundService.kt b/app/src/main/java/com/lygttpod/android/auto/ForegroundService.kt new file mode 100644 index 0000000..e8ee42a --- /dev/null +++ b/app/src/main/java/com/lygttpod/android/auto/ForegroundService.kt @@ -0,0 +1,89 @@ +package com.lygttpod.android.auto + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.BitmapFactory +import android.os.IBinder +import android.util.Log +import android.widget.Toast + +class ForegroundService : Service() { + companion object { + private const val TAG = "ForegroundService" + + private const val CHANNEL_ID = "ForegroundService" + private const val CHANNEL_NAME = "前台服务" + + const val ACTION_TOAST = "ACTION_TOAST" + const val KEY_TOAST_TEXT = "KEY_TOAST_TEXT" + } + + private val pendingIntent by lazy { + val intent = Intent(this, MainActivity::class.java) + PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) + } + + private val notification by lazy { + Notification.Builder(this, CHANNEL_ID) + .setContentTitle("前台服务") + .setContentText("重要,勿删!") + .setSmallIcon(R.mipmap.icon_logo) + .setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.icon_logo)) + .setContentIntent(pendingIntent) + .setOngoing(true) + .build() + } + + private var lastToastTime = 0L + private var lastToastText = "" + + private val receiver by lazy { + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val text = intent?.getStringExtra(KEY_TOAST_TEXT) + ?.takeIf { it.isNotBlank() } ?: return + Log.d(TAG, "收到Toast: $text") + + if (text == lastToastText && System.currentTimeMillis() - lastToastTime < 5000) { + Log.d(TAG, "文本重复,忽略") + return + } + + Toast.makeText(App.instance(), text, Toast.LENGTH_SHORT).show() + lastToastTime = System.currentTimeMillis() + lastToastText = text + } + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val nm = getSystemService(NotificationManager::class.java) + + nm.createNotificationChannel( + NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH) + ) + + startForeground(1, notification) + + registerReceiver(receiver, IntentFilter(ACTION_TOAST), RECEIVER_NOT_EXPORTED) + + return super.onStartCommand(intent, flags, startId) + } + + override fun onBind(intent: Intent): IBinder { + TODO("Return the communication channel to the service.") + } + + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(receiver) + stopForeground(STOP_FOREGROUND_REMOVE) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lygttpod/android/auto/MainActivity.kt b/app/src/main/java/com/lygttpod/android/auto/MainActivity.kt index 925a3db..dd80591 100644 --- a/app/src/main/java/com/lygttpod/android/auto/MainActivity.kt +++ b/app/src/main/java/com/lygttpod/android/auto/MainActivity.kt @@ -1,8 +1,12 @@ package com.lygttpod.android.auto import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.navigateUp @@ -43,6 +47,12 @@ class MainActivity : AppCompatActivity() { } } } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 666) + } else { + startForegroundService(Intent(this, ForegroundService::class.java)) + } } override fun onResume() { @@ -54,4 +64,16 @@ class MainActivity : AppCompatActivity() { val navController = findNavController(R.id.nav_host_fragment_content_main) return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == 666 && grantResults.getOrNull(0) + == PackageManager.PERMISSION_GRANTED) { + startForegroundService(Intent(this, ForegroundService::class.java)) + } + } } \ No newline at end of file diff --git a/auto-ad/src/main/java/com/lygttpod/android/auto/ad/task/FuckADTask.kt b/auto-ad/src/main/java/com/lygttpod/android/auto/ad/task/FuckADTask.kt index 47a7225..c6be724 100644 --- a/auto-ad/src/main/java/com/lygttpod/android/auto/ad/task/FuckADTask.kt +++ b/auto-ad/src/main/java/com/lygttpod/android/auto/ad/task/FuckADTask.kt @@ -1,5 +1,6 @@ package com.lygttpod.android.auto.ad.task +import android.content.Intent import android.util.Log import android.view.accessibility.AccessibilityEvent import androidx.lifecycle.MutableLiveData @@ -31,6 +32,9 @@ object FuckADTask { private const val APP_CONFIG = "appConfig" private const val FILTER_KEYWORD = "filterKeyword" + private const val ACTION_TOAST = "ACTION_TOAST" + private const val KEY_TOAST_TEXT = "KEY_TOAST_TEXT" + private val mutex = Mutex() private val fuckADTaskScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @@ -159,6 +163,9 @@ object FuckADTask { if (skipResult) { withContext(Dispatchers.Main) { Log.d("FuckADTask", "自动跳过【${adApp.appName}】的广告啦") + AppContext.sendBroadcast(Intent(ACTION_TOAST).apply { + putExtra(KEY_TOAST_TEXT, "自动跳过【${adApp.appName}】") + }) } } skipResult diff --git a/auto-wx/src/main/java/com/lygttpod/android/auto/wx/helper/DeleteTaskHelper.kt b/auto-wx/src/main/java/com/lygttpod/android/auto/wx/helper/DeleteTaskHelper.kt new file mode 100644 index 0000000..562d7b7 --- /dev/null +++ b/auto-wx/src/main/java/com/lygttpod/android/auto/wx/helper/DeleteTaskHelper.kt @@ -0,0 +1,55 @@ +package com.lygttpod.android.auto.wx.helper + +import android.content.Context +import android.util.Log +import android.view.accessibility.AccessibilityEvent +import com.android.accessibility.ext.toast +import com.lygttpod.android.auto.wx.page.WXDeleteDialogPage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +object DeleteTaskHelper { + private const val TAG = "DeleteTaskHelper" + + private var isEnabled = false + + private val mutex = Mutex() + private val eventFlow = MutableStateFlow(null) + + private var scope: CoroutineScope? = null + + fun enable(context: Context, enable: Boolean) { + isEnabled = enable + if (isEnabled) { + Log.d(TAG, "打开任务: DeleteTaskHelper") + context.toast("微信删除任务已开启") + scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + scope?.launch { + eventFlow.buffer().collect { + mutex.withLock { + WXDeleteDialogPage.handleEvent() + } + } + } + } else { + Log.d(TAG, "关闭任务: DeleteTaskHelper") + context.toast("微信删除任务已关闭") + scope?.cancel("关闭任务:DeleteTaskHelper") + } + } + + fun handle(event: AccessibilityEvent) { + if (!isEnabled) return + scope?.launch { + if (mutex.isLocked) return@launch + eventFlow.emit(event) + } + } +} \ No newline at end of file diff --git a/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/WXDeleteDialogPage.kt b/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/WXDeleteDialogPage.kt new file mode 100644 index 0000000..e95feea --- /dev/null +++ b/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/WXDeleteDialogPage.kt @@ -0,0 +1,40 @@ +package com.lygttpod.android.auto.wx.page + +import android.util.Log +import com.android.accessibility.ext.acc.* +import com.android.accessibility.ext.task.retryCheckTaskWithLog +import com.lygttpod.android.auto.wx.data.NodeInfo +import com.lygttpod.android.auto.wx.service.wxAccessibilityService +import com.lygttpod.android.auto.wx.version.nodeProxy + +object WXDeleteDialogPage : IPage { + + interface Nodes { + val dialogContentNode: NodeInfo + val dialogDeleteNode: NodeInfo + + companion object : Nodes by nodeProxy() + } + + override fun pageClassName() = "" + + override fun pageTitleName() = "微信抢红包" + + override fun isMe(): Boolean { + return wxAccessibilityService?.findByIdAndText( + Nodes.dialogContentNode.nodeId, + Nodes.dialogContentNode.nodeText + ) != null + } + + private suspend fun inPage() = retryCheckTaskWithLog("判断是否在删除聊天弹窗界面") { isMe() } + + suspend fun handleEvent() { + val inPage = inPage() + if (!inPage) return + wxAccessibilityService.clickByIdAndText( + Nodes.dialogDeleteNode.nodeId, + Nodes.dialogDeleteNode.nodeText + ) + } +} \ No newline at end of file diff --git a/auto-wx/src/main/java/com/lygttpod/android/auto/wx/service/WXAccessibility.kt b/auto-wx/src/main/java/com/lygttpod/android/auto/wx/service/WXAccessibility.kt index a88d559..37c3711 100644 --- a/auto-wx/src/main/java/com/lygttpod/android/auto/wx/service/WXAccessibility.kt +++ b/auto-wx/src/main/java/com/lygttpod/android/auto/wx/service/WXAccessibility.kt @@ -1,9 +1,11 @@ package com.lygttpod.android.auto.wx.service import android.accessibilityservice.AccessibilityService +import android.util.Log import android.view.accessibility.AccessibilityEvent import androidx.lifecycle.MutableLiveData import com.android.accessibility.ext.AsyncAccessibilityService +import com.lygttpod.android.auto.wx.helper.DeleteTaskHelper import com.lygttpod.android.auto.wx.helper.HBTaskHelper import java.util.concurrent.atomic.AtomicBoolean @@ -30,5 +32,6 @@ class WXAccessibility : AsyncAccessibilityService() { override fun asyncHandleAccessibilityEvent(event: AccessibilityEvent) { HBTaskHelper.hbTask(event) + DeleteTaskHelper.handle(event) } } \ No newline at end of file diff --git a/auto-wx/src/main/java/com/lygttpod/android/auto/wx/ui/WxMainFragment.kt b/auto-wx/src/main/java/com/lygttpod/android/auto/wx/ui/WxMainFragment.kt index ffb44f9..b6f036b 100644 --- a/auto-wx/src/main/java/com/lygttpod/android/auto/wx/ui/WxMainFragment.kt +++ b/auto-wx/src/main/java/com/lygttpod/android/auto/wx/ui/WxMainFragment.kt @@ -16,6 +16,7 @@ import com.android.accessibility.ext.openAccessibilitySetting import com.lygttpod.android.auto.wx.R import com.lygttpod.android.auto.wx.adapter.FriendInfoAdapter import com.lygttpod.android.auto.wx.databinding.FragmentWxMainBinding +import com.lygttpod.android.auto.wx.helper.DeleteTaskHelper import com.lygttpod.android.auto.wx.helper.FriendStatusHelper import com.lygttpod.android.auto.wx.helper.HBTaskHelper import com.lygttpod.android.auto.wx.helper.TaskByGroupHelper @@ -78,11 +79,13 @@ class WxMainFragment : Fragment() { private fun initObserver() { accServiceLiveData.observe(viewLifecycleOwner) { open -> binding.chAutoHb.isEnabled = open + checkAutoDeleteWhenDeleteChatStatus(open) binding.btnGetFriendList.isEnabled = open binding.btnCheck.isEnabled = open binding.btnCheckByGroup.isEnabled = open - binding.btnOpenService.text = if (open) "【微信自动化】服务已开启" else "打开【微信自动化】服务" + binding.btnOpenService.text = + if (open) "【微信自动化】服务已开启" else "打开【微信自动化】服务" } @@ -155,6 +158,10 @@ class WxMainFragment : Fragment() { HBTaskHelper.autoFuckMoney(isChecked) } + binding.chAutoDeleteWhenDeleteChat.setOnCheckedChangeListener { _, isChecked -> + DeleteTaskHelper.enable(requireContext(), isChecked) + } + binding.btnFilterAll.setOnClickListener { adapter.setData(FriendStatusHelper.filterAllData()) } @@ -194,4 +201,13 @@ class WxMainFragment : Fragment() { super.onDestroyView() _binding = null } + + private fun checkAutoDeleteWhenDeleteChatStatus(wxEnabled: Boolean) { + binding.chAutoDeleteWhenDeleteChat.isEnabled = wxEnabled + if (wxEnabled && binding.chAutoDeleteWhenDeleteChat.isChecked) { + DeleteTaskHelper.enable(requireContext(), true) + } else { + DeleteTaskHelper.enable(requireContext(), false) + } + } } \ No newline at end of file diff --git a/auto-wx/src/main/java/com/lygttpod/android/auto/wx/version/WeChatNodesImpl.kt b/auto-wx/src/main/java/com/lygttpod/android/auto/wx/version/WeChatNodesImpl.kt index 0202a33..840e708 100644 --- a/auto-wx/src/main/java/com/lygttpod/android/auto/wx/version/WeChatNodesImpl.kt +++ b/auto-wx/src/main/java/com/lygttpod/android/auto/wx/version/WeChatNodesImpl.kt @@ -1,9 +1,11 @@ package com.lygttpod.android.auto.wx.version import com.lygttpod.android.auto.wx.data.NodeInfo +import com.lygttpod.android.auto.wx.helper.DeleteTaskHelper import com.lygttpod.android.auto.wx.page.WXChattingPage import com.lygttpod.android.auto.wx.page.WXContactInfoPage import com.lygttpod.android.auto.wx.page.WXContactPage +import com.lygttpod.android.auto.wx.page.WXDeleteDialogPage import com.lygttpod.android.auto.wx.page.WXHBPage import com.lygttpod.android.auto.wx.page.WXHomePage import com.lygttpod.android.auto.wx.page.WXMinePage @@ -22,11 +24,12 @@ enum class WeChatNodesImpl(val version: String) : WXChattingPage.Nodes, WXRemittancePage.Nodes, WXHBPage.Nodes, + WXDeleteDialogPage.Nodes, WXCreateGroupPage.Nodes, WXGroupChatPage.Nodes, WXGroupInfoPage.Nodes, WXGroupManagerPage.Nodes { - + WechatVersion_8_0_42("8.0.42") { // 8.0.42版本和8.0.41版本元素没变化 //首页 override val homeBottomNavNode = NodeInfo("", "com.tencent.mm:id/huj", "首页底导布局") @@ -98,6 +101,12 @@ enum class WeChatNodesImpl(val version: String) : override val hbMissCloseNode = NodeInfo("", "com.tencent.mm:id/j6f", "关闭被抢完的红包弹框") + // TODO: 2023/10/31 未适配 + //删除聊天记录弹窗 + override val dialogContentNode = NodeInfo("删除后,将清空该聊天的消息记录", "com.tencent.mm:id/guo", "删除聊天记录弹窗中内容") + override val dialogDeleteNode = NodeInfo("删除", "com.tencent.mm:id/guw", "删除聊天记录弹窗中【删除】按钮") + + //群聊发起页 override val createGroupPageTitleNode = NodeInfo("发起群聊", "android:id/text1", "发起群聊页面") override val createGroupUserListNode = @@ -207,6 +216,12 @@ enum class WeChatNodesImpl(val version: String) : override val hbMissCloseNode = NodeInfo("", "com.tencent.mm:id/j6f", "关闭被抢完的红包弹框") + // TODO: 2023/10/31 未适配 + //删除聊天记录弹窗 + override val dialogContentNode = NodeInfo("删除后,将清空该聊天的消息记录", "com.tencent.mm:id/guo", "删除聊天记录弹窗中内容") + override val dialogDeleteNode = NodeInfo("删除", "com.tencent.mm:id/guw", "删除聊天记录弹窗中【删除】按钮") + + //群聊发起页 override val createGroupPageTitleNode = NodeInfo("发起群聊", "android:id/text1", "发起群聊页面") override val createGroupUserListNode = @@ -316,6 +331,11 @@ enum class WeChatNodesImpl(val version: String) : override val hbMissCloseNode = NodeInfo("", "com.tencent.mm:id/gip", "关闭被抢完的红包弹框") + //删除聊天记录弹窗 + override val dialogContentNode = NodeInfo("删除后,将清空该聊天的消息记录", "com.tencent.mm:id/guo", "删除聊天记录弹窗中内容") + override val dialogDeleteNode = NodeInfo("删除", "com.tencent.mm:id/guw", "删除聊天记录弹窗中【删除】按钮") + + //群聊发起页 override val createGroupPageTitleNode = NodeInfo("发起群聊", "android:id/text1", "发起群聊页面") override val createGroupUserListNode = diff --git a/auto-wx/src/main/res/layout/fragment_wx_main.xml b/auto-wx/src/main/res/layout/fragment_wx_main.xml index 9b8bd69..5924c56 100644 --- a/auto-wx/src/main/res/layout/fragment_wx_main.xml +++ b/auto-wx/src/main/res/layout/fragment_wx_main.xml @@ -48,6 +48,15 @@ app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="@+id/btn_open_service" /> + +