Compare commits

..

10 Commits

Author SHA1 Message Date
Administrator 727df6f21f 改机参数新增 2025-07-12 14:27:05 +08:00
Administrator e2800ffe8e 改机参数新增 2025-07-12 10:23:59 +08:00
Administrator 0239820321 . 2025-07-10 18:02:55 +08:00
Administrator 0e252b3751 . 2025-07-10 16:19:47 +08:00
Administrator 5bcecde83d . 2025-07-09 18:53:24 +08:00
Administrator 5772b91b9a . 2025-07-09 18:24:08 +08:00
Administrator ce0933ee94 . 2025-07-09 09:39:06 +08:00
Administrator fc62906c0e . 2025-07-08 22:16:46 +08:00
Administrator b504984268 . 2025-07-05 15:45:37 +08:00
Administrator 2ed86fb2b7 . 2025-07-05 14:56:11 +08:00
59 changed files with 1481 additions and 504 deletions

View File

@ -4,6 +4,14 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-07-07T06:37:35.266033300Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=8.217.74.194:8924;connection=1af33e42" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

View File

@ -1,21 +1,47 @@
# Add project specific ProGuard rules here. -keep class com.google.gson.** { *; }
# You can control the set of applied configuration files using the -keep class com.google.gson.stream.** { *; }
# 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 -keepattributes *Annotation*
# class: -keepattributes Signature
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for # 保留枚举类
# debugging stack traces. -keepclassmembers enum * {
#-keepattributes SourceFile,LineNumberTable public static **[] values();
public static ** valueOf(java.lang.String);
}
# If you keep the line number information, uncomment this to # 保留所有模型类(根据你的包结构调整)
# hide the original source file name. -keep class com.android.grape.pad.** { *; }
#-renamesourcefileattribute SourceFile -keep class com.android.grape.net.ApiResponse{ *; }
-keep class com.android.grape.net.ApiResponseList{ *; }
# 保留所有使用 @SerializedName 注解的字段
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# 保留所有模型类的无参构造函数
-keepclassmembers class com.android.grape.pad.** {
public <init>();
}
# 保留类型适配器
-keep class * extends com.google.gson.TypeAdapter {
public com.google.gson.TypeAdapter create(com.google.gson.Gson, com.google.gson.reflect.TypeToken);
}
# 保留 Gson 创建的类
-keep class com.google.gson.examples.android.model.** { *; }
-keepattributes Signature
# 保留 TypeToken 类及其子类
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
-keep class sun.misc.Unsafe { *; }
# 保留注解信息
-keepattributes *Annotation*
# 保留 Kotlin 元数据(如果使用 Kotlin
-keepclassmembers class **$TypeToken { *; }

View File

@ -9,8 +9,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.BIND_VPN_SERVICE" <uses-permission android:name="android.permission.BIND_VPN_SERVICE"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VPN_SERVICE" /> <uses-permission android:name="android.permission.VPN_SERVICE" />
@ -49,6 +48,11 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.AndroidGrape" android:theme="@style/Theme.AndroidGrape"
tools:targetApi="31"> tools:targetApi="31">
<provider
android:name=".provider.DeviceInfoProvider"
android:authorities="com.android.grape.deviceinfo.provider"
android:exported="true">
</provider>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true">

View File

@ -3,6 +3,7 @@ package com.android.grape
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
@ -11,11 +12,19 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import com.android.grape.databinding.ActivityMainBinding import com.android.grape.databinding.ActivityMainBinding
import com.android.grape.job.MonitorService
import com.android.grape.provider.DeviceDataAccessor
import com.android.grape.provider.DeviceInfoHelper
import com.android.grape.util.ClashUtil import com.android.grape.util.ClashUtil
import com.android.grape.util.MockTools
import com.android.grape.util.NotificationPermissionHandler import com.android.grape.util.NotificationPermissionHandler
import com.android.grape.util.ScriptUtil import com.android.grape.util.ScriptUtil
import com.android.grape.util.ShellUtils
import com.android.grape.util.StoragePermissionHelper import com.android.grape.util.StoragePermissionHelper
import com.blankj.utilcode.util.LogUtils import com.android.grape.util.Util
import com.android.grape.util.Util.AUTO_JSPACKAGENAME
import com.android.grape.util.Util.killRecordProcess
import com.google.gson.Gson
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<MainViewModel>() private val viewModel by viewModels<MainViewModel>()
@ -24,6 +33,7 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
init()
viewBinding = ActivityMainBinding.inflate(layoutInflater) viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root) setContentView(viewBinding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
@ -34,28 +44,23 @@ class MainActivity : AppCompatActivity() {
checkPermission() checkPermission()
ScriptUtil.registerScriptResultReceiver() ScriptUtil.registerScriptResultReceiver()
viewBinding.start.setOnClickListener { viewBinding.start.setOnClickListener {
try { MonitorService.onEvent(MainApplication.instance)
ClashUtil.startProxy(this) // DeviceDataAccessor.saveDeviceInfo(this, DeviceInfoHelper.getDeviceId(this))
ClashUtil.switchProxyGroup(
"GLOBAL",
ClashUtil.getRandomLocale(),
"http://127.0.0.1:6170"
)
} catch (e: Exception) {
LogUtils.e("startProxyVpn: Failed to start VPN", e)
Toast.makeText(
this,
"Failed to start VPN: " + (if (e.message != null) e.message else "Unknown error"),
Toast.LENGTH_SHORT
).show()
}
} }
viewBinding.stop.setOnClickListener { viewBinding.stop.setOnClickListener {
ClashUtil.stopProxy(this) killRecordProcess(this, packageName)
// AutoJsUtil.stopAutojsScript(this) // DeviceDataAccessor.deleteDeviceInfo(this, DeviceInfoHelper.getDeviceId(this))
// val deviceInfo = DeviceDataAccessor.getDeviceInfo(this, DeviceInfoHelper.getDeviceId(this))
// Log.d("TAG", "onCreate: ${Gson().toJson(deviceInfo?:"")}")
} }
} }
fun init() {
MockTools.exec("pm grant com.android.grape android.permission.INTERACT_ACROSS_USERS")
MockTools.exec("pm grant com.android.grape android.permission.WRITE_SECURE_SETTINGS")
MockTools.exec("pm setenforce 0")
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
ClashUtil.unregisterReceiver(this) ClashUtil.unregisterReceiver(this)

View File

@ -3,6 +3,7 @@ package com.android.grape
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.work.PeriodicWorkRequest import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager import androidx.work.WorkManager
import com.android.grape.job.MonitorService
import com.android.grape.service.MyAccessibilityService import com.android.grape.service.MyAccessibilityService
import com.android.grape.work.CheckAccessibilityWorker import com.android.grape.work.CheckAccessibilityWorker
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -21,6 +22,5 @@ class MainViewModel:ViewModel() {
.build() .build()
WorkManager.getInstance(MainApplication.instance).enqueue(workRequest) WorkManager.getInstance(MainApplication.instance).enqueue(workRequest)
} }
} }
} }

View File

@ -1,19 +0,0 @@
package com.android.grape.data
class AfInfo {
var advertiserId: String? = null
var model: String? = null
var brand: String? = null
var androidId: String? = null
var xPixels: Int = 0
var yPixels: Int = 0
var densityDpi: Int = 0
var country: String? = null
var batteryLevel: String? = null
var stackInfo: String? = null
var product: String? = null
var network: String? = null
var langCode: String? = null
var cpuAbi: String? = null
var yDp: Long = 0
}

View File

@ -1,21 +0,0 @@
package com.android.grape.data
class BigoInfo {
var cpuClockSpeed: String? = null
var gaid: String? = null
var userAgent: String? = null
var osLang: String? = null
var osVer: String? = null
var tz: String? = null
var systemCountry: String? = null
var simCountry: String? = null
var romFreeIn: Long = 0
var resolution: String? = null
var vendor: String? = null
var batteryScale: Int = 0
var net: String? = null
var dpi: Long = 0
var romFreeExt: Long = 0
var dpiF: String? = null
var cpuCoreNum: Long = 0
}

View File

@ -2,6 +2,8 @@ package com.android.grape.data
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import org.json.JSONArray
import org.json.JSONObject
data class Device( data class Device(
var advertiserId: String = "", var advertiserId: String = "",
@ -110,4 +112,32 @@ data class Device(
var vendingVersionName: String = "", var vendingVersionName: String = "",
@SerializedName("web_ua") @SerializedName("web_ua")
var webUa: String = "" var webUa: String = ""
) ){
fun toJson(): String {
return JSONObject().apply {
put("sensors", JSONArray(sensor))
put("arch", arch)
put("cpu_abi", cpuAbi)
put("cpu_abi2", cpuAbi2)
put("size", dim.size)
put("xdp", dim.xdp)
put("ydp", dim.ydp)
put("y_px", dim.yPx)
put("x_px", dim.xPx)
put("d_dpi", dim.dDpi)
put("btch", btch)
put("btl", batteryLevel)
put("disk", disk)
put("sdk", sdk)
put("network", network)
put("api_ver", vendingVersionCode)
put("api_ver_name", vendingVersionName)
put("carrier", carrier)
put("product", product)
put("last_boot_time", lastBootTime)
put("install_source_info", installerPackage)
put("advertiserId", advertiserId)
}.toString()
}
}

View File

@ -1,25 +0,0 @@
package com.android.grape.data
class DeviceInfo {
var lang: String? = null
var roProductBrand: String? = null
var roProductModel: String? = null
var roProductManufacturer: String? = null
var roProductDevice: String? = null
var roProductName: String? = null
var roBuildVersionIncremental: String? = null
var roBuildFingerprint: String? = null
var roOdmBuildFingerprint: String? = null
var roProductBuildFingerprint: String? = null
var roSystemBuildFingerprint: String? = null
var roSystemExtBuildFingerprint: String? = null
var roVendorBuildFingerprint: String? = null
var roBuildPlatform: String? = null
var persistSysCloudDrmId: String? = null
var persistSysCloudBatteryCapacity: Int = 0
var persistSysCloudGpuGlVendor: String? = null
var persistSysCloudGpuGlRenderer: String? = null
var persistSysCloudGpuGlVersion: String? = null
var persistSysCloudGpuEglVendor: String? = null
var persistSysCloudGpuEglVersion: String? = null
}

View File

@ -9,4 +9,6 @@ data class Sensor(
var sV: String = "", var sV: String = "",
var sVE: List<Double> = listOf(), var sVE: List<Double> = listOf(),
var sVS: List<Double> = listOf() var sVS: List<Double> = listOf()
) ) {
companion object
}

View File

@ -15,7 +15,7 @@ class AutoJobService : JobIntentService() {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
ScriptUtil.execScript( ScriptUtil.execScript(
this@AutoJobService, this@AutoJobService,
"/sdcard/autojs/main.js" "/sdcard/script/main.js"
) )
}, 2000L) }, 2000L)
} else { } else {

View File

@ -23,8 +23,8 @@ class DownloadAppJobService : JobIntentService() {
if (succ) { if (succ) {
errTime = 0L errTime = 0L
StartVpnServerJobService.onEvent(this) // InstallService.onEvent(this)
// StartVpnPortJobService.onEvent(this) StartVpnPortJobService.onEvent(this)
} else { } else {
Util.isClickRet = false Util.isClickRet = false
Util.setInstallRet(false) Util.setInstallRet(false)

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
import androidx.core.app.JobIntentService import androidx.core.app.JobIntentService
import com.android.grape.net.ChangeCallBack
import com.android.grape.util.ChangeDeviceInfoUtil import com.android.grape.util.ChangeDeviceInfoUtil
import com.android.grape.util.MockTools import com.android.grape.util.MockTools
import com.android.grape.util.ServiceUtils import com.android.grape.util.ServiceUtils
@ -23,9 +24,9 @@ class OpenAppService : JobIntentService() {
ServiceUtils.setEnableApp("com.android.chrome", true) ServiceUtils.setEnableApp("com.android.chrome", true)
ServiceUtils.setEnableApp("com.UCMobile", true) ServiceUtils.setEnableApp("com.UCMobile", true)
if (Util.isCanAuto && Util.canAutoLc.isNotEmpty()) { if (Util.isCanAuto && Util.canAutoLc.isNotEmpty()) {
MockTools.exec("pm grant org.autojs.autojs android.permission.READ_EXTERNAL_STORAGE") //sdcard权限 MockTools.exec("pm grant ${Util.AUTO_JSPACKAGENAME} android.permission.READ_EXTERNAL_STORAGE") //sdcard权限
MockTools.exec("pm grant org.autojs.autojs android.permission.SYSTEM_ALERT_WINDOW") //悬浮窗权限 MockTools.exec("pm grant ${Util.AUTO_JSPACKAGENAME} android.permission.SYSTEM_ALERT_WINDOW") //悬浮窗权限
MockTools.exec("settings put secure enabled_accessibility_services org.autojs.autojs/com.stardust.autojs.core.accessibility.AccessibilityService") MockTools.exec("settings put secure enabled_accessibility_services ${Util.AUTO_JSPACKAGENAME}/${Util.AUTO_JSPACKAGENAME}.core.accessibility.AccessibilityService")
Util.doScript(this, Util.AUTO_JSPACKAGENAME) //autojs Util.doScript(this, Util.AUTO_JSPACKAGENAME) //autojs
} }
try { try {
@ -37,13 +38,21 @@ class OpenAppService : JobIntentService() {
// Log.d("IOSTQ:", "执行新装任务") // Log.d("IOSTQ:", "执行新装任务")
// Util.setInfo(this) // Util.setInfo(this)
// } // }
ChangeDeviceInfoUtil.changeDeviceInfo() // ChangeDeviceInfoUtil.changeDeviceInfo()
ChangeDeviceInfoUtil.changeDevice(callBack = object : ChangeCallBack {
override fun changeSuccess() {
Util.openRecordApp(this@OpenAppService)
}
override fun changeFailed() {
Util.setFinish(this@OpenAppService)
}
})
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
} }
// Util.hookOpenApp(this); // Util.hookOpenApp(this);
Util.openRecordApp(this)
} }
companion object { companion object {

View File

@ -6,13 +6,17 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.core.app.JobIntentService import androidx.core.app.JobIntentService
import com.android.grape.net.MyGet
import com.android.grape.util.ClashUtil import com.android.grape.util.ClashUtil
import com.android.grape.util.ClashUtil.getProxyPort
import com.android.grape.util.Util import com.android.grape.util.Util
import java.util.Locale
class StartVpnPortJobService : JobIntentService() { class StartVpnPortJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) { override fun onHandleWork(intent: Intent) {
Log.i(TAG, "start to handle work") Log.i(TAG, "start to handle work")
if (ClashUtil.checkProxy( this)) { ClashUtil.switchProxyGroup("PROXY", "DIRECT", "http://127.0.0.1:6170")
if (exec()) {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
StartVpnServerJobService.onEvent( StartVpnServerJobService.onEvent(
this@StartVpnPortJobService this@StartVpnPortJobService
@ -25,6 +29,28 @@ class StartVpnPortJobService : JobIntentService() {
} }
} }
private fun exec(): Boolean {
try {
val port = getProxyPort()
var nRetryCount = 0
do {
val url =
"http://39.103.73.250/tt/test/testProxy.jsp?port=$port&country=" + Util.proxyCountry
?.uppercase(Locale.getDefault())
val result: String = MyGet.get(url)
Log.d(TAG, "request url == $url result$result")
if (result.contains("ok")) {
return true
}
} while (nRetryCount++ < 3)
return false
} catch (err: Exception) {
err.printStackTrace()
return false
}
}
companion object { companion object {
private const val TAG = "IOSTQ:StartVpnPort" private const val TAG = "IOSTQ:StartVpnPort"

View File

@ -8,6 +8,7 @@ import android.util.Log
import androidx.core.app.JobIntentService import androidx.core.app.JobIntentService
import com.android.grape.MainApplication import com.android.grape.MainApplication
import com.android.grape.util.ClashUtil import com.android.grape.util.ClashUtil
import com.android.grape.util.CountryCode
import com.android.grape.util.Util import com.android.grape.util.Util
/** /**
@ -27,19 +28,12 @@ class StartVpnServerJobService : JobIntentService() {
onEvent( onEvent(
this@StartVpnServerJobService this@StartVpnServerJobService
) )
}, 1000L) }, 2000L)
} }
} }
private fun exec(): Boolean { private fun exec(): Boolean {
ClashUtil.startProxy(MainApplication.instance) return ClashUtil.switchProxyGroup("PROXY", "my-socks5-proxy", "http://127.0.0.1:6170")
ClashUtil.switchProxyGroup("GLOBAL", Util.proxyCountry, "http://127.0.0.1:6170")
if (!ClashUtil.checkProxy(MainApplication.instance)) {
println("IOSTQ:start vpn error")
return false
}
println("IOSTQ:start vpn ok")
return true
} }
companion object { companion object {

View File

@ -0,0 +1,70 @@
package com.android.grape.net
import android.util.Log
import com.android.grape.pad.Pad
import com.android.grape.pad.PadTask
import com.android.grape.pad.TaskDetail
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import org.json.JSONArray
import org.json.JSONObject
object Api {
private const val TAG = "Api"
const val UPDATE_PAD: String = "/openapi/open/pad/updatePadProperties"
const val PAD_DETAIL: String = "/openapi/open/pad/padDetails"
const val PAD_TASK: String = "/task-center/open/task/padTaskDetail"
fun padDetail(): ApiResponse<Pad>{
val param = Gson().toJson(mapOf(
"page" to 1,
"rows" to 10,
"padCode" to listOf("ACP250702PWJCTLF")
))
val (response, code) = HttpUtils.postSync(PAD_DETAIL, param)
if (code == 200) {
Log.d("padDetail", response.toString())
var apiResponse: ApiResponse<Pad> = Gson().fromJson(response, object : TypeToken<ApiResponse<Pad>>() {}.type)
return apiResponse
} else {
Log.d("padDetail", "error: $code")
return ApiResponse(code, "error", 0, null)
}
}
fun updatePad(params: String): ApiResponseList<PadTask>{
val (response, code) = HttpUtils.postSync(UPDATE_PAD, params)
if (code == 200) {
Log.d("updatePad", response.toString())
var apiResponse: ApiResponseList<PadTask> = Gson().fromJson(response, object : TypeToken<ApiResponseList<PadTask>>() {}.type)
return apiResponse
} else {
Log.d("updatePad", "error: $code")
return ApiResponseList(code, "error", 0, emptyList())
}
}
fun padTaskDetail(taskId: Int): Int{
val jsonString = JSONObject().apply {
put("taskIds", JSONArray().apply {
put(taskId)
})
}.toString()
Log.d(TAG, "padTaskDetail: $jsonString")
val (response, code) = HttpUtils.postSync(PAD_TASK, jsonString)
if (code == 200) {
Log.d("padTaskDetail", response.toString())
var apiResponse: ApiResponseList<TaskDetail> = Gson().fromJson(response, object : TypeToken<ApiResponseList<TaskDetail>>() {}.type)
val taskList = apiResponse.data
if (apiResponse.isSuccess() && taskList!= null && taskList.isNotEmpty()){
val task = taskList[0]
return task.taskStatus
}
return -1
}else {
Log.d("padTaskDetail", "error: $code")
return -1
}
}
}

View File

@ -0,0 +1,23 @@
package com.android.grape.net
data class ApiResponse<T>(
val code: Int,
val message: String,
val ts: Long,
var data: T? = null
){
fun isSuccess(): Boolean {
return code == 200
}
}
data class ApiResponseList<T>(
val code: Int,
val message: String,
val ts: Long,
var data: List<T>? = null
){
fun isSuccess(): Boolean {
return code == 200
}
}

View File

@ -0,0 +1,39 @@
package com.android.grape.net
import android.util.Log
import java.nio.charset.StandardCharsets
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
object ArmCloudSignatureV2 {
const val ALGORITHM: String = "HmacSHA256"
const val SECRET_KEY: String = "gz8f1u0t63byzdu6ozbx8r5qs3e5lipt"
const val SECRET_ID: String = "3yc8c8bg1dym0zaiwjh867al"
@Throws(Exception::class)
fun calculateSignature(
timestamp: String?,
path: String,
body: String?,
secretKey: String = SECRET_ID
): String {
val stringToSign = timestamp + path + (body ?: "")
Log.d("TAG", "calculateSignature: $stringToSign")
val hmacSha256 = Mac.getInstance(ALGORITHM)
val secretKeySpec = SecretKeySpec(secretKey.toByteArray(StandardCharsets.UTF_8), ALGORITHM)
hmacSha256.init(secretKeySpec)
val hash = hmacSha256.doFinal(stringToSign.toByteArray(StandardCharsets.UTF_8))
return bytesToHex(hash)
}
private fun bytesToHex(bytes: ByteArray): String {
val hexString = StringBuilder()
for (b in bytes) {
val hex = Integer.toHexString(0xff and b.toInt())
if (hex.length == 1) hexString.append('0')
hexString.append(hex)
}
return hexString.toString()
}
}

View File

@ -0,0 +1,6 @@
package com.android.grape.net
interface ChangeCallBack {
fun changeSuccess()
fun changeFailed()
}

View File

@ -2,22 +2,28 @@ package com.android.grape.net
import android.util.Log import android.util.Log
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import com.google.gson.Gson
import okhttp3.* import okhttp3.*
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Level
import okhttp3.logging.HttpLoggingInterceptor.Logger
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
object HttpUtils { object HttpUtils {
const val HOST: String = "https://openapi-hk.armcloud.net"
// OkHttp客户端配置 // OkHttp客户端配置
private val client: OkHttpClient by lazy { private val client: OkHttpClient by lazy {
OkHttpClient.Builder() OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS) .connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(LoggingInterceptor()) // 添加日志拦截器 .addInterceptor(HttpLoggingInterceptor().apply {
level = Level.BODY
})
.build() .build()
} }
@ -30,47 +36,6 @@ object HttpUtils {
fun onFailure(error: String, code: Int?) fun onFailure(error: String, code: Int?)
} }
// 日志拦截器
private class LoggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url.toString()
val method = request.method
// 记录请求信息
LogUtils.d("HTTP_REQUEST", "URL: $url")
LogUtils.d("HTTP_REQUEST", "Method: $method")
if ("POST" == method) {
request.body?.let { body ->
if (body is FormBody) {
val formBody = StringBuilder()
for (i in 0 until body.size) {
formBody.append("${body.name(i)}:${body.value(i)}; ")
}
Log.d("HTTP_REQUEST", "Form Data: $formBody")
} else if (body is MultipartBody) {
Log.d("HTTP_REQUEST", "Multipart Form Data")
} else {
Log.d("HTTP_REQUEST", "Body: ${body.contentType()}")
}
}
}
val response = chain.proceed(request)
val responseBody = response.peekBody(Long.MAX_VALUE)
val responseString = responseBody.string()
// 记录响应信息
LogUtils.d("HTTP_RESPONSE", "Code: ${response.code}")
LogUtils.d("HTTP_RESPONSE", "Response: $responseString")
return response.newBuilder()
.body(responseBody)
.build()
}
}
/** /**
* GET 请求异步 * GET 请求异步
* *
@ -82,10 +47,9 @@ object HttpUtils {
fun getAsync( fun getAsync(
url: String, url: String,
params: Map<String, String>? = null, params: Map<String, String>? = null,
headers: Map<String, String>? = null,
callback: HttpCallback callback: HttpCallback
) { ) {
val request = buildGetRequest(url, params, headers) val request = buildGetRequest(url, params)
executeRequestAsync(request, callback) executeRequestAsync(request, callback)
} }
@ -99,31 +63,10 @@ object HttpUtils {
*/ */
fun postAsync( fun postAsync(
url: String, url: String,
body: String, body: String? = null,
headers: Map<String, String>? = null,
callback: HttpCallback callback: HttpCallback
) { ) {
val requestBody = body.toRequestBody(JSON_MEDIA_TYPE) val request = buildPostRequest(url, body)
val request = buildPostRequest(url, requestBody, headers)
executeRequestAsync(request, callback)
}
/**
* POST Form Data 请求异步
*
* @param url 请求URL
* @param formData 表单数据
* @param headers 请求头可选
* @param callback 回调接口
*/
fun postFormAsync(
url: String,
formData: Map<String, String>,
headers: Map<String, String>? = null,
callback: HttpCallback
) {
val formBody = buildFormBody(formData)
val request = buildPostRequest(url, formBody, headers)
executeRequestAsync(request, callback) executeRequestAsync(request, callback)
} }
@ -138,9 +81,8 @@ object HttpUtils {
fun getSync( fun getSync(
url: String, url: String,
params: Map<String, String>? = null, params: Map<String, String>? = null,
headers: Map<String, String>? = null
): Pair<String?, Int> { ): Pair<String?, Int> {
val request = buildGetRequest(url, params, headers) val request = buildGetRequest(url, params)
return executeRequestSync(request) return executeRequestSync(request)
} }
@ -154,11 +96,9 @@ object HttpUtils {
*/ */
fun postSync( fun postSync(
url: String, url: String,
body: String, body: String? = null
headers: Map<String, String>? = null
): Pair<String?, Int> { ): Pair<String?, Int> {
val requestBody = body.toRequestBody(JSON_MEDIA_TYPE) val request = buildPostRequest(url, body)
val request = buildPostRequest(url, requestBody, headers)
return executeRequestSync(request) return executeRequestSync(request)
} }
@ -172,46 +112,72 @@ object HttpUtils {
// 内部方法 -------------------------------- // 内部方法 --------------------------------
private fun buildGetRequest( private fun buildGetRequest(
url: String, path: String ,
params: Map<String, String>?, params: Map<String, String>?,
headers: Map<String, String>?
): Request { ): Request {
val url = "$HOST$path"
val httpUrlBuilder = url.toHttpUrlOrNull()?.newBuilder() val httpUrlBuilder = url.toHttpUrlOrNull()?.newBuilder()
?: throw IllegalArgumentException("Invalid URL: $url") ?: throw IllegalArgumentException("Invalid URL: $url")
// 添加查询参数 var param = ""
params?.forEach { (key, value) -> params?.forEach { (key, value) ->
httpUrlBuilder.addQueryParameter(key, value) httpUrlBuilder.addQueryParameter(key, value)
param += "&${key}=${value}"
}
if (param.isNotEmpty()){
param = param.substring(1)
} }
val requestBuilder = Request.Builder().url(httpUrlBuilder.build()) val requestBuilder = Request.Builder().url(httpUrlBuilder.build())
// 添加请求头 val authver = "2.0"
addHeaders(requestBuilder, headers) val timestamp = System.currentTimeMillis()
val sign =
ArmCloudSignatureV2.calculateSignature(timestamp.toString(), path, param)
addHeaders(
requestBuilder,
mapOf(
"authver" to authver,
"x-ak" to ArmCloudSignatureV2.SECRET_KEY,
"x-timestamp" to timestamp.toString(),
"x-sign" to sign
)
)
return requestBuilder.build() return requestBuilder.build()
} }
private fun buildPostRequest( private fun buildPostRequest(
url: String, path: String,
body: RequestBody, body: String? = null
headers: Map<String, String>?
): Request { ): Request {
val url = "$HOST$path"
val bodyString = body?:""
val requestBuilder = Request.Builder() val requestBuilder = Request.Builder()
.url(url) .url(url)
.post(body) .post(bodyString.toRequestBody(JSON_MEDIA_TYPE))
val authver = "2.0"
// 添加请求头 val timestamp = System.currentTimeMillis()
addHeaders(requestBuilder, headers) val sign =
ArmCloudSignatureV2.calculateSignature(timestamp.toString(), path, body)
addHeaders(
requestBuilder,
mapOf(
"authver" to authver,
"x-ak" to ArmCloudSignatureV2.SECRET_KEY,
"x-timestamp" to timestamp.toString(),
"x-sign" to sign
)
)
return requestBuilder.build() return requestBuilder.build()
} }
private fun addHeaders( private fun addHeaders(
builder: Request.Builder, builder: Request.Builder,
headers: Map<String, String>? headers: Map<String, String>
) { ) {
headers?.forEach { (key, value) -> headers.forEach { (key, value) ->
builder.addHeader(key, value) builder.addHeader(key, value)
} }
} }
@ -252,13 +218,13 @@ object HttpUtils {
private fun executeRequestSync(request: Request): Pair<String?, Int> { private fun executeRequestSync(request: Request): Pair<String?, Int> {
return try { return try {
val response = client.newCall(request).execute() val response = client.newCall(request).execute()
val responseBody = response.body?.string() val responseBody = response.peekBody(Long.MAX_VALUE).string()
val code = response.code val code = response.code
Log.d("TAG", "executeRequestSync: $responseBody")
if (response.isSuccessful && responseBody != null) { if (response.isSuccessful && responseBody.isNotEmpty()) {
Pair(responseBody, code) Pair(responseBody, code)
} else { } else {
Pair(null, code) Pair(null, -1)
} }
} catch (e: IOException) { } catch (e: IOException) {
Pair(null, -1) // 网络错误状态码 Pair(null, -1) // 网络错误状态码

View File

@ -2,16 +2,48 @@ package com.android.grape.net
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.concurrent.TimeUnit
object MyGet { object MyGet {
private const val TAG = "MyGet" private const val TAG = "MyGet"
val affHttpClient: OkHttpClient = OkHttpClient().apply {
dispatcher.maxRequests = 1000
dispatcher.maxRequestsPerHost = 1000
}
fun get(url_: String): String {
var response: Response? = null
val client: OkHttpClient = affHttpClient.newBuilder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build()
val request = Request.Builder()
.url(url_)
.addHeader("User-Agent", "PostmanRuntime/7.29.0")
.addHeader("Accept", "*/*")
.addHeader("Accept-Encoding", "gzip, deflate, br")
.addHeader("Connection", "keep-alive")
.build()
try {
response = client.newCall(request).execute()
return response.body?.string()?:""
} catch (e: java.lang.Exception) {
e.printStackTrace()
} finally {
response?.close()
}
return ""
}
fun getData(url_: String, ua: String?): String? { fun getData(url_: String, ua: String?): String? {
return getData(url_, ua, 0) return getData(url_, ua, 0)
@ -31,7 +63,7 @@ object MyGet {
httpUrlConnection = url.openConnection() as HttpURLConnection httpUrlConnection = url.openConnection() as HttpURLConnection
httpUrlConnection.allowUserInteraction = true httpUrlConnection.allowUserInteraction = true
httpUrlConnection!!.doOutput = false httpUrlConnection.doOutput = false
httpUrlConnection.doInput = true httpUrlConnection.doInput = true
httpUrlConnection.useCaches = false httpUrlConnection.useCaches = false
httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428 httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428
@ -140,7 +172,7 @@ object MyGet {
httpUrlConnection = url.openConnection() as HttpURLConnection httpUrlConnection = url.openConnection() as HttpURLConnection
httpUrlConnection.allowUserInteraction = true httpUrlConnection.allowUserInteraction = true
httpUrlConnection!!.doOutput = false httpUrlConnection.doOutput = false
httpUrlConnection.doInput = true httpUrlConnection.doInput = true
httpUrlConnection.useCaches = false httpUrlConnection.useCaches = false
httpUrlConnection.instanceFollowRedirects = false httpUrlConnection.instanceFollowRedirects = false

View File

@ -1,5 +0,0 @@
package com.android.grape.net
class NetworkHelper {
}

View File

@ -0,0 +1,14 @@
package com.android.grape.pad
import com.google.gson.annotations.SerializedName
data class DcInfo(
var area: String = "",
var dcCode: String = "",
var dcName: String = "",
var ossEndpoint: String = "",
var ossEndpointInternal: String = "",
var ossFileEndpoint: String = "",
var ossScreenshotEndpoint: String = ""
)

View File

@ -0,0 +1,13 @@
package com.android.grape.pad
import com.google.gson.annotations.SerializedName
data class Pad(
var page: Int = 0,
var pageData: List<PageData> = listOf(),
var rows: Int = 0,
var size: Int = 0,
var total: Int = 0,
var totalPage: Int = 0
)

View File

@ -0,0 +1,10 @@
package com.android.grape.pad
import com.google.gson.annotations.SerializedName
data class PadTask(
var padCode: String = "",
var taskId: Int = 0,
var vmStatus: Int = 0
)

View File

@ -0,0 +1,17 @@
package com.android.grape.pad
import com.google.gson.annotations.SerializedName
data class PageData(
var dataSize: Long = 0,
var dataSizeUsed: Long = 0,
var dcInfo: DcInfo = DcInfo(),
var deviceLevel: String = "",
var deviceStatus: Int = 0,
var imageId: String = "",
var online: Int = 0,
var padCode: String = "",
var padStatus: Int = 0,
var streamStatus: Int = 0
)

View File

@ -0,0 +1,14 @@
package com.android.grape.pad
import com.google.gson.annotations.SerializedName
data class TaskDetail(
var endTime: Long = 0,
var errorMsg: String = "",
var padCode: String = "",
var taskContent: Any? = Any(),
var taskId: Int = 0,
var taskResult: Any? = Any(),
var taskStatus: Int = 0
)

View File

@ -0,0 +1,100 @@
package com.android.grape.provider
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import com.android.grape.data.Device
// DeviceDataAccessor.kt
object DeviceDataAccessor {
// 保存设备信息到提供方
fun saveDeviceInfo(context: Context, device: Device): Uri? {
val resolver = context.contentResolver
val actualDeviceId = DeviceInfoHelper.getDeviceId()
val values = ContentValues().apply {
put(DeviceInfoContract.DeviceInfoEntry.COLUMN_DEVICE_ID, actualDeviceId)
put(DeviceInfoContract.DeviceInfoEntry.COLUMN_DATA, device.toJson())
}
// 尝试更新现有记录
val updateCount = resolver.update(
getDeviceUri(actualDeviceId),
values,
null,
null
)
// 如果没有更新记录,则插入新记录
return if (updateCount == 0) {
resolver.insert(DeviceInfoContract.CONTENT_URI, values)
} else {
getDeviceUri(actualDeviceId)
}
}
// 读取设备信息
fun getDeviceInfo(context: Context, deviceId: String? = null): String? {
val resolver = context.contentResolver
val actualDeviceId = deviceId ?: DeviceInfoHelper.getDeviceId()
val cursor = resolver.query(
getDeviceUri(actualDeviceId),
arrayOf(DeviceInfoContract.DeviceInfoEntry.COLUMN_DATA),
null,
null,
null
)
return cursor?.use {
if (it.moveToFirst()) {
val json =
it.getString(it.run { it.getColumnIndex(DeviceInfoContract.DeviceInfoEntry.COLUMN_DATA) })
json
} else {
null
}
}
}
// 获取所有设备信息
fun getAllDeviceInfo(context: Context): List<String> {
val resolver = context.contentResolver
val deviceList = mutableListOf<String>()
resolver.query(
DeviceInfoContract.CONTENT_URI,
arrayOf(DeviceInfoContract.DeviceInfoEntry.COLUMN_DATA),
null,
null,
"${DeviceInfoContract.DeviceInfoEntry.COLUMN_TIMESTAMP} DESC"
)?.use { cursor ->
val dataIndex = cursor.getColumnIndex(DeviceInfoContract.DeviceInfoEntry.COLUMN_DATA)
while (cursor.moveToNext()) {
val json = cursor.getString(dataIndex)
deviceList.add(json)
}
}
return deviceList
}
// 删除设备信息
fun deleteDeviceInfo(context: Context, deviceId: String? = null): Int {
val actualDeviceId = deviceId ?: DeviceInfoHelper.getDeviceId()
return context.contentResolver.delete(
getDeviceUri(actualDeviceId),
null,
null
)
}
private fun getDeviceUri(deviceId: String): Uri {
return Uri.withAppendedPath(
DeviceInfoContract.CONTENT_URI,
"device/$deviceId"
)
}
}

View File

@ -0,0 +1,27 @@
package com.android.grape.provider
import android.net.Uri
import androidx.core.net.toUri
object DeviceInfoContract {
const val AUTHORITY = "com.android.grape.deviceinfo.provider"
val CONTENT_URI: Uri = "content://$AUTHORITY/device_info".toUri()
object DeviceInfoEntry {
const val TABLE_NAME = "device_info"
const val COLUMN_DEVICE_ID = "device_id"
const val COLUMN_DATA = "data"
const val COLUMN_TIMESTAMP = "timestamp"
// JSON 键名
const val KEY_CPU_ABI = "cpu_abi"
const val KEY_DIM = "dim"
const val KEY_BTCH = "btch"
const val KEY_ARCH = "arch"
const val KEY_BTL = "btl"
const val KEY_CPU_ABI2 = "cpu_abi2"
const val KEY_DISK = "disk"
const val KEY_SDK = "sdk"
const val KEY_NETWORK = "network"
}
}

View File

@ -0,0 +1,33 @@
package com.android.grape.provider
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.provider.BaseColumns
class DeviceInfoDbHelper(context: Context) : SQLiteOpenHelper(
context, DATABASE_NAME, null, DATABASE_VERSION
) {
companion object {
const val DATABASE_NAME = "device_info.db"
const val DATABASE_VERSION = 1
const val SQL_CREATE_ENTRIES = """
CREATE TABLE ${DeviceInfoContract.DeviceInfoEntry.TABLE_NAME} (
${BaseColumns._ID} INTEGER PRIMARY KEY,
${DeviceInfoContract.DeviceInfoEntry.COLUMN_DEVICE_ID} TEXT UNIQUE,
${DeviceInfoContract.DeviceInfoEntry.COLUMN_DATA} TEXT NOT NULL,
${DeviceInfoContract.DeviceInfoEntry.COLUMN_TIMESTAMP} INTEGER NOT NULL DEFAULT (strftime('%s','now'))
)
"""
}
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(SQL_CREATE_ENTRIES)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS ${DeviceInfoContract.DeviceInfoEntry.TABLE_NAME}")
onCreate(db)
}
}

View File

@ -0,0 +1,8 @@
package com.android.grape.provider
object DeviceInfoHelper {
// 获取设备唯一ID
fun getDeviceId(): String {
return "d1b3e7f8c9a04b8e9f2c1d5e6f7a8b9c"
}
}

View File

@ -0,0 +1,148 @@
package com.android.grape.provider
import android.content.ContentProvider
import android.content.ContentUris
import android.content.ContentValues
import android.content.UriMatcher
import android.database.Cursor
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import android.provider.BaseColumns
import java.sql.SQLException
// DeviceInfoProvider.kt
class DeviceInfoProvider : ContentProvider() {
private lateinit var dbHelper: DeviceInfoDbHelper
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(DeviceInfoContract.AUTHORITY, "device_info", 1)
addURI(DeviceInfoContract.AUTHORITY, "device_info/#", 2)
addURI(DeviceInfoContract.AUTHORITY, "device_info/device/*", 3)
}
override fun onCreate(): Boolean {
dbHelper = DeviceInfoDbHelper(context!!)
return true
}
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
val db = dbHelper.readableDatabase
val qb = SQLiteQueryBuilder().apply {
tables = DeviceInfoContract.DeviceInfoEntry.TABLE_NAME
}
when (uriMatcher.match(uri)) {
1 -> {} // 查询所有
2 -> qb.appendWhere("${BaseColumns._ID} = ${uri.lastPathSegment}")
3 -> qb.appendWhere("${DeviceInfoContract.DeviceInfoEntry.COLUMN_DEVICE_ID} = '${uri.lastPathSegment}'")
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
return qb.query(db, projection, selection, selectionArgs, null, null, sortOrder).apply {
setNotificationUri(context!!.contentResolver, uri)
}
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val db = dbHelper.writableDatabase
val now = System.currentTimeMillis() / 1000
// 添加时间戳
values?.put(DeviceInfoContract.DeviceInfoEntry.COLUMN_TIMESTAMP, now)
val rowId = db.insert(DeviceInfoContract.DeviceInfoEntry.TABLE_NAME, null, values)
if (rowId > 0) {
val newUri = ContentUris.withAppendedId(DeviceInfoContract.CONTENT_URI, rowId)
context?.contentResolver?.notifyChange(newUri, null)
return newUri
}
return null
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?
): Int {
val db = dbHelper.writableDatabase
val now = System.currentTimeMillis() / 1000
// 更新时间戳
values?.put(DeviceInfoContract.DeviceInfoEntry.COLUMN_TIMESTAMP, now)
val count = when (uriMatcher.match(uri)) {
1 -> db.update(
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
values, selection, selectionArgs
)
2 -> {
val id = uri.lastPathSegment
db.update(
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
values,
"${BaseColumns._ID} = ?",
arrayOf(id)
)
}
3 -> {
val deviceId = uri.lastPathSegment
db.update(
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
values,
"${DeviceInfoContract.DeviceInfoEntry.COLUMN_DEVICE_ID} = ?",
arrayOf(deviceId)
)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
context?.contentResolver?.notifyChange(uri, null)
return count
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val db = dbHelper.writableDatabase
val count = when (uriMatcher.match(uri)) {
1 -> db.delete(
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
selection, selectionArgs
)
2 -> {
val id = uri.lastPathSegment
db.delete(
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
"${BaseColumns._ID} = ?",
arrayOf(id)
)
}
3 -> {
val deviceId = uri.lastPathSegment
db.delete(
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
"${DeviceInfoContract.DeviceInfoEntry.COLUMN_DEVICE_ID} = ?",
arrayOf(deviceId)
)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
context?.contentResolver?.notifyChange(uri, null)
return count
}
override fun getType(uri: Uri): String? {
return when (uriMatcher.match(uri)) {
1 -> "${DeviceInfoContract.AUTHORITY}/device_info"
2, 3 -> "vnd.android.cursor.item/vnd.example.deviceinfo"
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
}

View File

@ -83,7 +83,9 @@ class ApkSourceBuilder(private val mContext: Context) {
val apkFileDescriptors: MutableList<FileDescriptor> = ArrayList<FileDescriptor>( val apkFileDescriptors: MutableList<FileDescriptor> = ArrayList<FileDescriptor>(
mApkFiles?.size?: 0 mApkFiles?.size?: 0
) )
for (apkFile in mApkFiles!!) apkFileDescriptors.add(NormalFileDescriptor(apkFile)) mApkFiles?.let {
for (apkFile in it) apkFileDescriptors.add(NormalFileDescriptor(apkFile))
}
apkSource = DefaultApkSource(apkFileDescriptors) apkSource = DefaultApkSource(apkFileDescriptors)
} else if (mZipFile != null) { } else if (mZipFile != null) {

View File

@ -59,13 +59,16 @@ class FlexSaiPackageInstaller private constructor(c: Context) : SaiPackageInstal
installer: SaiPackageInstaller, installer: SaiPackageInstaller,
params: SaiPiSessionParams params: SaiPiSessionParams
): String { ): String {
val sessionId = installer.createSession(params) val sessionId = installer.createSession(params)?:""
mSessionIdToInstaller[sessionId!!] = installer mSessionIdToInstaller[sessionId] = installer
return sessionId return sessionId
} }
override fun createSession(params: SaiPiSessionParams): String { override fun createSession(params: SaiPiSessionParams): String {
return createSessionOnInstaller(mDefaultInstaller!!, params) mDefaultInstaller?.let {
return createSessionOnInstaller(it, params)
}
return ""
} }
override fun enqueueSession(sessionId: String) { override fun enqueueSession(sessionId: String) {

View File

@ -18,7 +18,7 @@ class MyBroadcastReceiver : BroadcastReceiver() {
//接收广播消息 //接收广播消息
fruit = intent.getStringExtra("fruit") fruit = intent.getStringExtra("fruit")
//调用接口MyReceiver里面的interFruit方法传入接收的内容 //调用接口MyReceiver里面的interFruit方法传入接收的内容
mReceiver!!.interFruit(fruit) mReceiver?.interFruit(fruit)
//使用Toast显示广播消息 //使用Toast显示广播消息
Toast.makeText(context, fruit, Toast.LENGTH_SHORT).show() Toast.makeText(context, fruit, Toast.LENGTH_SHORT).show()
} }

View File

@ -116,7 +116,7 @@ class RootlessSaiPiBroadcastReceiver(c: Context) : BroadcastReceiver() {
val androidPackageInstallerError: AndroidPackageInstallerError = val androidPackageInstallerError: AndroidPackageInstallerError =
getAndroidPmError(errorCode, error) getAndroidPmError(errorCode, error)
if (androidPackageInstallerError !== AndroidPackageInstallerError.UNKNOWN) { if (androidPackageInstallerError != AndroidPackageInstallerError.UNKNOWN) {
return androidPackageInstallerError.getDescription(mContext) return androidPackageInstallerError.getDescription(mContext)
} }

View File

@ -53,9 +53,9 @@ abstract class ShellSaiPackageInstaller protected constructor(c: Context?) :
val installedPackage: String val installedPackage: String
try { try {
installedPackage = intent.dataString!!.replace("package:", "") installedPackage = intent.dataString?.replace("package:", "")?:""
val installerPackage: String = val installerPackage: String =
context.getPackageManager().getInstallerPackageName(installedPackage)?:"" context.packageManager.getInstallerPackageName(installedPackage)?:""
Log.d(tag(), "installerPackage=$installerPackage") Log.d(tag(), "installerPackage=$installerPackage")
if ("com.android.grape" != installerPackage) return if ("com.android.grape" != installerPackage) return
} catch (e: Exception) { } catch (e: Exception) {
@ -130,74 +130,44 @@ abstract class ShellSaiPackageInstaller protected constructor(c: Context?) :
).build() ).build()
) )
unlockInstallation() unlockInstallation()
// Toast.makeText(getContext(),"Installation failed",Toast.LENGTH_SHORT).show();
Util.setInstallRet(false) Util.setInstallRet(false)
return return
} }
androidSessionId = createSession() androidSessionId = createSession()
val path = "/sdcard/apks/" + "com.zhiliaoapp.musically" //todo params.apkSource().apkLocalPath?
val path = "/sdcard/apks/${Util.recordPackageName}"
val file = File(path) val file = File(path)
val files = file.listFiles() val files = file.listFiles()
var currentApkFile = 0 var currentApkFile = 0
for (f in files) { files?.let {
if (f.length() <= 0) { for (f in files) {
setSessionState( if (f.length() <= 0) {
sessionId, setSessionState(sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName(appTempName).error(MainApplication.instance.getString(R.string.installer_error_unknown_apk_size), null).build())
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName( unlockInstallation()
appTempName Util.setInstallRet(false)
).error( return
MainApplication.instance.getString(R.string.installer_error_unknown_apk_size), }
null ensureCommandSucceeded(shell.exec(Shell.Command("pm", "install-write", f.length().toString(), androidSessionId.toString(), String.format("%d.apk", currentApkFile++), f.path)))
).build()
)
unlockInstallation()
// Toast.makeText(getContext(),"Installation failed",Toast.LENGTH_SHORT).show();
Util.setInstallRet(false)
return
} }
ensureCommandSucceeded(
shell.exec(
Shell.Command(
"pm",
"install-write",
f.length().toString(),
androidSessionId.toString(),
String.format("%d.apk", currentApkFile++),
f.path
)
)
)
} }
mAwaitingBroadcast.set(true) mAwaitingBroadcast.set(true)
val installationResult: Shell.Result = val installationResult: Shell.Result = shell.exec(Shell.Command("pm", "install-commit", androidSessionId.toString()))
shell.exec(Shell.Command("pm", "install-commit", androidSessionId.toString()))
Log.i(tag(), "installationResult:" + installationResult.isSuccessful) Log.i(tag(), "installationResult:" + installationResult.isSuccessful)
if (!installationResult.isSuccessful) { if (!installationResult.isSuccessful) {
mAwaitingBroadcast.set(false) mAwaitingBroadcast.set(false)
val shortError: String = MainApplication.instance.getString(R.string.installer_error_shell, installerName, """
val shortError: String = MainApplication.instance.getString(
R.string.installer_error_shell,
installerName, """
${getSessionInfo(apkSource)} ${getSessionInfo(apkSource)}
${parseError(installationResult)} ${parseError(installationResult)}
""".trimIndent() """.trimIndent())
) setSessionState(sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName(appTempName).error(
setSessionState(
sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED)
.appTempName(appTempName)
.error(
shortError, """ shortError, """
$shortError $shortError
${installationResult.toString()} ${installationResult.toString()}
""".trimIndent() """.trimIndent()).build())
)
.build()
)
unlockInstallation() unlockInstallation()
Util.setInstallRet(false) Util.setInstallRet(false)
@ -206,7 +176,6 @@ abstract class ShellSaiPackageInstaller protected constructor(c: Context?) :
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
//TODO this catches resources close exception causing a crash, same in rootless installer
Log.w(tag(), e) Log.w(tag(), e)
if (androidSessionId != null) { if (androidSessionId != null) {
@ -291,7 +260,7 @@ abstract class ShellSaiPackageInstaller protected constructor(c: Context?) :
DbgPreferencesHelper.customInstallCreateCommand DbgPreferencesHelper.customInstallCreateCommand
if (customInstallCreateCommand != null) { if (customInstallCreateCommand != null) {
val args = ArrayList( val args = ArrayList(
Arrays.asList( listOf(
*customInstallCreateCommand.split(" ".toRegex()).dropLastWhile { it.isEmpty() } *customInstallCreateCommand.split(" ".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray())) .toTypedArray()))
val command = args.removeAt(0) val command = args.removeAt(0)
@ -356,7 +325,7 @@ abstract class ShellSaiPackageInstaller protected constructor(c: Context?) :
val sessionIdPattern = Pattern.compile("(\\d+)") val sessionIdPattern = Pattern.compile("(\\d+)")
val sessionIdMatcher = sessionIdPattern.matcher(commandResult) val sessionIdMatcher = sessionIdPattern.matcher(commandResult)
sessionIdMatcher.find() sessionIdMatcher.find()
return sessionIdMatcher.group(1).toInt() return sessionIdMatcher.group(1)?.toInt()
} catch (e: Exception) { } catch (e: Exception) {
Log.w(tag(), commandResult, e) Log.w(tag(), commandResult, e)
return null return null
@ -365,7 +334,7 @@ abstract class ShellSaiPackageInstaller protected constructor(c: Context?) :
private fun parseError(installCommitResult: Shell.Result): String { private fun parseError(installCommitResult: Shell.Result): String {
var matchedError: AndroidPackageInstallerError = AndroidPackageInstallerError.UNKNOWN var matchedError: AndroidPackageInstallerError = AndroidPackageInstallerError.UNKNOWN
for (error in AndroidPackageInstallerError.values()) { for (error in AndroidPackageInstallerError.entries) {
if (installCommitResult.out.contains(error.error)) { if (installCommitResult.out.contains(error.error)) {
matchedError = error matchedError = error
break break

View File

@ -28,7 +28,7 @@ class CopyToFileApkSource(context: Context, wrappedApkSource: ApkSource) :
IOUtils.deleteRecursively(it) IOUtils.deleteRecursively(it)
} }
mCurrentApkFile = File(mTempDir, mWrappedApkSource.apkName) mCurrentApkFile = File(mTempDir, mWrappedApkSource.apkName?:"")
mWrappedApkSource.openApkInputStream().use { `in` -> mWrappedApkSource.openApkInputStream().use { `in` ->
FileOutputStream(mCurrentApkFile).use { out -> FileOutputStream(mCurrentApkFile).use { out ->

View File

@ -34,7 +34,7 @@ class SignerApkSource(private val mContext: Context, apkSource: ApkSource) : Apk
) )
} }
mCurrentSignedApkFile = File(mTempDir, apkName) mCurrentSignedApkFile = File(mTempDir, apkName?:"")
mWrappedApkSource.openApkInputStream()?.let { mWrappedApkSource.openApkInputStream()?.let {
mApkSigner?.sign( mApkSigner?.sign(
it, it,

View File

@ -38,11 +38,11 @@ object FileUtils {
} }
private fun trimFilename(res: StringBuilder, maxBytes: Int) { private fun trimFilename(res: StringBuilder, maxBytes: Int) {
var maxBytes = maxBytes var bytes = maxBytes
var raw = res.toString().toByteArray(StandardCharsets.UTF_8) var raw = res.toString().toByteArray(StandardCharsets.UTF_8)
if (raw.size > maxBytes) { if (raw.size > bytes) {
maxBytes -= 3 bytes -= 3
while (raw.size > maxBytes) { while (raw.size > bytes) {
res.deleteCharAt(res.length / 2) res.deleteCharAt(res.length / 2)
raw = res.toString().toByteArray(StandardCharsets.UTF_8) raw = res.toString().toByteArray(StandardCharsets.UTF_8)
} }

View File

@ -25,14 +25,15 @@ class ZipApkSource(private val mContext: Context, private val mZipFileDescriptor
@Throws(Exception::class) @Throws(Exception::class)
override fun nextApk(): Boolean { override fun nextApk(): Boolean {
if (!mIsOpen) { if (!mIsOpen) {
mZipInputStream = ZipInputStream(mZipFileDescriptor.open()) mZipInputStream = ZipInputStream(mZipFileDescriptor.open()).apply {
mWrappedStream = ZipInputStreamWrapper(mZipInputStream!!) mWrappedStream = ZipInputStreamWrapper(this)
}
mIsOpen = true mIsOpen = true
} }
do { do {
try { try {
entry = mZipInputStream!!.nextEntry entry = mZipInputStream?.nextEntry
} catch (e: ZipException) { } catch (e: ZipException) {
if (e.message == "only DEFLATED entries can have EXT descriptor") { if (e.message == "only DEFLATED entries can have EXT descriptor") {
throw ZipException("only DEFLATED entries can have EXT descriptor") throw ZipException("only DEFLATED entries can have EXT descriptor")

View File

@ -35,13 +35,14 @@ class ZipFileApkSource(context: Context, private val mZipFileDescriptor: FileDes
if (mZipFile == null) copyAndOpenZip() if (mZipFile == null) copyAndOpenZip()
entry = null entry = null
while (entry == null && mZipEntries!!.hasMoreElements()) { while (entry == null && mZipEntries?.hasMoreElements() == true) {
val nextEntry = mZipEntries!!.nextElement() mZipEntries?.nextElement()?.let { nextEntry ->
if (!nextEntry.isDirectory && nextEntry.name.lowercase(Locale.getDefault()) if (!nextEntry.isDirectory && nextEntry.name.lowercase(Locale.getDefault())
.endsWith(".apk") .endsWith(".apk")
) { ) {
entry = nextEntry entry = nextEntry
mSeenApkFile = true mSeenApkFile = true
}
} }
} }

View File

@ -138,7 +138,7 @@ class PackageMeta : Parcelable {
return Builder(applicationInfo.packageName) return Builder(applicationInfo.packageName)
.setLabel(applicationInfo.loadLabel(pm).toString()) .setLabel(applicationInfo.loadLabel(pm).toString())
.setHasSplits(applicationInfo.splitPublicSourceDirs != null && applicationInfo.splitPublicSourceDirs!!.size > 0) .setHasSplits(applicationInfo.splitPublicSourceDirs != null && applicationInfo.splitPublicSourceDirs?.isNotEmpty() == true)
.setIsSystemApp((applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0) .setIsSystemApp((applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0)
.setVersionCode(if (Utils.apiIsAtLeast(Build.VERSION_CODES.P)) packageInfo.longVersionCode else packageInfo.versionCode.toLong()) .setVersionCode(if (Utils.apiIsAtLeast(Build.VERSION_CODES.P)) packageInfo.longVersionCode else packageInfo.versionCode.toLong())
.setVersionName(packageInfo.versionName) .setVersionName(packageInfo.versionName)

View File

@ -127,11 +127,11 @@ class SaiPiSessionState private constructor(
} }
fun error(shortError: String?, fullError: String?): Builder { fun error(shortError: String?, fullError: String?): Builder {
var fullError = fullError var error = fullError
mState.mShortError = shortError mState.mShortError = shortError
if (fullError == null) fullError = shortError if (error == null) error = shortError
mState.mFullError = fullError mState.mFullError = error
return this return this
} }

View File

@ -3,6 +3,7 @@ package com.android.grape.sai.prefers
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Environment import android.os.Environment
import androidx.core.content.edit
class PreferencesHelper private constructor(c: Context) { class PreferencesHelper private constructor(c: Context) {
@ -50,7 +51,7 @@ class PreferencesHelper private constructor(c: Context) {
} }
fun setShouldSignApks(signApks: Boolean) { fun setShouldSignApks(signApks: Boolean) {
prefs.edit().putBoolean(PreferencesKeys.SIGN_APKS, signApks).apply() prefs.edit { putBoolean(PreferencesKeys.SIGN_APKS, signApks) }
} }
fun shouldExtractArchives(): Boolean { fun shouldExtractArchives(): Boolean {
@ -64,7 +65,7 @@ class PreferencesHelper private constructor(c: Context) {
var installer: Int var installer: Int
get() = prefs.getInt(PreferencesKeys.INSTALLER, PreferencesValues.INSTALLER_ROOTED) get() = prefs.getInt(PreferencesKeys.INSTALLER, PreferencesValues.INSTALLER_ROOTED)
set(installer) { set(installer) {
prefs.edit().putInt(PreferencesKeys.INSTALLER, installer).apply() prefs.edit { putInt(PreferencesKeys.INSTALLER, installer) }
} }
var backupFileNameFormat: String? var backupFileNameFormat: String?
@ -73,7 +74,7 @@ class PreferencesHelper private constructor(c: Context) {
PreferencesValues.BACKUP_FILE_NAME_FORMAT_DEFAULT PreferencesValues.BACKUP_FILE_NAME_FORMAT_DEFAULT
) )
set(format) { set(format) {
prefs.edit().putString(PreferencesKeys.BACKUP_FILE_NAME_FORMAT, format).apply() prefs.edit { putString(PreferencesKeys.BACKUP_FILE_NAME_FORMAT, format) }
} }
var installLocation: Int var installLocation: Int
@ -87,8 +88,9 @@ class PreferencesHelper private constructor(c: Context) {
} }
} }
set(installLocation) { set(installLocation) {
prefs.edit().putString(PreferencesKeys.INSTALL_LOCATION, installLocation.toString()) prefs.edit {
.apply() putString(PreferencesKeys.INSTALL_LOCATION, installLocation.toString())
}
} }
fun useOldInstaller(): Boolean { fun useOldInstaller(): Boolean {
@ -108,7 +110,7 @@ class PreferencesHelper private constructor(c: Context) {
} }
fun setSafTipShown() { fun setSafTipShown() {
prefs.edit().putBoolean(PreferencesKeys.SAF_TIP_SHOWN, true).apply() prefs.edit { putBoolean(PreferencesKeys.SAF_TIP_SHOWN, true) }
} }
val isInstallerXEnabled: Boolean val isInstallerXEnabled: Boolean
@ -120,19 +122,19 @@ class PreferencesHelper private constructor(c: Context) {
var isAnalyticsEnabled: Boolean var isAnalyticsEnabled: Boolean
get() = prefs.getBoolean(PreferencesKeys.ENABLE_ANALYTICS, true) get() = prefs.getBoolean(PreferencesKeys.ENABLE_ANALYTICS, true)
set(enabled) { set(enabled) {
prefs.edit().putBoolean(PreferencesKeys.ENABLE_ANALYTICS, enabled).apply() prefs.edit { putBoolean(PreferencesKeys.ENABLE_ANALYTICS, enabled) }
} }
var isInitialIndexingDone: Boolean var isInitialIndexingDone: Boolean
get() = prefs.getBoolean(PreferencesKeys.INITIAL_INDEXING_RUN, false) get() = prefs.getBoolean(PreferencesKeys.INITIAL_INDEXING_RUN, false)
set(done) { set(done) {
prefs.edit().putBoolean(PreferencesKeys.INITIAL_INDEXING_RUN, done).apply() prefs.edit { putBoolean(PreferencesKeys.INITIAL_INDEXING_RUN, done) }
} }
var isSingleApkExportEnabled: Boolean var isSingleApkExportEnabled: Boolean
get() = prefs.getBoolean(PreferencesKeys.BACKUP_APK_EXPORT, false) get() = prefs.getBoolean(PreferencesKeys.BACKUP_APK_EXPORT, false)
set(enabled) { set(enabled) {
prefs.edit().putBoolean(PreferencesKeys.BACKUP_APK_EXPORT, enabled).apply() prefs.edit { putBoolean(PreferencesKeys.BACKUP_APK_EXPORT, enabled) }
} }
companion object { companion object {

View File

@ -40,9 +40,7 @@ class MyAccessibilityService:AccessibilityService(){
val manager: NotificationManager = getSystemService<NotificationManager>( val manager: NotificationManager = getSystemService<NotificationManager>(
NotificationManager::class.java NotificationManager::class.java
) )
if (manager != null) { manager.createNotificationChannel(channel)
manager.createNotificationChannel(channel)
}
} }
} }

View File

@ -0,0 +1,128 @@
package com.android.grape.util
import android.content.Context
import android.os.Environment
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
class AndroidFileDownloader(private val context: Context) {
private val client = OkHttpClient()
/**
* 下载文件到指定路径
*
* @param url 文件下载URL
* @param relativePath 相对于外部存储目录的路径 "Downloads/MyApp/file.zip"
* @param fileName 文件名可选如未提供则从URL提取
* @param progressCallback 进度回调
* @param completionCallback 完成回调
*/
fun downloadFile(
url: String,
relativePath: String,
fileName: String? = null,
): Boolean {
try {
// 检查存储权限
if (!hasStoragePermission()) {
throw IOException("缺少存储权限")
}
// 获取目标文件
val file = getOutputFile(relativePath, fileName ?: extractFileName(url))
// 确保目录存在
file.parentFile?.let { parentDir ->
if (!parentDir.exists()) {
val dirsCreated = parentDir.mkdirs()
if (!dirsCreated) {
throw IOException("无法创建目录: ${parentDir.absolutePath}")
}
}
}
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException("下载失败: ${response.code} ${response.message}")
}
val body = response.body ?: throw IOException("响应体为空")
val totalBytes = body.contentLength()
// 写入文件
body.byteStream().use { inputStream ->
FileOutputStream(file).use { outputStream ->
copyStreamWithProgress(inputStream, outputStream, totalBytes)
}
}
return true
}
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
/**
* 检查存储权限
*/
private fun hasStoragePermission(): Boolean {
return ContextCompat.checkSelfPermission(
context,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
) == android.content.pm.PackageManager.PERMISSION_GRANTED
}
/**
* 获取输出文件路径
*/
private fun getOutputFile(relativePath: String, fileName: String): File {
val baseDir = context.getExternalFilesDir(null)
?: Environment.getExternalStorageDirectory()
?: throw IOException("无法访问外部存储")
return File(baseDir, "$relativePath/$fileName")
}
/**
* 从URL提取文件名
*/
private fun extractFileName(url: String): String {
return url.substringAfterLast('/').takeIf { it.isNotBlank() } ?: "downloaded_file"
}
/**
* 带进度回调的流复制
*/
private fun copyStreamWithProgress(
inputStream: InputStream,
outputStream: FileOutputStream,
totalBytes: Long?,
progressCallback: ((Long, Long?) -> Unit)? = null
) {
val buffer = ByteArray(4096)
var bytesCopied = 0L
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
bytesCopied += bytesRead
progressCallback?.invoke(bytesCopied, totalBytes)
}
outputStream.flush()
}
}

View File

@ -5,14 +5,175 @@ import android.content.Context
import android.util.Log import android.util.Log
import com.android.grape.MainApplication import com.android.grape.MainApplication
import com.android.grape.data.Device import com.android.grape.data.Device
import com.android.grape.net.Api
import com.android.grape.net.ChangeCallBack
import com.android.grape.provider.DeviceDataAccessor
import com.android.grape.util.Util.paramsJson import com.android.grape.util.Util.paramsJson
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
object ChangeDeviceInfoUtil { object ChangeDeviceInfoUtil {
val scope = CoroutineScope(Dispatchers.IO)
fun changeDevice(callBack: ChangeCallBack){
try {
val deviceObject = paramsJson?.getJSONObject("device")
if (deviceObject == null) {
LogUtils.d("ERROR", "ChangeDeviceInfoUtil", "device is null")
return
}
val device = GsonUtils.fromJsonObject(deviceObject.toString(), Device::class.java)
DeviceDataAccessor.saveDeviceInfo(MainApplication.instance, device)
val padCode = ShellUtils.execRootCmdAndGetResult("getprop ro.boot.pad_code")
Log.d("TAG", "changeDevice: $padCode")
val jsonString = JSONObject().apply {
put("padCodes", JSONArray().apply {
put(padCode)
})
put("modemPropertiesList", JSONArray().apply {
put(JSONObject().apply {
put("propertiesName", "MCCMNC")
put("propertiesValue", "${device.mcc},${device.mnc}")
})
put(JSONObject().apply {
put("propertiesName", "OpName")
put("propertiesValue", device.operator)
})
})
put("systemPropertiesList", JSONArray().apply {
put(JSONObject().apply {
put("propertiesName", "ro.product.manufacturer")
put("propertiesValue", device.manufactor)
})
put(JSONObject().apply {
put("propertiesName", "ro.product.brand")
put("propertiesValue", device.brand)
})
put(JSONObject().apply {
put("propertiesName", "ro.product.model")
put("propertiesValue", device.model)
})
// put(JSONObject().apply {
// put("propertiesName", "ro.build.id")
// put("propertiesValue", device.buildDisplayId)
// })
put(JSONObject().apply {
put("propertiesName", "ro.build.display.id")
put("propertiesValue", device.buildDisplayId)
})
put(JSONObject().apply {
put("propertiesName", "ro.product.name")
put("propertiesValue", device.product)
})
put(JSONObject().apply {
put("propertiesName", "ro.product.device")
put("propertiesValue", device.device)
})
put(JSONObject().apply {
put("propertiesName", "ro.product.board")
put("propertiesValue", device.expand.board)
})
// put(JSONObject().apply {
// put("propertiesName", "ro.build.tags")
// put("propertiesValue", device.expand)
// })
put(JSONObject().apply {
put("propertiesName", "ro.build.fingerprint")
put("propertiesValue", device.expand.fingerprint)
})
put(JSONObject().apply {
put("propertiesName", "ro.build.date.utc")
put("propertiesValue", device.expand.roBuildDateUtc)
})
put(JSONObject().apply {
put("propertiesName", "ro.build.user")
put("propertiesValue", device.expand.uname)
})
// put(JSONObject().apply {
// put("propertiesName", "ro.build.host")
// put("propertiesValue", device.expand.)
// })
put(JSONObject().apply {
put("propertiesName", "ro.build.description")
put("propertiesValue", device.expand.roBuildDescription)
})
put(JSONObject().apply {
put("propertiesName", "ro.build.version.incremental")
put("propertiesValue", device.expand.incremental)
})
// put(JSONObject().apply {
// put("propertiesName", "ro.build.version.codename")
// put("propertiesValue", device.cod)
// })
})
put("settingPropertiesList", JSONArray().apply {
put(JSONObject().apply {
put("propertiesName", "ssaid/${Util.recordPackageName}")
put("propertiesValue", device.androidId)
})
put(JSONObject().apply {
put("propertiesName", "bt/mac")
put("propertiesValue", device.expand.lyMAC)
})
put(JSONObject().apply {
put("propertiesName", "language")
put("propertiesValue", device.locale.lang)
})
put(JSONObject().apply {
put("propertiesName", "timezone")
put("propertiesValue", device.tzDisplayName)
})
// put(JSONObject().apply {
// put("propertiesName", "systemvolume")
// put("propertiesValue", device.expand.)
// })
})
put("oaidPropertiesList", JSONArray().apply {
put(JSONObject().apply {
put("propertiesName", "AAID")
put("propertiesValue", device.advertiserId)
})
})
}.toString()
val response = Api.updatePad(jsonString)
val dataList = response.data
if (response.isSuccess() && dataList!= null && dataList.isNotEmpty()){
val padTask = dataList[0]
scope.launch {
Log.d("TAG", "changeDevice: $padTask")
var loop = true
while (loop) {
delay(5000)
val result = Api.padTaskDetail(padTask.taskId)
if (result == 3) {
Log.d("ChangeDeviceInfoUtil", "changeDeviceInfo changeDeviceInfo success")
loop = false
callBack.changeSuccess()
} else if (result == -1) {
Log.d("ChangeDeviceInfoUtil", "changeDeviceInfo changeDeviceInfo fail")
loop = false
callBack.changeFailed()
}
}
}
}
} catch (e: JSONException) {
e.printStackTrace()
callBack.changeFailed()
}
}
fun changeDeviceInfo() { fun changeDeviceInfo() {
try { try {
MockTools.exec("pm grant com.android.grape android.permission.INTERACT_ACROSS_USERS")
MockTools.exec("pm grant com.android.grape android.permission.WRITE_SECURE_SETTINGS")
MockTools.exec("pm setenforce 0")
val deviceObject = paramsJson?.getJSONObject("device") val deviceObject = paramsJson?.getJSONObject("device")
if (deviceObject == null) { if (deviceObject == null) {
LogUtils.d("ERROR", "ChangeDeviceInfoUtil", "device is null") LogUtils.d("ERROR", "ChangeDeviceInfoUtil", "device is null")
@ -32,7 +193,7 @@ object ChangeDeviceInfoUtil {
vcloudsettingsPut("$currentPkgName.vendor", device.expand.glVendor, context) vcloudsettingsPut("$currentPkgName.vendor", device.expand.glVendor, context)
// callVCloudSettings_put("$currentPkgName.battery_scale", batteryScale.toString(), context) // callVCloudSettings_put("$currentPkgName.battery_scale", batteryScale.toString(), context)
vcloudsettingsPut("$currentPkgName.os_lang", device.lang, context) vcloudsettingsPut("$currentPkgName.os_lang", device.lang, context)
vcloudsettingsPut("$currentPkgName.model", device.model, context); vcloudsettingsPut("$currentPkgName.model", device.model, context)
vcloudsettingsPut("$currentPkgName.net", device.network, context) vcloudsettingsPut("$currentPkgName.net", device.network, context)
vcloudsettingsPut("$currentPkgName.dpi", device.expand.dPI.toString(), context) vcloudsettingsPut("$currentPkgName.dpi", device.expand.dPI.toString(), context)
// callVCloudSettings_put( // callVCloudSettings_put(
@ -51,7 +212,7 @@ object ChangeDeviceInfoUtil {
// **os_ver** // **os_ver**
vcloudsettingsPut(currentPkgName + "_os_ver", device.sdkVer, context) vcloudsettingsPut(currentPkgName + "_os_ver", device.sdkVer, context)
// **tz** (时区) // **tz** (时区)
vcloudsettingsPut(currentPkgName + "_tz", device.tzOffTime.toString(), context) vcloudsettingsPut(currentPkgName + "_tz", TimeZoneUtils.formatTimeZoneOffset(device.tzOffTime), context)
vcloudsettingsPut("$currentPkgName.advertiserId", device.advertiserId, context) vcloudsettingsPut("$currentPkgName.advertiserId", device.advertiserId, context)
vcloudsettingsPut("$currentPkgName.brand", device.brand, context) vcloudsettingsPut("$currentPkgName.brand", device.brand, context)
@ -121,6 +282,9 @@ object ChangeDeviceInfoUtil {
ShellUtils.execRootCmd("setprop persist.sys.cloud.battery.capacity ${device.battery}") ShellUtils.execRootCmd("setprop persist.sys.cloud.battery.capacity ${device.battery}")
ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_vendor ${device.expand.glVendor}") ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_vendor ${device.expand.glVendor}")
ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_renderer ${device.expand.glRenderer}") ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_renderer ${device.expand.glRenderer}")
ShellUtils.execRootCmd("setprop persist.sys.timezone ${TimeZoneUtils.formatTimeZoneOffset(device.tzOffTime)}")
ShellUtils.execRootCmd("setprop persist.sys.country ${device.country}")
ShellUtils.execRootCmd("setprop persist.sys.language ${device.expand.displayLang}")
// 这个值不能随便改 必须是 OpenGL ES %d.%d 这个格式 // 这个值不能随便改 必须是 OpenGL ES %d.%d 这个格式
ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_version ${device.expand.glVersion}") ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_version ${device.expand.glVersion}")
// ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_vendor $persist_sys_cloud_gpu_egl_vendor") // ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_vendor $persist_sys_cloud_gpu_egl_vendor")
@ -137,7 +301,7 @@ object ChangeDeviceInfoUtil {
} }
private fun vcloudsettingsPut(key: String, value: String, context: Context) { fun vcloudsettingsPut(key: String, value: String, context: Context) {
if (key.isEmpty()) { if (key.isEmpty()) {
LogUtils.e(Log.ERROR, "ChangeDeviceInfoUtil", "Key cannot be null or empty", null) LogUtils.e(Log.ERROR, "ChangeDeviceInfoUtil", "Key cannot be null or empty", null)
throw IllegalArgumentException("Key cannot be null or empty") throw IllegalArgumentException("Key cannot be null or empty")

View File

@ -4,24 +4,35 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Environment
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.io.IOException import java.io.IOException
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
object ClashUtil { object ClashUtil {
private val sharedClient: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // 连接超时
.readTimeout(30, TimeUnit.SECONDS) // 读取超时
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.build()
fun startProxy(context: Context) { fun startProxy(context: Context) {
val intent = Intent("com.github.kr328.clash.intent.action.SESSION_CREATE") val intent = Intent("com.github.kr328.clash.intent.action.SESSION_CREATE")
intent.putExtra("profile", "default") // 可选择您在 Clash 中配置的 Profile intent.putExtra("profile", "default") // 可选择您在 Clash 中配置的 Profile
@ -35,6 +46,66 @@ object ClashUtil {
}.start() }.start()
} }
fun getProxyPort(): Int {
val scriptDir = File(Environment.getExternalStorageDirectory(), "script")
val portFile = File(scriptDir, "ip.port.json")
val text = StringBuilder()
try {
BufferedReader(FileReader(portFile)).use { br ->
var line: String?
while ((br.readLine().also { line = it }) != null) {
text.append(line).append('\n')
}
}
} catch (e: IOException) {
Log.e("TAG", "getProxyPort: ", e)
return -1
}
var port = -1
try {
Log.d("TAG", "getProxyPort: $text")
val config = JSONObject(text.toString())
port = config.optInt("port", -1)
} catch (e: java.lang.Exception) {
Log.e("TAG", "getProxyPort: ", e)
}
return port
}
fun switchProxyWithPort(country: String?) {
val port = getProxyPort()
// 安全构建 URL
val url = "http://39.103.73.250/tt/test/testProxy.jsp".toHttpUrl()
.newBuilder()
.addQueryParameter("port", port.toString() + "")
.addQueryParameter("country", country)
.build()
val request = Request.Builder()
.url(url)
.build()
try {
sharedClient.newCall(request).execute().use { response ->
// 读取并记录响应内容
val responseBody = response.body?.string() ?: "Empty response body"
if (response.isSuccessful) {
Log.d(
"ClashUtil",
"switchProxyGroup: Success | Status: " + response.code + " | Response: " + responseBody
)
} else {
Log.d(
"ClashUtil",
"switchProxyGroup: Failed | Status: " + response.code + " | Response: " + responseBody
)
}
}
} catch (e: java.lang.Exception) {
Log.d("ClashUtil", "switchProxyGroup: Unexpected error", e)
}
}
var isRunning: Boolean = false var isRunning: Boolean = false
val clashStatusReceiver: BroadcastReceiver = object : BroadcastReceiver() { val clashStatusReceiver: BroadcastReceiver = object : BroadcastReceiver() {
@ -90,26 +161,7 @@ object ClashUtil {
} }
fun switchProxyGroup(groupName: String?, proxyName: String?, controllerUrl: String) { fun switchProxyGroup(groupName: String?, proxyName: String?, controllerUrl: String): Boolean {
if (groupName == null || groupName.trim { it <= ' ' }
.isEmpty() || proxyName == null || proxyName.trim { it <= ' ' }.isEmpty()) {
LogUtils.log(
Log.ERROR,
"ClashUtil",
"switchProxyGroup: Invalid arguments",
null
)
throw IllegalArgumentException("Group name and proxy name must not be empty")
}
if (!controllerUrl.matches("^https?://.*".toRegex())) {
LogUtils.log(
Log.ERROR,
"ClashUtil",
"switchProxyGroup: Invalid controller URL",
null
)
throw IllegalArgumentException("Invalid controller URL")
}
val client = OkHttpClient() val client = OkHttpClient()
val json = JSONObject() val json = JSONObject()
@ -133,39 +185,27 @@ object ClashUtil {
.put(requestBody) .put(requestBody)
.build() .build()
client.newCall(request).enqueue(object : Callback { try {
override fun onFailure(call: Call, e: IOException) { val result = sharedClient.newCall(request).execute().use { response ->
LogUtils.log( // 读取并记录响应内容
Log.ERROR, val responseBody = if (response.body != null) response.body?.string() else "Empty response body"
"ClashUtil", val isSuccess = if (response.isSuccessful) {
"switchProxyGroup: Failed to switch proxy", LogUtils.d(
e "ClashUtil",
) "switchProxyGroup: Success | Status: " + response.code + " | Response: " + responseBody)
println("Failed to switch proxy: " + e.message) true
} } else {
LogUtils.d(
@Throws(IOException::class) "ClashUtil",
override fun onResponse(call: Call, response: Response) { "switchProxyGroup: Failed | Status: " + response.code + " | Response: " + responseBody)
try { false
if (response.body != null) {
LogUtils.log(
Log.INFO,
"ClashUtil",
"switchProxyGroup: Switch proxy response",
null
)
} else {
LogUtils.log(
Log.ERROR,
"ClashUtil",
"switchProxyGroup: Response body is null",
null
)
}
} finally {
response.close()
} }
return isSuccess
} }
}) return result
} catch (e: Exception) {
LogUtils.d("ClashUtil", "switchProxyGroup: Unexpected error", e)
}
return false
} }
} }

View File

@ -0,0 +1,18 @@
package com.android.grape.util
import android.util.Log
import com.blankj.utilcode.util.LogUtils
object CountryCode {
const val DEVICE_TYPE: Int = 2
const val US: String = "US"
const val RU: String = "RU"
val DEFAULT: String = US
var currentCountry: String = DEFAULT
fun switchCountry(): String {
currentCountry = if (currentCountry == US) RU else US
LogUtils.d("TAG", "Switched country to: $currentCountry")
return currentCountry
}
}

View File

@ -1,6 +1,8 @@
package com.android.grape.util package com.android.grape.util
import android.os.Environment
import android.util.Log import android.util.Log
import com.blankj.utilcode.util.LogUtils
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.BufferedReader import java.io.BufferedReader
@ -16,6 +18,7 @@ import java.io.InputStreamReader
import java.io.OutputStream import java.io.OutputStream
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import java.nio.charset.StandardCharsets
import java.util.Enumeration import java.util.Enumeration
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
@ -387,4 +390,31 @@ object FileUtils {
output.close() output.close()
input.close() input.close()
} }
fun writePackageName(packageName: String) {
val file = File(
Environment.getExternalStorageDirectory(),
"script/packagesname.txt"
)
val parentDir = file.getParentFile()
if (parentDir != null && !parentDir.exists()) {
val dirsCreated = parentDir.mkdirs()
if (!dirsCreated) {
Log.e("FileWrite", "Failed to create directories: $parentDir")
return
}
}
LogUtils.d("TAG", "writePackageName: $packageName", null)
try {
BufferedOutputStream(
FileOutputStream(file)
).use { bos ->
bos.write(packageName.toByteArray(StandardCharsets.UTF_8))
bos.flush() // 确保数据写入磁盘
}
} catch (e: IOException) {
Log.e("FileWrite", "Failed to write package name: $packageName", e)
// 6. 可以考虑添加重试机制或通知用户
}
}
} }

View File

@ -12,25 +12,26 @@ object MockTools {
fun exec(cmd: String): String { fun exec(cmd: String): String {
var retString = "" var retString = ""
try { try {
//创建socket retString = ShellUtils.execRootCmdAndGetResult(cmd)
val myCmd = "SU|$cmd" // //创建socket
// val myCmd = "SU|$cmd"
val mSocket = Socket() //
val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345) // val mSocket = Socket()
mSocket.connect(inetSocketAddress) // val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345)
val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream())) // mSocket.connect(inetSocketAddress)
val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream())) // val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream()))
bufferedWriter.write(myCmd + "\r\n") // val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream()))
bufferedWriter.flush() // bufferedWriter.write(myCmd + "\r\n")
val stringBuilder = StringBuilder() // bufferedWriter.flush()
var line: String? = null // val stringBuilder = StringBuilder()
while ((bufferedReader.readLine().also { line = it }) != null) { // var line: String? = null
stringBuilder.append(line + "\n") // while ((bufferedReader.readLine().also { line = it }) != null) {
} // stringBuilder.append(line + "\n")
//retString = bufferedReader.readLine(); // }
retString = stringBuilder.toString() // //retString = bufferedReader.readLine();
bufferedReader.close() // retString = stringBuilder.toString()
bufferedWriter.close() // bufferedReader.close()
// bufferedWriter.close()
} catch (eeeee: Exception) { } catch (eeeee: Exception) {
eeeee.printStackTrace() eeeee.printStackTrace()
} }
@ -40,25 +41,26 @@ object MockTools {
fun execRead(cmd: String): String { fun execRead(cmd: String): String {
var retString = "" var retString = ""
try { try {
//创建socket retString = ShellUtils.execRootCmdAndGetResult(cmd)
val myCmd = "SU_1|$cmd" // //创建socket
// val myCmd = "SU_1|$cmd"
val mSocket = Socket() //
val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345) // val mSocket = Socket()
mSocket.connect(inetSocketAddress) // val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345)
val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream())) // mSocket.connect(inetSocketAddress)
val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream())) // val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream()))
bufferedWriter.write(myCmd + "\r\n") // val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream()))
bufferedWriter.flush() // bufferedWriter.write(myCmd + "\r\n")
val stringBuilder = StringBuilder() // bufferedWriter.flush()
var line: String? = null // val stringBuilder = StringBuilder()
while ((bufferedReader.readLine().also { line = it }) != null) { // var line: String? = null
stringBuilder.append(line + "\n") // while ((bufferedReader.readLine().also { line = it }) != null) {
} // stringBuilder.append(line + "\n")
retString = stringBuilder.toString() // }
//retString = bufferedReader.readLine(); // retString = stringBuilder.toString()
bufferedReader.close() // //retString = bufferedReader.readLine();
bufferedWriter.close() // bufferedReader.close()
// bufferedWriter.close()
} catch (eeeee: Exception) { } catch (eeeee: Exception) {
eeeee.printStackTrace() eeeee.printStackTrace()
} }

View File

@ -8,6 +8,7 @@ import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.util.Log
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@ -106,7 +107,6 @@ class NotificationPermissionHandler(
fun requestNotificationPermission() { fun requestNotificationPermission() {
// 保存当前权限状态,用于比较设置后是否发生变化 // 保存当前权限状态,用于比较设置后是否发生变化
lastPermissionState = areNotificationsEnabled() lastPermissionState = areNotificationsEnabled()
when { when {
// Android 13+ - 使用运行时权限请求 // Android 13+ - 使用运行时权限请求
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
@ -198,19 +198,18 @@ class NotificationPermissionHandler(
} }
val activity = context as? AppCompatActivity ?: return val activity = context as? AppCompatActivity ?: return
when { when {
// 已经拥有权限 // 已经拥有权限
areNotificationsEnabled() -> { areNotificationsEnabled() -> {
callback(PermissionResult.GRANTED) callback(PermissionResult.GRANTED)
} }
// 需要显示权限解释 // 需要显示权限解释
ActivityCompat.shouldShowRequestPermissionRationale( // ActivityCompat.shouldShowRequestPermissionRationale(
activity, // activity,
Manifest.permission.POST_NOTIFICATIONS // Manifest.permission.POST_NOTIFICATIONS
) -> { // ) -> {
showPermissionRationaleDialog() // showPermissionRationaleDialog()
} // }
// 请求权限 // 请求权限
else -> { else -> {
permissionRequestPending = true permissionRequestPending = true
@ -276,6 +275,7 @@ class NotificationPermissionHandler(
} }
companion object { companion object {
private const val TAG = "NotificationPermissionH"
/** /**
* 快速检查当前设备的通知权限状态 * 快速检查当前设备的通知权限状态
*/ */

View File

@ -103,31 +103,22 @@ public class ShellUtils {
public static String execRootCmdAndGetResult(String cmd) { public static String execRootCmdAndGetResult(String cmd) {
Log.d("ShellUtils", "execRootCmdAndGetResult - Started execution for command: " + cmd); Log.d("ShellUtils", "execRootCmdAndGetResult - Started execution for command: " + cmd);
if (cmd == null || cmd.trim().isEmpty()) { // if (!isCommandSafe(cmd)) { // 检查命令的合法性
LogUtils.e(Log.ERROR, "ShellUtils", "Unsafe or empty command. Aborting execution.", null); // Log.e("ShellUtils", "Detected unsafe command. Aborting execution.");
throw new IllegalArgumentException("Unsafe or empty command."); // throw new IllegalArgumentException("Detected unsafe command.");
} // }
// if (!isCommandSafe(cmd)) { // 检查命令的合法性
// Log.e("ShellUtils", "Detected unsafe command. Aborting execution.");
// throw new IllegalArgumentException("Detected unsafe command.");
// }
Process process = null; Process process = null;
ExecutorService executor = Executors.newFixedThreadPool(2); ExecutorService executor = Executors.newFixedThreadPool(2);
try { try {
Log.d("ShellUtils", "Determining appropriate shell for execution...");
if (hasBin("su")) { if (hasBin("su")) {
Log.d("ShellUtils", "'su' binary found, using 'su' shell.");
process = Runtime.getRuntime().exec("su"); process = Runtime.getRuntime().exec("su");
} else if (hasBin("xu")) { } else if (hasBin("xu")) {
Log.d("ShellUtils", "'xu' binary found, using 'xu' shell.");
process = Runtime.getRuntime().exec("xu"); process = Runtime.getRuntime().exec("xu");
} else if (hasBin("vu")) { } else if (hasBin("vu")) {
Log.d("ShellUtils", "'vu' binary found, using 'vu' shell.");
process = Runtime.getRuntime().exec("vu"); process = Runtime.getRuntime().exec("vu");
} else { } else {
Log.d("ShellUtils", "No specific binary found, using 'sh' shell.");
process = Runtime.getRuntime().exec("sh"); process = Runtime.getRuntime().exec("sh");
} }
@ -137,7 +128,6 @@ public class ShellUtils {
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream, StandardCharsets.UTF_8))) { BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream, StandardCharsets.UTF_8))) {
Log.d("ShellUtils", "Starting separate thread to process error stream...");
executor.submit(() -> { executor.submit(() -> {
String line; String line;
try { try {
@ -149,28 +139,21 @@ public class ShellUtils {
} }
}); });
Log.d("ShellUtils", "Writing the command to the shell...");
os.write((cmd + "\n").getBytes()); os.write((cmd + "\n").getBytes());
os.write("exit\n".getBytes()); os.write("exit\n".getBytes());
os.flush(); os.flush();
Log.d("ShellUtils", "Command written to shell. Waiting for process to complete.");
StringBuilder output = new StringBuilder(); StringBuilder output = new StringBuilder();
String line; String line;
while ((line = br.readLine()) != null) { while ((line = br.readLine()) != null) {
Log.d("ShellUtils", "Shell Output: " + line);
output.append(line).append("\n"); output.append(line).append("\n");
} }
Log.d("ShellUtils", "Awaiting process termination...");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!process.waitFor(10, TimeUnit.SECONDS)) { if (!process.waitFor(10, TimeUnit.SECONDS)) {
LogUtils.e(Log.ERROR, "ShellUtils", "Process execution timed out. Destroying process.", null);
process.destroyForcibly(); process.destroyForcibly();
throw new RuntimeException("Shell command execution timeout."); throw new RuntimeException("Shell command execution timeout.");
} }
} else { } else {
Log.d("ShellUtils", "Using manual time tracking method for process termination (API < 26).");
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
while (true) { while (true) {
try { try {
@ -178,7 +161,6 @@ public class ShellUtils {
break; break;
} catch (IllegalThreadStateException e) { } catch (IllegalThreadStateException e) {
if (System.currentTimeMillis() - startTime > 10000) { // 10 seconds if (System.currentTimeMillis() - startTime > 10000) { // 10 seconds
LogUtils.e(Log.ERROR, "ShellUtils", "Process execution timed out (manual tracking). Destroying process.", null);
process.destroy(); process.destroy();
throw new RuntimeException("Shell command execution timeout."); throw new RuntimeException("Shell command execution timeout.");
} }
@ -186,8 +168,6 @@ public class ShellUtils {
} }
} }
} }
Log.d("ShellUtils", "Process terminated successfully. Returning result.");
return output.toString().trim(); return output.toString().trim();
} }
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
@ -196,7 +176,6 @@ public class ShellUtils {
return "Error: " + e.getMessage(); return "Error: " + e.getMessage();
} finally { } finally {
if (process != null) { if (process != null) {
Log.d("ShellUtils", "Finalizing process. Attempting to destroy it.");
process.destroy(); process.destroy();
} }
executor.shutdown(); executor.shutdown();
@ -206,10 +185,9 @@ public class ShellUtils {
public static void execRootCmd(String cmd) { public static void execRootCmd(String cmd) {
// 校验命令是否安全 // 校验命令是否安全
if (!isCommandSafe(cmd)) { // if (!isCommandSafe(cmd)) {
LogUtils.e(Log.ERROR, "ShellUtils", "Unsafe command, aborting.", null); // return;
return; // }
}
List<String> cmds = new ArrayList<>(); List<String> cmds = new ArrayList<>();
cmds.add(cmd); cmds.add(cmd);

View File

@ -0,0 +1,75 @@
package com.android.grape.util
import android.content.Context
import com.android.grape.util.TimeZoneUtils.formatTimeZoneOffset
import java.util.*
import kotlin.math.abs
object TimeZoneUtils {
/**
* 从偏移时间获取最匹配的时区ID
*
* @param offsetMillis 时区偏移毫秒
* @param useDaylight 是否考虑夏令时
*/
fun getBestMatchTimeZoneId(offsetMillis: Int, useDaylight: Boolean = false): String {
val availableIds = TimeZone.getAvailableIDs()
// 首选:完全匹配的时区
availableIds.firstOrNull { id ->
val tz = TimeZone.getTimeZone(id)
if (useDaylight) {
tz.getOffset(System.currentTimeMillis()) == offsetMillis
} else {
tz.rawOffset == offsetMillis
}
}?.let { return it }
// 次选:最接近的时区
return availableIds.minByOrNull { id ->
val tz = TimeZone.getTimeZone(id)
val diff = if (useDaylight) {
abs(tz.getOffset(System.currentTimeMillis()) - offsetMillis)
} else {
abs(tz.rawOffset - offsetMillis)
}
diff
} ?: TimeZone.getDefault().id
}
/**
* 获取时区偏移的格式化字符串
*/
fun formatTimeZoneOffset(offsetMillis: Int): String {
val hours = offsetMillis / (1000 * 60 * 60)
val minutes = abs(offsetMillis) / (1000 * 60) % 60
return when {
hours > 0 -> String.format("UTC+%d:%02d", hours, minutes)
hours < 0 -> String.format("UTC%d:%02d", hours, minutes) // 自动显示负号
else -> "UTC"
}
}
/**
* 从设备获取当前时区偏移
*/
fun getCurrentTimeZoneOffset(): Int {
return TimeZone.getDefault().getOffset(System.currentTimeMillis())
}
/**
* 将时间转换为指定时区偏移的时间
*/
fun convertTimeToOffset(time: Date, targetOffsetMillis: Int): Date {
val currentOffset = TimeZone.getDefault().getOffset(time.time)
val diff = targetOffsetMillis - currentOffset
return Date(time.time + diff)
}
}
fun main() {
val offset = formatTimeZoneOffset(19800000)
println("当前时区偏移:$offset")
}

View File

@ -73,7 +73,7 @@ object Util {
private const val sendRefer = true private const val sendRefer = true
var isNeedRestored: Boolean = false var isNeedRestored: Boolean = false
const val AUTO_PACKAGENAME: String = "com.play4u.luabox" const val AUTO_PACKAGENAME: String = "com.play4u.luabox"
const val AUTO_JSPACKAGENAME: String = "org.autojs.autojs" const val AUTO_JSPACKAGENAME: String = "org.autojs.autojs6"
const val proxy_packagename: String = "com.tunnelworkshop.postern" const val proxy_packagename: String = "com.tunnelworkshop.postern"
const val AUTO_CLASSNAME: String = "com.cyjh.elfin.activity.SplashActivity" const val AUTO_CLASSNAME: String = "com.cyjh.elfin.activity.SplashActivity"
private const val hookPackageName = "com.affsystem.androidhooker" private const val hookPackageName = "com.affsystem.androidhooker"
@ -275,10 +275,7 @@ object Util {
} }
fun isInstallRet(): Boolean { fun isInstallRet(): Boolean {
if (installRet == null) { return installRet?:false
return false
}
return installRet!!
} }
fun setAfLog(afLogV: String) { fun setAfLog(afLogV: String) {
@ -699,8 +696,8 @@ object Util {
val result: String? = MyPost.postData(" ".toByteArray(charset = Charsets.UTF_8), val result: String? = MyPost.postData(" ".toByteArray(charset = Charsets.UTF_8),
"$url?$params" "$url?$params"
) )
LogUtils.e("IOSTQ:execReloginTask->result:$result")
if (result != null && result.length > 0) { if (result != null && result.isNotEmpty()) {
taskJson = JSONObject(result).apply { taskJson = JSONObject(result).apply {
val code = getInt("code") val code = getInt("code")
if (code == 1) { if (code == 1) {
@ -752,9 +749,9 @@ object Util {
"$url?$params" "$url?$params"
) )
printStr("request result : $result") LogUtils.e("request result : $result")
if (result != null && result.length > 0) { if (result != null && result.isNotEmpty()) {
taskJson = JSONObject(result) taskJson = JSONObject(result)
val code = taskJson!!.getInt("code") val code = taskJson!!.getInt("code")
@ -765,7 +762,7 @@ object Util {
execSetJson(context) execSetJson(context)
clickTime = 1 clickTime = 1
} else { } else {
LogUtils.i(TAG, "request result code invalid : "+code); LogUtils.i(TAG, "request result code invalid : $code");
setFinish(context) setFinish(context)
} }
} else { } else {
@ -788,7 +785,6 @@ object Util {
nRandom++ nRandom++
if (nRandom % 3 == 0) { if (nRandom % 3 == 0) {
// execInstallTask(context);
execReloginTask(context) execReloginTask(context)
} else { } else {
execInstallTask(context) execInstallTask(context)
@ -949,8 +945,6 @@ object Util {
videoProxy = extJo.getString("videoProxy") videoProxy = extJo.getString("videoProxy")
} }
if (extJo.has("proxy") && !extJo.isNull("proxy")) { if (extJo.has("proxy") && !extJo.isNull("proxy")) {
val proxyJo = extJo.getJSONObject("proxy") val proxyJo = extJo.getJSONObject("proxy")
proxyIp = proxyJo.getString("proxyIp") proxyIp = proxyJo.getString("proxyIp")
@ -1048,7 +1042,7 @@ object Util {
val ret: String? = MyPost.postData("".toByteArray(), url) val ret: String? = MyPost.postData("".toByteArray(), url)
Log.i(TAG, "ret:$ret") Log.i(TAG, "ret:$ret")
val jo = JSONObject(ret) val jo = JSONObject(ret?:"")
if (jo.getInt("code") == 1) { if (jo.getInt("code") == 1) {
regEmailJson = jo.getJSONObject("emailInfo") regEmailJson = jo.getJSONObject("emailInfo")
@ -1146,7 +1140,7 @@ object Util {
return false return false
} }
unzipAPkSh(script_path, "/sdcard/autojs/") unzipAPkSh(script_path, "/sdcard/script/")
delFileSh(script_path) delFileSh(script_path)
} }
return isDownload return isDownload
@ -1557,10 +1551,12 @@ object Util {
fun openRecordApp(context: Context) { fun openRecordApp(context: Context) {
if (scriptOpenApp == 0) { if (scriptOpenApp == 0) {
execTargetApp( recordPackageName?.let {
context, execTargetApp(
recordPackageName!! context,
) it
)
}
} }
if (!isCanAuto || canAutoLc.length <= 10) { if (!isCanAuto || canAutoLc.length <= 10) {
@ -1621,10 +1617,12 @@ object Util {
if (!file.exists()) { if (!file.exists()) {
val parent = file.parentFile val parent = file.parentFile
if (parent!!.exists()) { if (parent?.exists() == true) {
file.mkdir() file.mkdir()
} else { } else {
forceCreteDir(parent) if (parent != null) {
forceCreteDir(parent)
}
file.mkdir() file.mkdir()
} }
} }
@ -1634,12 +1632,14 @@ object Util {
if (!file.exists()) { if (!file.exists()) {
val parent = file.parentFile val parent = file.parentFile
if (!parent!!.exists()) { parent?.exists()?.let {
MockTools.exec("mkdir $parent") if (!it) {
forceMakeDir(parent) MockTools.exec("mkdir $parent")
// file.mkdirs(); forceMakeDir(parent)
} else { // file.mkdirs();
MockTools.exec("mkdir $file") } else {
MockTools.exec("mkdir $file")
}
} }
} }
} }
@ -1767,7 +1767,11 @@ object Util {
} }
val create_dir = file.parentFile val create_dir = file.parentFile
forceMakeDir(create_dir!!) if (create_dir != null && !create_dir.exists()){
forceMakeDir(create_dir)
}else if (create_dir == null){
return false
}
val fileLength = 0L val fileLength = 0L
@ -1780,7 +1784,7 @@ object Util {
try { try {
conn = url.openConnection() as HttpURLConnection conn = url.openConnection() as HttpURLConnection
`is` = conn!!.inputStream `is` = conn.inputStream
fos = FileOutputStream(file) fos = FileOutputStream(file)
val buf = ByteArray(256) val buf = ByteArray(256)
conn.connect() conn.connect()
@ -1809,15 +1813,15 @@ object Util {
e.printStackTrace() e.printStackTrace()
} finally { } finally {
try { try {
conn!!.disconnect() conn?.disconnect()
} catch (e: Exception) { } catch (e: Exception) {
} }
try { try {
fos!!.close() fos?.close()
} catch (e: Exception) { } catch (e: Exception) {
} }
try { try {
`is`!!.close() `is`?.close()
} catch (e: Exception) { } catch (e: Exception) {
} }
} }
@ -2049,35 +2053,39 @@ object Util {
} }
fun installApks4Tmp(apkName: String?, context: Context?): Boolean { fun installApks4Tmp(apkName: String?, context: Context): Boolean {
var result = MockTools.execRead("pm install-create") var result = MockTools.execRead("pm install-create")
Log.d(TAG, "installApks4Tmp: successMsg $result") Log.d(TAG, "installApks4Tmp: successMsg $result")
val session = result.substring(result.indexOf("[") + 1, result.indexOf("]")) val session = result.substring(result.indexOf("[") + 1, result.indexOf("]"))
Log.d(TAG, "installApks4Tmp: session $session") Log.d(TAG, "installApks4Tmp: session $session")
val file = File("/sdcard/apks/$apkName") val file = File(getRecordDataDirName(context))
Log.d(TAG, "installApks4Tmp: ${file.absolutePath}")
val files = file.listFiles() val files = file.listFiles()
var bool = true var bool = true
var currentApkFile = 1 var currentApkFile = 1
for (f in files) { files?.let {
val extraName = f.name.substring(f.name.lastIndexOf('.') + 1) for (f in it) {
Log.d( val extraName = f.name.substring(f.name.lastIndexOf('.') + 1)
TAG, Log.d(
"installApks4Tmp: extraName $extraName" TAG,
) "installApks4Tmp: extraName $extraName"
if ("apk" == extraName) { )
//Log.d(TAG, "installApks4Tmp: getPath " + f.getPath()); if ("apk" == extraName) {
val commond = //Log.d(TAG, "installApks4Tmp: getPath " + f.getPath());
"pm install-write " + session + " " + currentApkFile + ".apk " + f.path val commond =
currentApkFile++ "pm install-write " + session + " " + currentApkFile + ".apk " + f.path
//Log.d(TAG, "installApks4Tmp: " + commond); currentApkFile++
result = MockTools.execRead(commond) //Log.d(TAG, "installApks4Tmp: " + commond);
if (!result.contains("Success")) { result = MockTools.execRead(commond)
bool = false if (!result.contains("Success")) {
break bool = false
break
}
} }
} }
} }
if (bool) { if (bool) {
result = MockTools.execRead("pm install-commit $session") result = MockTools.execRead("pm install-commit $session")
if (!result.contains("Success")) { if (!result.contains("Success")) {
@ -2086,8 +2094,10 @@ object Util {
} else { } else {
result = MockTools.execRead("pm install-abandon $session") result = MockTools.execRead("pm install-abandon $session")
} }
for (f in files) { files?.let {
MockTools.execRead("rm -rf $f") for (f in it) {
MockTools.execRead("rm -rf $f")
}
} }
setInstallRet(bool) setInstallRet(bool)
return bool return bool
@ -2423,7 +2433,7 @@ object Util {
} }
tags = filterStr(tags) tags = filterStr(tags)
// Log.e(TAG, "getCid:*"+tags+"*"); // Log.e(TAG, "getCid:*"+tags+"*");
return tags!! return tags?:""
} }
fun getPropertiesFromAssets(context: Context, fileName: String): Properties { fun getPropertiesFromAssets(context: Context, fileName: String): Properties {
@ -2561,9 +2571,9 @@ object Util {
*/ */
//当本应用位于后台时,则将它切换到最前端 //当本应用位于后台时,则将它切换到最前端
fun setTopApp(context: Context) { fun setTopApp(context: Context) {
// Log.i(TAG, "start to setTopApp"); Log.i(TAG, "start to setTopApp");
if (isRunningForeground(context)) { if (isRunningForeground(context)) {
// Log.i(TAG, "app isRunningForeground"); Log.i(TAG, "app isRunningForeground");
return; return;
} }
//获取ActivityManager //获取ActivityManager

View File

@ -9,6 +9,8 @@ import android.text.TextUtils
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.android.grape.service.MyAccessibilityService import com.android.grape.service.MyAccessibilityService
import androidx.core.content.edit
import com.android.grape.job.MonitorService
class CheckAccessibilityWorker( class CheckAccessibilityWorker(
context: Context, context: Context,
@ -32,7 +34,7 @@ class CheckAccessibilityWorker(
applicationContext.startActivity(intent) applicationContext.startActivity(intent)
// 更新状态 // 更新状态
sharedPreferences.edit().putBoolean("accessibility_prompted", true).apply() sharedPreferences.edit { putBoolean("accessibility_prompted", true) }
} }
return Result.retry() return Result.retry()
} }