Commit 1f4fded5 authored by wanglei's avatar wanglei

[修改]改为单模块代码

parent c4bbfecc
/build
\ No newline at end of file
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.koko.batteryinfo"
compileSdk = 34
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
viewBinding = true
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation(project(":Drouter"))
}
\ No newline at end of file
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
package com.koko.batteryinfo
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.koko.batteryinfo.test", appContext.packageName)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<activity
android:name=".BatteryInfoActivity"
android:screenOrientation="portrait"
tools:ignore="DiscouragedApi" />
</application>
</manifest>
\ No newline at end of file
package com.koko.batteryinfo
import android.animation.ValueAnimator
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Color
import android.os.Bundle
import android.view.animation.LinearInterpolator
import androidx.activity.SystemBarStyle
import androidx.activity.addCallback
import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.didi.drouter.api.DRouter
import com.koko.batteryinfo.databinding.ActivityBatteryInfoBinding
import com.koko.drouter.ad.InterAdService
import com.koko.drouter.ad.NativeBatteryType
import com.koko.drouter.ad.NativeAdService
import com.koko.drouter.ad.interAdService
import com.koko.drouter.app.AppService
import com.koko.drouter.app.appService
import com.koko.drouter.base.BaseActivity
import java.math.BigDecimal
import java.math.RoundingMode
class BatteryInfoActivity : BaseActivity<ActivityBatteryInfoBinding>(ActivityBatteryInfoBinding::inflate) {
private var batteryReceiver: BatteryReceiver? = null
override fun initView() {
useDefaultImmersive()
registerReceiver()
showNative()
showInter()
}
override fun initListener() {
super.initListener()
onBackPressedDispatcher.addCallback {
backMainShowAd()
}
binding.flBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() }
}
private fun showInter() {
interAdService.showInterAd(this) {
startProgressAnimation(binding.tvBattery.text.toString().toInt())
}
}
private fun backMainShowAd() {
interAdService.showInterAdBack(this) {
appService.finishToMainTop(this)
}
}
private fun showNative() {
val nativeService = DRouter.build(NativeAdService::class.java).getService()
nativeService.showNative(this, binding.flAd, NativeBatteryType)
}
override fun useDefaultImmersive() {
enableEdgeToEdge(SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT))
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
override fun onDestroy() {
super.onDestroy()
batteryReceiver?.let { unregisterReceiver(batteryReceiver) }
}
private fun registerReceiver() {
val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
batteryReceiver = BatteryReceiver { setBatteryInfo() }
registerReceiver(batteryReceiver, intentFilter)
}
fun formatTime(totalMinutes: Int): String {
val hours = totalMinutes / 60
val minutes = totalMinutes % 60
return "${hours}h${minutes}m"
}
private fun setBatteryInfo() {
val percent = BatteryReceiver.level / BatteryReceiver.scale.toFloat()
val remainingCapacity = BatteryReceiver.mAh * percent
val wTime = (remainingCapacity / 6.0f).toInt()
val battery = (percent * 100).toInt()
val temperature = calculate(BatteryReceiver.temperature, 10)
val voltage = calculate(BatteryReceiver.voltage, 1000)
binding.tvBattery.text = "$battery"
binding.tvEstimated.text = formatTime(wTime)
binding.tvTemperature.text = "$temperature℃"
binding.tvVoltage.text = "${voltage}V"
binding.tvPower.text = "${(BatteryReceiver.mAh * percent).toInt()}mAh"
binding.tvBatteryType.text = "${BatteryReceiver.technology}"
binding.tvBatteryCapacity.text = "${BatteryReceiver.mAh.toInt()}mAh"
binding.tvStatus.text = if (BatteryReceiver.isCharging) "Charging" else "Normal"
}
private fun startProgressAnimation(endProgress: Int) {
ValueAnimator.ofInt(0, endProgress).apply {
duration = 700
interpolator = LinearInterpolator()
addUpdateListener { animation ->
val progress = animation.animatedValue as Int
binding.pbBattery.setProgress(progress / 100f)
}
}.start()
}
private fun calculate(dividend: Int, divisor: Int): String {
val bigDecimalDividend = BigDecimal(dividend)
val bigDecimalDivisor = BigDecimal(divisor)
// 执行除法操作,并保留一位小数,不四舍五入
val result = bigDecimalDividend.divide(bigDecimalDivisor, 1, RoundingMode.DOWN)
// 将结果转换回Double
return result.toString()
}
}
\ No newline at end of file
package com.koko.batteryinfo
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
/**
* 作者:马海钊
* 时间:2025/2/11 11:59
* 功能:
*/
class BatteryReceiver(
private val block: (() -> Unit?)? = null
) : BroadcastReceiver() {
companion object {
var level = 0
var scale = 0
var voltage = 0
var temperature = 0
var technology = ""
var health = 0
var status = 0
var mAh = 0f
var isCharging = false
@JvmStatic
fun register(context: Context) {
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_BATTERY_CHANGED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(BatteryReceiver(), intentFilter, Context.RECEIVER_EXPORTED)
} else {
context.registerReceiver(BatteryReceiver(), intentFilter)
}
}
}
override fun onReceive(context: Context, intent: Intent) {
if (Intent.ACTION_BATTERY_CHANGED != intent.action) return
val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0)
voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0)
temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0)
technology = intent.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY).toString()
health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, 0)
status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN)
isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
val chargeCounter = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER)
val propertyCapacity = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
if (chargeCounter != Int.MIN_VALUE && propertyCapacity != Int.MIN_VALUE) {
mAh = (chargeCounter / (propertyCapacity.toFloat() / 100f)) / 1000f
}
block?.invoke()
}
}
\ No newline at end of file
package com.base.pdfreaderpro.ui.battery
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
class CircleProgressBar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val arcPaint: Paint
private val bgPaint: Paint
private val path: Path
private val bounds: RectF
private var progress = 1f // 进度
private var colorArray = intArrayOf()
init {
arcPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = dpToPx(6f)
strokeCap = Paint.Cap.ROUND
}
bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.TRANSPARENT
style = Paint.Style.STROKE
strokeWidth = dpToPx(6f)
strokeCap = Paint.Cap.ROUND
}
path = Path().apply {
}
bounds = RectF()
colorArray = intArrayOf(
Color.parseColor("#00a8ce"),
Color.parseColor("#00a7fa"),
Color.parseColor("#8afbff"),
)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 考虑Paint的strokeWidth,调整RectF的边界
val strokeWidth: Float = bgPaint.getStrokeWidth()
val halfStrokeWidth = strokeWidth / 2
// 绘制完整圆的背景
path.reset()
path.addCircle(
width / 2f,
height / 2f,
(Math.max(width, height) - strokeWidth) / 2,
Path.Direction.CW
)
canvas.drawPath(path, bgPaint)
// 绘制完整圆的进度
path.reset()
val sweepAngleForProgress: Float = 360 * progress
// 设置渐变色
// 计算渐变的终点X坐标,基于进度
val endX = width / 2f + (sweepAngleForProgress / 360) * (width / 2f - halfStrokeWidth)
// 设置渐变色
// 设置渐变色
val shader = LinearGradient(
width / 2f, 0f, // 起点坐标
width / 2f, height.toFloat(), // 终点坐标
colorArray, // 渐变颜色
null, // 颜色位置(null表示均匀分布)
Shader.TileMode.CLAMP // 填充模式
)
arcPaint.shader = shader
// 从圆的顶部开始绘制
bounds.set(halfStrokeWidth, halfStrokeWidth, width - halfStrokeWidth, height - halfStrokeWidth)
path.addArc(bounds, 270f, sweepAngleForProgress)
canvas.drawPath(path, arcPaint)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredWidth = dpToPx(144f)
val desiredHeight = dpToPx(144f)
setMeasuredDimension(resolveSize(desiredWidth, widthMeasureSpec), resolveSize(desiredHeight, heightMeasureSpec))
}
fun set(strokeWidth: Float, colorArray: IntArray) {
val width = dpToPx(strokeWidth)
arcPaint.strokeWidth = width
bgPaint.strokeWidth = width
this.colorArray = colorArray
}
fun setProgress(progress: Float) {
this.progress = progress
invalidate()
}
private fun dpToPx(dp: Float): Float {
return dp * resources.displayMetrics.density
}
private fun resolveSize(desiredSize: Float, measureSpec: Int): Int {
val mode = MeasureSpec.getMode(measureSpec)
val size = MeasureSpec.getSize(measureSpec)
return when (mode) {
MeasureSpec.EXACTLY -> size
MeasureSpec.AT_MOST -> Math.min(desiredSize.toInt(), size)
else -> desiredSize.toInt()
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#F2FAF8" />
<corners android:radius="15dp" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#ffffff" />
<corners android:radius="12dp" />
</shape>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00BDC3">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="20dp"
android:paddingBottom="6dp"
app:layout_constraintTop_toTopOf="parent">
<FrameLayout
android:id="@+id/fl_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:padding="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/icon_return_bar_nor" />
</FrameLayout>
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Battery Info"
android:textColor="#ffffff"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/fl_back"
app:layout_constraintStart_toEndOf="@id/fl_back"
app:layout_constraintTop_toTopOf="@id/fl_back"
tools:ignore="HardcodedText" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#f7fafa"
android:scrollbars="none"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/cl_top">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="20dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_battery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@mipmap/img_bjkuang_battery"
android:paddingBottom="85dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_quan_battery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:src="@mipmap/img_quan_battery"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.base.pdfreaderpro.ui.battery.CircleProgressBar
android:id="@+id/pb_battery"
android:layout_width="145dp"
android:layout_height="145dp"
app:layout_constraintBottom_toBottomOf="@id/iv_quan_battery"
app:layout_constraintEnd_toEndOf="@id/iv_quan_battery"
app:layout_constraintStart_toStartOf="@id/iv_quan_battery"
app:layout_constraintTop_toTopOf="@id/iv_quan_battery" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="@id/iv_quan_battery"
app:layout_constraintEnd_toEndOf="@id/iv_quan_battery"
app:layout_constraintStart_toStartOf="@id/iv_quan_battery"
app:layout_constraintTop_toTopOf="@id/iv_quan_battery">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_battery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/l_icon_dianchi3" />
<RelativeLayout
android:id="@+id/rl_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_battery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NA"
android:textColor="#ffffff"
android:textSize="28sp"
android:textStyle="bold"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_unit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/tv_battery"
android:layout_marginStart="2dp"
android:layout_toEndOf="@id/tv_battery"
android:text="%"
android:textColor="#ffffff"
android:textSize="18sp"
android:textStyle="bold"
tools:ignore="HardcodedText" />
</RelativeLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Normal"
android:textColor="#ffffff"
android:textSize="14sp"
tools:ignore="HardcodedText" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_battery_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="-65dp"
android:layout_marginEnd="15dp"
android:background="@drawable/bg_f2faf8_15"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/cl_battery">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_battery_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:text="Battery Status"
android:textColor="#181b1f"
android:textSize="18sp"
android:textStyle="bold"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/ll_battery_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:paddingBottom="24dp">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/b_temperature" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_temperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="NA"
android:textColor="#000000"
android:textSize="14sp"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Temperature"
android:textColor="#aeb4bd"
android:textSize="12sp"
tools:ignore="HardcodedText" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/b_voltage" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_voltage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="NA"
android:textColor="#000000"
android:textSize="14sp"
android:textStyle="bold"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Voltage"
android:textColor="#aeb4bd"
android:textSize="12sp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/b_power" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_power"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="NA"
android:textColor="#000000"
android:textSize="14sp"
android:textStyle="bold" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Power"
android:textColor="#aeb4bd"
android:textSize="12sp"
tools:ignore="TextContrastCheck" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
<FrameLayout
android:id="@+id/flAd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
android:layout_marginTop="10dp"
app:layout_constraintTop_toBottomOf="@id/ll_battery_status">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/img_adzhanwei_small"
tools:ignore="ContentDescription" />
</FrameLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="11dp"
android:layout_marginEnd="15dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/flAd">
<LinearLayout
android:id="@+id/llTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_ffffff_12"
android:orientation="horizontal"
android:paddingVertical="12dp"
app:layout_constraintTop_toBottomOf="@id/ll_battery_status">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:src="@mipmap/b_estimated"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="6dp"
android:layout_weight="1"
android:text="Estimated usable time"
android:textColor="#aeb4bd"
android:textSize="12sp"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvEstimated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="20dp"
android:text="NA"
android:textColor="#25C2B0"
android:textSize="16sp"
android:textStyle="bold"
tools:ignore="HardcodedText" />
</LinearLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_ffffff_12"
android:orientation="horizontal"
android:paddingStart="20dp"
android:paddingTop="16dp"
android:paddingEnd="20dp"
android:paddingBottom="16dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@mipmap/b_type"
tools:ignore="ContentDescription" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="6dp"
android:layout_weight="1"
android:text="Battery Type"
android:textColor="#aeb4bd"
android:textSize="12sp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_battery_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Li-poly"
android:textColor="#666666"
android:textSize="16sp"
android:textStyle="bold" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="#ffffff"
android:orientation="horizontal"
android:paddingStart="20dp"
android:paddingTop="16dp"
android:paddingEnd="20dp"
android:paddingBottom="16dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@mipmap/b_capacity"
tools:ignore="ContentDescription" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginHorizontal="6dp"
android:layout_weight="1"
android:text="Battery Capacity"
android:textColor="#aeb4bd"
android:textSize="12sp"
tools:ignore="HardcodedText" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_battery_capacity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="NA"
android:textColor="#666666"
android:textSize="16sp"
android:textStyle="bold" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
package com.koko.batteryinfo
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
\ No newline at end of file
/build
\ No newline at end of file
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.koko.drouter"
compileSdk = 34
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
viewBinding = true
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
api(libs.drouter.api)
api(libs.drouter.api.page)
api(libs.drouter.api.process)
// 语种切换框架:https://github.com/getActivity/MultiLanguages
api("com.github.getActivity:MultiLanguages:9.3")
}
\ No newline at end of file
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
package com.koko.drouter
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.koko.router.test", appContext.packageName)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
\ No newline at end of file
package com.koko.drouter.ad
import android.app.Activity
import com.didi.drouter.api.DRouter
const val INTER_BATTERY_INFO = "INTER_BATTERY_INFO"
val interAdService by lazy {
DRouter.build(InterAdService::class.java).getService()
}
interface InterAdService {
fun showInterAd(activity: Activity, interWhere: String = "", callBack: () -> Unit) = Unit
fun showInterAdBack(activity: Activity, interWhere: String = "", callBack: () -> Unit) = Unit
}
\ No newline at end of file
package com.koko.drouter.ad
import android.app.Activity
import android.widget.FrameLayout
import com.didi.drouter.api.DRouter
const val NativeBatteryType: String = "NativeBatteryType"
val nativeAdService by lazy {
DRouter.build(NativeAdService::class.java).getService()
}
interface NativeAdService {
fun showNative(activity: Activity, flAd: FrameLayout, nativeType: String)
}
\ No newline at end of file
package com.koko.drouter.aes
import com.didi.drouter.api.DRouter
val aesService by lazy {
DRouter.build(AesService::class.java).getService()
}
interface AesService {
fun decrypt(aes: String): String
}
\ No newline at end of file
package com.koko.drouter.app
import android.app.Activity
import com.didi.drouter.api.DRouter
import java.util.Locale
val appService by lazy {
DRouter.build(AppService::class.java).getService()
}
interface AppService {
fun getCurrentSpLocal(): Locale
fun changeActivityLanguage(
activity: Activity,
currentLocale: Locale,
changeAction: () -> Unit
): Boolean
fun finishToMain() = Unit
fun finishToMainTop(activity: Activity) = Unit
}
\ No newline at end of file
package com.koko.drouter.base
import android.content.Intent
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.contract.ActivityResultContracts
class ActivityLauncher(activityResultCaller: ActivityResultCaller) {
//region 权限
private var permissionCallback: ActivityResultCallback<Map<String, Boolean>>? = null
private val permissionLauncher =
activityResultCaller.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result: Map<String, Boolean> ->
permissionCallback?.onActivityResult(result)
}
fun launch(
permissionArray: Array<String>,
permissionCallback: ActivityResultCallback<Map<String, Boolean>>?
) {
this.permissionCallback = permissionCallback
permissionLauncher.launch(permissionArray)
}
//endregion
//region intent跳转
private var activityResultCallback: ActivityResultCallback<ActivityResult>? = null
private val intentLauncher =
activityResultCaller.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult: ActivityResult ->
activityResultCallback?.onActivityResult(activityResult)
}
/**
* it.resultCode == Activity.RESULT_OK
*/
fun launch(
intent: Intent,
activityResultCallback: ActivityResultCallback<ActivityResult>? = null
) {
this.activityResultCallback = activityResultCallback
intentLauncher.launch(intent)
}
//endregion
//region saf
// private var safResultCallback: ActivityResultCallback<Uri?>? = null
// private val safLauncher =
// activityResultCaller.registerForActivityResult(
// ActivityResultContracts.OpenDocument(),
// ) { uri ->
// safResultCallback?.onActivityResult(uri)
// }
//
// fun launch(array: Array<String>, safResultCallback: ActivityResultCallback<Uri?>?) {
// this.safResultCallback = safResultCallback
// safLauncher.launch(array)
// }
//end region
}
\ No newline at end of file
package com.koko.drouter.event
import android.app.Activity
import com.didi.drouter.api.DRouter
val eventService by lazy {
DRouter.build(EventService::class.java).getService()
}
interface EventService {
fun eventActivity(activity: Activity) = Unit
}
\ No newline at end of file
package com.koko.drouter.gobalConfig
import com.didi.drouter.api.DRouter
val globalConfigService by lazy {
DRouter.build(GlobalConfigService::class.java).getService()
}
interface GlobalConfigService {
fun getPackageName(): String
fun getApiUri(): String
fun getAppName(): String
}
\ No newline at end of file
package com.koko.drouter.sp
import com.didi.drouter.api.DRouter
val spService by lazy {
DRouter.build(SpService::class.java).getService()
}
interface SpService {
fun getBoolean(key: String): Boolean
fun putBoolean(key: String, value: Boolean)
fun getLong(key: String): Long
fun putLong(key: String, value: Long)
fun getInt(key: String): Int
fun putInt(key: String, value: Int)
fun getString(key: String): String
fun putString(key: String, value: String)
}
\ No newline at end of file
package com.koko.drouter
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
\ No newline at end of file
......@@ -6,7 +6,6 @@ plugins {
alias(libs.plugins.kotlin.android)
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
id("com.didi.drouter")
id("com.google.devtools.ksp")
}
......@@ -42,10 +41,6 @@ android {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
// 设置是否要自动上传
firebaseCrashlytics {
mappingFileUploadEnabled = true
}
signingConfig = signingConfigs.getByName("release")
}
}
......@@ -85,8 +80,6 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation(project(":Drouter"))
implementation(project(":BatteryInfo"))
//网络请求
implementation("com.google.code.gson:gson:2.11.0")
......@@ -140,4 +133,8 @@ dependencies {
//work
implementation("androidx.work:work-runtime-ktx:2.7.1") // 请使用最新版本
val billing_version = "7.1.1"
implementation("com.android.billingclient:billing:$billing_version")
implementation("com.android.billingclient:billing-ktx:$billing_version")
}
\ No newline at end of file
......@@ -104,7 +104,7 @@
</receiver>
<service
android:name=".service.StayJobService"
android:name=".business.service.StayJobService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:permission="android.permission.BIND_JOB_SERVICE" />
......
......@@ -20,10 +20,10 @@ import com.base.appzxhy.fcm.receiver.BatteryStatusReceiver
import com.base.appzxhy.fcm.receiver.PackageStatusReceiver
import com.base.appzxhy.fcm.receiver.ScreenStatusReceiver
import com.base.appzxhy.fcm.work.RepeatingWorker.Companion.schedulePeriodicWork
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.helper.InstallHelps
import com.base.appzxhy.helper.NewComUtils
import com.base.appzxhy.service.StayJobService.Companion.startJob
import com.base.appzxhy.business.helper.EventUtils
import com.base.appzxhy.business.helper.InstallHelps
import com.base.appzxhy.business.helper.NewComUtils
import com.base.appzxhy.business.service.StayJobService.Companion.startJob
import com.base.appzxhy.ui.start.StartActivity
import com.base.appzxhy.utils.ActivityManagerUtils
import com.base.appzxhy.utils.AppPreferences
......
package com.koko.drouter.base
package com.base.appzxhy.base
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
......@@ -10,12 +13,23 @@ import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import com.didi.drouter.api.DRouter
import com.base.appzxhy.R
import com.base.appzxhy.bean.ConstObject.appLanguageCountrySp
import com.base.appzxhy.bean.ConstObject.appLanguageSp
import com.base.appzxhy.business.helper.EventUtils
import com.base.appzxhy.ui.main.MainActivity
import com.base.appzxhy.utils.ActivityLauncher
import com.base.appzxhy.utils.ActivityManagerUtils
import com.base.appzxhy.utils.LogEx
import com.base.appzxhy.utils.VpnStatusListener
import com.base.appzxhy.utils.VpnStatusListener.Companion.vpnFlow
import com.hjq.language.MultiLanguages
import com.koko.drouter.app.AppService
import com.koko.drouter.app.appService
import com.koko.drouter.event.EventService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.util.Locale
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.random.Random
......@@ -24,6 +38,7 @@ abstract class BaseActivity<VB : ViewBinding>(
bindingInflater: (LayoutInflater) -> VB
) : AppCompatActivity() {
open val TAG = javaClass.simpleName
private val tagNo = Random.nextInt(500)
val binding by lazy(LazyThreadSafetyMode.NONE) {
......@@ -31,71 +46,77 @@ abstract class BaseActivity<VB : ViewBinding>(
}
lateinit var launcher: ActivityLauncher
private var currentLocale: Locale = appService.getCurrentSpLocal()
private var isLanguageRecreate: Boolean = false//判断当前activity有没有因为切换语言重新创建
private val eventService by lazy {
DRouter.build(EventService::class.java).getService()
}
private var currentLocale: Locale? = null
lateinit var vpnStatusListener: VpnStatusListener
private var onCreateI = 0
var onCreateI = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStatusBarColor()
useDefaultImmersive()
eventService.eventActivity(this)
onCreateI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onCreate ${javaClass.simpleName} $onCreateI")
LogEx.logDebug(TAG, "lifecycle $tagNo onCreate ${javaClass.simpleName} $onCreateI")
launcher = ActivityLauncher(this)
setContentView(binding.root)
eventActivity()
changeLanguage()
EventUtils.event("page_${javaClass.simpleName}")
currentLocale = Locale(appLanguageSp, appLanguageCountrySp)
initView()
initListener()
initData()
}
private fun changeLanguage() {
isLanguageRecreate = appService.changeActivityLanguage(this, currentLocale) {
currentLocale = appService.getCurrentSpLocal()
vpnStatusListener = VpnStatusListener(this)
lifecycleScope.launch {
vpnFlow.collectLatest {
actionVpn(it)
}
}
}
open fun initData() {}
open fun setStatusBarColor(@ColorInt color: Int = Color.TRANSPARENT, isLight: Boolean = true) {
enableEdgeToEdge(
if (isLight) SystemBarStyle.light(color, color) else SystemBarStyle.dark(
color
)
)
protected open fun actionVpn(isVpn: Boolean) {
// if (configBean.vpnCanUse) return
// if (isVpn) {
// if (this !is VpnBlockActivity) {
// val intent = Intent(this, VpnBlockActivity::class.java)
// startActivity(intent)
// finish()
// Log.e("VpnStatusListener", "startActivity")
// }
// } else {
// }
}
open fun useDefaultImmersive() {
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
private fun eventActivity() {
eventService.eventActivity(this)
}
protected open fun initView() {}
protected open fun initListener() {}
fun finishToMain(delay: Long = 0) {
val flag = this !is MainActivity && !ActivityManagerUtils.getInstance().isActivityInStack(MainActivity::class.java)
if (flag) {
startActivity(Intent(this, MainActivity::class.java).apply {
putExtra("where", "finishToMain")
})
}
lifecycleScope.launch(Dispatchers.Main) {
delay(delay)
finish()
}
}
fun finishToMainTop() {
val appService = DRouter.build(AppService::class.java).getService()
appService.finishToMainTop(this)
val intent = Intent(this, MainActivity::class.java)
intent.putExtra("where", "finishToMainTop")
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
startActivity(intent)
}
var dialog: Dialog? = null
override fun onDestroy() {
super.onDestroy()
ActivityManagerUtils.getInstance().removeActivity(this)
if (dialog != null) {
dialog?.dismiss()
dialog = null
}
vpnStatusListener.stopListen()
}
override fun attachBaseContext(newBase: Context?) {
......@@ -103,6 +124,54 @@ abstract class BaseActivity<VB : ViewBinding>(
}
fun changeLanguage(currentActivity: Activity, bundle: Bundle = Bundle()): Boolean {
val spLanguage = Locale(appLanguageSp, appLanguageCountrySp)
val flag = currentLocale != spLanguage
val restart = MultiLanguages.setAppLanguage(this, spLanguage)
LogEx.logDebug(
TAG, "changeLanguage " +
"flag=$flag restart=$restart currentLocale=$currentLocale spLanguage=$spLanguage"
)
if (restart) {
currentLocale = Locale(appLanguageSp, appLanguageCountrySp)
}
if (flag || restart) {
// 1.使用 recreate 来重启 Activity,效果差,有闪屏的缺陷
// recreate();
// 2.使用常规 startActivity 来重启 Activity,有从左向右的切换动画
// 稍微比 recreate 的效果好一点,但是这种并不是最佳的效果
// startActivity(new Intent(this, LanguageActivity.class));
// finish();
// 3.我们可以充分运用 Activity 跳转动画,在跳转的时候设置一个渐变的效果,相比前面的两种带来的体验是最佳的
//需要设置启动模式,避免AndroidManifest.xml设置singleTop启动模式导致重复启动不生效
//这种方式如果设置了singleTop或者singleTask的,再次回到该activity无法改变于语言,因为还在堆栈里没有重新创建
needRecreate()
if (needRecreate()) {
LogEx.logDebug(TAG, "changeLanguage recreate")
recreate()
} else {
startActivity(Intent(this, currentActivity::class.java).apply {
putExtras(bundle)
})
overridePendingTransition(R.anim.activity_alpha_in, R.anim.activity_alpha_out)
finish()
}
return true
}
return false
}
/**
* 判断是否需要重新创建对SingleTop启动模式的有效
*/
fun needRecreate(): Boolean {
return TAG.contains("MainActivity")
}
/**
* 避免在onResume--onPause周期内,多次调用
* 在onStop中重置标志
......@@ -111,33 +180,36 @@ abstract class BaseActivity<VB : ViewBinding>(
private var onResumeOneShootI = 0
open fun onResumeOneShoot() {
onResumeOneShootI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onResumeOneShoot ${javaClass.simpleName} $onResumeOneShootI")
LogEx.logDebug(TAG, "lifecycle $tagNo onResumeOneShoot ${javaClass.simpleName} $onResumeOneShootI")
}
private var onResumeI = 0
override fun onResume() {
super.onResume()
onResumeI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onResume ${javaClass.simpleName} $onResumeI")
LogEx.logDebug(TAG, "lifecycle $tagNo onResume ${javaClass.simpleName} $onResumeI")
vpnStatusListener.startListen()
if (!onResumeCalled.get()) {
onResumeCalled.set(true)
onResumeOneShoot()
}
actionVpn(vpnFlow.value)
}
private var onStartI = 0
override fun onStart() {
super.onStart()
onStartI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onStart ${javaClass.simpleName} $onStartI")
LogEx.logDebug(TAG, "lifecycle $tagNo onStart ${javaClass.simpleName} $onStartI")
}
private var onStopI = 0
override fun onStop() {
super.onStop()
onStopI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onStop ${javaClass.simpleName} $onStopI")
LogEx.logDebug(TAG, "lifecycle $tagNo onStop ${javaClass.simpleName} $onStopI")
onResumeCalled.set(false)
}
......@@ -145,7 +217,21 @@ abstract class BaseActivity<VB : ViewBinding>(
override fun onPause() {
super.onPause()
onPauseI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onPause ${javaClass.simpleName} $onPauseI")
LogEx.logDebug(TAG, "lifecycle $tagNo onPause ${javaClass.simpleName} $onPauseI")
}
open fun setStatusBarColor(@ColorInt color: Int = Color.TRANSPARENT, isLight: Boolean = true) {
enableEdgeToEdge(
if (isLight) SystemBarStyle.light(color, color) else SystemBarStyle.dark(color)
)
}
open fun useDefaultImmersive() {
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
}
\ No newline at end of file
@file:Suppress("unused")
package com.koko.drouter.base
package com.base.appzxhy.base
import android.app.Activity
import android.content.Intent
......@@ -10,7 +10,6 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import kotlin.random.Random
/**
*
......@@ -23,7 +22,6 @@ abstract class BaseFragment<VB : ViewBinding>(
private var _binding: VB? = null
private var fragmentInit = false
open val TAG = javaClass.simpleName
private val tagNo = Random.nextInt(500)
//重写binding get()方法不直接暴露真实的可空_binding数据
@Suppress("MemberVisibilityCanBePrivate")
......@@ -39,13 +37,10 @@ abstract class BaseFragment<VB : ViewBinding>(
return binding.root
}
private var onViewCreatedI = 0
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
initListener()
onViewCreatedI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onViewCreated ${javaClass.simpleName} $onViewCreatedI")
}
protected open fun initView() {}
......@@ -54,50 +49,18 @@ abstract class BaseFragment<VB : ViewBinding>(
open fun onResumeOneShoot() = Unit
private var onResumeOneShootI = 0
private var onResumeI = 0
override fun onResume() {
onResumeI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onResume ${javaClass.simpleName} $onResumeI")
if (!fragmentInit) {
onResumeOneShoot()
onResumeOneShootI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onResumeOneShoot ${javaClass.simpleName} $onResumeOneShootI")
fragmentInit = true
}
super.onResume()
}
private var onDestroyViewI = 0
override fun onDestroyView() {
super.onDestroyView()
//要手动置null防止内存泄漏
_binding = null
onDestroyViewI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onDestroyView ${javaClass.simpleName} $onDestroyViewI")
}
private var onPauseI = 0
override fun onPause() {
super.onPause()
onPauseI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onPause ${javaClass.simpleName} $onPauseI")
}
private var onStopI = 0
override fun onStop() {
super.onStop()
onStopI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onStop ${javaClass.simpleName} $onStopI")
fragmentInit = false
}
private var onDestroyI = 0
override fun onDestroy() {
super.onDestroy()
onDestroyI++
// LogEx.logDebug(TAG, "lifecycle $tagNo onDestroy ${javaClass.simpleName} $onDestroyI")
}
}
......
......@@ -13,8 +13,10 @@ class AdConfigBean(
var numClickLimit: Int = -1,//点击次数限制
var timeInterval: Int = 1,//广告间隔
var openAdLoading: Int = 15,//开屏广告拉取时间
var functionBackShowAd: Boolean = true,//功能退出显示广告
var functionInShowAd: Boolean = true,//功能进入显示广告
var rvEmptyShowNative: Boolean = true,//列表空的显示原生不
) {
......
package com.base.appzxhy.bean.config
/**
* 后台配置
*/
data class ConfigBean(
var isInBlackList: Boolean = false,
val ut: Int = 0,
var vpnCanUse: Boolean = true,
val adConfigBean: AdConfigBean = AdConfigBean(),
val popupConfigBean: PopupConfigBean = PopupConfigBean(),
val vipConfigBean: VipConfigBean = VipConfigBean(),
val noEventKey: List<String> = listOf(),
val getConfigInterval: Int = 4,//请求配间隔小时
) {
companion object {
var configBean: ConfigBean = ConfigBean()
}
......
......@@ -2,38 +2,25 @@ package com.base.appzxhy.bean.config
class PopupConfigBean(
var popupForegroundCanPush: Boolean = false,
var popupStatus: Boolean = true,
var popupCount: Int = 24,
var popupStart: Int = 0,
var popupEnd: Int = 24,
var popupInterval: Int = 1,
var popupForegroundCanPush: Boolean = true,//前台是否可以推送
var popupStatus: Boolean = true,//推送总开关
var popupCount: Int = 200,//推送总次数
var isDifferNotificationId: Boolean = true,//推送使用不同的通知ID
var popupLimitRangeTime: List<Int> = listOf(),//推送限制时间
var popupTimerInterval: Int = 1,
var popupFcmInterval: Int = 1,
var popupHoverStatus: Boolean = true,
var popupInterval: Int = 1,//推送总时间间隔分钟
var popupHoverStatus: Boolean = true,//推送悬停触开关
var popupHoverCount: Int = 5,
var popupHoverDelay: Int = 5000,
//定时器
var timerS: Boolean = true,
var timerDelay: Int = 1,
var timerInterval: Int = 1,
//解锁
var screenS: Boolean = true,
var popupScreenCount: Int = 20,
var popupScreenInterval: Int = 1,
//电量
var batteryS: Boolean = true,
var popupBatteryValue: Int = 20,
var popupBatteryInterval: Int = 1,
var timerS: Boolean = true,//定时器触发推送
var timerDelay: Int = 60,//定时器执行延迟 秒
var timerInterval: Int = 7,//定时器执行间隔 分钟
//安装卸载
var packageS: Boolean = true,
var popupPackageCount: Int = 20,
var popupPackageInterval: Int = 1
val screenS: Boolean = true,//解锁触发推送
val alarmS: Boolean = true,//是否闹钟触发推送
val workS: Boolean = true,//workManager触发推送
) {
companion object {
var popupConfigBean: PopupConfigBean = PopupConfigBean()
......
package com.base.appzxhy.bean.config
class VipConfigBean(
val productIdYear: String = "vip_year",
val basePlanIdYear: String = "vip-year",
val offerIdYear: String = "year-free-3",
val productIdMonth: String = "vip_month",
val basePlanIdMonth: String = "vip-month",
val offerIdMonth: String = "",
val productIdWeek: String = "vip_week",
val basePlanIdWeek: String = "vip-week",
val offerIdWeek: String = ""
) {
companion object {
var vipConfigBean = VipConfigBean()
}
override fun toString(): String {
return "VipConfigBean(productIdYear='$productIdYear', basePlanIdYear='$basePlanIdYear', offerIdYear='$offerIdYear', productIdMonth='$productIdMonth', basePlanMonth='$basePlanIdMonth', offerIdMonth='$offerIdMonth', productIdWeek='$productIdWeek', basePlanWeek='$basePlanIdWeek', offIdWeek='$offerIdWeek')"
}
}
package com.base.appzxhy.ads
package com.base.appzxhy.business.ads
import android.animation.ObjectAnimator
import android.animation.ValueAnimator.INFINITE
......
package com.base.appzxhy.ads
package com.base.appzxhy.business.ads
import com.base.appzxhy.MyApplication
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.business.helper.EventUtils
import com.base.appzxhy.utils.LogEx
import org.json.JSONObject
import java.util.UUID
......
package com.base.appzxhy.ads
package com.base.appzxhy.business.ads
import android.app.Activity
import android.app.Dialog
......
package com.base.appzxhy.ads
package com.base.appzxhy.business.ads
import android.app.Activity
import android.content.Context
......@@ -21,7 +21,7 @@ import com.base.appzxhy.ads.applovin.MaxOpenMgr
import com.base.appzxhy.bean.config.AdConfigBean
import com.base.appzxhy.bean.config.AdConfigBean.Companion.adsConfigBean
import com.base.appzxhy.bean.config.ConfigBean.Companion.configBean
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.business.helper.EventUtils
import com.base.appzxhy.utils.AppPreferences
import com.base.appzxhy.utils.LogEx
import com.base.appzxhy.utils.ToastUtils.toast
......
package com.base.appzxhy.ads
package com.base.appzxhy.business.ads
abstract class AdsShowCallBack {
open fun show() {}
......
package com.base.appzxhy.ads
package com.base.appzxhy.business.ads
/**
* 广告类型
......
package com.base.appzxhy.ads
package com.base.appzxhy.business.ads
import com.base.appzxhy.BuildConfig
import com.base.appzxhy.bean.config.AdConfigBean
......
package com.base.appzxhy.ads
package com.base.appzxhy.business.ads
import android.annotation.SuppressLint
import android.content.Context
......
package com.base.appzxhy.ads
package com.base.appzxhy.business.ads
import com.base.appzxhy.GlobalConfig
import com.google.android.gms.ads.appopen.AppOpenAd
......
package com.base.appzxhy.ads.admob
package com.base.appzxhy.business.ads.admob
import android.os.Bundle
import android.view.ViewGroup
......
package com.base.appzxhy.ads.admob
package com.base.appzxhy.business.ads.admob
import android.app.Activity
import android.content.Context
......
package com.base.appzxhy.ads.admob
package com.base.appzxhy.business.ads.admob
import android.content.Context
import android.util.Log
......
package com.base.appzxhy.ads.admob
package com.base.appzxhy.business.ads.admob
import android.app.Activity
import android.content.Context
......
package com.base.appzxhy.ads.admob
package com.base.appzxhy.business.ads.admob
import android.app.Activity
import android.os.Bundle
......@@ -8,7 +8,7 @@ import com.base.appzxhy.ads.SolarEngineEvent
import com.base.appzxhy.ads.taichiPref
import com.base.appzxhy.ads.taichiSharedPreferencesEditor
import com.base.appzxhy.bean.config.AdConfigBean
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.business.helper.EventUtils
import com.base.appzxhy.utils.LogEx
import com.facebook.appevents.AppEventsConstants
import com.facebook.appevents.AppEventsLogger
......
package com.base.appzxhy.ads.applovin
package com.base.appzxhy.business.ads.applovin
import android.os.Bundle
import com.applovin.mediation.MaxAd
......@@ -9,7 +9,7 @@ import com.base.appzxhy.MyApplication
import com.base.appzxhy.ads.AdEvent
import com.base.appzxhy.ads.taichiPref
import com.base.appzxhy.ads.taichiSharedPreferencesEditor
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.business.helper.EventUtils
import com.base.appzxhy.utils.LogEx.logDebug
import com.facebook.appevents.AppEventsConstants
import com.facebook.appevents.AppEventsLogger
......
package com.base.appzxhy.ads.applovin
package com.base.appzxhy.business.ads.applovin
import android.app.Activity
import android.content.Context
......
package com.base.appzxhy.ads.applovin
package com.base.appzxhy.business.ads.applovin
import androidx.annotation.LayoutRes
import com.applovin.mediation.MaxAd
......@@ -10,7 +10,7 @@ import com.base.appzxhy.GlobalConfig
import com.base.appzxhy.ads.AdsType
import com.base.appzxhy.ads.LimitUtils
import com.base.appzxhy.ads.NativeParentView
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.business.helper.EventUtils
import org.json.JSONObject
import java.util.UUID
......
package com.base.appzxhy.ads.applovin
package com.base.appzxhy.business.ads.applovin
import android.app.Activity
import android.content.Context
......
package com.base.appzxhy.business.billing
import android.app.Activity
import android.os.Bundle
import android.util.Log
import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.PendingPurchasesParams
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.ProductDetails.SubscriptionOfferDetails
import com.android.billingclient.api.Purchase
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams
import com.android.billingclient.api.acknowledgePurchase
import com.android.billingclient.api.queryProductDetails
import com.base.appzxhy.MyApplication
import com.base.appzxhy.bean.config.VipConfigBean
import com.base.appzxhy.business.helper.EventUtils
import com.base.appzxhy.utils.AppPreferences
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
object GoogleSubUtil {
private val TAG = "GoogleSubUtil"
const val KEY_SP_VIP = "spVip"
/**
* vip历史状态
* 用来判断没拿到数据时是否是vip,是否展示广告判断用这个
*/
fun getSpVip(): Boolean {
return AppPreferences.getInstance().getBoolean(KEY_SP_VIP, false)
}
/**
* 用来实时为UI提供状态
* null表示未知状态
*/
val vipResult: MutableStateFlow<Boolean?> = MutableStateFlow(null)
/**
* 定义链接状态
*/
val connectionResult = MutableStateFlow(false)
/**
*年产品详情
*/
var yearProductFlow: MutableStateFlow<ProductDetails?> = MutableStateFlow(null)
/**
*月产品详情
*/
var monthProductFlow: MutableStateFlow<ProductDetails?> = MutableStateFlow(null)
/**
*周产品详情
*/
var weekProductFlow: MutableStateFlow<ProductDetails?> = MutableStateFlow(null)
/**
* 订阅详情是否已经准备
*/
var subOffDetailReady = MutableStateFlow(false)
/**
* 获取订阅详情
*/
fun getSubOffDetail(productDetails: ProductDetails, basePlanId: String, offId: String): SubscriptionOfferDetails? {
val subOffDetails = productDetails.subscriptionOfferDetails
return if (subOffDetails.isNullOrEmpty()) {
null
} else {
subOffDetails.find { it.basePlanId == basePlanId && it.offerId == offId } ?: subOffDetails[0]
}
}
/**
* 更新sp里面的vip状态
*/
fun vipSpObserveForever() {
MainScope().launch {
vipResult.collectLatest {
if (it != null) {
AppPreferences.getInstance().putBoolean(KEY_SP_VIP, it)
}
}
}
}
private val client by lazy {
vipSpObserveForever()
BillingClient.newBuilder(MyApplication.appContext)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases(
PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()
)
// Configure other settings.
.build()
}
/**
* 购买状态更新监听
*/
private val purchasesUpdatedListener = object : PurchasesUpdatedListener {
override fun onPurchasesUpdated(
billingResult: BillingResult,
purchases: MutableList<Purchase>?,
) {
var purchase = purchases?.firstOrNull()
eventJSON.apply { put("purchaseToken", purchase?.purchaseToken) }
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
purchase = purchases?.firstOrNull { it.purchaseState == Purchase.PurchaseState.PURCHASED } ?: return
MainScope().launch {
val flag = acknowledgePurchase(purchase)
vipResult.update { flag }
}
EventUtils.event("google_billing_success", value = "success", ext = eventJSON)
val bundle = Bundle()
bundle.putString(FirebaseAnalytics.Param.CURRENCY, eventJSON.get(FirebaseAnalytics.Param.CURRENCY) as String)
bundle.putDouble(FirebaseAnalytics.Param.VALUE, eventJSON.get(FirebaseAnalytics.Param.VALUE) as Double)
bundle.putString(FirebaseAnalytics.Param.ITEM_ID, eventJSON.get(FirebaseAnalytics.Param.ITEM_ID) as String)
Firebase.analytics.logEvent(FirebaseAnalytics.Event.PURCHASE, bundle)
}
BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "处理过程中发生用户计费错误。")
})
}
BillingClient.BillingResponseCode.DEVELOPER_ERROR -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "API操作期间发生致命错误。")
})
}
BillingClient.BillingResponseCode.ERROR -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "错误使用API导致的错误。")
})
}
BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "当前设备上的Play商店不支持请求的功能。")
})
}
BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "购买失败,因为该物品已被拥有。")
})
}
BillingClient.BillingResponseCode.ITEM_NOT_OWNED -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "对该项目请求的操作失败,因为它不属于用户。")
})
}
BillingClient.BillingResponseCode.ITEM_UNAVAILABLE -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "所请求的产品不可购买。")
})
}
BillingClient.BillingResponseCode.NETWORK_ERROR -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "操作过程中发生网络错误。")
})
}
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "该应用程序未通过Google Play计费库连接到Play商店服务。")
})
}
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "该服务当前不可用。")
})
}
BillingClient.BillingResponseCode.USER_CANCELED -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "交易已被用户取消。")
})
}
else -> {
EventUtils.event("google_billing_error", ext = eventJSON.apply {
put("info", "未知错误。")
})
}
}
}
}
fun endConnection() {
client.endConnection()
connectionResult.update { false }
}
fun startConnection() {
Log.e(TAG, "startConnection")
if (client.isReady) {
return
}
if (connectionResult.value == true) {
return
}
var time = 3
MainScope().launch {
while (!connectionResult.value && time > 0) {
time--
realStartConnection()
delay(1500)
}
}
}
private fun realStartConnection() {
client.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
Log.e(TAG, "onBillingServiceDisconnected")
connectionResult.update { false }
}
override fun onBillingSetupFinished(billingResult: BillingResult) {
Log.e(TAG, "onBillingSetupFinished")
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.e(TAG, "onBillingSetupFinished ok")
connectionResult.update { true }
//查询订阅购买状态
queryPurchasesAsync()
//查询所有的订阅产品
queryAllProduct()
} else {
connectionResult.update { false }
}
}
})
}
private fun queryAllProduct() = MainScope().launch(Dispatchers.IO) {
val productYear: ProductDetails? = queryProduct(VipConfigBean.vipConfigBean.productIdYear)
val productMonth: ProductDetails? = queryProduct(VipConfigBean.vipConfigBean.productIdMonth)
val productWeek: ProductDetails? = queryProduct(VipConfigBean.vipConfigBean.productIdWeek)
Log.e(TAG, "productYear=${productYear}")
Log.e(TAG, "productMonth=${productMonth}")
Log.e(TAG, "productWeek=${productWeek}")
yearProductFlow.update { productYear }
monthProductFlow.update { productMonth }
weekProductFlow.update { productWeek }
val isReady = !(productYear?.subscriptionOfferDetails.isNullOrEmpty() ||
productMonth?.subscriptionOfferDetails.isNullOrEmpty() ||
productWeek?.subscriptionOfferDetails.isNullOrEmpty())
Log.e(TAG, "isReady=$isReady")
subOffDetailReady.update { isReady }
}
suspend fun queryProduct(productId: String): ProductDetails? {
val params = getParams(productId)
val productDetails = withContext(Dispatchers.IO) {
val productDetailsResult = client.queryProductDetails(params.build())
return@withContext productDetailsResult.productDetailsList?.firstOrNull {
//筛选匹配商品
productId == it.productId
}
}
return productDetails
}
private fun getParams(productId: String): QueryProductDetailsParams.Builder {
val params = QueryProductDetailsParams.newBuilder()
val productList = listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
params.setProductList(productList)
return params
}
/**
* 支付上报
*/
var eventJSON = JSONObject()
fun pay(activity: Activity, productDetails: ProductDetails, subOffDetail: SubscriptionOfferDetails) {
val pricingPhase = subOffDetail.pricingPhases.pricingPhaseList[0]
eventJSON = JSONObject().apply {
put("productId", productDetails.productId)
put("basePlanId", subOffDetail.basePlanId)
put("offerId", subOffDetail.offerId)
put("offerToken", subOffDetail.offerToken)
put(FirebaseAnalytics.Param.CURRENCY, pricingPhase.priceCurrencyCode)//订阅价格类型
put(FirebaseAnalytics.Param.VALUE, pricingPhase.formattedPrice.formattedPrice2Double())//订阅价格 4.66
put(FirebaseAnalytics.Param.ITEM_ID, productDetails.productId)//订阅产品ID
}
EventUtils.event("google_billing", value = "click", ext = eventJSON)
val currency = eventJSON.get(FirebaseAnalytics.Param.CURRENCY) as String
val value = eventJSON.get(FirebaseAnalytics.Param.VALUE) as Double
val item_id = eventJSON.get(FirebaseAnalytics.Param.ITEM_ID) as String
Log.e(TAG, "FirebaseAnalytics currency=$currency value=$value item_id=$item_id")
val productDetailsParamsList = listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(subOffDetail.offerToken)
.build()
)
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.build()
val billingResult = client.launchBillingFlow(activity, billingFlowParams)
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
queryPurchasesAsync()
} else {
}
}
/**
* 查询所有[购买]状态
*/
fun queryPurchasesAsync() = MainScope().launch(Dispatchers.IO) {
client.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
) { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
val noVip = purchases.isEmpty()
Log.e("vipResult", "noVip=$noVip")
if (noVip) {
vipResult.update { false }
} else {
MainScope().launch {
purchases.forEachIndexed { index, purchase: Purchase ->
// val vip = it.isAcknowledged && it.isAutoRenewing
// val purchaseState = purchase.purchaseState //买状态,表示购买交易的状态,例如是否已完成、是否被取消等。
var vip: Boolean?
val isAcknowledged = purchase.isAcknowledged // 是否已确认,表示该购买是否已经被应用确认。
vip = isAcknowledged
Log.e(TAG, "vipResult 1 vip=$vip index=$index")
if (!isAcknowledged) {//未确认再查询下购买状态
vip = acknowledgePurchase(purchase)
Log.e(TAG, "vipResult 2 vip=$vip index=$index")
}
vipResult.update { vip }
}
}
}
}
}
}
//确认商品购买状态,null表示未知状态
// BillingClient.BillingResponseCode.OK 已确认
// BillingClient.BillingResponseCode.USER_CANCELED 用户取消等
private suspend fun acknowledgePurchase(purchase: Purchase): Boolean? {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
val ackPurchaseResult = withContext(Dispatchers.IO) {
client.acknowledgePurchase(acknowledgePurchaseParams)
}
if (ackPurchaseResult.responseCode == BillingClient.BillingResponseCode.OK) {
EventUtils.event("google_billing", value = "consume", ext = eventJSON)
return true
}
return null
}
/**
* 价格提前数字转Double
*/
fun String.formattedPrice2Double(): Double {
val regex = """\$([\d.]+)""".toRegex()
val matchResult = regex.find(this)
if (matchResult != null) {
val price = matchResult.groupValues[1].toDouble()
return price
} else {
return 0.0
}
}
}
package com.base.appzxhy.helper
package com.base.appzxhy.business.helper
import android.util.Base64
import com.base.appzxhy.GlobalConfig
......
package com.base.appzxhy.helper
package com.base.appzxhy.business.helper
import android.os.Build
import com.base.appzxhy.BuildConfig
import com.base.appzxhy.GlobalConfig
import com.base.appzxhy.helper.ReportUtils.doPost
import com.base.appzxhy.business.helper.ReportUtils.doPost
import com.base.appzxhy.utils.AppPreferences
import com.base.appzxhy.utils.LogEx
import org.json.JSONException
......
package com.base.appzxhy.helper
package com.base.appzxhy.business.helper
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener
......
package com.base.appzxhy.helper
package com.base.appzxhy.business.helper
import android.util.Base64
import android.util.Log
import com.base.appzxhy.BuildConfig
import com.base.appzxhy.GlobalConfig
......@@ -25,9 +26,9 @@ object NewComUtils {
val packageName = GlobalConfig.PACKAGE_NAME
val appCode = packageName.substringAfter(PACKAGE_NAME_PREFIX).take(5).toLowerCase(Locale.getDefault())
val bRefer = android.util.Base64.encodeToString(
val bRefer = Base64.encodeToString(
AppPreferences.getInstance().getString("install_referrer", "").toByteArray(),
android.util.Base64.DEFAULT
Base64.DEFAULT
)
var s = "$API_URL/${appCode}spk?pkg=$packageName" +
......
package com.base.appzxhy.helper;
package com.base.appzxhy.business.helper;
import android.text.TextUtils;
......
package com.base.appzxhy.fcm
package com.base.appzxhy.push.fcm
import android.content.Context
import android.util.Log
import com.base.appzxhy.helper.EventUtils.event
import com.base.appzxhy.business.helper.EventUtils.event
import com.base.appzxhy.utils.AppPreferences
import com.base.appzxhy.utils.LogEx
import com.google.android.gms.tasks.OnCompleteListener
......
package com.base.appzxhy.fcm
package com.base.appzxhy.push.fcm
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.NotificationSendBean.Companion.POPUP_WHERE_FCM
import com.base.appzxhy.fcm.notification.MyNotificationManager
import com.base.appzxhy.helper.EventUtils.event
import com.base.appzxhy.push.notification.MyNotificationManager
import com.base.appzxhy.business.helper.EventUtils.event
class FcmReceiver : BroadcastReceiver() {
......@@ -14,7 +14,6 @@ class FcmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
event("FCM_Received", "FcmReceiver", null)
val sendBean = NotificationSendBean(context, POPUP_WHERE_FCM, canSend = { true }, sendSuccess = {})
MyNotificationManager.submitSendBean(sendBean)
MyNotificationManager.submitSendBean(NotificationSendBean(context, POPUP_WHERE_FCM))
}
}
package com.base.appzxhy.fcm
package com.base.appzxhy.push.fcm
import android.annotation.SuppressLint
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.NotificationSendBean.Companion.POPUP_WHERE_FCM
import com.base.appzxhy.fcm.notification.MyNotificationManager
import com.base.appzxhy.helper.EventUtils.event
import com.base.appzxhy.push.notification.MyNotificationManager
import com.base.appzxhy.business.helper.EventUtils.event
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import org.json.JSONObject
......@@ -15,7 +15,6 @@ class MessagingService : FirebaseMessagingService() {
super.onMessageReceived(remoteMessage)
val data = JSONObject(remoteMessage.data.toString())
event("FCM_Received", "MessagingService", data)
val sendBean = NotificationSendBean(this, POPUP_WHERE_FCM, canSend = { true }, sendSuccess = {})
MyNotificationManager.submitSendBean(sendBean)
MyNotificationManager.submitSendBean(NotificationSendBean(this, POPUP_WHERE_FCM))
}
}
\ No newline at end of file
package com.base.appzxhy.fcm.notification
package com.base.appzxhy.push.notification
import android.content.Context
import com.base.appzxhy.BuildConfig
import com.base.appzxhy.MyApplication
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.NotificationSendBean.Companion.ACTION_ID_SCAN
import com.base.appzxhy.bean.NotificationSendBean.Companion.POPUP_WHERE_TIMER
import com.base.appzxhy.bean.NotificationSendBean.Companion.ACTION_ID_BEAN_MEDIA
import com.base.appzxhy.bean.NotificationSendBean.Companion.POPUP_WHERE_WORK_MANAGER
import com.base.appzxhy.bean.config.PopupConfigBean.Companion.popupConfigBean
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.push.notification.NotificationBeanUtils.nextNotificationInfoBean
import com.base.appzxhy.business.helper.EventUtils
import com.base.appzxhy.utils.AppPreferences
import com.base.appzxhy.utils.KotlinExt.currentDate
import com.base.appzxhy.utils.LogEx
......@@ -19,6 +20,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.Calendar
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
/**
......@@ -26,7 +28,7 @@ import java.util.concurrent.atomic.AtomicBoolean
*/
object MyNotificationManager {
private val TAG = "NotificationManager"
private val TAG = "MyNotificationManager"
private var sendBeanBlockingQueue = ArrayBlockingQueue<NotificationSendBean>(10)
......@@ -49,53 +51,66 @@ object MyNotificationManager {
private var isStartSendQueue = AtomicBoolean(false)
private var isUnLimit: Boolean = if (BuildConfig.DEBUG) false else false
private var isUnLimit: Boolean = if (BuildConfig.DEBUG) true else false
fun startNotificationQueue() {
if (isStartSendQueue.get()) return
isStartSendQueue.set(true)
MainScope().launch(Dispatchers.IO) {
while (isActive) {
val bean = sendBeanBlockingQueue.take()
//测试哪些位置触发
if (BuildConfig.DEBUG) {
if (!testWhere.contains(bean.where)) continue
}
LogEx.logDebug(TAG, "sendNotificationIfCan where=${bean.where}")
EventUtils.event("Notification_Popup_Start", "where=${bean.where}")
// 尝试从队列中获取元素,最多等待 5 秒
var bean = sendBeanBlockingQueue.poll(5, TimeUnit.SECONDS)
// Log.e(TAG, "bean=$bean")
if (bean != null) {
LogEx.logDebug(TAG, "sendNotificationIfCan where=${bean.where}")
// EventUtils.event("Notification_Popup_Start", "where=${bean.where}")
if (!isUnLimit) {
//应用在前台不推
if (MyApplication.PAUSED_VALUE == 1 && !popupConfigBean.popupForegroundCanPush) {
LogEx.logDebug(TAG, "app Foreground")
continue
}
//总的限制条件
if (!canSendNotification(bean.context)) continue
//其他条推送是否可以推送
if (!bean.canSend.invoke()) continue
}
if (!isUnLimit) {
//应用在前台不推
if (MyApplication.PAUSED_VALUE == 1 && !popupConfigBean.popupForegroundCanPush) {
LogEx.logDebug(TAG, "app Foreground")
continue
//提前准备数据
if (bean.actionId == ACTION_ID_BEAN_MEDIA) {
val uiBean = nextNotificationInfoBean()
if (uiBean == null) {
LogEx.logDebug(TAG, "sendNotification no bean")
continue
}
bean.valueMap["bean"] = uiBean
}
//总的限制条件
if (!canSendNotification(bean.context)) continue
//其他条推送是否可以推送
if (!bean.canSend.invoke()) continue
bean = async(Dispatchers.Main) {
NotificationUiUtil.setNotification(bean)
}.await()
LogEx.logDebug(TAG, "sendNotificationCan where=${bean.where}")
//上报通知
EventUtils.event("Notification_Popup_${bean.where}", "actionId=${bean.actionId}")
//当天次数加一
dayPopupCount += 1
//推送时间
lastPopupTime = System.currentTimeMillis()
//这条推送回调
bean.sendSuccess?.invoke()
async(Dispatchers.Main) {
//发送悬停
NotificationHoverUtils.sendHoverNotification(bean)
}.await()
}
async(Dispatchers.Main) {
NotificationUiUtil.setNotification(bean)
}.await()
//上报通知
EventUtils.event("Notification_Popup", "where=${bean.where} actionId=${bean.actionId}")
//当天次数加一
dayPopupCount += 1
//推送时间
lastPopupTime = System.currentTimeMillis()
//这条推送回调
bean.sendSuccess?.invoke()
async(Dispatchers.Main) {
//发送悬停
NotificationHoverUtils.sendHoverNotification(bean)
}.await()
}
isStartSendQueue.set(false)
}
......@@ -146,14 +161,13 @@ object MyNotificationManager {
return false
}
//判断是否在时间区域
val start = popupConfigBean.popupStart
val end = popupConfigBean.popupEnd
//判断是否在限制时间区域
val calendar = Calendar.getInstance()
val currentHour = calendar.get(Calendar.HOUR_OF_DAY)
if (currentHour !in start until end) {
LogEx.logDebug("canSendNotification", "start-end currentHour=$currentHour start=$start end=$end")
EventUtils.event("Notification_Error", "start=$start end=$end currentHour=$currentHour")
val limitRangeTime = popupConfigBean.popupLimitRangeTime
if (limitRangeTime.contains(currentHour)) {
LogEx.logDebug("canSendNotification", "currentHour=$currentHour popupLimitRangeTime=$limitRangeTime")
EventUtils.event("Notification_Error", "currentHour=$currentHour popupLimitRangeTime=$limitRangeTime")
return false
}
......@@ -161,7 +175,7 @@ object MyNotificationManager {
val interval = popupConfigBean.popupInterval
val passedTime = System.currentTimeMillis() - lastPopupTime
if (passedTime < interval * 60 * 1000L) {
EventUtils.event("Notification_Error", "interval=$interval passedTime=$passedTime")
// EventUtils.event("Notification_Error", "interval=$interval passedTime=$passedTime")
LogEx.logDebug("canSendNotification", "interval=$interval passedTime=$passedTime")
return false
}
......@@ -172,21 +186,26 @@ object MyNotificationManager {
* 值测某些类型
*/
private var testWhere = listOf(
POPUP_WHERE_TIMER
// POPUP_WHERE_TIMER,
// POPUP_WHERE_LOCK,
// POPUP_WHERE_ALARM,
POPUP_WHERE_WORK_MANAGER
)
private val looper_actionId = listOf(
ACTION_ID_SCAN,
ACTION_ID_BEAN_MEDIA,
)
private var actionIdList = arrayListOf<String>()
private fun getNextActionId(): String {
if (BuildConfig.DEBUG) {
return ACTION_ID_BEAN_MEDIA
}
if (actionIdList.isEmpty()) {
actionIdList.addAll(looper_actionId)
}
val next = actionIdList[0]
actionIdList.removeAt(0)
if (BuildConfig.DEBUG) {
return ACTION_ID_SCAN
}
return next
}
}
\ No newline at end of file
package com.base.appzxhy.push.notification
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Shader
import androidx.core.graphics.createBitmap
import com.base.appzxhy.bean.NotificationInfoBean
import com.base.appzxhy.helper.PushApiUtils
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.mbridge.msdk.thrid.okhttp.OkHttpClient
import com.mbridge.msdk.thrid.okhttp.Request
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.LinkedList
/**
* 处理推送要到bean类
*/
object NotificationBeanUtils {
var notificationQueue = LinkedList<NotificationInfoBean>()
private fun getNotificationInfoBeanList() {
try {
val gson = Gson()
val type = object : TypeToken<List<NotificationInfoBean>>() {}.type
val list = gson.fromJson<List<NotificationInfoBean>>(PushApiUtils.notificationInfoJson, type)
notificationQueue.addAll(list)
} catch (e: Exception) {
}
}
suspend fun nextNotificationInfoBean(): NotificationInfoBean? {
if (notificationQueue.isEmpty()) {
getNotificationInfoBeanList()
}
val bean = notificationQueue.poll()
bean?.bitmap = downloadAndApplyRoundedCorners(bean.imageUrl)
return bean
}
suspend fun downloadAndApplyRoundedCorners(url: String): Bitmap? {
return withContext(Dispatchers.IO) {
try {
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
return@withContext null
}
val inputStream: InputStream = response.body()!!.byteStream()
val bitmap = BitmapFactory.decodeStream(inputStream)
val roundedBitmap = getRoundedCornersBitmap(bitmap, 8f)
roundedBitmap
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
fun getRoundedCornersBitmap(source: Bitmap, radius: Float): Bitmap {
val width = source.width
val height = source.height
val bitmap = createBitmap(width, height)
val canvas = Canvas(bitmap)
val paint = Paint().apply {
isAntiAlias = true
setShader(BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP))
}
val path = Path()
val rect = RectF(0f, 0f, width.toFloat(), height.toFloat())
path.addRoundRect(rect, radius, radius, Path.Direction.CW)
path.close()
canvas.drawPath(path, paint)
return bitmap
}
}
\ No newline at end of file
package com.base.appzxhy.fcm.notification
package com.base.appzxhy.push.notification
import android.os.Handler
import android.os.HandlerThread
import com.base.appzxhy.MyApplication
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.config.PopupConfigBean.Companion.popupConfigBean
import com.base.appzxhy.utils.LogEx
/**
* 悬停
* 通知悬停
*/
object NotificationHoverUtils {
......@@ -46,7 +47,7 @@ object NotificationHoverUtils {
if (MyApplication.PAUSED_VALUE == 1) {
handler?.removeCallbacksAndMessages(null)
} else {
NotificationUiUtil.setNotification(sendBean)
NotificationUiUtil.sendCustomNotification(sendBean)
}
}, time)
}
......
package com.base.appzxhy.fcm.notification
package com.base.appzxhy.push.notification
import android.app.NotificationChannel
import android.app.NotificationManager
......@@ -11,8 +11,11 @@ import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.IconCompat
import com.base.appzxhy.R
import com.base.appzxhy.bean.NotificationInfoBean
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.ui.start.StartActivity
import com.base.appzxhy.bean.NotificationSendBean.Companion.ACTION_ID_BEAN_MEDIA
import com.base.appzxhy.bean.config.PopupConfigBean
import com.base.appzxhy.ui.splash.SplashActivity
import kotlin.random.Random
/**
......@@ -20,25 +23,49 @@ import kotlin.random.Random
*/
object NotificationUiUtil {
private val CHANNEL_ID = "video downloader saver Channel"
private val CHANNEL_NAME = "video downloader saver Channel NAME"
private val CHANNEL_ID = "San Qr Channel"
private val CHANNEL_NAME = "San Qr Channel NAME"
fun setNotification(sendBean: NotificationSendBean) {
/**
* 这部分准备UI的代码不能放入IO
*/
fun setNotification(sendBean: NotificationSendBean): NotificationSendBean {
val context = sendBean.context
when (sendBean.actionId) {
ACTION_ID_BEAN_MEDIA -> {
val bean = sendBean.valueMap.get("bean") as NotificationInfoBean
val smallRemoteViews = RemoteViews(context.packageName, R.layout.notification_small).apply {
setTextViewText(R.id.tvTitle, bean.title)
setTextViewText(R.id.tvContent, bean.content)
setImageViewBitmap(R.id.ivImage, bean.bitmap)
}
val bigRemoteViews =
RemoteViews(context.packageName, R.layout.notification_big).apply {
setTextViewText(R.id.tvTitle, bean.title)
setTextViewText(R.id.tvContent, bean.content)
setImageViewBitmap(R.id.ivImage, bean.bitmap)
}
sendBean.smallRemoteViews = smallRemoteViews
sendBean.bigRemoteViews = bigRemoteViews
}
}
val intent = Intent(sendBean.context, StartActivity::class.java)
if (PopupConfigBean.popupConfigBean.isDifferNotificationId) {
sendBean.notificationId = Random.nextInt(1000, 2000)
}
val intent = Intent(sendBean.context, SplashActivity::class.java)
val bean = sendBean.valueMap["bean"] as NotificationInfoBean
intent.putExtra("actionId", sendBean.actionId)
intent.putExtra("linkUrl", bean.linkUrl)
// intent.putExtra("notificationId", sendBean.notificationId)
sendBean.intent = intent
sendCustomNotification(sendBean)
return sendBean
}
private fun sendCustomNotification(
fun sendCustomNotification(
sendBean: NotificationSendBean
) {
val notificationManager = sendBean.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val context = sendBean.context
......@@ -60,7 +87,7 @@ object NotificationUiUtil {
//设置状态栏内的小图标
val smallIcon = IconCompat.createFromIcon(
context, Icon.createWithResource(
context, R.drawable.log_svg
context, R.mipmap.logo_r
)
)
smallIcon?.let {
......
package com.base.appzxhy.push.receiver
import android.annotation.SuppressLint
import android.app.AlarmManager
import android.app.PendingIntent
import android.app.job.JobService
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.base.appzxhy.BuildConfig
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.config.PopupConfigBean
import com.base.appzxhy.business.helper.EventUtils
import com.base.appzxhy.push.notification.MyNotificationManager
import com.base.appzxhy.push.work.RepeatingWorker
import com.base.appzxhy.utils.LogEx
import java.util.Calendar
/**
*
*/
class AlarmReceiver : BroadcastReceiver() {
private val TAG = "AlarmJobReceiver"
companion object {
fun startAlarm(context: Context, hour: Int) {
// context.startJob()
//闹钟定时任务
val alarmManager = context.getSystemService(JobService.ALARM_SERVICE) as AlarmManager
val pendingIntent = PendingIntent.getBroadcast(
context, hour, Intent(context, AlarmReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val calendar = Calendar.getInstance()
calendar.set(Calendar.HOUR_OF_DAY, 6)
calendar.set(Calendar.MINUTE, 0)
calendar.set(Calendar.SECOND, 0)
val oneHour = 60 * 60 * 1000L
var delay = oneHour * hour
if (BuildConfig.DEBUG) {
delay = 1 * 60 * 1000L
}
alarmManager.setRepeating(
AlarmManager.RTC_WAKEUP, calendar.timeInMillis, delay, pendingIntent
)
RepeatingWorker.Companion.schedulePeriodicWork(context)
}
}
@SuppressLint("UnsafeProtectedBroadcastReceiver")
override fun onReceive(context: Context, intent: Intent?) {
LogEx.logDebug(TAG, "AlarmJobReceiver onReceive")
EventUtils.event("alarm_push")
if (PopupConfigBean.popupConfigBean.alarmS) {
MyNotificationManager.submitSendBean(NotificationSendBean(context, NotificationSendBean.Companion.POPUP_WHERE_ALARM))
}
RepeatingWorker.Companion.schedulePeriodicWork(context)
}
}
\ No newline at end of file
package com.base.appzxhy.fcm.receiver
package com.base.appzxhy.push.receiver
import android.content.BroadcastReceiver
import android.content.Context
......@@ -8,11 +8,7 @@ import android.os.Build
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.NotificationSendBean.Companion.POPUP_WHERE_LOCK
import com.base.appzxhy.bean.config.PopupConfigBean.Companion.popupConfigBean
import com.base.appzxhy.fcm.notification.MyNotificationManager
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.utils.AppPreferences
import com.base.appzxhy.utils.KotlinExt.currentDate
import com.base.appzxhy.utils.LogEx
import com.base.appzxhy.push.notification.MyNotificationManager
import java.util.Objects
class ScreenStatusReceiver : BroadcastReceiver() {
......@@ -31,12 +27,7 @@ class ScreenStatusReceiver : BroadcastReceiver() {
isSecureLockActive = false
if (isDeviceInteractive && !isSecureLockActive) {
if (popupConfigBean.screenS) {
val sendBean = NotificationSendBean(
context,
POPUP_WHERE_LOCK,
canSend = { canScreenStatusReceiverPush() },
sendSuccess = { saveScreenPushedData() })
MyNotificationManager.submitSendBean(sendBean)
MyNotificationManager.submitSendBean(NotificationSendBean(context, POPUP_WHERE_LOCK))
}
}
}
......@@ -64,70 +55,5 @@ class ScreenStatusReceiver : BroadcastReceiver() {
}
}
/**
* 当前天推送数量
*/
private var todayScreenPush = 0
get() {
return AppPreferences.getInstance().getInt("todayScreenPush_${currentDate()}", field)
}
set(value) {
field = value
AppPreferences.getInstance().put("todayScreenPush_${currentDate()}", value, true)
}
/**
* 上次成功推送
*/
private var screenLastPushTime = 0L
get() {
return AppPreferences.getInstance().getLong("screenLastPushTime", field)
}
set(value) {
field = value
AppPreferences.getInstance().put("screenLastPushTime", value, true)
}
/**
* 解锁是否可推送
* 总的限制条件判断后才判断
*/
fun canScreenStatusReceiverPush(): Boolean {
if (!popupConfigBean.popupStatus) return false
val popupScreenCount = popupConfigBean.popupScreenCount
val flag1 = todayScreenPush <= popupScreenCount
val minute = 60 * 1000L
val interval = popupConfigBean.popupScreenInterval
val passTime = System.currentTimeMillis() - screenLastPushTime
val flag2 = screenLastPushTime == 0L || passTime > interval * minute
val flag = flag1 && flag2
if (!flag) {
EventUtils.event(
"Notification_Error", "todayScreenPush=$todayScreenPush " +
"popupScreenCount=$popupScreenCount where=$POPUP_WHERE_LOCK"
)
LogEx.logDebug(
"canSendNotification",
"Notification_Error todayScreenPush=$todayScreenPush " +
"popupScreenCount=$popupScreenCount where=$POPUP_WHERE_LOCK"
)
}
return flag
}
/**
* 推送成功后保存值
*/
fun saveScreenPushedData() {
todayScreenPush += 1
screenLastPushTime = System.currentTimeMillis()
}
}
}
\ No newline at end of file
package com.base.appzxhy.fcm.timer
package com.base.appzxhy.push.timer
import android.util.Log
import com.base.appzxhy.MyApplication
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.NotificationSendBean.Companion.POPUP_WHERE_TIMER
import com.base.appzxhy.bean.config.PopupConfigBean.Companion.popupConfigBean
import com.base.appzxhy.fcm.notification.MyNotificationManager
import com.base.appzxhy.fcm.receiver.ScreenStatusReceiver
import com.base.appzxhy.utils.LogEx.logDebug
import com.base.appzxhy.push.notification.MyNotificationManager
import com.base.appzxhy.push.receiver.ScreenStatusReceiver
import java.util.Timer
import java.util.TimerTask
......@@ -17,27 +17,28 @@ class TimerManager private constructor() {
private var taskTimer: Timer? = null
private var isTaskTimerActive: Boolean = false
fun createNewTask(): TimerTask {
val task: TimerTask = object : TimerTask() {
override fun run() {
Log.e(TAG, "TimerTask run")
// 确保设备处于交互状态,未锁定,且应用未暂停
val deviceRunning = (ScreenStatusReceiver.isDeviceInteractive &&
!ScreenStatusReceiver.isSecureLockActive &&
MyApplication.PAUSED_VALUE != 1)
if (deviceRunning || popupConfigBean.popupForegroundCanPush) {
MyNotificationManager.submitSendBean(NotificationSendBean(MyApplication.appContext, POPUP_WHERE_TIMER))
}
}
}
return task
}
fun scheduleTask(delay: Long, period: Long) {
logDebug(TAG, "scheduleTask")
synchronized(TimerManager::class.java) {
ensureTimerIsStopped() // 确保定时器未运行
taskTimer = Timer() // 创建新的 Timer 实例
val task: TimerTask = object : TimerTask() {
override fun run() {
logDebug(TAG, "scheduleTask run")
// 确保设备处于交互状态,未锁定,且应用未暂停
if (ScreenStatusReceiver.isDeviceInteractive
&& !ScreenStatusReceiver.isSecureLockActive
&& MyApplication.PAUSED_VALUE != 1
) {
logDebug(TAG, "scheduleTask send")
val sendBean =
NotificationSendBean(MyApplication.appContext, POPUP_WHERE_TIMER, canSend = { true }, sendSuccess = {})
MyNotificationManager.submitSendBean(sendBean)
}
}
}
taskTimer?.schedule(task, delay, period) // 调度任务
taskTimer?.schedule(createNewTask(), delay, period) // 调度任务
isTaskTimerActive = true // 设置定时器状态为活跃
}
}
......@@ -62,7 +63,6 @@ class TimerManager private constructor() {
private val instance: TimerManager by lazy((LazyThreadSafetyMode.SYNCHRONIZED)) { TimerManager() }
fun changeTimer() {
if (!popupConfigBean.timerS) {
......@@ -74,7 +74,7 @@ class TimerManager private constructor() {
val isTaskTimerActive = instance.isTaskTimerActive
if (!isTaskTimerActive) {
instance.scheduleTask(
(timerDelay * 60 * 1000).toLong(),
(timerDelay * 1000).toLong(),
(timerInterval * 60 * 1000).toLong()
)
}
......
package com.base.appzxhy.fcm.work
package com.base.appzxhy.push.work
import android.content.Context
import androidx.work.Constraints
......@@ -10,8 +10,9 @@ import androidx.work.Worker
import androidx.work.WorkerParameters
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.NotificationSendBean.Companion.POPUP_WHERE_WORK_MANAGER
import com.base.appzxhy.fcm.notification.MyNotificationManager
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.bean.config.PopupConfigBean
import com.base.appzxhy.push.notification.MyNotificationManager
import com.base.appzxhy.business.helper.EventUtils
import java.util.concurrent.TimeUnit
class RepeatingWorker(val appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) {
......@@ -21,8 +22,9 @@ class RepeatingWorker(val appContext: Context, workerParams: WorkerParameters) :
// 例如,更新UI,发送网络请求等
EventUtils.event("workManager_live")
try {
val sendBean = NotificationSendBean(appContext, POPUP_WHERE_WORK_MANAGER, canSend = { true }, sendSuccess = {})
MyNotificationManager.submitSendBean(sendBean)
if (PopupConfigBean.popupConfigBean.workS) {
MyNotificationManager.submitSendBean(NotificationSendBean(appContext, POPUP_WHERE_WORK_MANAGER))
}
} catch (e: Exception) {
EventUtils.event("WorkManager Error")
}
......
package com.base.appzxhy.service
package com.base.appzxhy.business.service
import android.app.NotificationManager
import android.app.job.JobInfo
......@@ -13,8 +13,8 @@ import android.os.Build
import android.os.CountDownTimer
import androidx.work.Configuration
import com.base.appzxhy.BuildConfig
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.service.StayNotification.createPermanentNotification
import com.base.appzxhy.business.helper.EventUtils
import com.base.appzxhy.business.service.StayNotification.createPermanentNotification
import com.base.appzxhy.utils.LogEx
......@@ -81,7 +81,7 @@ class StayJobService : JobService() {
private fun notifyForeground() {
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(
NOTIFICATION_STAY_ID,
......
package com.base.appzxhy.service
package com.base.appzxhy.business.service
import android.app.Notification
import android.app.NotificationChannel
......@@ -13,7 +13,7 @@ import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import androidx.core.graphics.drawable.IconCompat
import com.base.appzxhy.R
import com.base.appzxhy.service.StayJobService.Companion.NOTIFICATION_STAY_ID
import com.base.appzxhy.business.service.StayJobService.Companion.NOTIFICATION_STAY_ID
import com.base.appzxhy.ui.main.MainActivity
object StayNotification {
......
package com.base.appzxhy.drouterimp
import android.app.Activity
import android.content.Intent
import com.base.appzxhy.ui.main.MainActivity
import com.didi.drouter.annotation.Service
import com.koko.drouter.app.AppService
import java.util.Locale
@Service(function = [AppService::class])
class AppServiceImp : AppService {
override fun getCurrentSpLocal(): Locale {
return Locale.getDefault()
}
override fun changeActivityLanguage(activity: Activity, currentLocale: Locale, changeAction: () -> Unit): Boolean {
return false
}
override fun finishToMainTop(activity: Activity) {
val intent = Intent(activity, MainActivity::class.java)
intent.putExtra("where", "finishToMainTop")
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
activity.startActivity(intent)
activity.finish()
}
}
\ No newline at end of file
package com.base.appzxhy.drouterimp
import android.app.Activity
import com.base.appzxhy.helper.EventUtils
import com.didi.drouter.annotation.Service
import com.koko.drouter.app.AppService
import com.koko.drouter.event.EventService
@Service(function = [EventService::class])
class EventServiceImp : EventService {
override fun eventActivity(activity: Activity) {
super.eventActivity(activity)
EventUtils.event("page_${javaClass.simpleName}")
}
}
\ No newline at end of file
package com.base.appzxhy.drouterimp
import android.app.Activity
import com.base.appzxhy.ads.AdsMgr
import com.base.appzxhy.ads.AdsShowCallBack
import com.base.appzxhy.bean.config.AdConfigBean
import com.didi.drouter.annotation.Service
import com.koko.drouter.ad.InterAdService
@Service(function = [InterAdService::class])
class InterAdServiceImp : InterAdService {
override fun showInterAd(activity: Activity, interWhere: String, callBack: () -> Unit) {
if (AdConfigBean.adsConfigBean.functionInShowAd) {
AdsMgr.showInsert(activity, object : AdsShowCallBack() {
override fun next() {
callBack.invoke()
}
})
} else {
callBack.invoke()
}
}
override fun showInterAdBack(activity: Activity, interWhere: String, callBack: () -> Unit) {
if (AdConfigBean.adsConfigBean.functionBackShowAd) {
AdsMgr.showInsert(activity, object : AdsShowCallBack() {
override fun next() {
callBack.invoke()
}
})
} else {
callBack.invoke()
}
}
}
\ No newline at end of file
package com.base.appzxhy.drouterimp
import android.app.Activity
import android.widget.FrameLayout
import com.base.appzxhy.R
import com.base.appzxhy.ads.AdsMgr
import com.base.appzxhy.utils.LogEx
import com.didi.drouter.annotation.Service
import com.koko.drouter.ad.NativeBatteryType
import com.koko.drouter.ad.NativeAdService
@Service(function = [NativeAdService::class])
class NativeAdServiceImp : NativeAdService {
private val TAG = "NativeServiceImp"
override fun showNative(activity: Activity, flAd: FrameLayout, nativeType: String) {
LogEx.logDebug(TAG, "activity=${activity::class.java.simpleName}")
val admobLayout = when (nativeType) {
NativeBatteryType -> R.layout.layout_admob_native_custom
else -> R.layout.layout_admob_native_custom
}
val maxLayout = R.layout.layout_max_native_custom
// AdsMgr.showNative(activity, flAd, admobLayout, maxLayout)
}
}
\ No newline at end of file
package com.base.appzxhy.fcm.alarm
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.NotificationSendBean.Companion.POPUP_WHERE_ALARM
import com.base.appzxhy.fcm.notification.MyNotificationManager
import com.base.appzxhy.fcm.work.RepeatingWorker.Companion.schedulePeriodicWork
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.utils.LogEx
/**
*
*/
class AlarmReceiver : BroadcastReceiver() {
private val TAG = "AlarmJobReceiver"
@SuppressLint("UnsafeProtectedBroadcastReceiver")
override fun onReceive(context: Context, intent: Intent?) {
LogEx.logDebug(TAG, "AlarmJobReceiver onReceive")
EventUtils.event("alarm_push")
val sendBean = NotificationSendBean(context, POPUP_WHERE_ALARM, canSend = { true }, sendSuccess = {})
MyNotificationManager.submitSendBean(sendBean)
schedulePeriodicWork(context)
return
}
}
\ No newline at end of file
import android.app.AlarmManager
import android.app.PendingIntent
import android.app.job.JobService
import android.content.Context
import android.content.Intent
import com.base.appzxhy.BuildConfig
import com.base.appzxhy.fcm.alarm.AlarmReceiver
import com.base.appzxhy.fcm.work.RepeatingWorker.Companion.schedulePeriodicWork
import java.util.Calendar
object AlarmUtils {
fun startAlarm(context: Context, hour: Int) {
// context.startJob()
//闹钟定时任务
val alarmManager = context.getSystemService(JobService.ALARM_SERVICE) as AlarmManager
val pendingIntent = PendingIntent.getBroadcast(
context, hour, Intent(context, AlarmReceiver::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val calendar = Calendar.getInstance()
calendar.set(Calendar.HOUR_OF_DAY, 6)
calendar.set(Calendar.MINUTE, 0)
calendar.set(Calendar.SECOND, 0)
val oneHour = 60 * 60 * 1000L
var delay = oneHour * hour
if (BuildConfig.DEBUG) {
delay = 1 * 60 * 1000L
}
alarmManager.setRepeating(
AlarmManager.RTC_WAKEUP, calendar.timeInMillis, delay, pendingIntent
)
schedulePeriodicWork(context)
}
}
\ No newline at end of file
package com.base.appzxhy.fcm.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.NotificationSendBean.Companion.POPUP_WHERE_BATTERY
import com.base.appzxhy.bean.config.PopupConfigBean.Companion.popupConfigBean
import com.base.appzxhy.fcm.notification.MyNotificationManager
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.utils.AppPreferences
import com.base.appzxhy.utils.KotlinExt.currentDate
import com.base.appzxhy.utils.LogEx
import kotlin.math.absoluteValue
/**
*电量监听
*/
class BatteryStatusReceiver() : BroadcastReceiver() {
companion object {
private val TAG = "BatteryStatusReceiver"
fun registerBatteryStatusReceiver(context: Context) {
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_BATTERY_CHANGED)
addAction(Intent.ACTION_POWER_CONNECTED)
addAction(Intent.ACTION_POWER_DISCONNECTED)
}
val applicationContext = context.applicationContext
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
applicationContext.registerReceiver(
BatteryStatusReceiver(),
intentFilter,
Context.RECEIVER_EXPORTED
)
} else {
applicationContext.registerReceiver(BatteryStatusReceiver(), intentFilter)
}
}
/**
* 当前天推送数量
*/
private var todayBatteryPush = 0
get() {
return AppPreferences.getInstance().getInt("todayBatteryPush_${currentDate()}", field)
}
set(value) {
field = value
AppPreferences.getInstance().put("todayBatteryPush_${currentDate()}", value, true)
}
/**
* 上次成功推送
*/
private var batteryLastPushTime = 0L
get() {
return AppPreferences.getInstance().getLong("batteryLastPushTime", field)
}
set(value) {
field = value
AppPreferences.getInstance().put("batteryLastPushTime", value, true)
}
/**
* 电池电量是否可推送
* 总的限制条件判断后才判断
*/
fun canBatteryStatusReceiverPush(): Boolean {
if (!popupConfigBean.batteryS) return false
val popupBatteryCount = popupConfigBean.popupBatteryValue
val flag1 = todayBatteryPush <= popupBatteryCount
val minute = 60 * 1000L
val interval = popupConfigBean.popupBatteryInterval
val passTime = System.currentTimeMillis() - batteryLastPushTime
val flag2 = batteryLastPushTime == 0L || passTime > interval * minute
val flag = flag1 && flag2
if (!flag) {
EventUtils.event(
"Notification_Error", "todayBatteryPush=$todayBatteryPush " +
"popupBatteryCount=$popupBatteryCount where=$POPUP_WHERE_BATTERY"
)
LogEx.logDebug(
"canSendNotification", "Notification_Error todayBatteryPush=$todayBatteryPush " +
"popupBatteryCount=$popupBatteryCount where=$POPUP_WHERE_BATTERY"
)
}
return flag
}
/**
* 推送成功后保存值
*/
fun saveBatteryPushedData() {
todayBatteryPush += 1
batteryLastPushTime = System.currentTimeMillis()
}
}
private var currentBatteryPercentage = 0f
override fun onReceive(context: Context, intent: Intent?) {
val action = intent?.action
when (action) {
Intent.ACTION_BATTERY_CHANGED -> {
val batteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val batteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
val batteryPercentage = (batteryLevel / batteryScale.toFloat()) * 100
//避免频繁触发
val changeValue = currentBatteryPercentage - batteryPercentage
if (batteryPercentage < popupConfigBean.popupBatteryValue && changeValue.absoluteValue >= 1f) {
//推送次数没有达到限制并且展示的最小时间间隔大于配置时间(分钟)
LogEx.logDebug(TAG, "onReceive changed")
val sendBean = NotificationSendBean(context, POPUP_WHERE_BATTERY,
canSend = {
canBatteryStatusReceiverPush()
},
sendSuccess = {
saveBatteryPushedData()
})
MyNotificationManager.submitSendBean(sendBean)
}
currentBatteryPercentage = batteryPercentage
}
Intent.ACTION_POWER_CONNECTED -> {
val sendBean = NotificationSendBean(context, POPUP_WHERE_BATTERY,
canSend = {
canBatteryStatusReceiverPush()
},
sendSuccess = {
saveBatteryPushedData()
})
MyNotificationManager.submitSendBean(sendBean)
}
}
}
}
\ No newline at end of file
package com.base.appzxhy.fcm.receiver
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.NotificationSendBean.Companion.POPUP_WHERE_FILE_JOB
import com.base.appzxhy.fcm.notification.MyNotificationManager
class FileJobReceiver : BroadcastReceiver() {
@SuppressLint("UnsafeProtectedBroadcastReceiver")
override fun onReceive(context: Context?, intent: Intent?) {
// context?.startJob()
context?.let {
val sendBean = NotificationSendBean(context, POPUP_WHERE_FILE_JOB, canSend = { true }, sendSuccess = {})
MyNotificationManager.submitSendBean(sendBean)
}
}
}
\ No newline at end of file
package com.base.appzxhy.fcm.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import com.base.appzxhy.bean.NotificationSendBean
import com.base.appzxhy.bean.NotificationSendBean.Companion.POPUP_WHERE_PACKAGE
import com.base.appzxhy.bean.config.PopupConfigBean.Companion.popupConfigBean
import com.base.appzxhy.fcm.notification.MyNotificationManager
import com.base.appzxhy.helper.EventUtils
import com.base.appzxhy.utils.AppPreferences
import com.base.appzxhy.utils.KotlinExt.currentDate
import com.base.appzxhy.utils.LogEx
class PackageStatusReceiver() : BroadcastReceiver() {
companion object {
private val TAG = "PackageStatusReceiver"
fun registerPackageStatusReceiver(context: Context) {
LogEx.logDebug(TAG, "registerPackageStatusReceiver")
val intentFilter = IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_ADDED)
addAction(Intent.ACTION_PACKAGE_REMOVED)
addDataScheme("package")
}
val applicationContext = context.applicationContext
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
applicationContext.registerReceiver(
PackageStatusReceiver(),
intentFilter,
Context.RECEIVER_EXPORTED
)
} else {
applicationContext.registerReceiver(PackageStatusReceiver(), intentFilter)
}
}
/**
* 当前天推送数量
*/
private var todayPackagePush = 0
get() {
return AppPreferences.getInstance().getInt("todayPackagePush_${currentDate()}", field)
}
set(value) {
field = value
AppPreferences.getInstance().put("todayPackagePush_${currentDate()}", value, true)
}
/**
* 上次成功推送
*/
private var packageLastPushTime = 0L
get() {
return AppPreferences.getInstance().getLong("packageLastPushTime", field)
}
set(value) {
field = value
AppPreferences.getInstance().put("packageLastPushTime", value, true)
}
/**
* 安装卸载是否可推送
* 总的限制条件判断后才判断
*/
fun canPackageStatusReceiverPush(): Boolean {
if (!popupConfigBean.packageS) return false
val popupPackageCount = popupConfigBean.popupPackageCount
val flag1 = todayPackagePush <= popupPackageCount
val minute = 60 * 1000L
val interval = popupConfigBean.popupPackageInterval
val passTime = System.currentTimeMillis() - packageLastPushTime
val flag2 = packageLastPushTime == 0L || passTime > interval * minute
val flag = flag1 && flag2
if (!flag) {
EventUtils.event(
"Notification_Error", "todayPackagePush=$todayPackagePush " +
"popupPackageCount=$popupPackageCount where=$POPUP_WHERE_PACKAGE"
)
LogEx.logDebug(
"canSendNotification", "Notification_Error todayPackagePush=$todayPackagePush " +
"popupPackageCount=$popupPackageCount where=$POPUP_WHERE_PACKAGE"
)
}
return flag
}
/**
* 推送成功后保存值
*/
fun savePackagePushedData() {
todayPackagePush += 1
packageLastPushTime = System.currentTimeMillis()
}
}
override fun onReceive(context: Context, intent: Intent?) {
val action = intent?.action
LogEx.logDebug(TAG, "onReceive action=$action")
if (action == Intent.ACTION_PACKAGE_ADDED || action == Intent.ACTION_PACKAGE_REMOVED) {
val sendBean = NotificationSendBean(context, POPUP_WHERE_PACKAGE, canSend = {
canPackageStatusReceiverPush()
}, sendSuccess = {
savePackagePushedData()
})
MyNotificationManager.submitSendBean(sendBean)
}
}
}
\ No newline at end of file
package com.base.appzxhy.utils
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
class VpnStatusListener(context: Context) {
private val TAG = "VpnStatusListener"
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private var isListening = false
private var currentVpnStatus: Boolean = false
var actionVpn: ((isVip: Boolean) -> Unit)? = null
companion object {
var vpnFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
}
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
val caps = connectivityManager.getNetworkCapabilities(network)
checkVpnStatus(caps)
}
override fun onLost(network: Network) {
super.onLost(network)
val caps = connectivityManager.getNetworkCapabilities(network)
checkVpnStatus(caps)
}
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
super.onCapabilitiesChanged(network, networkCapabilities)
checkVpnStatus(networkCapabilities)
}
}
fun startListen() {
if (!isListening) {
val networkRequest = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build()
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
isListening = true
}
}
fun stopListen() {
if (isListening) {
connectivityManager.unregisterNetworkCallback(networkCallback)
isListening = false
}
}
private fun checkVpnStatus(networkCapabilities: NetworkCapabilities?) {
val isVpn = networkCapabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true
// 这里可以根据isVpn的值进行相应处理,比如更新UI等
if (currentVpnStatus != isVpn) {
currentVpnStatus = isVpn
Log.e(TAG, "VpnStatusListener =$isVpn")
actionVpn?.invoke(isVpn)
vpnFlow.update { isVpn }
}
}
}
\ No newline at end of file
......@@ -9,9 +9,3 @@ plugins {
id("com.google.devtools.ksp") version "1.9.24-1.0.20" apply false
}
buildscript {
dependencies {
classpath(libs.drouter.plugin)
}
}
\ No newline at end of file
......@@ -54,12 +54,6 @@ applovin_facebook = { group = "com.applovin.mediation", name = "facebook-adapter
applovin_mintegral = { group = "com.applovin.mediation", name = "mintegral-adapter", version.ref = "mintegral" }
applovin_pangle = { group = "com.applovin.mediation", name = "bytedance-adapter", version.ref = "pangle" }
drouter-api = { module = "io.github.didi:drouter-api", version.ref = "drouterApi" }
drouter-plugin = { module = "io.github.didi:drouter-plugin", version.ref = "drouterPlugin" }
drouter-api-page = { module = "io.github.didi:drouter-api-page", version.ref = "drouterApiPage" }
drouter-api-process = { module = "io.github.didi:drouter-api-process", version.ref = "drouterApiProcess" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
......
......@@ -28,5 +28,3 @@ dependencyResolutionManagement {
rootProject.name = "appzxhy"
include(":app")
include(":Drouter")
include(":BatteryInfo")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment