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>
<SelectionState runConfigName="app">
<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>
</selectionStates>
</component>

View File

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

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<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.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
-keep class com.google.gson.** { *; }
-keep class com.google.gson.stream.** { *; }
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# 保留所有注解
-keepattributes *Annotation*
-keepattributes Signature
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# 保留枚举类
-keepclassmembers enum * {
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.
#-renamesourcefileattribute SourceFile
# 保留所有模型类(根据你的包结构调整)
-keep class com.android.grape.pad.** { *; }
-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.BIND_VPN_SERVICE"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"
android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VPN_SERVICE" />
@ -49,6 +48,11 @@
android:supportsRtl="true"
android:theme="@style/Theme.AndroidGrape"
tools:targetApi="31">
<provider
android:name=".provider.DeviceInfoProvider"
android:authorities="com.android.grape.deviceinfo.provider"
android:exported="true">
</provider>
<activity
android:name=".MainActivity"
android:exported="true">

View File

@ -3,6 +3,7 @@ package com.android.grape
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
@ -11,11 +12,19 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
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.MockTools
import com.android.grape.util.NotificationPermissionHandler
import com.android.grape.util.ScriptUtil
import com.android.grape.util.ShellUtils
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() {
private val viewModel by viewModels<MainViewModel>()
@ -24,6 +33,7 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
init()
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
@ -34,28 +44,23 @@ class MainActivity : AppCompatActivity() {
checkPermission()
ScriptUtil.registerScriptResultReceiver()
viewBinding.start.setOnClickListener {
try {
ClashUtil.startProxy(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()
}
MonitorService.onEvent(MainApplication.instance)
// DeviceDataAccessor.saveDeviceInfo(this, DeviceInfoHelper.getDeviceId(this))
}
viewBinding.stop.setOnClickListener {
ClashUtil.stopProxy(this)
// AutoJsUtil.stopAutojsScript(this)
killRecordProcess(this, packageName)
// 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() {
super.onDestroy()
ClashUtil.unregisterReceiver(this)

View File

@ -3,6 +3,7 @@ package com.android.grape
import androidx.lifecycle.ViewModel
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import com.android.grape.job.MonitorService
import com.android.grape.service.MyAccessibilityService
import com.android.grape.work.CheckAccessibilityWorker
import java.util.concurrent.TimeUnit
@ -21,6 +22,5 @@ class MainViewModel:ViewModel() {
.build()
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 org.json.JSONArray
import org.json.JSONObject
data class Device(
var advertiserId: String = "",
@ -110,4 +112,32 @@ data class Device(
var vendingVersionName: String = "",
@SerializedName("web_ua")
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 sVE: List<Double> = listOf(),
var sVS: List<Double> = listOf()
)
) {
companion object
}

View File

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

View File

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

View File

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

View File

@ -6,13 +6,17 @@ import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.JobIntentService
import com.android.grape.net.MyGet
import com.android.grape.util.ClashUtil
import com.android.grape.util.ClashUtil.getProxyPort
import com.android.grape.util.Util
import java.util.Locale
class StartVpnPortJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
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({
StartVpnServerJobService.onEvent(
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 {
private const val TAG = "IOSTQ:StartVpnPort"

View File

@ -8,6 +8,7 @@ import android.util.Log
import androidx.core.app.JobIntentService
import com.android.grape.MainApplication
import com.android.grape.util.ClashUtil
import com.android.grape.util.CountryCode
import com.android.grape.util.Util
/**
@ -27,19 +28,12 @@ class StartVpnServerJobService : JobIntentService() {
onEvent(
this@StartVpnServerJobService
)
}, 1000L)
}, 2000L)
}
}
private fun exec(): Boolean {
ClashUtil.startProxy(MainApplication.instance)
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
return ClashUtil.switchProxyGroup("PROXY", "my-socks5-proxy", "http://127.0.0.1:6170")
}
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 com.blankj.utilcode.util.LogUtils
import com.google.gson.Gson
import okhttp3.*
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull
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.util.concurrent.TimeUnit
object HttpUtils {
const val HOST: String = "https://openapi-hk.armcloud.net"
// OkHttp客户端配置
private val client: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(LoggingInterceptor()) // 添加日志拦截器
.addInterceptor(HttpLoggingInterceptor().apply {
level = Level.BODY
})
.build()
}
@ -30,47 +36,6 @@ object HttpUtils {
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 请求异步
*
@ -82,10 +47,9 @@ object HttpUtils {
fun getAsync(
url: String,
params: Map<String, String>? = null,
headers: Map<String, String>? = null,
callback: HttpCallback
) {
val request = buildGetRequest(url, params, headers)
val request = buildGetRequest(url, params)
executeRequestAsync(request, callback)
}
@ -99,31 +63,10 @@ object HttpUtils {
*/
fun postAsync(
url: String,
body: String,
headers: Map<String, String>? = null,
body: String? = null,
callback: HttpCallback
) {
val requestBody = body.toRequestBody(JSON_MEDIA_TYPE)
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)
val request = buildPostRequest(url, body)
executeRequestAsync(request, callback)
}
@ -138,9 +81,8 @@ object HttpUtils {
fun getSync(
url: String,
params: Map<String, String>? = null,
headers: Map<String, String>? = null
): Pair<String?, Int> {
val request = buildGetRequest(url, params, headers)
val request = buildGetRequest(url, params)
return executeRequestSync(request)
}
@ -154,11 +96,9 @@ object HttpUtils {
*/
fun postSync(
url: String,
body: String,
headers: Map<String, String>? = null
body: String? = null
): Pair<String?, Int> {
val requestBody = body.toRequestBody(JSON_MEDIA_TYPE)
val request = buildPostRequest(url, requestBody, headers)
val request = buildPostRequest(url, body)
return executeRequestSync(request)
}
@ -172,46 +112,72 @@ object HttpUtils {
// 内部方法 --------------------------------
private fun buildGetRequest(
url: String,
path: String ,
params: Map<String, String>?,
headers: Map<String, String>?
): Request {
val url = "$HOST$path"
val httpUrlBuilder = url.toHttpUrlOrNull()?.newBuilder()
?: throw IllegalArgumentException("Invalid URL: $url")
// 添加查询参数
var param = ""
params?.forEach { (key, value) ->
httpUrlBuilder.addQueryParameter(key, value)
param += "&${key}=${value}"
}
if (param.isNotEmpty()){
param = param.substring(1)
}
val requestBuilder = Request.Builder().url(httpUrlBuilder.build())
// 添加请求头
addHeaders(requestBuilder, headers)
val authver = "2.0"
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()
}
private fun buildPostRequest(
url: String,
body: RequestBody,
headers: Map<String, String>?
path: String,
body: String? = null
): Request {
val url = "$HOST$path"
val bodyString = body?:""
val requestBuilder = Request.Builder()
.url(url)
.post(body)
// 添加请求头
addHeaders(requestBuilder, headers)
.post(bodyString.toRequestBody(JSON_MEDIA_TYPE))
val authver = "2.0"
val timestamp = System.currentTimeMillis()
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()
}
private fun addHeaders(
builder: Request.Builder,
headers: Map<String, String>?
headers: Map<String, String>
) {
headers?.forEach { (key, value) ->
headers.forEach { (key, value) ->
builder.addHeader(key, value)
}
}
@ -252,13 +218,13 @@ object HttpUtils {
private fun executeRequestSync(request: Request): Pair<String?, Int> {
return try {
val response = client.newCall(request).execute()
val responseBody = response.body?.string()
val responseBody = response.peekBody(Long.MAX_VALUE).string()
val code = response.code
if (response.isSuccessful && responseBody != null) {
Log.d("TAG", "executeRequestSync: $responseBody")
if (response.isSuccessful && responseBody.isNotEmpty()) {
Pair(responseBody, code)
} else {
Pair(null, code)
Pair(null, -1)
}
} catch (e: IOException) {
Pair(null, -1) // 网络错误状态码

View File

@ -2,16 +2,48 @@ package com.android.grape.net
import android.content.Context
import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.TimeUnit
object 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? {
return getData(url_, ua, 0)
@ -31,7 +63,7 @@ object MyGet {
httpUrlConnection = url.openConnection() as HttpURLConnection
httpUrlConnection.allowUserInteraction = true
httpUrlConnection!!.doOutput = false
httpUrlConnection.doOutput = false
httpUrlConnection.doInput = true
httpUrlConnection.useCaches = false
httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428
@ -140,7 +172,7 @@ object MyGet {
httpUrlConnection = url.openConnection() as HttpURLConnection
httpUrlConnection.allowUserInteraction = true
httpUrlConnection!!.doOutput = false
httpUrlConnection.doOutput = false
httpUrlConnection.doInput = true
httpUrlConnection.useCaches = 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>(
mApkFiles?.size?: 0
)
for (apkFile in mApkFiles!!) apkFileDescriptors.add(NormalFileDescriptor(apkFile))
mApkFiles?.let {
for (apkFile in it) apkFileDescriptors.add(NormalFileDescriptor(apkFile))
}
apkSource = DefaultApkSource(apkFileDescriptors)
} else if (mZipFile != null) {

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@ class CopyToFileApkSource(context: Context, wrappedApkSource: ApkSource) :
IOUtils.deleteRecursively(it)
}
mCurrentApkFile = File(mTempDir, mWrappedApkSource.apkName)
mCurrentApkFile = File(mTempDir, mWrappedApkSource.apkName?:"")
mWrappedApkSource.openApkInputStream().use { `in` ->
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 {
mApkSigner?.sign(
it,

View File

@ -38,11 +38,11 @@ object FileUtils {
}
private fun trimFilename(res: StringBuilder, maxBytes: Int) {
var maxBytes = maxBytes
var bytes = maxBytes
var raw = res.toString().toByteArray(StandardCharsets.UTF_8)
if (raw.size > maxBytes) {
maxBytes -= 3
while (raw.size > maxBytes) {
if (raw.size > bytes) {
bytes -= 3
while (raw.size > bytes) {
res.deleteCharAt(res.length / 2)
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)
override fun nextApk(): Boolean {
if (!mIsOpen) {
mZipInputStream = ZipInputStream(mZipFileDescriptor.open())
mWrappedStream = ZipInputStreamWrapper(mZipInputStream!!)
mZipInputStream = ZipInputStream(mZipFileDescriptor.open()).apply {
mWrappedStream = ZipInputStreamWrapper(this)
}
mIsOpen = true
}
do {
try {
entry = mZipInputStream!!.nextEntry
entry = mZipInputStream?.nextEntry
} catch (e: ZipException) {
if (e.message == "only DEFLATED entries can have EXT descriptor") {
throw ZipException("only DEFLATED entries can have EXT descriptor")

View File

@ -35,8 +35,8 @@ class ZipFileApkSource(context: Context, private val mZipFileDescriptor: FileDes
if (mZipFile == null) copyAndOpenZip()
entry = null
while (entry == null && mZipEntries!!.hasMoreElements()) {
val nextEntry = mZipEntries!!.nextElement()
while (entry == null && mZipEntries?.hasMoreElements() == true) {
mZipEntries?.nextElement()?.let { nextEntry ->
if (!nextEntry.isDirectory && nextEntry.name.lowercase(Locale.getDefault())
.endsWith(".apk")
) {
@ -44,6 +44,7 @@ class ZipFileApkSource(context: Context, private val mZipFileDescriptor: FileDes
mSeenApkFile = true
}
}
}
if (entry == null) {
require(mSeenApkFile) { mContext.getString(R.string.installer_error_zip_contains_no_apks) }

View File

@ -138,7 +138,7 @@ class PackageMeta : Parcelable {
return Builder(applicationInfo.packageName)
.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)
.setVersionCode(if (Utils.apiIsAtLeast(Build.VERSION_CODES.P)) packageInfo.longVersionCode else packageInfo.versionCode.toLong())
.setVersionName(packageInfo.versionName)

View File

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

View File

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

View File

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

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 com.android.grape.MainApplication
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.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 java.lang.reflect.InvocationTargetException
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() {
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")
if (deviceObject == null) {
LogUtils.d("ERROR", "ChangeDeviceInfoUtil", "device is null")
@ -32,7 +193,7 @@ object ChangeDeviceInfoUtil {
vcloudsettingsPut("$currentPkgName.vendor", device.expand.glVendor, context)
// callVCloudSettings_put("$currentPkgName.battery_scale", batteryScale.toString(), 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.dpi", device.expand.dPI.toString(), context)
// callVCloudSettings_put(
@ -51,7 +212,7 @@ object ChangeDeviceInfoUtil {
// **os_ver**
vcloudsettingsPut(currentPkgName + "_os_ver", device.sdkVer, context)
// **tz** (时区)
vcloudsettingsPut(currentPkgName + "_tz", device.tzOffTime.toString(), context)
vcloudsettingsPut(currentPkgName + "_tz", TimeZoneUtils.formatTimeZoneOffset(device.tzOffTime), context)
vcloudsettingsPut("$currentPkgName.advertiserId", device.advertiserId, 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.gpu.gl_vendor ${device.expand.glVendor}")
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 这个格式
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")
@ -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()) {
LogUtils.e(Log.ERROR, "ChangeDeviceInfoUtil", "Key cannot be null or empty", null)
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.Intent
import android.content.IntentFilter
import android.os.Environment
import android.util.Log
import androidx.core.content.ContextCompat
import com.blankj.utilcode.util.LogUtils
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONException
import org.json.JSONObject
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
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) {
val intent = Intent("com.github.kr328.clash.intent.action.SESSION_CREATE")
intent.putExtra("profile", "default") // 可选择您在 Clash 中配置的 Profile
@ -35,6 +46,66 @@ object ClashUtil {
}.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
val clashStatusReceiver: BroadcastReceiver = object : BroadcastReceiver() {
@ -90,26 +161,7 @@ object ClashUtil {
}
fun switchProxyGroup(groupName: String?, proxyName: String?, controllerUrl: String) {
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")
}
fun switchProxyGroup(groupName: String?, proxyName: String?, controllerUrl: String): Boolean {
val client = OkHttpClient()
val json = JSONObject()
@ -133,39 +185,27 @@ object ClashUtil {
.put(requestBody)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
LogUtils.log(
Log.ERROR,
"ClashUtil",
"switchProxyGroup: Failed to switch proxy",
e
)
println("Failed to switch proxy: " + e.message)
}
@Throws(IOException::class)
override fun onResponse(call: Call, response: Response) {
try {
if (response.body != null) {
LogUtils.log(
Log.INFO,
val result = sharedClient.newCall(request).execute().use { response ->
// 读取并记录响应内容
val responseBody = if (response.body != null) response.body?.string() else "Empty response body"
val isSuccess = if (response.isSuccessful) {
LogUtils.d(
"ClashUtil",
"switchProxyGroup: Switch proxy response",
null
)
"switchProxyGroup: Success | Status: " + response.code + " | Response: " + responseBody)
true
} else {
LogUtils.log(
Log.ERROR,
LogUtils.d(
"ClashUtil",
"switchProxyGroup: Response body is null",
null
)
"switchProxyGroup: Failed | Status: " + response.code + " | Response: " + responseBody)
false
}
} 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
import android.os.Environment
import android.util.Log
import com.blankj.utilcode.util.LogUtils
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.BufferedReader
@ -16,6 +18,7 @@ import java.io.InputStreamReader
import java.io.OutputStream
import java.io.RandomAccessFile
import java.io.UnsupportedEncodingException
import java.nio.charset.StandardCharsets
import java.util.Enumeration
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
@ -387,4 +390,31 @@ object FileUtils {
output.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 {
var retString = ""
try {
//创建socket
val myCmd = "SU|$cmd"
val mSocket = Socket()
val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345)
mSocket.connect(inetSocketAddress)
val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream()))
val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream()))
bufferedWriter.write(myCmd + "\r\n")
bufferedWriter.flush()
val stringBuilder = StringBuilder()
var line: String? = null
while ((bufferedReader.readLine().also { line = it }) != null) {
stringBuilder.append(line + "\n")
}
//retString = bufferedReader.readLine();
retString = stringBuilder.toString()
bufferedReader.close()
bufferedWriter.close()
retString = ShellUtils.execRootCmdAndGetResult(cmd)
// //创建socket
// val myCmd = "SU|$cmd"
//
// val mSocket = Socket()
// val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345)
// mSocket.connect(inetSocketAddress)
// val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream()))
// val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream()))
// bufferedWriter.write(myCmd + "\r\n")
// bufferedWriter.flush()
// val stringBuilder = StringBuilder()
// var line: String? = null
// while ((bufferedReader.readLine().also { line = it }) != null) {
// stringBuilder.append(line + "\n")
// }
// //retString = bufferedReader.readLine();
// retString = stringBuilder.toString()
// bufferedReader.close()
// bufferedWriter.close()
} catch (eeeee: Exception) {
eeeee.printStackTrace()
}
@ -40,25 +41,26 @@ object MockTools {
fun execRead(cmd: String): String {
var retString = ""
try {
//创建socket
val myCmd = "SU_1|$cmd"
val mSocket = Socket()
val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345)
mSocket.connect(inetSocketAddress)
val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream()))
val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream()))
bufferedWriter.write(myCmd + "\r\n")
bufferedWriter.flush()
val stringBuilder = StringBuilder()
var line: String? = null
while ((bufferedReader.readLine().also { line = it }) != null) {
stringBuilder.append(line + "\n")
}
retString = stringBuilder.toString()
//retString = bufferedReader.readLine();
bufferedReader.close()
bufferedWriter.close()
retString = ShellUtils.execRootCmdAndGetResult(cmd)
// //创建socket
// val myCmd = "SU_1|$cmd"
//
// val mSocket = Socket()
// val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345)
// mSocket.connect(inetSocketAddress)
// val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream()))
// val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream()))
// bufferedWriter.write(myCmd + "\r\n")
// bufferedWriter.flush()
// val stringBuilder = StringBuilder()
// var line: String? = null
// while ((bufferedReader.readLine().also { line = it }) != null) {
// stringBuilder.append(line + "\n")
// }
// retString = stringBuilder.toString()
// //retString = bufferedReader.readLine();
// bufferedReader.close()
// bufferedWriter.close()
} catch (eeeee: Exception) {
eeeee.printStackTrace()
}

View File

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

View File

@ -103,31 +103,22 @@ public class ShellUtils {
public static String execRootCmdAndGetResult(String cmd) {
Log.d("ShellUtils", "execRootCmdAndGetResult - Started execution for command: " + cmd);
if (cmd == null || cmd.trim().isEmpty()) {
LogUtils.e(Log.ERROR, "ShellUtils", "Unsafe or empty command. Aborting execution.", null);
throw new IllegalArgumentException("Unsafe or empty command.");
}
// if (!isCommandSafe(cmd)) { // 检查命令的合法性
// Log.e("ShellUtils", "Detected unsafe command. Aborting execution.");
// 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;
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Log.d("ShellUtils", "Determining appropriate shell for execution...");
if (hasBin("su")) {
Log.d("ShellUtils", "'su' binary found, using 'su' shell.");
process = Runtime.getRuntime().exec("su");
} else if (hasBin("xu")) {
Log.d("ShellUtils", "'xu' binary found, using 'xu' shell.");
process = Runtime.getRuntime().exec("xu");
} else if (hasBin("vu")) {
Log.d("ShellUtils", "'vu' binary found, using 'vu' shell.");
process = Runtime.getRuntime().exec("vu");
} else {
Log.d("ShellUtils", "No specific binary found, using 'sh' shell.");
process = Runtime.getRuntime().exec("sh");
}
@ -137,7 +128,6 @@ public class ShellUtils {
BufferedReader br = new BufferedReader(new InputStreamReader(is, 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(() -> {
String line;
try {
@ -149,28 +139,21 @@ public class ShellUtils {
}
});
Log.d("ShellUtils", "Writing the command to the shell...");
os.write((cmd + "\n").getBytes());
os.write("exit\n".getBytes());
os.flush();
Log.d("ShellUtils", "Command written to shell. Waiting for process to complete.");
StringBuilder output = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
Log.d("ShellUtils", "Shell Output: " + line);
output.append(line).append("\n");
}
Log.d("ShellUtils", "Awaiting process termination...");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!process.waitFor(10, TimeUnit.SECONDS)) {
LogUtils.e(Log.ERROR, "ShellUtils", "Process execution timed out. Destroying process.", null);
process.destroyForcibly();
throw new RuntimeException("Shell command execution timeout.");
}
} else {
Log.d("ShellUtils", "Using manual time tracking method for process termination (API < 26).");
long startTime = System.currentTimeMillis();
while (true) {
try {
@ -178,7 +161,6 @@ public class ShellUtils {
break;
} catch (IllegalThreadStateException e) {
if (System.currentTimeMillis() - startTime > 10000) { // 10 seconds
LogUtils.e(Log.ERROR, "ShellUtils", "Process execution timed out (manual tracking). Destroying process.", null);
process.destroy();
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();
}
} catch (IOException | InterruptedException e) {
@ -196,7 +176,6 @@ public class ShellUtils {
return "Error: " + e.getMessage();
} finally {
if (process != null) {
Log.d("ShellUtils", "Finalizing process. Attempting to destroy it.");
process.destroy();
}
executor.shutdown();
@ -206,10 +185,9 @@ public class ShellUtils {
public static void execRootCmd(String cmd) {
// 校验命令是否安全
if (!isCommandSafe(cmd)) {
LogUtils.e(Log.ERROR, "ShellUtils", "Unsafe command, aborting.", null);
return;
}
// if (!isCommandSafe(cmd)) {
// return;
// }
List<String> cmds = new ArrayList<>();
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
var isNeedRestored: Boolean = false
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 AUTO_CLASSNAME: String = "com.cyjh.elfin.activity.SplashActivity"
private const val hookPackageName = "com.affsystem.androidhooker"
@ -275,10 +275,7 @@ object Util {
}
fun isInstallRet(): Boolean {
if (installRet == null) {
return false
}
return installRet!!
return installRet?:false
}
fun setAfLog(afLogV: String) {
@ -699,8 +696,8 @@ object Util {
val result: String? = MyPost.postData(" ".toByteArray(charset = Charsets.UTF_8),
"$url?$params"
)
if (result != null && result.length > 0) {
LogUtils.e("IOSTQ:execReloginTask->result:$result")
if (result != null && result.isNotEmpty()) {
taskJson = JSONObject(result).apply {
val code = getInt("code")
if (code == 1) {
@ -752,9 +749,9 @@ object Util {
"$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)
val code = taskJson!!.getInt("code")
@ -765,7 +762,7 @@ object Util {
execSetJson(context)
clickTime = 1
} else {
LogUtils.i(TAG, "request result code invalid : "+code);
LogUtils.i(TAG, "request result code invalid : $code");
setFinish(context)
}
} else {
@ -788,7 +785,6 @@ object Util {
nRandom++
if (nRandom % 3 == 0) {
// execInstallTask(context);
execReloginTask(context)
} else {
execInstallTask(context)
@ -949,8 +945,6 @@ object Util {
videoProxy = extJo.getString("videoProxy")
}
if (extJo.has("proxy") && !extJo.isNull("proxy")) {
val proxyJo = extJo.getJSONObject("proxy")
proxyIp = proxyJo.getString("proxyIp")
@ -1048,7 +1042,7 @@ object Util {
val ret: String? = MyPost.postData("".toByteArray(), url)
Log.i(TAG, "ret:$ret")
val jo = JSONObject(ret)
val jo = JSONObject(ret?:"")
if (jo.getInt("code") == 1) {
regEmailJson = jo.getJSONObject("emailInfo")
@ -1146,7 +1140,7 @@ object Util {
return false
}
unzipAPkSh(script_path, "/sdcard/autojs/")
unzipAPkSh(script_path, "/sdcard/script/")
delFileSh(script_path)
}
return isDownload
@ -1557,11 +1551,13 @@ object Util {
fun openRecordApp(context: Context) {
if (scriptOpenApp == 0) {
recordPackageName?.let {
execTargetApp(
context,
recordPackageName!!
it
)
}
}
if (!isCanAuto || canAutoLc.length <= 10) {
Handler(Looper.getMainLooper()).postDelayed(Runnable {
@ -1621,10 +1617,12 @@ object Util {
if (!file.exists()) {
val parent = file.parentFile
if (parent!!.exists()) {
if (parent?.exists() == true) {
file.mkdir()
} else {
if (parent != null) {
forceCreteDir(parent)
}
file.mkdir()
}
}
@ -1634,7 +1632,8 @@ object Util {
if (!file.exists()) {
val parent = file.parentFile
if (!parent!!.exists()) {
parent?.exists()?.let {
if (!it) {
MockTools.exec("mkdir $parent")
forceMakeDir(parent)
// file.mkdirs();
@ -1643,6 +1642,7 @@ object Util {
}
}
}
}
fun writeFile1(context: Context?, dirName: String, fileName: String, contents: String) {
val fs = File("$dirName/$fileName")
@ -1767,7 +1767,11 @@ object Util {
}
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
@ -1780,7 +1784,7 @@ object Util {
try {
conn = url.openConnection() as HttpURLConnection
`is` = conn!!.inputStream
`is` = conn.inputStream
fos = FileOutputStream(file)
val buf = ByteArray(256)
conn.connect()
@ -1809,15 +1813,15 @@ object Util {
e.printStackTrace()
} finally {
try {
conn!!.disconnect()
conn?.disconnect()
} catch (e: Exception) {
}
try {
fos!!.close()
fos?.close()
} catch (e: Exception) {
}
try {
`is`!!.close()
`is`?.close()
} catch (e: Exception) {
}
}
@ -2049,17 +2053,19 @@ object Util {
}
fun installApks4Tmp(apkName: String?, context: Context?): Boolean {
fun installApks4Tmp(apkName: String?, context: Context): Boolean {
var result = MockTools.execRead("pm install-create")
Log.d(TAG, "installApks4Tmp: successMsg $result")
val session = result.substring(result.indexOf("[") + 1, result.indexOf("]"))
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()
var bool = true
var currentApkFile = 1
for (f in files) {
files?.let {
for (f in it) {
val extraName = f.name.substring(f.name.lastIndexOf('.') + 1)
Log.d(
TAG,
@ -2078,6 +2084,8 @@ object Util {
}
}
}
}
if (bool) {
result = MockTools.execRead("pm install-commit $session")
if (!result.contains("Success")) {
@ -2086,9 +2094,11 @@ object Util {
} else {
result = MockTools.execRead("pm install-abandon $session")
}
for (f in files) {
files?.let {
for (f in it) {
MockTools.execRead("rm -rf $f")
}
}
setInstallRet(bool)
return bool
}
@ -2423,7 +2433,7 @@ object Util {
}
tags = filterStr(tags)
// Log.e(TAG, "getCid:*"+tags+"*");
return tags!!
return tags?:""
}
fun getPropertiesFromAssets(context: Context, fileName: String): Properties {
@ -2561,9 +2571,9 @@ object Util {
*/
//当本应用位于后台时,则将它切换到最前端
fun setTopApp(context: Context) {
// Log.i(TAG, "start to setTopApp");
Log.i(TAG, "start to setTopApp");
if (isRunningForeground(context)) {
// Log.i(TAG, "app isRunningForeground");
Log.i(TAG, "app isRunningForeground");
return;
}
//获取ActivityManager

View File

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