Compare commits
10 Commits
ef7f148e13
...
727df6f21f
Author | SHA1 | Date |
---|---|---|
|
727df6f21f | |
|
e2800ffe8e | |
|
0239820321 | |
|
0e252b3751 | |
|
5bcecde83d | |
|
5772b91b9a | |
|
ce0933ee94 | |
|
fc62906c0e | |
|
b504984268 | |
|
2ed86fb2b7 |
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 { *; }
|
|
@ -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">
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.android.grape.net
|
||||||
|
|
||||||
|
interface ChangeCallBack {
|
||||||
|
fun changeSuccess()
|
||||||
|
fun changeFailed()
|
||||||
|
}
|
|
@ -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) // 网络错误状态码
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
package com.android.grape.net
|
|
||||||
|
|
||||||
class NetworkHelper {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 = ""
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.android.grape.provider
|
||||||
|
|
||||||
|
object DeviceInfoHelper {
|
||||||
|
// 获取设备唯一ID
|
||||||
|
fun getDeviceId(): String {
|
||||||
|
return "d1b3e7f8c9a04b8e9f2c1d5e6f7a8b9c"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
files?.let {
|
||||||
for (f in files) {
|
for (f in files) {
|
||||||
if (f.length() <= 0) {
|
if (f.length() <= 0) {
|
||||||
setSessionState(
|
setSessionState(sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName(appTempName).error(MainApplication.instance.getString(R.string.installer_error_unknown_apk_size), null).build())
|
||||||
sessionId,
|
|
||||||
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName(
|
|
||||||
appTempName
|
|
||||||
).error(
|
|
||||||
MainApplication.instance.getString(R.string.installer_error_unknown_apk_size),
|
|
||||||
null
|
|
||||||
).build()
|
|
||||||
)
|
|
||||||
unlockInstallation()
|
unlockInstallation()
|
||||||
// Toast.makeText(getContext(),"Installation failed",Toast.LENGTH_SHORT).show();
|
|
||||||
Util.setInstallRet(false)
|
Util.setInstallRet(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ensureCommandSucceeded(
|
ensureCommandSucceeded(shell.exec(Shell.Command("pm", "install-write", f.length().toString(), androidSessionId.toString(), String.format("%d.apk", currentApkFile++), f.path)))
|
||||||
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
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -35,8 +35,8 @@ 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")
|
||||||
) {
|
) {
|
||||||
|
@ -44,6 +44,7 @@ class ZipFileApkSource(context: Context, private val mZipFileDescriptor: FileDes
|
||||||
mSeenApkFile = true
|
mSeenApkFile = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
require(mSeenApkFile) { mContext.getString(R.string.installer_error_zip_contains_no_apks) }
|
require(mSeenApkFile) { mContext.getString(R.string.installer_error_zip_contains_no_apks) }
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -40,11 +40,9 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun startForegroundService() {
|
private fun startForegroundService() {
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
|
|
@ -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 {
|
|
||||||
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 {
|
try {
|
||||||
if (response.body != null) {
|
val result = sharedClient.newCall(request).execute().use { response ->
|
||||||
LogUtils.log(
|
// 读取并记录响应内容
|
||||||
Log.INFO,
|
val responseBody = if (response.body != null) response.body?.string() else "Empty response body"
|
||||||
|
val isSuccess = if (response.isSuccessful) {
|
||||||
|
LogUtils.d(
|
||||||
"ClashUtil",
|
"ClashUtil",
|
||||||
"switchProxyGroup: Switch proxy response",
|
"switchProxyGroup: Success | Status: " + response.code + " | Response: " + responseBody)
|
||||||
null
|
true
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
LogUtils.log(
|
LogUtils.d(
|
||||||
Log.ERROR,
|
|
||||||
"ClashUtil",
|
"ClashUtil",
|
||||||
"switchProxyGroup: Response body is null",
|
"switchProxyGroup: Failed | Status: " + response.code + " | Response: " + responseBody)
|
||||||
null
|
false
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} finally {
|
return isSuccess
|
||||||
response.close()
|
}
|
||||||
}
|
return result
|
||||||
}
|
} catch (e: Exception) {
|
||||||
})
|
LogUtils.d("ClashUtil", "switchProxyGroup: Unexpected error", e)
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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. 可以考虑添加重试机制或通知用户
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
/**
|
/**
|
||||||
* 快速检查当前设备的通知权限状态
|
* 快速检查当前设备的通知权限状态
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -103,10 +103,6 @@ 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()) {
|
|
||||||
LogUtils.e(Log.ERROR, "ShellUtils", "Unsafe or empty command. Aborting execution.", null);
|
|
||||||
throw new IllegalArgumentException("Unsafe or empty command.");
|
|
||||||
}
|
|
||||||
// if (!isCommandSafe(cmd)) { // 检查命令的合法性
|
// if (!isCommandSafe(cmd)) { // 检查命令的合法性
|
||||||
// Log.e("ShellUtils", "Detected unsafe command. Aborting execution.");
|
// Log.e("ShellUtils", "Detected unsafe command. Aborting execution.");
|
||||||
// throw new IllegalArgumentException("Detected unsafe command.");
|
// throw new IllegalArgumentException("Detected unsafe command.");
|
||||||
|
@ -116,18 +112,13 @@ public class ShellUtils {
|
||||||
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);
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
|
@ -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,11 +1551,13 @@ object Util {
|
||||||
|
|
||||||
fun openRecordApp(context: Context) {
|
fun openRecordApp(context: Context) {
|
||||||
if (scriptOpenApp == 0) {
|
if (scriptOpenApp == 0) {
|
||||||
|
recordPackageName?.let {
|
||||||
execTargetApp(
|
execTargetApp(
|
||||||
context,
|
context,
|
||||||
recordPackageName!!
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isCanAuto || canAutoLc.length <= 10) {
|
if (!isCanAuto || canAutoLc.length <= 10) {
|
||||||
Handler(Looper.getMainLooper()).postDelayed(Runnable {
|
Handler(Looper.getMainLooper()).postDelayed(Runnable {
|
||||||
|
@ -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 {
|
||||||
|
if (parent != null) {
|
||||||
forceCreteDir(parent)
|
forceCreteDir(parent)
|
||||||
|
}
|
||||||
file.mkdir()
|
file.mkdir()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1634,7 +1632,8 @@ object Util {
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
val parent = file.parentFile
|
val parent = file.parentFile
|
||||||
|
|
||||||
if (!parent!!.exists()) {
|
parent?.exists()?.let {
|
||||||
|
if (!it) {
|
||||||
MockTools.exec("mkdir $parent")
|
MockTools.exec("mkdir $parent")
|
||||||
forceMakeDir(parent)
|
forceMakeDir(parent)
|
||||||
// file.mkdirs();
|
// file.mkdirs();
|
||||||
|
@ -1643,6 +1642,7 @@ object Util {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun writeFile1(context: Context?, dirName: String, fileName: String, contents: String) {
|
fun writeFile1(context: Context?, dirName: String, fileName: String, contents: String) {
|
||||||
val fs = File("$dirName/$fileName")
|
val fs = File("$dirName/$fileName")
|
||||||
|
@ -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,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")
|
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 {
|
||||||
|
for (f in it) {
|
||||||
val extraName = f.name.substring(f.name.lastIndexOf('.') + 1)
|
val extraName = f.name.substring(f.name.lastIndexOf('.') + 1)
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
|
@ -2078,6 +2084,8 @@ object Util {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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,9 +2094,11 @@ object Util {
|
||||||
} else {
|
} else {
|
||||||
result = MockTools.execRead("pm install-abandon $session")
|
result = MockTools.execRead("pm install-abandon $session")
|
||||||
}
|
}
|
||||||
for (f in files) {
|
files?.let {
|
||||||
|
for (f in it) {
|
||||||
MockTools.execRead("rm -rf $f")
|
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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue