GIT COMMIT MESSAGE:

删除无障碍服务并重构广告处理

- 重构广告管理,删除过时的无障碍功能,以简化应用程序。
- 引入新库并更新依赖以改善功能和兼容性。
- 增强展示插页广告的UI组件,并简化主活动结构。
This commit is contained in:
lvlisong 2025-01-23 22:28:30 +08:00
parent 7713c2cfc3
commit c622b7e435
10 changed files with 160 additions and 369 deletions

View File

@ -1,6 +1,30 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
@ -9,6 +33,9 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />

View File

@ -64,6 +64,7 @@ dependencies {
implementation("com.applovin:applovin-sdk:+")
implementation("com.applovin.dsp:linkedin-adapter:+")
implementation("com.adjust.sdk:adjust-android:5.0.2")
implementation(libs.mediation.test.suite)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@ -2,8 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
<application
android:name=".BreakingNewsApplication"
android:allowBackup="true"
@ -31,18 +29,6 @@
android:theme="@style/Theme.Breakingnews">
</activity>
<service
android:name=".MyAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="false"> <!-- 明确指定 exported 属性 -->
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<!-- 这里可以添加自定义的配置文件,根据你的需求添加 -->
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>

View File

@ -1,75 +0,0 @@
package io.sixminutes.breakingnews
import android.content.Context
import com.google.android.gms.ads.identifier.AdvertisingIdClient
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.lang.Exception
class GAIDHelper(private val context: Context) {
/**
* 检查 Google Play 服务是否可用
*
* @return true 如果 Google Play 服务可用否则返回 false
*/
fun isGooglePlayServicesAvailable(): Boolean {
val googleApiAvailability = GoogleApiAvailability.getInstance()
val resultCode = googleApiAvailability.isGooglePlayServicesAvailable(context)
return resultCode == ConnectionResult.SUCCESS
}
/**
* 异步获取 GAID
*
* @param callback 获取结果回调接口
*/
suspend fun getAdvertisingId(callback: OnAdvertisingIdListener) {
// 使用 withContext(Dispatchers.IO) 将获取 GAID 的操作切换到 IO 线程,避免阻塞主线程
withContext(Dispatchers.IO) {
try {
// 使用 AdvertisingIdClient.getAdvertisingIdInfo 获取 GAID 信息
val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context)
// 获取 GAID 字符串
val gaid = adInfo.id
// 使用 withContext(Dispatchers.Main) 将回调切换到主线程
withContext(Dispatchers.Main) {
// 调用回调接口的 onSuccess 方法,传递 GAID
if (gaid != null) {
callback.onSuccess(gaid)
} else {
callback.onFailure(Exception("Failed to retrieve advertising ID. GAID is null."))
}
}
} catch (e: Exception) {
// 使用 withContext(Dispatchers.Main) 将回调切换到主线程
withContext(Dispatchers.Main) {
// 调用回调接口的 onFailure 方法,传递异常信息
callback.onFailure(e)
}
}
}
}
/**
* GAID 获取结果回调接口
*/
interface OnAdvertisingIdListener {
/**
* 获取 GAID 成功回调
*
* @param gaid 获取到的 GAID
*/
fun onSuccess(gaid: String)
/**
* 获取 GAID 失败回调
*
* @param e 异常信息
*/
fun onFailure(e: Exception)
}
}

View File

@ -14,65 +14,66 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
class InterstitialActivity : Activity(), MaxAdListener {
private lateinit var interstitialAd: MaxInterstitialAd
private var retryAttempt = 0
private val maxRetryAttempts = 6 // 最大重试次数
private var loadCount = 0 // 用于计数广告加载次数
private val maxLoadCount = 10 // 最大加载次数
private var isAdShowing = false // 用于跟踪广告是否正在显示
private val scope = CoroutineScope(Dispatchers.Main) // 创建一个CoroutineScope
private var adId: String? = null // 广告 ID
private val scope = CoroutineScope(Dispatchers.Main) // 创建一个 CoroutineScope
private val TAG = "BreakingNews"
fun createInterstitialAd() {
interstitialAd = MaxInterstitialAd("be20b7a9d66e8895", applicationContext)
interstitialAd.setListener(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 获取从 MainActivity 传递过来的广告 ID
adId = intent.getStringExtra("AD_ID")
if (adId != null) {
createInterstitialAd(adId!!)
} else {
Log.e(TAG, "Ad ID is null. Cannot load ad.")
finish()
}
}
// Load the first ad
private fun createInterstitialAd(adId: String) {
interstitialAd = MaxInterstitialAd(adId, this)
interstitialAd.setListener(this)
interstitialAd.loadAd()
}
// MAX Ad Listener
override fun onAdLoaded(maxAd: MaxAd) {
// Interstitial ad is ready to be shown. interstitialAd.isReady() will now return 'true'
Log.d(TAG, "Ad loaded successfully: ${maxAd.adUnitId}")
// Reset retry attempt
retryAttempt = 0
// 显示广告
if (interstitialAd.isReady) {
interstitialAd.showAd(this)
interstitialAd.showAd()
}
// 返回广告加载成功状态
setAdStatusResult("Ad Loaded: ${maxAd.adUnitId}")
}
override fun onAdLoadFailed(ad: String, error: MaxError) {
// Interstitial ad failed to load
Log.d(TAG, "Ad load failed: ${ad}, error code: ${error.code}, message: ${error.message}")
override fun onAdLoadFailed(adUnitId: String, error: MaxError) {
Log.d(TAG, "Ad load failed: $adUnitId, error code: ${error.code}, message: ${error.message}")
if (retryAttempt < maxRetryAttempts) {
retryAttempt++
val delayMillis =
TimeUnit.SECONDS.toMillis(Math.pow(2.0, retryAttempt.toDouble()).toLong())
// 使用 Coroutine 延迟重试
val delayMillis = TimeUnit.SECONDS.toMillis(Math.pow(2.0, retryAttempt.toDouble()).toLong())
scope.launch {
delay(delayMillis)
interstitialAd.loadAd()
}
} else {
Log.d(TAG, "Maximum retry attempts reached. No more retries.")
// 返回广告加载失败状态
setAdStatusResult("Ad Load Failed: $adUnitId")
}
}
override fun onAdDisplayFailed(ad: MaxAd, error: MaxError) {
// Interstitial ad failed to display. AppLovin recommends that you load the next ad.
Log.d(TAG, "Ad display failed: ${ad.adUnitId}, error code: ${error.code}, message: ${error.message}")
interstitialAd.loadAd()
Log.d(
TAG,
"Ad disable failed: ${ad.adUnitId}, error code: ${error.code}, message: ${error.message}"
)
}
override fun onAdDisplayed(maxAd: MaxAd) {
@ -84,23 +85,16 @@ class InterstitialActivity : Activity(), MaxAdListener {
}
override fun onAdHidden(maxAd: MaxAd) {
// Interstitial ad is hidden. Pre-load the next ad
interstitialAd.loadAd()
Log.d(TAG, "Ad hidden: ${maxAd.adUnitId}")
interstitialAd.loadAd()
finish() // 广告关闭后结束当前 Activity
}
// 返回主页面的方法
private fun returnToMainPage() {
val intent = Intent(this, MainActivity::class.java) // 假设主页面是 MainActivity
startActivity(intent)
finish() // 结束当前 Activity
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//setContentView(R.layout.activity_main)
createInterstitialAd() // 初始调用,创建并加载插页广告
// 设置广告状态结果
private fun setAdStatusResult(status: String) {
val resultIntent = Intent().apply {
putExtra("AD_STATUS", status)
}
setResult(RESULT_OK, resultIntent)
}
}

View File

@ -0,0 +1,24 @@
package io.sixminutes.breakingnews
import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
/**
* InterstitialAdManager 是一个单例对象用于管理插页广告的显示逻辑
*/
object InterstitialAdManager {
/**
* 显示插页广告
*
* @param activity 当前的 Activity
* @param adId 广告 ID
* @param launcher 用于启动 InterstitialActivity ActivityResultLauncher
*/
fun showAd(activity: ComponentActivity, adId: String, launcher: ActivityResultLauncher<Intent>) {
val intent = Intent(activity, InterstitialActivity::class.java).apply {
putExtra("AD_ID", adId) // 传递广告 ID
}
launcher.launch(intent) // 启动 InterstitialActivity
}
}

View File

@ -1,53 +1,37 @@
package io.sixminutes.breakingnews
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.provider.Settings
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.applovin.mediation.MaxAd
import com.applovin.mediation.MaxAdViewAdListener
import com.applovin.mediation.MaxError
import com.applovin.mediation.ads.MaxAdView
import io.sixminutes.breakingnews.ui.theme.BreakingnewsTheme
class MainActivity : ComponentActivity() {
private val gaidHelper by lazy { GAIDHelper(this) }
private val TAG = "BreakingNews"
private var accessibilityService: MyAccessibilityService? = null
private val ACCESSIBILITY_SERVICE_NAME = "io.sixminutes.breakingnews.MyAccessibilityService"
// ActivityResultLauncher 用于启动无障碍设置页面
private lateinit var enableAccessibilityLauncher: ActivityResultLauncher<Intent>
// 定义 ActivityResultLauncher
private val interstitialLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val status = result.data?.getStringExtra("AD_STATUS") ?: "Unknown"
// 更新广告状态
updateAdStatus(status)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 初始化 ActivityResultLauncher
enableAccessibilityLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
// 用户从设置页面返回后,再次检查无障碍服务是否可用
checkAccessibilityService()
}
// 检查无障碍服务是否可用
checkAccessibilityService()
setContent {
BreakingnewsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
@ -57,117 +41,75 @@ class MainActivity : ComponentActivity() {
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
val gaid = remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
gaidHelper.getAdvertisingId(object : GAIDHelper.OnAdvertisingIdListener {
override fun onSuccess(gaidValue: String) {
gaid.value = gaidValue
Log.d(TAG, "GAID: $gaidValue")
}
override fun onFailure(e: Exception) {
Log.e(TAG, "Failed to retrieve GAID: ${e.message}")
}
})
}
GAIDInfo(gaid = gaid.value)
MainContent()
}
}
}
}
// 显示插页式广告
//InterstitialAdManager.showAd(this)
}
/**
* 检查无障碍服务是否可用如果不可用则打开设置页面
*/
private fun checkAccessibilityService() {
accessibilityService = MyAccessibilityService.getInstance()
if (accessibilityService == null) {
Log.e(TAG, "Accessibility service is not available")
openAccessibilitySettings()
} else {
Log.d(TAG, "Accessibility service is available: $accessibilityService")
continueMainActivityExecution()
@Composable
fun MainContent() {
val deviceModel = Build.MODEL
var adStatus by remember { mutableStateOf("Ad Status: Unknown") }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Device Model: $deviceModel")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
// 调用 InterstitialAdManager 显示广告
InterstitialAdManager.showAd(this@MainActivity, "be20b7a9d66e8895", interstitialLauncher)
adStatus = "Ad 1 Loading..."
}) {
Text(text = "Show Inter Ad 1: be20b7a9d66e8895")
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
// 调用 InterstitialAdManager 显示广告
InterstitialAdManager.showAd(this@MainActivity, "be20b7a9d66e8895", interstitialLauncher)
adStatus = "Ad 2 Loading..."
}) {
Text(text = "Show Inter Ad 2: be20b7a9d66e8896")
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
finish()
}) {
Text(text = "Kill")
}
Spacer(modifier = Modifier.height(16.dp))
Text(text = adStatus)
}
}
/**
* 打开无障碍设置页面
*/
private fun openAccessibilitySettings() {
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
enableAccessibilityLauncher.launch(intent)
Log.d(TAG, "Opening accessibility settings")
}
/**
* 无障碍服务可用后继续执行 MainActivity 的逻辑
*/
private fun continueMainActivityExecution() {
// 在这里添加 MainActivity 的后续操作,例如更新 UI 或执行其他任务
Log.d(TAG, "Continuing MainActivity execution after accessibility service is enabled.")
}
override fun onDestroy() {
super.onDestroy()
}
// 调用点击功能的方法
fun simulateTap(x: Int, y: Int) {
accessibilityService?.simulateTap(x, y)
}
}
@Composable
fun GAIDInfo(gaid: String?) {
Text(text = gaid ?: "Loading GAID...")
}
@Composable
fun BannerAdView(
adUnitId: String,
modifier: Modifier = Modifier,
onAdLoaded: () -> Unit = {},
onAdLoadFailed: (MaxError) -> Unit = {}
) {
val context = LocalContext.current
AndroidView(
factory = { ctx ->
val adView = MaxAdView(adUnitId, ctx)
adView.setListener(object : MaxAdViewAdListener {
override fun onAdLoaded(ad: MaxAd) {
onAdLoaded()
private fun updateAdStatus(status: String) {
// 更新 UI 中的广告状态
runOnUiThread {
setContent {
BreakingnewsTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
MainContent()
}
}
}
override fun onAdDisplayed(ad: MaxAd) {}
override fun onAdHidden(ad: MaxAd) {}
override fun onAdClicked(ad: MaxAd) {}
override fun onAdLoadFailed(adUnitId: String, error: MaxError) {
onAdLoadFailed(error)
}
override fun onAdDisplayFailed(ad: MaxAd, error: MaxError) {}
override fun onAdExpanded(ad: MaxAd) {}
override fun onAdCollapsed(ad: MaxAd) {}
})
adView.setExtraParameter("adaptive_banner", "true")
adView.setBackgroundColor(android.graphics.Color.TRANSPARENT)
adView.loadAd()
adView
},
modifier = modifier
.fillMaxWidth()
.height(80.dp)
)
}
object InterstitialAdManager {
fun showAd(activity: ComponentActivity) {
val intent = Intent(activity, InterstitialActivity::class.java)
activity.startActivity(intent)
}
}
}
}

View File

@ -1,102 +0,0 @@
package io.sixminutes.breakingnews
import android.accessibilityservice.AccessibilityService
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import androidx.compose.ui.geometry.Rect
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
class MyAccessibilityService : AccessibilityService() {
private val TAG = "BreakingNews"
private var serviceInstance: MyAccessibilityService? = null
private val binder = MyBinder()
companion object {
private var instance: MyAccessibilityService? = null
fun getInstance(): MyAccessibilityService? {
return instance
}
}
inner class MyBinder : Binder() {
fun getService(): MyAccessibilityService? {
Log.d(TAG, "Binder getService called")
return serviceInstance
}
}
override fun onServiceConnected() {
super.onServiceConnected()
serviceInstance = this
instance = this // 设置静态实例
Log.d(TAG, "Service connected, instance set to $instance")
// 无障碍服务已启用,启动 MainActivity
startMainActivity()
}
override fun onDestroy() {
super.onDestroy()
instance = null // 清除静态实例
}
override fun onInterrupt() {
Log.d(TAG, "Service interrupted")
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
// 处理无障碍事件
}
override fun onUnbind(intent: Intent?): Boolean {
serviceInstance = null
Log.d(TAG, "Service unbound, serviceInstance set to null")
return super.onUnbind(intent)
}
fun simulateTap(x: Int, y: Int) {
val rootNode = AccessibilityNodeInfoCompat.wrap(rootInActiveWindow)
if (rootNode != null) {
val clickableNodes = ArrayList<AccessibilityNodeInfoCompat>()
findClickableNodes(rootNode, clickableNodes)
for (node in clickableNodes) {
val bounds = android.graphics.Rect()
node.getBoundsInScreen(bounds)
if (bounds.contains(x, y)) {
node.performAction(AccessibilityNodeInfoCompat.ACTION_CLICK)
Log.d(TAG, "Clicked node at ($x,$y)")
break
}
}
} else {
Log.e(TAG, "No root node found")
}
}
private fun findClickableNodes(node: AccessibilityNodeInfoCompat, clickableNodes: ArrayList<AccessibilityNodeInfoCompat>) {
if (node.isClickable) {
clickableNodes.add(node)
}
val childCount = node.childCount
for (i in 0 until childCount) {
val child = node.getChild(i)
if (child != null) {
findClickableNodes(child, clickableNodes)
child.recycle()
}
}
}
private fun startMainActivity() {
// 创建 Intent 启动 MainActivity
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK // 必须设置 FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
Log.d(TAG, "MainActivity started")
}
}

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackSpoken"
android:notificationTimeout="100"
android:canPerformGestures="true"
/>

View File

@ -8,6 +8,7 @@ espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3"
composeBom = "2024.04.01"
mediationTestSuite = "3.0.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -24,6 +25,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
mediation-test-suite = { group = "com.google.android.ads", name = "mediation-test-suite", version.ref = "mediationTestSuite" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }