This commit is contained in:
Administrator 2025-07-04 10:38:52 +08:00
commit 890539cbc0
131 changed files with 13846 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

6
.idea/compiler.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

429
.idea/dbnavigator.xml Normal file
View File

@ -0,0 +1,429 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DDLFileAttachmentManager">
<mappings />
<preferences />
</component>
<component name="DBNavigator.Project.DatabaseAssistantManager">
<assistants />
</component>
<component name="DBNavigator.Project.DatabaseFileManager">
<open-files />
</component>
<component name="DBNavigator.Project.Settings">
<connections />
<browser-settings>
<general>
<display-mode value="TABBED" />
<navigation-history-size value="100" />
<show-object-details value="false" />
<enable-sticky-paths value="true" />
</general>
<filters>
<object-type-filter>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="JAVA_CLASS" enabled="true" />
<object-type name="JAVA_INNER_CLASS" enabled="true" />
<object-type name="JAVA_FIELD" enabled="true" />
<object-type name="JAVA_METHOD" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
<object-type name="CREDENTIAL" enabled="true" />
<object-type name="AI_PROFILE" enabled="true" />
</object-type-filter>
</filters>
<sorting>
<object-type name="COLUMN" sorting-type="NAME" />
<object-type name="FUNCTION" sorting-type="NAME" />
<object-type name="PROCEDURE" sorting-type="NAME" />
<object-type name="ARGUMENT" sorting-type="POSITION" />
<object-type name="TYPE ATTRIBUTE" sorting-type="POSITION" />
</sorting>
<default-editors>
<object-type name="VIEW" editor-type="SELECTION" />
<object-type name="PACKAGE" editor-type="SELECTION" />
<object-type name="TYPE" editor-type="SELECTION" />
</default-editors>
</browser-settings>
<navigation-settings>
<lookup-filters>
<lookup-objects>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="false" />
<object-type name="ROLE" enabled="false" />
<object-type name="PRIVILEGE" enabled="false" />
<object-type name="CHARSET" enabled="false" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED VIEW" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET TRIGGER" enabled="true" />
<object-type name="DATABASE TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="false" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="JAVA CLASS" enabled="true" />
<object-type name="INNER CLASS" enabled="true" />
<object-type name="JAVA FIELD" enabled="true" />
<object-type name="JAVA METHOD" enabled="true" />
<object-type name="JAVA PARAMETER" enabled="true" />
<object-type name="DIMENSION" enabled="false" />
<object-type name="CLUSTER" enabled="false" />
<object-type name="DBLINK" enabled="false" />
<object-type name="CREDENTIAL" enabled="false" />
</lookup-objects>
<force-database-load value="false" />
<prompt-connection-selection value="true" />
<prompt-schema-selection value="true" />
</lookup-filters>
</navigation-settings>
<dataset-grid-settings>
<general>
<enable-zooming value="true" />
<enable-column-tooltip value="true" />
</general>
<sorting>
<nulls-first value="true" />
<max-sorting-columns value="4" />
</sorting>
<audit-columns>
<column-names value="" />
<visible value="true" />
<editable value="false" />
</audit-columns>
</dataset-grid-settings>
<dataset-editor-settings>
<text-editor-popup>
<active value="false" />
<active-if-empty value="false" />
<data-length-threshold value="100" />
<popup-delay value="1000" />
</text-editor-popup>
<values-actions-popup>
<show-popup-button value="true" />
<element-count-threshold value="1000" />
<data-length-threshold value="250" />
</values-actions-popup>
<general>
<fetch-block-size value="100" />
<fetch-timeout value="30" />
<trim-whitespaces value="true" />
<convert-empty-strings-to-null value="true" />
<select-content-on-cell-edit value="true" />
<large-value-preview-active value="true" />
</general>
<filters>
<prompt-filter-dialog value="true" />
<default-filter-type value="BASIC" />
</filters>
<qualified-text-editor text-length-threshold="300">
<content-types>
<content-type name="Text" enabled="true" />
<content-type name="Properties" enabled="true" />
<content-type name="XML" enabled="true" />
<content-type name="DTD" enabled="true" />
<content-type name="HTML" enabled="true" />
<content-type name="XHTML" enabled="true" />
<content-type name="Java" enabled="true" />
<content-type name="SQL" enabled="true" />
<content-type name="PL/SQL" enabled="true" />
<content-type name="JSON" enabled="true" />
<content-type name="JSON5" enabled="true" />
<content-type name="Groovy" enabled="true" />
<content-type name="AIDL" enabled="true" />
<content-type name="YAML" enabled="true" />
<content-type name="Manifest" enabled="true" />
</content-types>
</qualified-text-editor>
<record-navigation>
<navigation-target value="VIEWER" />
</record-navigation>
</dataset-editor-settings>
<code-editor-settings>
<general>
<show-object-navigation-gutter value="false" />
<show-spec-declaration-navigation-gutter value="true" />
<enable-spellchecking value="true" />
<enable-reference-spellchecking value="false" />
</general>
<confirmations>
<save-changes value="false" />
<revert-changes value="true" />
<exit-on-changes value="ASK" />
</confirmations>
</code-editor-settings>
<code-completion-settings>
<filters>
<basic-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="false" />
<filter-element type="OBJECT" id="view" selected="false" />
<filter-element type="OBJECT" id="materialized view" selected="false" />
<filter-element type="OBJECT" id="index" selected="false" />
<filter-element type="OBJECT" id="constraint" selected="false" />
<filter-element type="OBJECT" id="trigger" selected="false" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="false" />
<filter-element type="OBJECT" id="procedure" selected="false" />
<filter-element type="OBJECT" id="function" selected="false" />
<filter-element type="OBJECT" id="package" selected="false" />
<filter-element type="OBJECT" id="type" selected="false" />
<filter-element type="OBJECT" id="dimension" selected="false" />
<filter-element type="OBJECT" id="cluster" selected="false" />
<filter-element type="OBJECT" id="dblink" selected="false" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</basic-filter>
<extended-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</extended-filter>
</filters>
<sorting enabled="true">
<sorting-element type="RESERVED_WORD" id="keyword" />
<sorting-element type="RESERVED_WORD" id="datatype" />
<sorting-element type="OBJECT" id="column" />
<sorting-element type="OBJECT" id="table" />
<sorting-element type="OBJECT" id="view" />
<sorting-element type="OBJECT" id="materialized view" />
<sorting-element type="OBJECT" id="index" />
<sorting-element type="OBJECT" id="constraint" />
<sorting-element type="OBJECT" id="trigger" />
<sorting-element type="OBJECT" id="synonym" />
<sorting-element type="OBJECT" id="sequence" />
<sorting-element type="OBJECT" id="procedure" />
<sorting-element type="OBJECT" id="function" />
<sorting-element type="OBJECT" id="package" />
<sorting-element type="OBJECT" id="type" />
<sorting-element type="OBJECT" id="dimension" />
<sorting-element type="OBJECT" id="cluster" />
<sorting-element type="OBJECT" id="dblink" />
<sorting-element type="OBJECT" id="schema" />
<sorting-element type="OBJECT" id="role" />
<sorting-element type="OBJECT" id="user" />
<sorting-element type="RESERVED_WORD" id="function" />
<sorting-element type="RESERVED_WORD" id="parameter" />
</sorting>
<format>
<enforce-code-style-case value="true" />
</format>
</code-completion-settings>
<execution-engine-settings>
<statement-execution>
<fetch-block-size value="100" />
<execution-timeout value="20" />
<debug-execution-timeout value="600" />
<focus-result value="false" />
<prompt-execution value="false" />
</statement-execution>
<script-execution>
<command-line-interfaces />
<execution-timeout value="300" />
</script-execution>
<method-execution>
<execution-timeout value="30" />
<debug-execution-timeout value="600" />
<parameter-history-size value="10" />
</method-execution>
</execution-engine-settings>
<operation-settings>
<transactions>
<uncommitted-changes>
<on-project-close value="ASK" />
<on-disconnect value="ASK" />
<on-autocommit-toggle value="ASK" />
</uncommitted-changes>
<multiple-uncommitted-changes>
<on-commit value="ASK" />
<on-rollback value="ASK" />
</multiple-uncommitted-changes>
</transactions>
<session-browser>
<disconnect-session value="ASK" />
<kill-session value="ASK" />
<reload-on-filter-change value="false" />
</session-browser>
<compiler>
<compile-type value="KEEP" />
<compile-dependencies value="ASK" />
<always-show-controls value="false" />
</compiler>
</operation-settings>
<ddl-file-settings>
<extensions>
<mapping file-type-id="VIEW" extensions="vw" />
<mapping file-type-id="TRIGGER" extensions="trg" />
<mapping file-type-id="PROCEDURE" extensions="prc" />
<mapping file-type-id="FUNCTION" extensions="fnc" />
<mapping file-type-id="PACKAGE" extensions="pkg" />
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
<mapping file-type-id="TYPE" extensions="tpe" />
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
<mapping file-type-id="JAVA_SOURCE" extensions="sql" />
</extensions>
<general>
<lookup-ddl-files value="true" />
<create-ddl-files value="false" />
<synchronize-ddl-files value="true" />
<use-qualified-names value="false" />
<make-scripts-rerunnable value="true" />
</general>
</ddl-file-settings>
<assistant-settings>
<credential-settings>
<credentials />
</credential-settings>
</assistant-settings>
<general-settings>
<regional-settings>
<date-format value="MEDIUM" />
<number-format value="UNGROUPED" />
<locale value="SYSTEM_DEFAULT" />
<use-custom-formats value="false" />
</regional-settings>
<environment>
<environment-types>
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
</environment-types>
<visibility-settings>
<connection-tabs value="true" />
<dialog-headers value="true" />
<object-editor-tabs value="true" />
<script-editor-tabs value="false" />
<execution-result-tabs value="true" />
</visibility-settings>
</environment>
</general-settings>
</component>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

18
.idea/gradle.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

10
.idea/migrations.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

10
.idea/misc.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

60
app/build.gradle.kts Normal file
View File

@ -0,0 +1,60 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.android.grape"
compileSdk = 35
defaultConfig {
applicationId = "com.android.grape"
minSdk = 23
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
aidl = true
viewBinding = true
buildConfig = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.work.runtime.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation ("com.blankj:utilcodex:1.31.1")
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0"))
implementation("com.squareup.okhttp3:okhttp")
implementation("com.squareup.okhttp3:logging-interceptor")
implementation ("dev.rikka.shizuku:api:11.0.3")
implementation ("dev.rikka.shizuku:provider:11.0.3")
implementation ("com.google.code.gson:gson:2.10.1")
}

21
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,24 @@
package com.android.grape
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.android.grape", appContext.packageName)
}
}

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.BIND_VPN_SERVICE"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VPN_SERVICE" />
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.MANAGE_USERS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.CREATE_USERS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.QUERY_USERS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_PHONE_STATE"
tools:ignore="CoarseFineLocation" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
tools:ignore="CoarseFineLocation" />
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.MEDIA_PROJECTION" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidGrape"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.android.grape.sai.rootless.ConfirmationIntentWrapperActivity2" />
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"
android:multiprocess="false"
android:enabled="true"
android:exported="true"
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
<service android:name="com.android.grape.job.MonitorService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service android:name="com.android.grape.job.StartVpnServerJobService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<service android:name="com.android.grape.job.AutoJobService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<service android:name="com.android.grape.job.CheckIpJobService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<service android:name="com.android.grape.job.DownloadAppJobService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<service android:name="com.android.grape.job.InstallService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<service android:name="com.android.grape.job.OpenAppService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<service android:name="com.android.grape.job.RecoverJobService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<service android:name="com.android.grape.job.SendCallbackJobService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<service android:name="com.android.grape.job.UnInstallService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<service android:name="com.android.grape.service.MyAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:enabled="true"
android:foregroundServiceType="mediaProjection"
android:exported="true">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
<receiver android:name="com.android.grape.receiver.ScriptReceiver" />
</application>
</manifest>

View File

@ -0,0 +1,12 @@
// IMikRom.aidl
package android.app;
// Declare any non-default types here with import statements
interface IMikRom {
String shellExec(String cmd);
String readFile(String path);
void writeFile(String path,String data);
void setEnableApp(String pkgName,boolean isEnable);
boolean isEnableApp(String pkgName);
}

View File

@ -0,0 +1,133 @@
package com.android.grape
import android.annotation.SuppressLint
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.android.grape.databinding.ActivityMainBinding
import com.android.grape.job.MonitorService
import com.android.grape.receiver.ScriptReceiver
import com.android.grape.util.ClashUtil
import com.android.grape.util.StoragePermissionHelper
import com.blankj.utilcode.util.LogUtils
class MainActivity : AppCompatActivity() {
private val viewModel by viewModels<MainViewModel>()
private lateinit var viewBinding: ActivityMainBinding
private var intentFilter: IntentFilter? = null
private var scriptReceiver: ScriptReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
checkPermission()
registerReceiver()
viewBinding.start.setOnClickListener {
try {
ClashUtil.startProxy(this) // 在主线程中调用
ClashUtil.switchProxyGroup("GLOBAL", "us", "http://127.0.0.1:6170")
} catch (e: Exception) {
LogUtils.log(
Log.ERROR,
"MainActivity",
"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 {
ClashUtil.stopProxy(this)
// AutoJsUtil.stopAutojsScript(this)
}
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
private fun registerReceiver() {
intentFilter = IntentFilter().apply {
addAction("com.ak.lu.SCRIPT_START")
scriptReceiver = ScriptReceiver()
registerReceiver(scriptReceiver, this)
}
}
override fun onDestroy() {
super.onDestroy()
ClashUtil.unregisterReceiver(this)
unregisterReceiver(scriptReceiver)
}
private fun checkPermission() {
viewModel.checkAccessibilityService()
StoragePermissionHelper.requestFullStoragePermission(
activity = this,
onGranted = { performFileOperation() },
onDenied = { showPermissionDeniedDialog() }
)
}
private fun performFileOperation() {
// 执行文件读写操作
// Toast.makeText(this, "文件访问权限已授予", Toast.LENGTH_SHORT).show()
MonitorService.onEvent(this)
}
private fun showPermissionDeniedDialog() {
AlertDialog.Builder(this)
.setTitle("权限被拒绝")
.setMessage("您拒绝了文件访问权限")
.setPositiveButton("设置") { _, _ ->
StoragePermissionHelper.openAppSettings(this)
}
.setNegativeButton("稍后", null)
.show()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
StoragePermissionHelper.handlePermissionResult(
activity = this,
requestCode = requestCode,
grantResults = grantResults,
onGranted = { performFileOperation() },
onDenied = { showPermissionDeniedDialog() }
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
StoragePermissionHelper.handleActivityResult(
activity = this,
requestCode = requestCode,
onGranted = { performFileOperation() },
onDenied = { showPermissionDeniedDialog() }
)
}
}
}

View File

@ -0,0 +1,20 @@
package com.android.grape
import android.app.Application
import com.blankj.utilcode.util.LogUtils
class MainApplication: Application() {
companion object {
lateinit var instance: MainApplication
}
override fun onCreate() {
super.onCreate()
instance = this
LogUtils.getConfig().setLog2FileSwitch(true)
initEvn()
}
fun initEvn(){
System.setProperty("java.library.path", this.applicationInfo.nativeLibraryDir)
}
}

View File

@ -0,0 +1,26 @@
package com.android.grape
import androidx.lifecycle.ViewModel
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import com.android.grape.service.MyAccessibilityService
import com.android.grape.work.CheckAccessibilityWorker
import java.util.concurrent.TimeUnit
class MainViewModel:ViewModel() {
fun checkAccessibilityService() {
val accessibilityServiceEnabled = CheckAccessibilityWorker.isAccessibilityServiceEnabled(
MainApplication.instance,
MyAccessibilityService::class.java
)
if (!accessibilityServiceEnabled){
val workRequest = PeriodicWorkRequest.Builder(
CheckAccessibilityWorker::class.java, 15, TimeUnit.MINUTES
)
.build()
WorkManager.getInstance(MainApplication.instance).enqueue(workRequest)
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,44 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import androidx.core.app.JobIntentService
import com.android.grape.util.ScriptUtil
class AutoJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
val openZzzj = true
if (openZzzj) {
Handler(Looper.getMainLooper()).postDelayed({
ScriptUtil.execScript(
this@AutoJobService,
"/sdcard/autojs/main.js"
)
}, 2000L)
} else {
Handler(Looper.getMainLooper()).postDelayed({
onEvent(
this@AutoJobService
)
}, 5000L)
}
}
companion object {
private const val TAG = "AutoJobService"
private const val jobId = 731
fun onEvent(context: Context) {
enqueueWork(
context,
AutoJobService::class.java, jobId, Intent(
context,
AutoJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,70 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.app.JobIntentService
import com.android.grape.net.MyGet
import com.android.grape.util.Util
class CheckIpJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
if (checkIp()) {
InstallService.onEvent(this)
} else {
Util.isClickRet = false
Util.setInstallRet(false)
Util.clickErrReason = "networkErr"
Util.setFinish(this)
}
}
private fun checkIp(): Boolean {
try {
var nRetryCount = 0
do {
val url = "http://8.218.80.200/myipp"
Log.i(TAG, "start to getIp : " + url + " ; " + Util.ua)
val resp: String? = MyGet.getData(url, Util.ua)
Log.i(TAG, "checkIp : $resp")
if (!resp.isNullOrEmpty() && resp.length >= 15) {
try {
val conn =
resp.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (conn.size > 1 && Util.proxyCountry.equals(conn[1])) {
val ip = conn[0]
Util.delegateIp = ip
return true
}
} catch (e: Exception) {
}
}
Thread.sleep(2000)
} while (nRetryCount++ < 3)
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
companion object {
private const val TAG = "IOSTQ:CheckIpJobService"
private const val tryNum = 0
private const val maxTryNum = 5
private const val jobId = 509
fun onEvent(context: Context) {
enqueueWork(
context,
CheckIpJobService::class.java, jobId, Intent(
context,
CheckIpJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,51 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.app.JobIntentService
import com.android.grape.util.Util
import com.blankj.utilcode.util.LogUtils
class DownloadAppJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
LogUtils.i(TAG, "start to handle work")
var succ: Boolean = false
try {
succ = Util.execDownload(this)
} catch (e: Exception) {
e.printStackTrace()
}
LogUtils.i(TAG, "download app : $succ")
if (succ) {
errTime = 0L
StartVpnServerJobService.onEvent(this)
// StartVpnPortJobService.onEvent(this)
} else {
Util.isClickRet = false
Util.setInstallRet(false)
Util.clickErrReason = "downloadErr"
Util.setFinish(this)
}
}
companion object {
private const val TAG = "IOSTQ:DownloadJobService"
private var errTime = 0L
private const val jobId = 103
fun onEvent(context: Context) {
enqueueWork(
context,
DownloadAppJobService::class.java, jobId, Intent(
context,
DownloadAppJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,67 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.JobIntentService
import com.android.grape.util.Util
class InstallService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
Log.i(TAG, "onHandleWork ...")
if (Util.installRecord(this)) {
Log.i(TAG, "installRecord succ")
tryNum = 0
Util.setInstallRet(true)
println("IOSTQ:isNeedRestored == " + Util.isNeedRestored)
if (Util.isNeedRestored) {
Handler(Looper.getMainLooper()).postDelayed({
RecoverJobService.onEvent(
this@InstallService
)
}, 0)
} else {
Handler(Looper.getMainLooper()).postDelayed(
{ OpenAppService.onEvent(this@InstallService) },
1000L
)
}
} else {
Log.d(TAG, "onHandleWork: installRecord fail")
tryNum += 1
if (tryNum <= 5) {
Handler(Looper.getMainLooper()).postDelayed({
onEvent(
this@InstallService
)
}, 5000)
} else { //超过5次安装失败直接取消后续
Log.i(TAG, "install error : " + Util.recordPackageName + " ; " + tryNum)
Util.setFinish(this)
}
}
}
companion object {
private const val TAG = "IOSTQ:InstallService"
private var tryNum = 0
private const val jobId = 107
fun onEvent(context: Context) {
enqueueWork(
context,
InstallService::class.java, jobId, Intent(
context,
InstallService::class.java
)
)
}
}
}

View File

@ -0,0 +1,66 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.app.JobIntentService
import com.android.grape.util.ClashUtil
import com.android.grape.util.MockTools
import com.android.grape.util.Util
/**
* 监听任务
*/
class MonitorService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
Log.i(TAG, "onHandleWork ... $running")
if (!running) {
running = true
exec()
}
}
protected fun exec() {
try {
val apkRoot = "chmod 777 $packageCodePath"
MockTools.exec(apkRoot)
Log.i(TAG, "set to top")
Util.setTopApp(this)
Log.i(TAG, "auto stop vpn")
ClashUtil.stopProxy(this)
Thread.sleep(3000)
if (Util.clickTime > 0L) {
Util.clickTime = 0L
SendCallbackJobService.onEvent(this)
} else {
Util.execTask(applicationContext)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
companion object {
private const val TAG = "IOSTQ:MonitorService"
private const val jobId = 101
private var running = false
fun onEvent(context: Context) {
enqueueWork(
context,
MonitorService::class.java, jobId, Intent(
context,
MonitorService::class.java
)
)
}
fun setRunning(runningV: Boolean) {
running = runningV
}
}
}

View File

@ -0,0 +1,61 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.app.JobIntentService
import com.android.grape.util.MockTools
import com.android.grape.util.ServiceUtils
import com.android.grape.util.Util
import java.io.IOException
class OpenAppService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
println("IOSTQ:isCanAuto() == " + Util.isCanAuto)
println("IOSTQ:getCanAutoLc() == " + Util.canAutoLc)
Util.recordPackageName?.let {
ServiceUtils.setEnableApp(it, true)
}
ServiceUtils.setEnableApp("org.mozilla.firefox", true)
ServiceUtils.setEnableApp("com.google.android.webview", true)
ServiceUtils.setEnableApp("com.android.chrome", true)
ServiceUtils.setEnableApp("com.UCMobile", true)
if (Util.isCanAuto && Util.canAutoLc.isNotEmpty()) {
MockTools.exec("pm grant org.autojs.autojs android.permission.READ_EXTERNAL_STORAGE") //sdcard权限
MockTools.exec("pm grant org.autojs.autojs android.permission.SYSTEM_ALERT_WINDOW") //悬浮窗权限
MockTools.exec("settings put secure enabled_accessibility_services org.autojs.autojs/com.stardust.autojs.core.accessibility.AccessibilityService")
Util.doScript(this, Util.AUTO_JSPACKAGENAME) //autojs
}
try {
if (Util.isNeedRestored) {
Log.d("IOSTQ:", "执行留存任务")
Util.setRrInfo(this)
} else {
Log.d("IOSTQ:", "执行新装任务")
Util.setInfo(this)
}
} catch (e: IOException) {
e.printStackTrace()
}
// Util.hookOpenApp(this);
Util.openRecordApp(this)
}
companion object {
private const val TAG = "IOSTQ:OpenAppService"
private const val jobId = 111
fun onEvent(context: Context) {
enqueueWork(
context,
OpenAppService::class.java, jobId, Intent(
context,
OpenAppService::class.java
)
)
}
}
}

View File

@ -0,0 +1,52 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.JobIntentService
import com.android.grape.util.Util
class RecoverJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
Log.i(TAG, "onHandleWork")
var succ = false
try {
succ = Util.recoverRecordData(this)
} catch (e: Exception) {
e.printStackTrace()
}
if (succ) {
Handler(Looper.getMainLooper()).postDelayed(
{ OpenAppService.onEvent(this@RecoverJobService) },
3000L
)
} else {
Handler(Looper.getMainLooper()).postDelayed({
onEvent(
this@RecoverJobService
)
}, 3000L)
}
}
companion object {
private const val TAG = "IOSTQ:RecoverJobService"
private const val jobId = 109
fun onEvent(context: Context) {
enqueueWork(
context,
RecoverJobService::class.java, jobId, Intent(
context,
RecoverJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,293 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import androidx.core.app.JobIntentService
import com.android.grape.net.MyPost
import com.android.grape.util.Util
import com.blankj.utilcode.util.LogUtils
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import org.json.JSONObject
import java.io.File
import java.util.concurrent.TimeUnit
class SendCallbackJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
LogUtils.i(TAG, "onHandleWork")
if (Util.isInstallRet()) {
SendBackup()
if (Util.isNeedRestored) {
sendReloginEvent()
} else {
sendLogEvent()
}
}
Util.setFinish(this@SendCallbackJobService)
}
fun sendReloginEvent(): Boolean {
try {
val url = "http://39.103.73.250/tt/ddj/preCallback!rr.do"
val paramsJo = JSONObject()
val datajson = JSONObject()
if (Util.isNeedBackup) {
val cachejson = JSONObject()
cachejson.put("firstInstallTime", Util.installTime)
cachejson.put("lastUpdateTime", Util.lastUpdateTime)
cachejson.put("installServerTimeFromGP", Util.installServerTimeFromGP)
cachejson.put("clickServerTimeToGP", Util.clickServerTimeFromGP)
cachejson.put("installTimeFromGP", Util.instalTimeFromGp)
datajson.put("cacheJson", cachejson)
}
paramsJo.put("recordId", Util.recordId)
paramsJo.put("preClickRecordId", Util.preClickRecordId)
paramsJo.put("dataJson", datajson)
paramsJo.put("reloginRecordId", Util.reloginRecordId)
val clickInfoJo = JSONObject()
clickInfoJo.put("clickRet", Util.isClickRet)
clickInfoJo.put("clickIp", Util.delegateIp)
clickInfoJo.put("clickTime", Util.clickTime)
clickInfoJo.put("clickErrReason", Util.clickErrReason)
paramsJo.put("clickInfo", clickInfoJo)
val installInfoJo = JSONObject()
installInfoJo.put("installRet", Util.isInstallRet())
installInfoJo.put("installTime", Util.installTime)
paramsJo.put("installInfo", installInfoJo)
if (null != Util.backupResult) {
paramsJo.put("backUpFiles", Util.backupResult)
}
val logInfoJo = JSONObject()
logInfoJo.put("afLog", Util.getAfLog() + "\r\n" + Util.logBuffer.toString())
// logInfoJo.put("setConfLog", Util.getParamsJson());
paramsJo.put("logInfo", logInfoJo)
var params: String? = null
params = paramsJo?.toString() ?: "error"
var nRetryCount = 0
do {
val ret: String? = MyPost.postData(params.toByteArray(charset("utf-8")), url)
LogUtils.i(TAG, "ret:$ret")
val jo = ret?.let { JSONObject(it) }
if (jo?.getInt("code") == 1) {
return true
}
} while (nRetryCount++ < 3)
return false
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
fun sendLogEvent(): Boolean {
try {
val url = "http://39.103.73.250/tt/ddj/preCallback!install.do"
val paramsJo = JSONObject()
val datajson = JSONObject()
if (Util.isNeedBackup) {
val cachejson = JSONObject()
cachejson.put("firstInstallTime", Util.installTime)
cachejson.put("lastUpdateTime", Util.lastUpdateTime)
cachejson.put("installServerTimeFromGP", Util.installServerTimeFromGP)
cachejson.put("clickServerTimeToGP", Util.clickServerTimeFromGP)
cachejson.put("installTimeFromGP", Util.instalTimeFromGp)
datajson.put("cacheJson", cachejson)
}
paramsJo.put("recordId", Util.recordId)
paramsJo.put("preClickRecordId", Util.preClickRecordId)
paramsJo.put("dataJson", datajson)
val clickInfoJo = JSONObject()
clickInfoJo.put("clickRet", Util.isClickRet)
clickInfoJo.put("clickIp", Util.delegateIp)
clickInfoJo.put("clickTime", Util.clickTime)
clickInfoJo.put("clickErrReason", Util.clickErrReason)
paramsJo.put("clickInfo", clickInfoJo)
val installInfoJo = JSONObject()
installInfoJo.put("installRet", Util.isInstallRet())
installInfoJo.put("installTime", Util.installTime)
paramsJo.put("installInfo", installInfoJo)
val logInfoJo = JSONObject()
logInfoJo.put("afLog", Util.getAfLog() + "\r\n" + Util.logBuffer.toString())
// logInfoJo.put("setConfLog", Util.getParamsJson());
logInfoJo.put("hookVer", Util.HkVer())
logInfoJo.put("afVer", Util.getAppAfVer())
logInfoJo.put("afApp", Util.afApp)
paramsJo.put("logInfo", logInfoJo)
if (null != Util.backupResult) {
paramsJo.put("backUpFiles", Util.backupResult)
}
var params: String? = null
params = paramsJo?.toString() ?: "error"
var nRetryCount = 0
do {
val ret: String = MyPost.postData( params.toByteArray(charset("utf-8")), url)?:""
LogUtils.i(TAG, "ret:$ret")
val jo = JSONObject(ret)
if (jo.getInt("code") == 1) {
return true
}
} while (nRetryCount++ < 3)
return false
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
protected fun SendBackup() {
LogUtils.i(TAG, "onHandleWork")
var succ = false
try {
val fileName: String? = Util.backFileName
val fileName1: String = Util.backFileName1?:""
val fileName2: String = Util.backFileName2?:""
LogUtils.i(
TAG,
"backupDataFile to $fileName;$fileName1;$fileName2"
)
if (fileName != null) {
for (i in 0..4) {
if (sendBackupEvent(fileName, fileName1)) {
succ = true
break
}
}
if (succ) {
Util.delFileSh(fileName)
Util.delFileSh(fileName1)
Util.delFileSh(fileName2)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun sendBackupEvent(fileName: String, fileName1: String?): Boolean {
val file = File(fileName)
var result = false
var file1: File? = null
if (fileName1 != null && fileName1.length > 0) {
file1 = File(fileName1)
}
Util.chownSh(this, fileName, Util.getMainUserAndGroup(this))
var url = "http://192.168.1.111/tt/ddj/backup.do"
if (Util.backUpServerIp != "") {
url = "http://" + Util.backUpServerIp + "/tt/ddj/backup.do"
}
LogUtils.i(TAG, "sendBackupEvent-> file length = " + file.length())
try {
val mOkHttpClient = OkHttpClient().newBuilder()
.connectTimeout(60, TimeUnit.SECONDS) //设置超时时间
.readTimeout(60, TimeUnit.SECONDS) //设置读取超时时间
.writeTimeout(60, TimeUnit.SECONDS) //设置写入超时时间
.build()
//初始化Handler
val okHttpHandler = Handler(Looper.getMainLooper())
//补全请求地址
val builder = MultipartBody.Builder()
//设置类型
builder.setType(MultipartBody.FORM)
//追加参数
builder.addFormDataPart("recordId", "${Util.recordId}")
builder.addFormDataPart("afVer", Util.getAppAfVer())
if (file.exists() && file.length() > 0) {
builder.addFormDataPart(
"file1",
file.name,
RequestBody.create("multipart/form-data".toMediaTypeOrNull(), file)
)
}
if (file1 != null && file1.exists()) {
builder.addFormDataPart(
"file2",
file1.name,
RequestBody.create("multipart/form-data".toMediaTypeOrNull(), file1)
)
}
//创建RequestBody
val body: RequestBody = builder.build()
LogUtils.d(TAG, "sendBackupEvent-> url = $url")
LogUtils.d(TAG, "sendBackupEvent-> recordId= " + Util.recordId)
//创建Request
val request = Request.Builder().url(url).post(body).build()
//单独设置参数 比如读取超时时间
val call = mOkHttpClient.newBuilder().writeTimeout(50, TimeUnit.SECONDS).build()
.newCall(request)
val response = call.execute()
if (response.isSuccessful) {
val ret = response.body!!.string()
val retJo = JSONObject(ret)
if (retJo.getInt("code") == 1) {
if (retJo.has("backUpFiles")) {
Util.backupResult = retJo.getJSONObject("backUpFiles")
}
result = true
}
}
} catch (e: Exception) {
LogUtils.e(TAG, e.toString())
result = false
}
return result
}
internal class PostRet {
var succ: Int = 0
}
companion object {
private const val TAG = "IOSTQ:SendCallbackJobService"
private const val jobId = 700
fun onEvent(context: Context) {
enqueueWork(
context,
SendCallbackJobService::class.java, jobId, Intent(
context,
SendCallbackJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,45 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.JobIntentService
import com.android.grape.util.ClashUtil
import com.android.grape.util.Util
class StartVpnPortJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
Log.i(TAG, "start to handle work")
if (ClashUtil.checkProxy( this)) {
Handler(Looper.getMainLooper()).postDelayed({
StartVpnServerJobService.onEvent(
this@StartVpnPortJobService
)
}, 1000L)
} else {
Util.setInstallRet(false)
Util.isClickRet = false
Util.setFinish(this)
}
}
companion object {
private const val TAG = "IOSTQ:StartVpnPort"
private const val jobId = 503
fun onEvent(context: Context) {
enqueueWork(
context,
StartVpnPortJobService::class.java, jobId, Intent(
context,
StartVpnPortJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,60 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.JobIntentService
import com.android.grape.MainApplication
import com.android.grape.util.ClashUtil
import com.android.grape.util.Util
/**
* 本地vpn功能
*/
class StartVpnServerJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
Log.i(TAG, "start to handle work")
if (exec()) {
Handler(Looper.getMainLooper()).postDelayed(
{ CheckIpJobService.onEvent(this@StartVpnServerJobService) },
5000L
)
} else {
Handler(Looper.getMainLooper()).postDelayed({
onEvent(
this@StartVpnServerJobService
)
}, 1000L)
}
}
private fun exec(): Boolean {
ClashUtil.startProxy(MainApplication.instance)
ClashUtil.switchProxyGroup("GLOBAL", Util.proxyCountry, "http://127.0.0.1:6170")
if (!ClashUtil.checkProxy(MainApplication.instance)) {
println("IOSTQ:start vpn error")
return false
}
println("IOSTQ:start vpn ok")
return true
}
companion object {
private const val TAG = "IOSTQ:StartVpnServer"
private const val jobId = 505
fun onEvent(context: Context) {
enqueueWork(
context,
StartVpnServerJobService::class.java, jobId, Intent(
context,
StartVpnServerJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,36 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import androidx.core.app.JobIntentService
import com.android.grape.util.MockTools
import com.android.grape.util.Util
class UnInstallService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
Util.backUp(this)
Util.setInstallRet(true)
MockTools.exec("pm clear " + Util.recordPackageName)
Util.delFiles(this@UnInstallService)
Util.setFinish(this@UnInstallService)
}
companion object {
private const val TAG = "IOSTQ:UnInstallService"
private const val jobId = 115
fun onEvent(context: Context) {
enqueueWork(
context,
UnInstallService::class.java, jobId, Intent(
context,
UnInstallService::class.java
)
)
}
}
}

View File

@ -0,0 +1,267 @@
package com.android.grape.net
import android.util.Log
import com.blankj.utilcode.util.LogUtils
import okhttp3.*
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.util.concurrent.TimeUnit
object HttpUtils {
// OkHttp客户端配置
private val client: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(LoggingInterceptor()) // 添加日志拦截器
.build()
}
// JSON媒体类型
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
// 回调接口
interface HttpCallback {
fun onSuccess(response: 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 请求异步
*
* @param url 请求URL
* @param params 查询参数可选
* @param headers 请求头可选
* @param callback 回调接口
*/
fun getAsync(
url: String,
params: Map<String, String>? = null,
headers: Map<String, String>? = null,
callback: HttpCallback
) {
val request = buildGetRequest(url, params, headers)
executeRequestAsync(request, callback)
}
/**
* POST 请求异步
*
* @param url 请求URL
* @param body 请求体JSON格式
* @param headers 请求头可选
* @param callback 回调接口
*/
fun postAsync(
url: String,
body: String,
headers: Map<String, String>? = null,
callback: HttpCallback
) {
val requestBody = body.toRequestBody(JSON_MEDIA_TYPE)
val request = buildPostRequest(url, requestBody, headers)
executeRequestAsync(request, callback)
}
/**
* POST Form Data 请求异步
*
* @param url 请求URL
* @param formData 表单数据
* @param headers 请求头可选
* @param callback 回调接口
*/
fun postFormAsync(
url: String,
formData: Map<String, String>,
headers: Map<String, String>? = null,
callback: HttpCallback
) {
val formBody = buildFormBody(formData)
val request = buildPostRequest(url, formBody, headers)
executeRequestAsync(request, callback)
}
/**
* GET 请求同步
*
* @param url 请求URL
* @param params 查询参数可选
* @param headers 请求头可选
* @return Pair<响应内容, 状态码>
*/
fun getSync(
url: String,
params: Map<String, String>? = null,
headers: Map<String, String>? = null
): Pair<String?, Int> {
val request = buildGetRequest(url, params, headers)
return executeRequestSync(request)
}
/**
* POST 请求同步
*
* @param url 请求URL
* @param body 请求体JSON格式
* @param headers 请求头可选
* @return Pair<响应内容, 状态码>
*/
fun postSync(
url: String,
body: String,
headers: Map<String, String>? = null
): Pair<String?, Int> {
val requestBody = body.toRequestBody(JSON_MEDIA_TYPE)
val request = buildPostRequest(url, requestBody, headers)
return executeRequestSync(request)
}
/**
* 取消所有请求
*/
fun cancelAllRequests() {
client.dispatcher.cancelAll()
}
// 内部方法 --------------------------------
private fun buildGetRequest(
url: String,
params: Map<String, String>?,
headers: Map<String, String>?
): Request {
val httpUrlBuilder = url.toHttpUrlOrNull()?.newBuilder()
?: throw IllegalArgumentException("Invalid URL: $url")
// 添加查询参数
params?.forEach { (key, value) ->
httpUrlBuilder.addQueryParameter(key, value)
}
val requestBuilder = Request.Builder().url(httpUrlBuilder.build())
// 添加请求头
addHeaders(requestBuilder, headers)
return requestBuilder.build()
}
private fun buildPostRequest(
url: String,
body: RequestBody,
headers: Map<String, String>?
): Request {
val requestBuilder = Request.Builder()
.url(url)
.post(body)
// 添加请求头
addHeaders(requestBuilder, headers)
return requestBuilder.build()
}
private fun addHeaders(
builder: Request.Builder,
headers: Map<String, String>?
) {
headers?.forEach { (key, value) ->
builder.addHeader(key, value)
}
}
private fun buildFormBody(formData: Map<String, String>): RequestBody {
val formBodyBuilder = FormBody.Builder()
formData.forEach { (key, value) ->
formBodyBuilder.add(key, value)
}
return formBodyBuilder.build()
}
private fun executeRequestAsync(
request: Request,
callback: HttpCallback
) {
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val responseBody = response.body?.string()
val code = response.code
if (response.isSuccessful && responseBody != null) {
callback.onSuccess(responseBody, code)
} else {
callback.onFailure(
responseBody ?: "Empty response",
code
)
}
}
override fun onFailure(call: Call, e: IOException) {
callback.onFailure(e.message ?: "Unknown network error", null)
}
})
}
private fun executeRequestSync(request: Request): Pair<String?, Int> {
return try {
val response = client.newCall(request).execute()
val responseBody = response.body?.string()
val code = response.code
if (response.isSuccessful && responseBody != null) {
Pair(responseBody, code)
} else {
Pair(null, code)
}
} catch (e: IOException) {
Pair(null, -1) // 网络错误状态码
}
}
}

View File

@ -0,0 +1,241 @@
package com.android.grape.net
import android.content.Context
import android.util.Log
import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
object MyGet {
private const val TAG = "MyGet"
fun getData(url_: String, ua: String?): String? {
return getData(url_, ua, 0)
}
fun getData( url_: String, ua: String?, num: Int): String? {
Log.i(TAG, "start getData : $url_")
var result: String? = null
var httpUrlConnection: HttpURLConnection? = null
var inStrm: InputStream? = null
var baos: ByteArrayOutputStream? = null
var bis: BufferedInputStream? = null
try {
val url = URL(url_)
httpUrlConnection = url.openConnection() as HttpURLConnection
httpUrlConnection.allowUserInteraction = true
httpUrlConnection!!.doOutput = false
httpUrlConnection.doInput = true
httpUrlConnection.useCaches = false
httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428
httpUrlConnection.setRequestProperty("Content-Type", "text/html;charset=utf-8")
httpUrlConnection.setRequestProperty("User-Agent", ua)
httpUrlConnection.instanceFollowRedirects = false
httpUrlConnection.requestMethod = "GET"
httpUrlConnection.connectTimeout = 60000
httpUrlConnection.readTimeout = 60000
val statusCode = httpUrlConnection.responseCode
Log.i(TAG, "getData status code : $statusCode")
val location = httpUrlConnection.getHeaderField("Location")
if (location != null && location.length > 0) {
return if (location.startsWith("http")) {
if (num < 6) {
getData(location, ua, num + 1)
} else {
"jumpOver"
}
} else {
"unknowSchema__$location"
}
} else if (statusCode >= 200 && statusCode < 300) {
inStrm = httpUrlConnection.inputStream
baos = ByteArrayOutputStream()
bis = BufferedInputStream(inStrm)
val buf = ByteArray(1024)
var readSize = -1
while ((bis.read(buf).also { readSize = it }) != -1) {
baos.write(buf, 0, readSize)
}
val data = baos.toByteArray()
return String(data, charset("UTF-8"))
} else {
return "errorOccur"
}
} catch (e: Exception) {
e.printStackTrace()
result = null
} finally {
if (baos != null) {
try {
baos.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (bis != null) {
try {
bis.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (inStrm != null) {
try {
inStrm.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (httpUrlConnection != null) {
httpUrlConnection.disconnect()
httpUrlConnection = null
}
System.gc()
}
return null
}
fun getData(context: Context?, url_: String, ua: String?, forwardIp: String?): String? {
return getData(context, url_, ua, forwardIp, 0)
}
fun getData(
context: Context?,
url_: String,
ua: String?,
forwardIp: String?,
num: Int
): String? {
var url_ = url_
url_ = url_.replace(" ", "")
Log.i(TAG, "start getData : $url_")
var result: String? = null
var httpUrlConnection: HttpURLConnection? = null
var inStrm: InputStream? = null
var baos: ByteArrayOutputStream? = null
var bis: BufferedInputStream? = null
try {
val url = URL(url_)
httpUrlConnection = url.openConnection() as HttpURLConnection
httpUrlConnection.allowUserInteraction = true
httpUrlConnection!!.doOutput = false
httpUrlConnection.doInput = true
httpUrlConnection.useCaches = false
httpUrlConnection.instanceFollowRedirects = false
httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428
httpUrlConnection.setRequestProperty("Content-Type", "text/html;charset=utf-8")
httpUrlConnection.setRequestProperty("User-Agent", ua)
httpUrlConnection.setRequestProperty("X-Forwarded-For", forwardIp)
httpUrlConnection.requestMethod = "GET"
httpUrlConnection.connectTimeout = 60000
httpUrlConnection.readTimeout = 60000
val statusCode = httpUrlConnection.responseCode
Log.i(TAG, "getData status code : $statusCode")
val location = httpUrlConnection.getHeaderField("Location")
if (location != null && location.length > 0) {
if (location.startsWith("http")) {
return if (num >= 6) {
"jumpOver"
} else {
getData(context, location, ua, forwardIp, num + 1)
}
} else if (location.contains("://")) {
return "unknowSchema__$location"
} else {
if (num >= 6) {
return "jumpOver"
} else {
val arr =
url_.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
var newloc = ""
if (location.startsWith("/")) {
newloc = arr[0] + "//" + arr[2] + "/" + location
} else {
val pos = url_.lastIndexOf("/")
newloc = url_.substring(0, pos) + "/" + location
}
return getData(context, newloc, ua, forwardIp, num + 1)
}
}
} else if (statusCode >= 200 && statusCode < 300) {
inStrm = httpUrlConnection.inputStream
baos = ByteArrayOutputStream()
bis = BufferedInputStream(inStrm)
val buf = ByteArray(1024)
var readSize = -1
while ((bis.read(buf).also { readSize = it }) != -1) {
baos.write(buf, 0, readSize)
}
val data = baos.toByteArray()
return String(data, charset("UTF-8"))
} else {
return "errorOccur"
}
} catch (e: Exception) {
e.printStackTrace()
result = null
} finally {
if (baos != null) {
try {
baos.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (bis != null) {
try {
bis.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (inStrm != null) {
try {
inStrm.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (httpUrlConnection != null) {
httpUrlConnection.disconnect()
httpUrlConnection = null
}
System.gc()
}
return null
}
}

View File

@ -0,0 +1,127 @@
package com.android.grape.net
import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.UnsupportedEncodingException
import java.net.HttpURLConnection
import java.net.URL
object MyPost {
fun postData(byt: ByteArray?, url: String): String? {
val bytes: ByteArray? = postDataBytes(byt, url)
if (bytes != null && bytes.isNotEmpty()) {
try {
return String(bytes, charset("UTF-8"))
} catch (e: UnsupportedEncodingException) {
e.printStackTrace()
}
}
return null
}
fun postDataBytes(byt: ByteArray?, url: String): ByteArray? {
var result: String? = null
var httpUrlConnection: HttpURLConnection? = null
var inStrm: InputStream? = null
var baos: ByteArrayOutputStream? = null
var bis: BufferedInputStream? = null
try {
httpUrlConnection = getHttpURLConnection(url)
httpUrlConnection.allowUserInteraction = true
httpUrlConnection.doOutput = true
httpUrlConnection.doInput = true
httpUrlConnection.useCaches = false
httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428
httpUrlConnection.setRequestProperty(
"Content-type",
"application/x-java-serialized-object"
)
// httpUrlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
httpUrlConnection.requestMethod = "POST"
httpUrlConnection.connectTimeout = 20000
var outStrm: OutputStream? = null
outStrm = httpUrlConnection.outputStream
outStrm.write(byt)
outStrm.flush()
outStrm.close()
inStrm = httpUrlConnection.inputStream
baos = ByteArrayOutputStream()
bis = BufferedInputStream(inStrm)
val buf = ByteArray(1024)
var readSize = -1
while ((bis.read(buf).also { readSize = it }) != -1) {
baos.write(buf, 0, readSize)
}
val data = baos.toByteArray()
return data
} catch (e: Exception) {
e.printStackTrace()
result = null
} finally {
if (baos != null) {
try {
baos.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (bis != null) {
try {
bis.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (inStrm != null) {
try {
inStrm.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (httpUrlConnection != null) {
httpUrlConnection.disconnect()
httpUrlConnection = null
}
System.gc()
}
return null
}
const val dynamicPort0: Int = 20380
const val dynamicPort: Int = dynamicPort0
const val dynamicPort1: Int = 30379
fun getHttpURLConnection(
_url: String
): HttpURLConnection {
val url = URL(_url)
var httpUrlConnection: HttpURLConnection? = null
httpUrlConnection = url.openConnection() as HttpURLConnection
return httpUrlConnection
}
}

View File

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

View File

@ -0,0 +1,819 @@
package com.android.grape.pseudo
import java.io.UnsupportedEncodingException
/**
* Utilities for encoding and decoding the Base64 representation of
* binary data. See RFCs [2045](http://www.ietf.org/rfc/rfc2045.txt) and [3548](http://www.ietf.org/rfc/rfc3548.txt).
*/
object Base64 {
/**
* Default values for encoder/decoder flags.
*/
const val DEFAULT: Int = 0
/**
* Encoder flag bit to omit the padding '=' characters at the end
* of the output (if any).
*/
const val NO_PADDING: Int = 1
/**
* Encoder flag bit to omit all line terminators (i.e., the output
* will be on one long line).
*/
const val NO_WRAP: Int = 2
/**
* Encoder flag bit to indicate lines should be terminated with a
* CRLF pair instead of just an LF. Has no effect if `NO_WRAP` is specified as well.
*/
const val CRLF: Int = 4
/**
* Encoder/decoder flag bit to indicate using the "URL and
* filename safe" variant of Base64 (see RFC 3548 section 4) where
* `-` and `_` are used in place of `+` and
* `/`.
*/
const val URL_SAFE: Int = 8
// --------------------------------------------------------
// decoding
// --------------------------------------------------------
/**
* Decode the Base64-encoded data in input and return the data in
* a new byte array.
*
*
* The padding '=' characters at the end are considered optional, but
* if any are present, there must be the correct number of them.
*
* @param str the input String to decode, which is converted to
* bytes using the default charset
* @param flags controls certain features of the decoded output.
* Pass `DEFAULT` to decode standard Base64.
* @throws IllegalArgumentException if the input contains
* incorrect padding
*/
fun decode(str: String, flags: Int): ByteArray {
return decode(str.toByteArray(), flags)
}
/**
* Decode the Base64-encoded data in input and return the data in
* a new byte array.
*
*
* The padding '=' characters at the end are considered optional, but
* if any are present, there must be the correct number of them.
*
* @param input the input array to decode
* @param flags controls certain features of the decoded output.
* Pass `DEFAULT` to decode standard Base64.
* @throws IllegalArgumentException if the input contains
* incorrect padding
*/
fun decode(input: ByteArray, flags: Int): ByteArray {
return decode(input, 0, input.size, flags)
}
/**
* Decode the Base64-encoded data in input and return the data in
* a new byte array.
*
*
* The padding '=' characters at the end are considered optional, but
* if any are present, there must be the correct number of them.
*
* @param input the data to decode
* @param offset the position within the input array at which to start
* @param len the number of bytes of input to decode
* @param flags controls certain features of the decoded output.
* Pass `DEFAULT` to decode standard Base64.
* @throws IllegalArgumentException if the input contains
* incorrect padding
*/
fun decode(input: ByteArray, offset: Int, len: Int, flags: Int): ByteArray {
// Allocate space for the most data the input could represent.
// (It could contain less if it contains whitespace, etc.)
val decoder = Decoder(flags, ByteArray(len * 3 / 4))
require(decoder.process(input, offset, len, true)) { "bad base-64" }
// Maybe we got lucky and allocated exactly enough output space.
if (decoder.op == (decoder.output?.size ?: 0)) {
return decoder.output?: ByteArray(0)
}
// Need to shorten the array, so allocate a new one of the
// right size and copy.
val temp = ByteArray(decoder.op)
decoder.output?.let { System.arraycopy(it, 0, temp, 0, decoder.op) }
return temp
}
// --------------------------------------------------------
// encoding
// --------------------------------------------------------
/**
* Base64-encode the given data and return a newly allocated
* String with the result.
*
* @param input the data to encode
* @param flags controls certain features of the encoded output.
* Passing `DEFAULT` results in output that
* adheres to RFC 2045.
*/
fun encodeToString(input: ByteArray, flags: Int): String {
try {
return String(encode(input, flags), charset("US-ASCII"))
} catch (e: UnsupportedEncodingException) {
// US-ASCII is guaranteed to be available.
throw AssertionError(e)
}
}
/**
* Base64-encode the given data and return a newly allocated
* String with the result.
*
* @param input the data to encode
* @param offset the position within the input array at which to
* start
* @param len the number of bytes of input to encode
* @param flags controls certain features of the encoded output.
* Passing `DEFAULT` results in output that
* adheres to RFC 2045.
*/
fun encodeToString(input: ByteArray, offset: Int, len: Int, flags: Int): String {
try {
return String(encode(input, offset, len, flags), charset("US-ASCII"))
} catch (e: UnsupportedEncodingException) {
// US-ASCII is guaranteed to be available.
throw AssertionError(e)
}
}
/**
* Base64-encode the given data and return a newly allocated
* byte[] with the result.
*
* @param input the data to encode
* @param flags controls certain features of the encoded output.
* Passing `DEFAULT` results in output that
* adheres to RFC 2045.
*/
fun encode(input: ByteArray, flags: Int): ByteArray {
return encode(input, 0, input.size, flags)
}
/**
* Base64-encode the given data and return a newly allocated
* byte[] with the result.
*
* @param input the data to encode
* @param offset the position within the input array at which to
* start
* @param len the number of bytes of input to encode
* @param flags controls certain features of the encoded output.
* Passing `DEFAULT` results in output that
* adheres to RFC 2045.
*/
fun encode(input: ByteArray, offset: Int, len: Int, flags: Int): ByteArray {
val encoder = Encoder(flags, null)
// Compute the exact length of the array we will produce.
var output_len = len / 3 * 4
// Account for the tail of the data and the padding bytes, if any.
if (encoder.do_padding) {
if (len % 3 > 0) {
output_len += 4
}
} else {
when (len % 3) {
0 -> {}
1 -> output_len += 2
2 -> output_len += 3
}
}
// Account for the newlines, if any.
if (encoder.do_newline && len > 0) {
output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) *
(if (encoder.do_cr) 2 else 1)
}
encoder.output = ByteArray(output_len)
encoder.process(input, offset, len, true)
assert(encoder.op == output_len)
return encoder.output?: ByteArray(0)
}
// --------------------------------------------------------
// shared code
// --------------------------------------------------------
/* package */
internal abstract class Coder {
var output: ByteArray? = null
var op: Int = 0
/**
* Encode/decode another block of input data. this.output is
* provided by the caller, and must be big enough to hold all
* the coded data. On exit, this.opwill be set to the length
* of the coded data.
*
* @param finish true if this is the final call to process for
* this object. Will finalize the coder state and
* include any final bytes in the output.
* @return true if the input so far is good; false if some
* error has been detected in the input stream..
*/
abstract fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean
/**
* @return the maximum number of bytes a call to process()
* could produce for the given number of input bytes. This may
* be an overestimate.
*/
abstract fun maxOutputSize(len: Int): Int
}
/* package */
internal class Decoder(flags: Int, output: ByteArray?) : Coder() {
/**
* States 0-3 are reading through the next input tuple.
* State 4 is having read one '=' and expecting exactly
* one more.
* State 5 is expecting no more data or padding characters
* in the input.
* State 6 is the error state; an error has been detected
* in the input and no future input can "fix" it.
*/
private var state: Int // state number (0 to 6)
private var value: Int
private val alphabet: IntArray
init {
this.output = output
alphabet = if ((flags and URL_SAFE) == 0) DECODE else DECODE_WEBSAFE
state = 0
value = 0
}
/**
* @return an overestimate for the number of bytes `len` bytes could decode to.
*/
override fun maxOutputSize(len: Int): Int {
return len * 3 / 4 + 10
}
/**
* Decode another block of input data.
*
* @return true if the state machine is still healthy. false if
* bad base-64 data has been detected in the input stream.
*/
override fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean {
var len = len
if (this.state == 6) return false
var p = offset
len += offset
// Using local variables makes the decoder about 12%
// faster than if we manipulate the member variables in
// the loop. (Even alphabet makes a measurable
// difference, which is somewhat surprising to me since
// the member variable is final.)
var state = this.state
var value = this.value
var op = 0
val output = this.output
val alphabet = this.alphabet
while (p < len) {
// Try the fast path: we're starting a new tuple and the
// next four bytes of the input stream are all data
// bytes. This corresponds to going through states
// 0-1-2-3-0. We expect to use this method for most of
// the data.
//
// If any of the next four bytes of input are non-data
// (whitespace, etc.), value will end up negative. (All
// the non-data values in decode are small negative
// numbers, so shifting any of them up and or'ing them
// together will result in a value with its top bit set.)
//
// You can remove this whole block and the output should
// be the same, just slower.
if (state == 0) {
while (p + 4 <= len &&
(((alphabet[input[p].toInt() and 0xff] shl 18) or
(alphabet[input[p + 1].toInt() and 0xff] shl 12) or
(alphabet[input[p + 2].toInt() and 0xff] shl 6) or
(alphabet[input[p + 3].toInt() and 0xff])).also { value = it }) >= 0
) {
output?.set(op + 2, value.toByte())
output?.set(op + 1, (value shr 8).toByte())
output?.set(op, (value shr 16).toByte())
op += 3
p += 4
}
if (p >= len) break
}
// The fast path isn't available -- either we've read a
// partial tuple, or the next four input bytes aren't all
// data, or whatever. Fall back to the slower state
// machine implementation.
val d = alphabet[input[p++].toInt() and 0xff]
when (state) {
0 -> if (d >= 0) {
value = d
++state
} else if (d != SKIP) {
this.state = 6
return false
}
1 -> if (d >= 0) {
value = (value shl 6) or d
++state
} else if (d != SKIP) {
this.state = 6
return false
}
2 -> if (d >= 0) {
value = (value shl 6) or d
++state
} else if (d == EQUALS) {
// Emit the last (partial) output tuple;
// expect exactly one more padding character.
output?.set(op++, (value shr 4).toByte())
state = 4
} else if (d != SKIP) {
this.state = 6
return false
}
3 -> if (d >= 0) {
// Emit the output triple and return to state 0.
value = (value shl 6) or d
output?.set(op + 2, value.toByte())
output?.set(op + 1, (value shr 8).toByte())
output?.set(op, (value shr 16).toByte())
op += 3
state = 0
} else if (d == EQUALS) {
// Emit the last (partial) output tuple;
// expect no further data or padding characters.
output?.set(op + 1, (value shr 2).toByte())
output?.set(op, (value shr 10).toByte())
op += 2
state = 5
} else if (d != SKIP) {
this.state = 6
return false
}
4 -> if (d == EQUALS) {
++state
} else if (d != SKIP) {
this.state = 6
return false
}
5 -> if (d != SKIP) {
this.state = 6
return false
}
}
}
if (!finish) {
// We're out of input, but a future call could provide
// more.
this.state = state
this.value = value
this.op = op
return true
}
// Done reading input. Now figure out where we are left in
// the state machine and finish up.
when (state) {
0 -> {}
1 -> {
// Read one extra input byte, which isn't enough to
// make another output byte. Illegal.
this.state = 6
return false
}
2 -> // Read two extra input bytes, enough to emit 1 more
// output byte. Fine.
output?.set(op++, (value shr 4).toByte())
3 -> {
// Read three extra input bytes, enough to emit 2 more
// output bytes. Fine.
output?.set(op++, (value shr 10).toByte())
output?.set(op++, (value shr 2).toByte())
}
4 -> {
// Read one padding '=' when we expected 2. Illegal.
this.state = 6
return false
}
5 -> {}
}
this.state = state
this.op = op
return true
}
companion object {
/**
* Lookup table for turning bytes into their position in the
* Base64 alphabet.
*/
private val DECODE = intArrayOf(
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
)
/**
* Decode lookup table for the "web safe" variant (RFC 3548
* sec. 4) where - and _ replace + and /.
*/
private val DECODE_WEBSAFE = intArrayOf(
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
)
/**
* Non-data values in the DECODE arrays.
*/
private const val SKIP = -1
private const val EQUALS = -2
}
}
/* package */
internal class Encoder(flags: Int, output: ByteArray?) : Coder() {
private val tail: ByteArray
/* package */
var tailLen: Int
private var count: Int
val do_padding: Boolean
val do_newline: Boolean
val do_cr: Boolean
private val alphabet: ByteArray
init {
this.output = output
do_padding = (flags and NO_PADDING) == 0
do_newline = (flags and NO_WRAP) == 0
do_cr = (flags and CRLF) != 0
alphabet = if ((flags and URL_SAFE) == 0) ENCODE else ENCODE_WEBSAFE
tail = ByteArray(2)
tailLen = 0
count = if (do_newline) LINE_GROUPS else -1
}
/**
* @return an overestimate for the number of bytes `len` bytes could encode to.
*/
override fun maxOutputSize(len: Int): Int {
return len * 8 / 5 + 10
}
override fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean {
// Using local variables makes the encoder about 9% faster.
var len = len
val alphabet = this.alphabet
val output = this.output
var op = 0
var count = this.count
var p = offset
len += offset
var v = -1
// First we need to concatenate the tail of the previous call
// with any input bytes available now and see if we can empty
// the tail.
when (tailLen) {
0 -> {}
1 -> {
if (p + 2 <= len) {
// A 1-byte tail with at least 2 bytes of
// input available now.
v = ((tail[0].toInt() and 0xff) shl 16) or
((input[p++].toInt() and 0xff) shl 8) or
(input[p++].toInt() and 0xff)
tailLen = 0
}
}
2 -> if (p + 1 <= len) {
// A 2-byte tail with at least 1 byte of input.
v = ((tail[0].toInt() and 0xff) shl 16) or
((tail[1].toInt() and 0xff) shl 8) or
(input[p++].toInt() and 0xff)
tailLen = 0
}
}
if (v != -1) {
output?.set(op++, alphabet[(v shr 18) and 0x3f])
output?.set(op++, alphabet[(v shr 12) and 0x3f])
output?.set(op++, alphabet[(v shr 6) and 0x3f])
output?.set(op++, alphabet[v and 0x3f])
if (--count == 0) {
if (do_cr) output?.set(op++, '\r'.code.toByte())
output?.set(op++, '\n'.code.toByte())
count = LINE_GROUPS
}
}
// At this point either there is no tail, or there are fewer
// than 3 bytes of input available.
// The main loop, turning 3 input bytes into 4 output bytes on
// each iteration.
while (p + 3 <= len) {
v = ((input[p].toInt() and 0xff) shl 16) or
((input[p + 1].toInt() and 0xff) shl 8) or
(input[p + 2].toInt() and 0xff)
output?.set(op, alphabet[(v shr 18) and 0x3f])
output?.set(op + 1, alphabet[(v shr 12) and 0x3f])
output?.set(op + 2, alphabet[(v shr 6) and 0x3f])
output?.set(op + 3, alphabet[v and 0x3f])
p += 3
op += 4
if (--count == 0) {
if (do_cr) output?.set(op++, '\r'.code.toByte())
output?.set(op++, '\n'.code.toByte())
count = LINE_GROUPS
}
}
if (finish) {
// Finish up the tail of the input. Note that we need to
// consume any bytes in tail before any bytes
// remaining in input; there should be at most two bytes
// total.
if (p - tailLen == len - 1) {
var t = 0
v = ((if (tailLen > 0) tail[t++] else input[p++]).toInt() and 0xff) shl 4
tailLen -= t
output?.set(op++, alphabet[(v shr 6) and 0x3f])
output?.set(op++, alphabet[v and 0x3f])
if (do_padding) {
output?.set(op++, '='.code.toByte())
output?.set(op++, '='.code.toByte())
}
if (do_newline) {
if (do_cr) output?.set(op++, '\r'.code.toByte())
output?.set(op++, '\n'.code.toByte())
}
} else if (p - tailLen == len - 2) {
var t = 0
v = (((if (tailLen > 1) tail[t++] else input[p++]).toInt() and 0xff) shl 10) or
(((if (tailLen > 0) tail[t++] else input[p++]).toInt() and 0xff) shl 2)
tailLen -= t
output?.set(op++, alphabet[(v shr 12) and 0x3f])
output?.set(op++, alphabet[(v shr 6) and 0x3f])
output?.set(op++, alphabet[v and 0x3f])
if (do_padding) {
output?.set(op++, '='.code.toByte())
}
if (do_newline) {
if (do_cr) output?.set(op++, '\r'.code.toByte())
output?.set(op++, '\n'.code.toByte())
}
} else if (do_newline && op > 0 && count != LINE_GROUPS) {
if (do_cr) output?.set(op++, '\r'.code.toByte())
output?.set(op++, '\n'.code.toByte())
}
assert(tailLen == 0)
assert(p == len)
} else {
// Save the leftovers in tail to be consumed on the next
// call to encodeInternal.
if (p == len - 1) {
tail[tailLen++] = input[p]
} else if (p == len - 2) {
tail[tailLen++] = input[p]
tail[tailLen++] = input[p + 1]
}
}
this.op = op
this.count = count
return true
}
companion object {
/**
* Emit a new line every this many output tuples. Corresponds to
* a 76-character line length (the maximum allowable according to
* [RFC 2045](http://www.ietf.org/rfc/rfc2045.txt)).
*/
const val LINE_GROUPS: Int = 19
/**
* Lookup table for turning Base64 alphabet positions (6 bits)
* into output bytes.
*/
private val ENCODE = byteArrayOf(
'A'.code.toByte(),
'B'.code.toByte(),
'C'.code.toByte(),
'D'.code.toByte(),
'E'.code.toByte(),
'F'.code.toByte(),
'G'.code.toByte(),
'H'.code.toByte(),
'I'.code.toByte(),
'J'.code.toByte(),
'K'.code.toByte(),
'L'.code.toByte(),
'M'.code.toByte(),
'N'.code.toByte(),
'O'.code.toByte(),
'P'.code.toByte(),
'Q'.code.toByte(),
'R'.code.toByte(),
'S'.code.toByte(),
'T'.code.toByte(),
'U'.code.toByte(),
'V'.code.toByte(),
'W'.code.toByte(),
'X'.code.toByte(),
'Y'.code.toByte(),
'Z'.code.toByte(),
'a'.code.toByte(),
'b'.code.toByte(),
'c'.code.toByte(),
'd'.code.toByte(),
'e'.code.toByte(),
'f'.code.toByte(),
'g'.code.toByte(),
'h'.code.toByte(),
'i'.code.toByte(),
'j'.code.toByte(),
'k'.code.toByte(),
'l'.code.toByte(),
'm'.code.toByte(),
'n'.code.toByte(),
'o'.code.toByte(),
'p'.code.toByte(),
'q'.code.toByte(),
'r'.code.toByte(),
's'.code.toByte(),
't'.code.toByte(),
'u'.code.toByte(),
'v'.code.toByte(),
'w'.code.toByte(),
'x'.code.toByte(),
'y'.code.toByte(),
'z'.code.toByte(),
'0'.code.toByte(),
'1'.code.toByte(),
'2'.code.toByte(),
'3'.code.toByte(),
'4'.code.toByte(),
'5'.code.toByte(),
'6'.code.toByte(),
'7'.code.toByte(),
'8'.code.toByte(),
'9'.code.toByte(),
'+'.code.toByte(),
'/'.code.toByte(),
)
/**
* Lookup table for turning Base64 alphabet positions (6 bits)
* into output bytes.
*/
private val ENCODE_WEBSAFE = byteArrayOf(
'A'.code.toByte(),
'B'.code.toByte(),
'C'.code.toByte(),
'D'.code.toByte(),
'E'.code.toByte(),
'F'.code.toByte(),
'G'.code.toByte(),
'H'.code.toByte(),
'I'.code.toByte(),
'J'.code.toByte(),
'K'.code.toByte(),
'L'.code.toByte(),
'M'.code.toByte(),
'N'.code.toByte(),
'O'.code.toByte(),
'P'.code.toByte(),
'Q'.code.toByte(),
'R'.code.toByte(),
'S'.code.toByte(),
'T'.code.toByte(),
'U'.code.toByte(),
'V'.code.toByte(),
'W'.code.toByte(),
'X'.code.toByte(),
'Y'.code.toByte(),
'Z'.code.toByte(),
'a'.code.toByte(),
'b'.code.toByte(),
'c'.code.toByte(),
'd'.code.toByte(),
'e'.code.toByte(),
'f'.code.toByte(),
'g'.code.toByte(),
'h'.code.toByte(),
'i'.code.toByte(),
'j'.code.toByte(),
'k'.code.toByte(),
'l'.code.toByte(),
'm'.code.toByte(),
'n'.code.toByte(),
'o'.code.toByte(),
'p'.code.toByte(),
'q'.code.toByte(),
'r'.code.toByte(),
's'.code.toByte(),
't'.code.toByte(),
'u'.code.toByte(),
'v'.code.toByte(),
'w'.code.toByte(),
'x'.code.toByte(),
'y'.code.toByte(),
'z'.code.toByte(),
'0'.code.toByte(),
'1'.code.toByte(),
'2'.code.toByte(),
'3'.code.toByte(),
'4'.code.toByte(),
'5'.code.toByte(),
'6'.code.toByte(),
'7'.code.toByte(),
'8'.code.toByte(),
'9'.code.toByte(),
'-'.code.toByte(),
'_'.code.toByte(),
)
}
}
}

View File

@ -0,0 +1,8 @@
package com.android.grape.pseudo
internal object Constants {
const val LINE_ENDING: String = "\r\n"
val GENERATOR_NAME: String =
java.lang.String.format("PseudoApkSigner %s", "1.0")
const val UTF8: String = "UTF-8"
}

View File

@ -0,0 +1,71 @@
package com.android.grape.pseudo
internal class ManifestBuilder {
private val mEntries: ArrayList<ManifestEntry>
private var mVersion: Long = 0
private var mCachedManifest: String = ""
private var mCachedVersion: Long = -1
init {
mEntries = ArrayList()
}
fun build(): String {
if (mVersion == mCachedVersion) return mCachedManifest
val stringBuilder = StringBuilder()
stringBuilder.append(generateHeader().toString())
for (entry in mEntries) {
stringBuilder.append(entry.toString())
}
mCachedVersion = mVersion
mCachedManifest = stringBuilder.toString()
return mCachedManifest
}
private fun generateHeader(): ManifestEntry {
val header = ManifestEntry()
header.setAttribute("Manifest-Version", "1.0")
header.setAttribute("Created-By", Constants.GENERATOR_NAME)
return header
}
internal class ManifestEntry {
private val mAttributes = LinkedHashMap<String, String>()
fun setAttribute(attribute: String, value: String) {
mAttributes[attribute] = value
}
fun getAttribute(attribute: String): String? {
return mAttributes[attribute]
}
override fun toString(): String {
val stringBuilder = StringBuilder()
for (key in mAttributes.keys) stringBuilder.append(
String.format(
"%s: %s" + Constants.LINE_ENDING, key,
mAttributes[key]
)
)
stringBuilder.append(Constants.LINE_ENDING)
return stringBuilder.toString()
}
}
fun addEntry(entry: ManifestEntry) {
mEntries.add(entry)
mVersion++
}
val entries: List<ManifestEntry>
get() = mEntries
}

View File

@ -0,0 +1,114 @@
package com.android.grape.pseudo
import android.provider.SyncStateContract
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.security.DigestInputStream
import java.security.MessageDigest
import java.security.interfaces.RSAPrivateKey
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
class PseudoApkSigner(private val mTemplateFile: File, privateKey: File) {
private val mPrivateKey: RSAPrivateKey = Utils.readPrivateKey(privateKey)
private var mSignerName = "CERT"
@Throws(Exception::class)
fun sign(apkFile: File, output: File) {
sign(FileInputStream(apkFile), FileOutputStream(output))
}
@Throws(Exception::class)
fun sign(apkInputStream: InputStream, output: OutputStream) {
val manifest: ManifestBuilder = ManifestBuilder()
val signature: SignatureFileGenerator = SignatureFileGenerator(manifest, HASHING_ALGORITHM)
val apkZipInputStream = ZipInputStream(apkInputStream)
val zipOutputStream: ZipAlignZipOutputStream = ZipAlignZipOutputStream.create(output, 4)
val messageDigest = MessageDigest.getInstance(HASHING_ALGORITHM)
var zipEntry: ZipEntry
OUTER@ while ((apkZipInputStream.nextEntry.also { zipEntry = it }) != null) {
if (zipEntry.isDirectory) continue
if (zipEntry.name.lowercase(Locale.getDefault()).startsWith("meta-inf/")) {
for (fileToSkipEnding in META_INF_FILES_TO_SKIP_ENDINGS) {
if (zipEntry.name.lowercase(Locale.getDefault()).endsWith(
fileToSkipEnding
)
) continue@OUTER
}
}
messageDigest.reset()
val entryInputStream = DigestInputStream(apkZipInputStream, messageDigest)
val newZipEntry = ZipEntry(zipEntry.name)
newZipEntry.method = zipEntry.method
if (zipEntry.method == ZipEntry.STORED) {
newZipEntry.size = zipEntry.size
newZipEntry.compressedSize = zipEntry.size
newZipEntry.crc = zipEntry.crc
}
zipOutputStream.alignment = if (newZipEntry.name.endsWith(".so")) 4096 else 4
zipOutputStream.putNextEntry(newZipEntry)
Utils.copyStream(entryInputStream, zipOutputStream)
zipOutputStream.closeEntry()
apkZipInputStream.closeEntry()
val manifestEntry: ManifestBuilder.ManifestEntry = ManifestBuilder.ManifestEntry()
manifestEntry.setAttribute("Name", zipEntry.name)
manifestEntry.setAttribute(
HASHING_ALGORITHM + "-Digest",
Utils.base64Encode(messageDigest.digest())
)
manifest.addEntry(manifestEntry)
}
zipOutputStream.putNextEntry(ZipEntry("META-INF/MANIFEST.MF"))
zipOutputStream.write(manifest.build().toByteArray())
zipOutputStream.closeEntry()
zipOutputStream.putNextEntry(ZipEntry(String.format("META-INF/%s.SF", mSignerName)))
zipOutputStream.write(signature.generate().toByteArray())
zipOutputStream.closeEntry()
zipOutputStream.putNextEntry(ZipEntry(String.format("META-INF/%s.RSA", mSignerName)))
zipOutputStream.write(Utils.readFile(mTemplateFile))
zipOutputStream.write(
Utils.sign(
HASHING_ALGORITHM,
mPrivateKey,
signature.generate().toByteArray()
)
)
zipOutputStream.closeEntry()
apkZipInputStream.close()
zipOutputStream.close()
}
/**
* Sets name of the .SF and .RSA file in META-INF
*
* @param signerName desired .SF and .RSA files name
*/
fun setSignerName(signerName: String) {
mSignerName = signerName
}
companion object {
private val META_INF_FILES_TO_SKIP_ENDINGS =
arrayOf("manifest.mf", ".sf", ".rsa", ".dsa", ".ec")
private const val HASHING_ALGORITHM = "SHA1"
}
}

View File

@ -0,0 +1,47 @@
package com.android.grape.pseudo
import com.android.grape.pseudo.Utils.base64Encode
import com.android.grape.pseudo.Utils.hash
internal class SignatureFileGenerator(
private val mManifest: ManifestBuilder,
private val mHashingAlgorithm: String
) {
@Throws(Exception::class)
fun generate(): String {
val stringBuilder = StringBuilder()
stringBuilder.append(generateHeader().toString())
for (manifestEntry in mManifest.entries) {
val sfEntry = ManifestBuilder.ManifestEntry()
sfEntry.setAttribute("Name", manifestEntry.getAttribute("Name")!!)
sfEntry.setAttribute(
"$mHashingAlgorithm-Digest", base64Encode(
hash(
manifestEntry.toString().toByteArray(
charset(Constants.UTF8)
), mHashingAlgorithm
)
)
)
stringBuilder.append(sfEntry.toString())
}
return stringBuilder.toString()
}
@Throws(Exception::class)
private fun generateHeader(): ManifestBuilder.ManifestEntry {
val header = ManifestBuilder.ManifestEntry()
header.setAttribute("Signature-Version", "1.0")
header.setAttribute("Created-By", Constants.GENERATOR_NAME)
header.setAttribute(
"$mHashingAlgorithm-Digest-Manifest", base64Encode(
hash(
mManifest.build()!!.toByteArray(charset(Constants.UTF8)), mHashingAlgorithm
)
)
)
return header
}
}

View File

@ -0,0 +1,84 @@
package com.android.grape.pseudo
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.Signature
import java.security.interfaces.RSAPrivateKey
import java.security.spec.PKCS8EncodedKeySpec
object Utils {
@Throws(Exception::class)
fun getFileHash(file: File?, hashingAlgorithm: String): ByteArray {
return getFileHash(FileInputStream(file), hashingAlgorithm)
}
@Throws(Exception::class)
fun getFileHash(fileInputStream: InputStream, hashingAlgorithm: String): ByteArray {
val messageDigest = MessageDigest.getInstance(hashingAlgorithm)
val buffer = ByteArray(1024 * 1024)
var read: Int
while ((fileInputStream.read(buffer).also { read = it }) > 0) messageDigest.update(
buffer,
0,
read
)
fileInputStream.close()
return messageDigest.digest()
}
@Throws(Exception::class)
fun hash(bytes: ByteArray, hashingAlgorithm: String): ByteArray {
val messageDigest = MessageDigest.getInstance(hashingAlgorithm)
messageDigest.update(bytes)
return messageDigest.digest()
}
fun base64Encode(bytes: ByteArray): String {
return Base64.encodeToString(bytes, 0)
}
@Throws(IOException::class)
fun copyStream(from: InputStream, to: OutputStream) {
val buf = ByteArray(1024 * 1024)
var len: Int
while ((from.read(buf).also { len = it }) > 0) {
to.write(buf, 0, len)
}
}
@Throws(Exception::class)
fun sign(hashingAlgorithm: String, privateKey: PrivateKey?, message: ByteArray?): ByteArray {
val sign = Signature.getInstance(hashingAlgorithm + "withRSA")
sign.initSign(privateKey)
sign.update(message)
return sign.sign()
}
@Throws(Exception::class)
fun readPrivateKey(file: File): RSAPrivateKey {
val keySpec = PKCS8EncodedKeySpec(readFile(file))
return KeyFactory.getInstance("RSA").generatePrivate(keySpec) as RSAPrivateKey
}
@Throws(IOException::class)
fun readFile(file: File): ByteArray {
val fileBytes = ByteArray(file.length().toInt())
val inputStream = FileInputStream(file)
inputStream.read(fileBytes)
inputStream.close()
return fileBytes
}
}

View File

@ -0,0 +1,64 @@
package com.android.grape.pseudo
import java.io.IOException
import java.io.OutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class ZipAlignZipOutputStream private constructor(
outputStream: BytesCounterOutputStream,
var alignment: Int
) :
ZipOutputStream(outputStream) {
private var mBytesCounter: BytesCounterOutputStream? = null
@Throws(IOException::class)
override fun putNextEntry(zipEntry: ZipEntry) {
if (zipEntry.method == ZipEntry.STORED) {
var headerSize = 30
headerSize += zipEntry.name.toByteArray().size
val temp = ((mBytesCounter?.bytesWritten?.plus(headerSize))?.rem(alignment))?:0
val requiredPadding =
(alignment - temp).toInt()
zipEntry.extra = ByteArray(requiredPadding)
}
super.putNextEntry(zipEntry)
}
private class BytesCounterOutputStream(private val mWrappedOutputStream: OutputStream) :
OutputStream() {
var bytesWritten: Long = 0
private set
@Throws(IOException::class)
override fun write(b: ByteArray) {
mWrappedOutputStream.write(b)
bytesWritten += b.size.toLong()
}
@Throws(IOException::class)
override fun write(b: Int) {
mWrappedOutputStream.write(b)
bytesWritten++
}
@Throws(IOException::class)
override fun write(b: ByteArray, off: Int, len: Int) {
mWrappedOutputStream.write(b, off, len)
bytesWritten += len.toLong()
}
}
companion object {
fun create(outputStream: OutputStream, alignment: Int): ZipAlignZipOutputStream {
val bytesCounterOutputStream = BytesCounterOutputStream(outputStream)
val zipAlignZipOutputStream =
ZipAlignZipOutputStream(bytesCounterOutputStream, alignment)
zipAlignZipOutputStream.mBytesCounter = bytesCounterOutputStream
return zipAlignZipOutputStream
}
}
}

View File

@ -0,0 +1,40 @@
package com.android.grape.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.android.grape.job.UnInstallService
import com.android.grape.util.ScriptUtil
import com.android.grape.util.Util
class ScriptReceiver : BroadcastReceiver() {
var hook_action: String = "com.android.task.board"
var EVENT: String = "event"
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
Log.i(ScriptUtil.TAG, context.packageName + " Receive action:" + action)
val event = intent.getIntExtra(ScriptUtil.EVENT, -1)
when (event) {
ScriptUtil.START -> Log.i(ScriptUtil.TAG, "script start run...")
ScriptUtil.STOP -> {
Log.i(ScriptUtil.TAG, "proxyme revice script stop!")
val hook_intent = Intent(this.hook_action)
hook_intent.putExtra(this.EVENT, 0)
context.sendBroadcast(hook_intent)
Util.script_status = 0
Log.i(
"IOSTQ:脚本结束",
context.packageName + " send broadcast:" + this.hook_action + ":" + this.EVENT
)
Handler(Looper.getMainLooper()).postDelayed(
{ UnInstallService.onEvent(context) },
3000
)
}
}
}
}

View File

@ -0,0 +1,142 @@
package com.android.grape.sai
import android.content.Context
import android.net.Uri
import com.android.grape.sai.apksource.CopyToFileApkSource
import com.android.grape.sai.apksource.FilterApkSource
import com.android.grape.sai.apksource.SignerApkSource
import com.android.grape.sai.filedescriptor.ContentUriFileDescriptor
import com.android.grape.sai.filedescriptor.DefaultApkSource
import com.android.grape.sai.filedescriptor.FileDescriptor
import com.android.grape.sai.filedescriptor.NormalFileDescriptor
import com.android.grape.sai.filedescriptor.ZipApkSource
import com.android.grape.sai.filedescriptor.ZipBackedApkSource
import com.android.grape.sai.filedescriptor.ZipFileApkSource
import com.android.grape.sai.inter.ApkSource
import java.io.File
class ApkSourceBuilder(private val mContext: Context) {
private var mSourceSet = false
private var mApkFiles: List<File>? = null
private var mZipFile: File? = null
private var mZipUri: Uri? = null
private var mApkUris: List<Uri>? = null
private var mSigningEnabled = false
private var mZipExtractionEnabled = false
private var mReadZipViaZipFileEnabled = false
private var mFilteredApks: Set<String>? = null
private var mBlacklist = false
fun fromApkFiles(apkFiles: List<File>?): ApkSourceBuilder {
ensureSourceSetOnce()
mApkFiles = apkFiles
return this
}
fun fromZipFile(zipFile: File?): ApkSourceBuilder {
ensureSourceSetOnce()
mZipFile = zipFile
return this
}
fun fromZipContentUri(zipUri: Uri?): ApkSourceBuilder {
ensureSourceSetOnce()
mZipUri = zipUri
return this
}
fun fromApkContentUris(uris: List<Uri>?): ApkSourceBuilder {
ensureSourceSetOnce()
mApkUris = uris
return this
}
fun setSigningEnabled(enabled: Boolean): ApkSourceBuilder {
mSigningEnabled = enabled
return this
}
fun setZipExtractionEnabled(enabled: Boolean): ApkSourceBuilder {
mZipExtractionEnabled = enabled
return this
}
fun setReadZipViaZipFileEnabled(enabled: Boolean): ApkSourceBuilder {
mReadZipViaZipFileEnabled = enabled
return this
}
fun filterApksByLocalPath(filteredApks: Set<String>?, blacklist: Boolean): ApkSourceBuilder {
mFilteredApks = filteredApks
mBlacklist = blacklist
return this
}
fun build(): ApkSource {
var apkSource: ApkSource? = null
var sourceIsZip = false
if (mApkFiles != null) {
val apkFileDescriptors: MutableList<FileDescriptor> = ArrayList<FileDescriptor>(
mApkFiles?.size?: 0
)
for (apkFile in mApkFiles!!) apkFileDescriptors.add(NormalFileDescriptor(apkFile))
apkSource = DefaultApkSource(apkFileDescriptors)
} else if (mZipFile != null) {
mZipFile?.let {
val zipBackedApkSource: ZipBackedApkSource
if (mReadZipViaZipFileEnabled) zipBackedApkSource =
ZipFileApkSource(mContext, NormalFileDescriptor(it))
else zipBackedApkSource = ZipApkSource(mContext, NormalFileDescriptor(it))
apkSource = zipBackedApkSource
sourceIsZip = true
}
} else if (mZipUri != null) {
mZipUri?.let {
val zipBackedApkSource: ZipBackedApkSource
if (mReadZipViaZipFileEnabled) zipBackedApkSource =
ZipFileApkSource(mContext, ContentUriFileDescriptor(mContext, it))
else zipBackedApkSource =
ZipApkSource(mContext, ContentUriFileDescriptor(mContext, it))
apkSource = zipBackedApkSource
sourceIsZip = true
}
} else if (mApkUris != null) {
val apkUriDescriptors: MutableList<FileDescriptor> = ArrayList<FileDescriptor>(
mApkUris?.size?: 0
)
for (apkUri in mApkUris!!) apkUriDescriptors.add(
ContentUriFileDescriptor(
mContext,
apkUri
)
)
apkSource = DefaultApkSource(apkUriDescriptors)
} else {
throw IllegalStateException("No source set")
}
apkSource?.let {
if (mSigningEnabled) apkSource = SignerApkSource(mContext, it)
//Signing already uses temp files, so there's not reason to use CopyToFileApkSource with it
if (mZipExtractionEnabled && sourceIsZip && !mSigningEnabled) {
apkSource = CopyToFileApkSource(mContext, it)
}
if (mFilteredApks != null) apkSource = FilterApkSource(it, mFilteredApks?:setOf(), mBlacklist)
}
return apkSource?: throw IllegalStateException("No source set")
}
private fun ensureSourceSetOnce() {
check(!mSourceSet) { "Source can be only be set once" }
mSourceSet = true
}
}

View File

@ -0,0 +1,77 @@
package com.android.grape.sai
import android.annotation.SuppressLint
import android.content.Context
import com.android.grape.sai.inter.SaiPackageInstaller
import com.android.grape.sai.inter.SaiPiSessionObserver
import com.android.grape.sai.param.SaiPiSessionParams
import com.android.grape.sai.param.SaiPiSessionState
import com.android.grape.sai.param.SaiPiSessionStatus
import com.blankj.utilcode.util.LogUtils
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentSkipListMap
@SuppressLint("UseSparseArrays")
abstract class BaseSaiPackageInstaller protected constructor(c: Context) :
SaiPackageInstaller {
protected val context: Context = c.applicationContext
private var mLastSessionId: Long = 0
private val mCreatedSessions: ConcurrentHashMap<String, SaiPiSessionParams> =
ConcurrentHashMap<String, SaiPiSessionParams>()
private val mSessionStates: ConcurrentSkipListMap<String, SaiPiSessionState> =
ConcurrentSkipListMap<String, SaiPiSessionState>()
private val mObservers: MutableSet<SaiPiSessionObserver> =
Collections.newSetFromMap<SaiPiSessionObserver>(
ConcurrentHashMap<SaiPiSessionObserver, Boolean>()
)
override fun createSession(params: SaiPiSessionParams): String {
val sessionId = newSessionId()
mCreatedSessions[sessionId] = params
setSessionState(sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.CREATED).build())
return sessionId
}
override fun registerSessionObserver(observer: SaiPiSessionObserver) {
mObservers.add(observer)
}
override fun unregisterSessionObserver(observer: SaiPiSessionObserver) {
mObservers.remove(observer)
}
override fun getSessions():List<SaiPiSessionState> {
return Collections.unmodifiableList<SaiPiSessionState>(
ArrayList<SaiPiSessionState>(
mSessionStates.values
)
)
}
protected fun setSessionState(sessionId: String, state: SaiPiSessionState) {
LogUtils.d(
tag(),
String.format("%s->setSessionState(%s, %s)", javaClass.simpleName, sessionId, state)
)
mSessionStates[sessionId] = state
Utils.onMainThread {
for (observer in mObservers) observer.onSessionStateChanged(state)
}
}
protected fun takeCreatedSession(sessionId: String): SaiPiSessionParams? {
return mCreatedSessions.remove(sessionId)
}
@SuppressLint("DefaultLocale")
protected fun newSessionId(): String {
val sessionId = mLastSessionId++
return String.format("%d@%s", sessionId, javaClass.name)
}
protected abstract fun tag(): String?
}

View File

@ -0,0 +1,112 @@
package com.android.grape.sai
import android.annotation.SuppressLint
import android.content.Context
import com.android.grape.MainApplication
import com.android.grape.sai.inter.SaiPackageInstaller
import com.android.grape.sai.inter.SaiPiSessionObserver
import com.android.grape.sai.param.SaiPiSessionParams
import com.android.grape.sai.param.SaiPiSessionState
import com.android.grape.sai.prefers.PreferencesValues
import com.android.grape.sai.rootless.RootlessSaiPackageInstaller
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
class FlexSaiPackageInstaller private constructor(c: Context) : SaiPackageInstaller,
SaiPiSessionObserver {
private val mContext: Context = c.applicationContext
private var mDefaultInstaller: SaiPackageInstaller? = null
private val mInstallers = HashMap<Int, SaiPackageInstaller>()
private val mSessionIdToInstaller = ConcurrentHashMap<String?, SaiPackageInstaller>()
private val mObservers: MutableSet<SaiPiSessionObserver> = Collections.newSetFromMap(
ConcurrentHashMap()
)
init {
addInstaller(
PreferencesValues.INSTALLER_ROOTLESS,
RootlessSaiPackageInstaller.getInstance(mContext)
)
addInstaller(
PreferencesValues.INSTALLER_ROOTED,
RootedSaiPackageInstaller.getInstance(mContext)
)
addInstaller(
PreferencesValues.INSTALLER_SHIZUKU,
ShizukuSaiPackageInstaller.getInstance()
)
sInstance = this
}
fun addInstaller(id: Int, installer: SaiPackageInstaller) {
check(!mInstallers.containsKey(id)) { "Installer with this id already added" }
if (mDefaultInstaller == null) mDefaultInstaller = installer
mInstallers[id] = installer
installer.registerSessionObserver(this)
}
fun createSessionOnInstaller(installerId: Int, params: SaiPiSessionParams): String {
return createSessionOnInstaller(
mInstallers[installerId]?: throw IllegalArgumentException("Unknown installer id"), params
)
}
private fun createSessionOnInstaller(
installer: SaiPackageInstaller,
params: SaiPiSessionParams
): String {
val sessionId = installer.createSession(params)
mSessionIdToInstaller[sessionId!!] = installer
return sessionId
}
override fun createSession(params: SaiPiSessionParams): String {
return createSessionOnInstaller(mDefaultInstaller!!, params)
}
override fun enqueueSession(sessionId: String) {
val installer = mSessionIdToInstaller.remove(sessionId)
requireNotNull(installer) { "Unknown sessionId" }
installer.enqueueSession(sessionId)
}
override fun registerSessionObserver(observer: SaiPiSessionObserver) {
mObservers.add(observer)
}
override fun unregisterSessionObserver(observer: SaiPiSessionObserver) {
mObservers.remove(observer)
}
override fun getSessions(): List<SaiPiSessionState>
{
val sessions = ArrayList<SaiPiSessionState>()
for (installer in mInstallers.values) sessions.addAll(installer.getSessions())
sessions.sort()
return sessions
}
override fun onSessionStateChanged(state: SaiPiSessionState?) {
for (observer in mObservers) observer.onSessionStateChanged(state)
}
companion object {
@SuppressLint("StaticFieldLeak")
private var sInstance: FlexSaiPackageInstaller? = null
fun getInstance(): FlexSaiPackageInstaller {
synchronized(FlexSaiPackageInstaller::class.java) {
return sInstance
?: FlexSaiPackageInstaller(MainApplication.instance)
}
}
}
}

View File

@ -0,0 +1,179 @@
package com.android.grape.sai
import android.content.Context
import android.util.Log
import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.Closeable
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.io.OutputStream
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.security.DigestInputStream
import java.security.MessageDigest
import java.util.zip.CRC32
object IOUtils {
private const val TAG = "IOUtils"
@Throws(IOException::class)
fun copyStream(from: InputStream, to: OutputStream) {
val buf = ByteArray(1024 * 1024)
var len: Int
while ((from.read(buf).also { len = it }) > 0) {
to.write(buf, 0, len)
}
}
@Throws(IOException::class)
fun copyFile(original: File?, destination: File?) {
FileInputStream(original).use { inputStream ->
FileOutputStream(destination).use { outputStream ->
copyStream(inputStream, outputStream)
}
}
}
@Throws(IOException::class)
fun copyFileFromAssets(context: Context, assetFileName: String, destination: File?) {
context.assets.open(assetFileName).use { inputStream ->
FileOutputStream(destination).use { outputStream ->
copyStream(inputStream, outputStream)
}
}
}
fun deleteRecursively(f: File) {
if (f.isDirectory) {
val files = f.listFiles()
if (files != null) {
for (child in files) deleteRecursively(child)
}
}
f.delete()
}
@Throws(IOException::class)
fun calculateFileCrc32(file: File?): Long {
return calculateCrc32(FileInputStream(file))
}
@Throws(IOException::class)
fun calculateBytesCrc32(bytes: ByteArray?): Long {
return calculateCrc32(ByteArrayInputStream(bytes))
}
@Throws(IOException::class)
fun calculateCrc32(inputStream: InputStream): Long {
inputStream.use { `in` ->
val crc32 = CRC32()
val buffer = ByteArray(1024 * 1024)
var read: Int
while ((`in`.read(buffer).also { read = it }) > 0) crc32.update(buffer, 0, read)
return crc32.value
}
}
fun writeStreamToStringBuilder(builder: StringBuilder, inputStream: InputStream?): Thread {
val t = Thread {
try {
val buf = CharArray(1024)
var len: Int
val reader = BufferedReader(InputStreamReader(inputStream))
while ((reader.read(buf).also { len = it }) > 0) builder.append(buf, 0, len)
reader.close()
} catch (e: Exception) {
Log.wtf(TAG, e)
}
}
t.start()
return t
}
/**
* Read contents of input stream to a byte array and close it
*
* @param inputStream
* @return contents of input stream
* @throws IOException
*/
@Throws(IOException::class)
fun readStream(inputStream: InputStream): ByteArray {
inputStream.use { `in` ->
return readStreamNoClose(`in`)
}
}
@Throws(IOException::class)
fun readStream(inputStream: InputStream, charset: Charset): String {
return String(readStream(inputStream), charset)
}
/**
* Read contents of input stream to a byte array, but don't close the stream
*
* @param inputStream
* @return contents of input stream
* @throws IOException
*/
@Throws(IOException::class)
fun readStreamNoClose(inputStream: InputStream): ByteArray {
val buffer = ByteArrayOutputStream()
copyStream(inputStream, buffer)
return buffer.toByteArray()
}
fun closeSilently(closeable: Closeable?) {
if (closeable == null) return
try {
closeable.close()
} catch (e: Exception) {
Log.w(TAG, String.format("Unable to close %s", closeable.javaClass.canonicalName), e)
}
}
/**
* Hashes stream content using passed [MessageDigest], closes the stream and returns digest bytes
*
* @param inputStream
* @param messageDigest
* @return
* @throws IOException
*/
@Throws(IOException::class)
fun hashStream(inputStream: InputStream?, messageDigest: MessageDigest): ByteArray {
DigestInputStream(inputStream, messageDigest).use { digestInputStream ->
val buffer = ByteArray(1024 * 64)
var read: Int
while ((digestInputStream.read(buffer).also { read = it }) > 0) {
//Do nothing
}
return messageDigest.digest()
}
}
@Throws(IOException::class)
fun hashString(s: String, messageDigest: MessageDigest): ByteArray {
return hashStream(
ByteArrayInputStream(s.toByteArray(StandardCharsets.UTF_8)),
messageDigest
)
}
@Throws(IOException::class)
fun readFile(file: File?): ByteArray {
FileInputStream(file).use { `in` ->
return readStream(`in`)
}
}
}

View File

@ -0,0 +1,56 @@
package com.android.grape.sai
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
/**
* @author litianxiang
* @description:
* @date :2021/6/21 18:23
*/
class MyBroadcastReceiver : BroadcastReceiver() {
private var mReceiver: MyReceiver? = null
private var fruit: String? = null
override fun onReceive(context: Context, intent: Intent) {
//接收广播消息
fruit = intent.getStringExtra("fruit")
//调用接口MyReceiver里面的interFruit方法传入接收的内容
mReceiver!!.interFruit(fruit)
//使用Toast显示广播消息
Toast.makeText(context, fruit, Toast.LENGTH_SHORT).show()
}
//创建一个接口把接收到的广播内容传递回MainActivity
interface MyReceiver {
fun interFruit(fruit: String?)
}
fun MyThis(mr: MyReceiver?) {
mReceiver = mr
}
fun sendSuccess(intent: Intent, context: Context) {
// 发送广播
var intent = intent
intent = Intent("myBroadCast")
//android版本为8以上的静态声明广播注册需要设置包名
intent.setPackage("com.aefyr.sai.fdroid")
intent.putExtra("fruit", "Installation success")
context.sendBroadcast(intent)
}
fun sendFailed(intent: Intent, context: Context) {
// 发送广播
var intent = intent
intent = Intent("myBroadCast")
//android版本为8以上的静态声明广播注册需要设置包名
intent.setPackage("com.aefyr.sai.fdroid")
intent.putExtra("fruit", "Installation failed")
context.sendBroadcast(intent)
}
}

View File

@ -0,0 +1,41 @@
package com.android.grape.sai
import android.annotation.SuppressLint
import android.content.Context
import com.android.grape.MainApplication
import com.android.grape.R
import com.android.grape.sai.shell.Shell
import com.android.grape.sai.shell.SuShell
class RootedSaiPackageInstaller private constructor(c: Context) :
ShellSaiPackageInstaller(c) {
init {
sInstance = this
}
override val shell: Shell
get() = SuShell.instance
override val installerName: String
get() = "Rooted"
override val shellUnavailableMessage: String
get() = MainApplication.instance.getString(R.string.installer_error_root_no_root)
override fun tag(): String {
return "RootedSaiPi"
}
companion object {
@SuppressLint("StaticFieldLeak")
private var sInstance: RootedSaiPackageInstaller? = null
fun getInstance(c: Context): RootedSaiPackageInstaller {
synchronized(RootedSaiPackageInstaller::class.java) {
return sInstance
?: RootedSaiPackageInstaller(c)
}
}
}
}

View File

@ -0,0 +1,188 @@
package com.android.grape.sai
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.util.Log
import android.widget.Toast
import com.android.grape.R
import com.android.grape.sai.rootless.AndroidPackageInstallerError
import com.android.grape.sai.rootless.ConfirmationIntentWrapperActivity2
class RootlessSaiPiBroadcastReceiver(c: Context) : BroadcastReceiver() {
private val mContext: Context = c.applicationContext
private val mObservers = HashSet<EventObserver>()
private var myBroadcastReceiver: MyBroadcastReceiver? = null
fun addEventObserver(observer: EventObserver) {
mObservers.add(observer)
}
fun removeEventObserver(observer: EventObserver) {
mObservers.remove(observer)
}
override fun onReceive(context: Context, intent: Intent) {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
Log.d(TAG, "Requesting user confirmation for installation")
dispatchOnConfirmationPending(
intent.getIntExtra(
PackageInstaller.EXTRA_SESSION_ID,
-1
), intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
)
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
ConfirmationIntentWrapperActivity2.start(
context,
intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1),
confirmationIntent
)
}
PackageInstaller.STATUS_SUCCESS -> {
Log.d(TAG, "Installation succeed")
dispatchOnInstallationSucceeded(
intent.getIntExtra(
PackageInstaller.EXTRA_SESSION_ID,
-1
), intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
)
myBroadcastReceiver = MyBroadcastReceiver().apply {
sendSuccess(intent, context)
}
Toast.makeText(context, "Installation succeed", Toast.LENGTH_SHORT).show()
}
else -> {
Log.d(TAG, "Installation failed")
dispatchOnInstallationFailed(
intent.getIntExtra(
PackageInstaller.EXTRA_SESSION_ID,
-1
), parseError(intent), getRawError(intent), null
)
myBroadcastReceiver = MyBroadcastReceiver().apply {
sendFailed(intent, context)
}
Toast.makeText(context, "Installation failed", Toast.LENGTH_SHORT).show()
}
}
}
private fun dispatchOnConfirmationPending(sessionId: Int, packageName: String?) {
for (observer in mObservers) observer.onConfirmationPending(sessionId, packageName)
}
private fun dispatchOnInstallationSucceeded(sessionId: Int, packageName: String?) {
for (observer in mObservers) observer.onInstallationSucceeded(sessionId, packageName)
}
private fun dispatchOnInstallationFailed(
sessionId: Int,
shortError: String,
fullError: String?,
exception: Exception?
) {
for (observer in mObservers) observer.onInstallationFailed(
sessionId,
shortError,
fullError,
exception
)
}
private fun getRawError(intent: Intent): String? {
return intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
}
private fun parseError(intent: Intent): String {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
val otherPackage = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)
val error = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
val errorCode = intent.getIntExtra(
ANDROID_PM_EXTRA_LEGACY_STATUS,
AndroidPackageInstallerError.UNKNOWN.legacyErrorCode
)
if (status == STATUS_BAD_ROM) {
return mContext.getString(R.string.installer_error_lidl_rom)
}
val androidPackageInstallerError: AndroidPackageInstallerError =
getAndroidPmError(errorCode, error)
if (androidPackageInstallerError !== AndroidPackageInstallerError.UNKNOWN) {
return androidPackageInstallerError.getDescription(mContext)
}
return getSimplifiedErrorDescription(status, otherPackage)
}
fun getSimplifiedErrorDescription(status: Int, blockingPackage: String?): String {
when (status) {
PackageInstaller.STATUS_FAILURE_ABORTED -> return mContext.getString(R.string.installer_error_aborted)
PackageInstaller.STATUS_FAILURE_BLOCKED -> {
var blocker = mContext.getString(R.string.installer_error_blocked_device)
if (blockingPackage != null) {
val appLabel = Utils.getAppLabel(mContext, blockingPackage)
if (appLabel != null) blocker = appLabel
}
return mContext.getString(R.string.installer_error_blocked, blocker)
}
PackageInstaller.STATUS_FAILURE_CONFLICT -> return mContext.getString(R.string.installer_error_conflict)
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> return mContext.getString(R.string.installer_error_incompatible)
PackageInstaller.STATUS_FAILURE_INVALID -> return mContext.getString(R.string.installer_error_bad_apks)
PackageInstaller.STATUS_FAILURE_STORAGE -> return mContext.getString(R.string.installer_error_storage)
STATUS_BAD_ROM -> return mContext.getString(R.string.installer_error_lidl_rom)
}
return mContext.getString(R.string.installer_error_generic)
}
fun getAndroidPmError(legacyErrorCode: Int, error: String?): AndroidPackageInstallerError {
for (androidPackageInstallerError in AndroidPackageInstallerError.entries) {
if (androidPackageInstallerError.legacyErrorCode == legacyErrorCode || (error != null && error.startsWith(
androidPackageInstallerError.error
))
) return androidPackageInstallerError
}
return AndroidPackageInstallerError.UNKNOWN
}
interface EventObserver {
fun onConfirmationPending(sessionId: Int, packageName: String?) {
}
fun onInstallationSucceeded(sessionId: Int, packageName: String?) {
}
fun onInstallationFailed(
sessionId: Int,
shortError: String?,
fullError: String?,
exception: Exception?
) {
}
}
companion object {
private const val TAG = "RootlessSaiPiBR"
const val ANDROID_PM_EXTRA_LEGACY_STATUS: String = "android.content.pm.extra.LEGACY_STATUS"
val ACTION_DELIVER_PI_EVENT: String = "com.android.grape.action.RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT"
const val STATUS_BAD_ROM: Int = -322
}
}

View File

@ -0,0 +1,100 @@
package com.android.grape.sai
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile
import com.android.grape.sai.filedescriptor.FileUtils
import java.io.File
object SafUtils {
private const val PATH_TREE = "tree"
fun getRootForPath(docUri: Uri): String {
val path = DocumentsContract.getTreeDocumentId(docUri)
val indexOfLastColon = path.lastIndexOf(':')
require(indexOfLastColon != -1) { "Given uri does not contain a colon: $docUri" }
return path.substring(0, indexOfLastColon)
}
fun getPathWithoutRoot(docUri: Uri): String {
val path = DocumentsContract.getTreeDocumentId(docUri)
val indexOfLastColon = path.lastIndexOf(':')
require(indexOfLastColon != -1) { "Given uri does not contain a colon: $docUri" }
return path.substring(indexOfLastColon + 1)
}
fun buildChildDocumentUri(directoryUri: Uri, childDisplayName: String): Uri {
require(isTreeUri(directoryUri)) { "directoryUri must be a tree uri" }
val rootPath = getRootForPath(directoryUri)
val directoryPath = getPathWithoutRoot(directoryUri)
val childPath =
rootPath + ":" + directoryPath + "/" + FileUtils.buildValidFatFilename(childDisplayName)
return DocumentsContract.buildDocumentUriUsingTree(directoryUri, childPath)
}
/**
* Test if the given URI represents a [DocumentsContract.Document] tree.
*/
fun isTreeUri(uri: Uri): Boolean {
val paths = uri.pathSegments
return (paths.size >= 2 && PATH_TREE == paths[0])
}
fun docFileFromSingleUriOrFileUri(context: Context, contentUri: Uri): DocumentFile? {
if (ContentResolver.SCHEME_FILE == contentUri.scheme) {
val path = contentUri.path ?: return null
val file = File(path)
if (file.isDirectory) return null
return DocumentFile.fromFile(file)
} else {
return DocumentFile.fromSingleUri(context, contentUri)
}
}
fun docFileFromTreeUriOrFileUri(context: Context, contentUri: Uri): DocumentFile? {
if (ContentResolver.SCHEME_FILE == contentUri.scheme) {
val path = contentUri.path ?: return null
val file = File(path)
if (!file.isDirectory) return null
return DocumentFile.fromFile(file)
} else {
return DocumentFile.fromTreeUri(context, contentUri)
}
}
fun getFileNameFromContentUri(context: Context, contentUri: Uri): String? {
val documentFile = docFileFromSingleUriOrFileUri(context, contentUri) ?: return null
return documentFile.name
}
/**
* @param context
* @param contentUri
* @return file length or 0 if it's unknown
*/
fun getFileLengthFromContentUri(context: Context, contentUri: Uri): Long {
val documentFile = docFileFromSingleUriOrFileUri(context, contentUri) ?: return 0
return documentFile.length()
}
fun parcelFdToFile(fd: ParcelFileDescriptor): File {
return File("/proc/self/fd/" + fd.fd)
}
}

View File

@ -0,0 +1,389 @@
package com.android.grape.sai
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.util.Pair
import com.android.grape.MainApplication
import com.android.grape.R
import com.android.grape.sai.inter.ApkSource
import com.android.grape.sai.param.SaiPiSessionParams
import com.android.grape.sai.param.SaiPiSessionState
import com.android.grape.sai.param.SaiPiSessionStatus
import com.android.grape.sai.prefers.DbgPreferencesHelper
import com.android.grape.sai.prefers.PreferencesHelper
import com.android.grape.sai.rootless.AndroidPackageInstallerError
import com.android.grape.sai.shell.MiuiUtils
import com.android.grape.sai.shell.Shell
import com.android.grape.util.Util
import com.blankj.utilcode.util.LogUtils
import java.io.File
import java.util.Arrays
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Semaphore
import java.util.concurrent.atomic.AtomicBoolean
import java.util.regex.Pattern
abstract class ShellSaiPackageInstaller protected constructor(c: Context?) :
BaseSaiPackageInstaller(c!!) {
private val mAwaitingBroadcast = AtomicBoolean(false)
private val mExecutor: ExecutorService = Executors.newFixedThreadPool(4)
private val mWorkerThread = HandlerThread("RootlessSaiPi Worker")
private val mWorkerHandler: Handler
private var mCurrentSessionId: String? = null
//TODO read package from apk stream, this is too potentially inconsistent
private val mPackageInstalledBroadcastReceiver: BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(tag(), intent.toString())
if (!mAwaitingBroadcast.get()) return
mAwaitingBroadcast.set(false)
val installedPackage: String
try {
installedPackage = intent.dataString!!.replace("package:", "")
val installerPackage: String =
context.getPackageManager().getInstallerPackageName(installedPackage)?:""
Log.d(tag(), "installerPackage=$installerPackage")
if ("com.android.grape" != installerPackage) return
} catch (e: Exception) {
Log.wtf(tag(), e)
return
}
mCurrentSessionId?.let {
setSessionState(
it,
SaiPiSessionState.Builder(
it,
SaiPiSessionStatus.INSTALLATION_SUCCEED
).packageName(
installedPackage
).resolvePackageMeta(MainApplication.instance).build()
)
}
unlockInstallation()
// Toast.makeText(context,"Installation succeed",Toast.LENGTH_SHORT).show();
Util.setInstallRet(true)
}
}
init {
mWorkerThread.start()
mWorkerHandler = Handler(mWorkerThread.looper)
val packageAddedFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED)
packageAddedFilter.addDataScheme("package")
MainApplication.instance.registerReceiver(
mPackageInstalledBroadcastReceiver,
packageAddedFilter,
null,
mWorkerHandler
)
}
override fun enqueueSession(sessionId: String) {
val params: SaiPiSessionParams? = takeCreatedSession(sessionId)
setSessionState(
sessionId,
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.QUEUED).appTempName(
params?.apkSource()?.appName
).build()
)
mExecutor.submit {
params?.let {
install(sessionId, it)
}
}
}
private fun install(sessionId: String, params: SaiPiSessionParams) {
lockInstallation(sessionId)
val appTempName: String? = params.apkSource().appName
setSessionState(
sessionId,
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLING).appTempName(appTempName).build()
)
var androidSessionId: Int? = null
try {
params.apkSource().use { apkSource ->
if (!shell.isAvailable()) {
setSessionState(
sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).error(
MainApplication.instance.getString(
R.string.installer_error_shell,
installerName,
shellUnavailableMessage
), null
).build()
)
unlockInstallation()
// Toast.makeText(getContext(),"Installation failed",Toast.LENGTH_SHORT).show();
Util.setInstallRet(false)
return
}
androidSessionId = createSession()
val path = "/sdcard/apks/" + "com.zhiliaoapp.musically"
val file = File(path)
val files = file.listFiles()
var currentApkFile = 0
for (f in files) {
if (f.length() <= 0) {
setSessionState(
sessionId,
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName(
appTempName
).error(
MainApplication.instance.getString(R.string.installer_error_unknown_apk_size),
null
).build()
)
unlockInstallation()
// Toast.makeText(getContext(),"Installation failed",Toast.LENGTH_SHORT).show();
Util.setInstallRet(false)
return
}
ensureCommandSucceeded(
shell.exec(
Shell.Command(
"pm",
"install-write",
f.length().toString(),
androidSessionId.toString(),
String.format("%d.apk", currentApkFile++),
f.path
)
)
)
}
mAwaitingBroadcast.set(true)
val installationResult: Shell.Result =
shell.exec(Shell.Command("pm", "install-commit", androidSessionId.toString()))
Log.i(tag(), "installationResult:" + installationResult.isSuccessful)
if (!installationResult.isSuccessful) {
mAwaitingBroadcast.set(false)
val shortError: String = MainApplication.instance.getString(
R.string.installer_error_shell,
installerName, """
${getSessionInfo(apkSource)}
${parseError(installationResult)}
""".trimIndent()
)
setSessionState(
sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED)
.appTempName(appTempName)
.error(
shortError, """
$shortError
${installationResult.toString()}
""".trimIndent()
)
.build()
)
unlockInstallation()
Util.setInstallRet(false)
} else {
Util.isClickRet = true
}
}
} catch (e: Exception) {
//TODO this catches resources close exception causing a crash, same in rootless installer
Log.w(tag(), e)
if (androidSessionId != null) {
shell.exec(Shell.Command("pm", "install-abandon", androidSessionId.toString()))
}
setSessionState(
sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED)
.appTempName(appTempName)
.error(
MainApplication.instance.getString(
R.string.installer_error_shell,
installerName, """
${getSessionInfo(params.apkSource())}
${e.localizedMessage}
""".trimIndent()
), MainApplication.instance.getString(
R.string.installer_error_shell,
installerName, """
${getSessionInfo(params.apkSource())}
${Utils.throwableToString(e)}
""".trimIndent()
)
)
.build()
)
unlockInstallation()
Util.setInstallRet(false)
}
}
private fun lockInstallation(sessionId: String) {
try {
mSharedSemaphore.acquire()
} catch (e: InterruptedException) {
throw RuntimeException("wtf", e)
}
mCurrentSessionId = sessionId
}
private fun unlockInstallation() {
mSharedSemaphore.release()
}
private fun ensureCommandSucceeded(result: Shell.Result): String {
if (!result.isSuccessful) throw RuntimeException(result.toString())
return result.out
}
private fun getSessionInfo(apkSource: ApkSource): String {
var saiVersion = "???"
try {
saiVersion = MainApplication.instance.getPackageManager()
.getPackageInfo(MainApplication.instance.getPackageName(), 0).versionName?:""
} catch (e: PackageManager.NameNotFoundException) {
Log.wtf(tag(), "Unable to get SAI version", e)
}
return java.lang.String.format(
"%s: %s %s | %s | Android %s | Using %s ApkSource implementation | SAI %s",
MainApplication.instance.getString(R.string.installer_device),
Build.BRAND,
Build.MODEL,
if (MiuiUtils.isMiui) "MIUI" else "Not MIUI",
Build.VERSION.RELEASE,
apkSource.javaClass.getSimpleName(),
saiVersion
)
}
@Throws(RuntimeException::class)
private fun createSession(): Int {
val installLocation: String = java.lang.String.valueOf(
PreferencesHelper.getInstance(MainApplication.instance).installLocation
)
val commandsToAttempt: ArrayList<Shell.Command> = ArrayList<Shell.Command>()
val customInstallCreateCommand: String? =
DbgPreferencesHelper.customInstallCreateCommand
if (customInstallCreateCommand != null) {
val args = ArrayList(
Arrays.asList(
*customInstallCreateCommand.split(" ".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()))
val command = args.removeAt(0)
commandsToAttempt.add(Shell.Command(command, *args.toTypedArray()))
LogUtils.d(tag(), "Using custom install-create command: $customInstallCreateCommand")
} else {
commandsToAttempt.add(
Shell.Command(
"pm",
"install-create",
"-r",
"--install-location",
installLocation,
"-i",
shell.makeLiteral("com.android.grape")
)
)
commandsToAttempt.add(
Shell.Command(
"pm",
"install-create",
"-r",
"-i",
shell.makeLiteral("com.android.grape")
)
)
}
val attemptedCommands: MutableList<Pair<Shell.Command, String>> =
ArrayList<Pair<Shell.Command, String>>()
for (commandToAttempt in commandsToAttempt) {
val result: Shell.Result = shell.exec(commandToAttempt)
attemptedCommands.add(Pair(commandToAttempt, result.toString()))
if (!result.isSuccessful) {
Log.w(tag(), String.format("Command failed: %s > %s", commandToAttempt, result))
continue
}
val sessionId = extractSessionId(result.out)
if (sessionId != null) return sessionId
else Log.w(tag(), String.format("Command failed: %s > %s", commandToAttempt, result))
}
val exceptionMessage = StringBuilder("Unable to create session, attempted commands: ")
var i = 1
for (attemptedCommand in attemptedCommands) {
exceptionMessage.append("\n\n").append(i++).append(") ==========================\n")
.append(attemptedCommand.first)
.append("\nVVVVVVVVVVVVVVVV\n")
.append(attemptedCommand.second)
}
exceptionMessage.append("\n")
throw IllegalStateException(exceptionMessage.toString())
}
private fun extractSessionId(commandResult: String): Int? {
try {
val sessionIdPattern = Pattern.compile("(\\d+)")
val sessionIdMatcher = sessionIdPattern.matcher(commandResult)
sessionIdMatcher.find()
return sessionIdMatcher.group(1).toInt()
} catch (e: Exception) {
Log.w(tag(), commandResult, e)
return null
}
}
private fun parseError(installCommitResult: Shell.Result): String {
var matchedError: AndroidPackageInstallerError = AndroidPackageInstallerError.UNKNOWN
for (error in AndroidPackageInstallerError.values()) {
if (installCommitResult.out.contains(error.error)) {
matchedError = error
break
}
}
return matchedError.getDescription(MainApplication.instance)
}
protected abstract val shell: Shell
protected abstract val installerName: String?
protected abstract val shellUnavailableMessage: String?
abstract override fun tag(): String?
companion object {
private val mSharedSemaphore = Semaphore(1)
}
}

View File

@ -0,0 +1,41 @@
package com.android.grape.sai
import android.annotation.SuppressLint
import android.content.Context
import com.android.grape.MainApplication
import com.android.grape.R
import com.android.grape.sai.shell.Shell
import com.android.grape.sai.shell.ShizukuShell
class ShizukuSaiPackageInstaller private constructor(c: Context) :
ShellSaiPackageInstaller(c) {
init {
sInstance = this
}
override val shell: Shell
get() = ShizukuShell.instance
override val installerName: String
get() = "Shizuku"
override val shellUnavailableMessage: String
get() = MainApplication.instance.getString(R.string.installer_error_shizuku_unavailable)
override fun tag(): String {
return "ShizukuSaiPi"
}
companion object {
@SuppressLint("StaticFieldLeak")
private var sInstance: ShizukuSaiPackageInstaller? = null
fun getInstance(): ShizukuSaiPackageInstaller {
synchronized(ShizukuSaiPackageInstaller::class.java) {
return sInstance
?: ShizukuSaiPackageInstaller(MainApplication.instance)
}
}
}
}

View File

@ -0,0 +1,9 @@
package com.android.grape.sai
class Stopwatch {
private val mStart = System.currentTimeMillis()
fun millisSinceStart(): Long {
return System.currentTimeMillis() - mStart
}
}

View File

@ -0,0 +1,243 @@
package com.android.grape.sai
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityManager
import android.app.UiModeManager
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.util.TypedValue
import android.view.inputmethod.InputMethodManager
import androidx.annotation.AttrRes
import androidx.fragment.app.Fragment
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.Locale
import java.util.UUID
import java.util.zip.ZipEntry
import kotlin.math.pow
object Utils {
private const val TAG = "SAIUtils"
fun getAppLabel(c: Context, packageName: String): String? {
try {
val pm = c.packageManager
val appInfo = pm.getApplicationInfo(packageName, 0)
return pm.getApplicationLabel(appInfo).toString()
} catch (e: Exception) {
return null
}
}
fun throwableToString(throwable: Throwable): String {
val sw = StringWriter(1024)
val pw = PrintWriter(sw)
throwable.printStackTrace(pw)
pw.close()
return sw.toString()
}
@SuppressLint("PrivateApi")
fun getSystemProperty(key: String?): String? {
try {
return Class.forName("android.os.SystemProperties")
.getDeclaredMethod("get", String::class.java)
.invoke(null, key) as String
} catch (e: Exception) {
Log.w("SAIUtils", "Unable to use SystemProperties.get", e)
return null
}
}
fun copyTextToClipboard(c: Context, text: CharSequence?) {
val clipboardManager = c.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager.setPrimaryClip(ClipData.newPlainText("text", text))
}
fun getFileNameFromZipEntry(zipEntry: ZipEntry): String {
val path = zipEntry.name
val lastIndexOfSeparator = path.lastIndexOf("/")
if (lastIndexOfSeparator == -1) return path
return path.substring(lastIndexOfSeparator + 1)
}
fun apiIsAtLeast(sdkInt: Int): Boolean {
return Build.VERSION.SDK_INT >= sdkInt
}
fun hideKeyboard(activity: Activity) {
val inputMethodManager =
activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(activity.window.decorView.windowToken, 0)
}
fun hideKeyboard(fragment: Fragment) {
val activity: Activity? = fragment.activity
if (activity != null) {
hideKeyboard(activity)
return
}
val inputMethodManager = fragment.requireContext()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(fragment.requireView().windowToken, 0)
}
fun escapeFileName(name: String): String {
return name.replace("[\\\\/:*?\"<>|]".toRegex(), "_")
}
private var sSizeDecimalFormat: DecimalFormat? = null
fun getThemeColor(c: Context, @AttrRes attribute: Int): Int {
val typedValue = TypedValue()
c.theme.resolveAttribute(attribute, typedValue, true)
return typedValue.data
}
private val sMainThreadHandler = Handler(Looper.getMainLooper())
fun onMainThread(r: Runnable) {
sMainThreadHandler.post(r)
}
fun getExtension(fileName: String): String? {
val lastDotIndex = fileName.lastIndexOf('.')
if (lastDotIndex == -1) return null
return fileName.substring(lastDotIndex + 1)
}
fun getFileNameWithoutExtension(fileName: String): String? {
val lastDotIndex = fileName.lastIndexOf('.')
if (lastDotIndex == -1) return null
return fileName.substring(0, lastDotIndex)
}
fun softRestartApp(c: Context) {
val activityManager = c.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (task in activityManager.appTasks) task.finishAndRemoveTask()
val intent = c.packageManager.getLaunchIntentForPackage(c.packageName)
c.startActivity(intent)
}
fun hardRestartApp(c: Context) {
val activityManager = c.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (task in activityManager.appTasks) task.finishAndRemoveTask()
val intent = c.packageManager.getLaunchIntentForPackage(c.packageName)
c.startActivity(intent)
System.exit(0)
}
fun dpToPx(c: Context, dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
c.resources.displayMetrics
).toInt()
}
fun spToPx(c: Context, dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
dp.toFloat(),
c.resources.displayMetrics
).toInt()
}
fun isTv(c: Context): Boolean {
val uiModeManager = c.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
return uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
}
fun <T> getParentAs(fragment: Fragment, asClass: Class<T>): T? {
var parent: Any? = fragment.parentFragment
if (parent == null) parent = fragment.activity
if (asClass.isInstance(parent)) return asClass.cast(parent)
return null
}
/**
* Create a file within `dir` directory in app's cache directory. File will have a random name.
* Even though this method is called createTEMPfile, created file won't be deleted automatically
*
* @param context
* @param dir
* @param extension
* @return
*/
fun createTempFileInCache(context: Context, dir: String, extension: String): File? {
val directory = File(context.cacheDir, dir)
if (!directory.exists() && !directory.mkdir()) return null
return createUniqueFileInDirectory(directory, extension)
}
fun createUniqueFileInDirectory(dir: File, extension: String): File? {
if (!dir.exists() && !dir.mkdirs() && !dir.exists()) return null
if (!dir.canWrite()) return null
var file: File? = null
while (file == null || file.exists()) file =
File(dir, UUID.randomUUID().toString() + "." + extension)
return file
}
@Throws(Exception::class)
fun saveDrawableAsPng(drawable: Drawable, pngFile: File?) {
var bitmap: Bitmap? = null
try {
bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
drawable.draw(canvas)
FileOutputStream(pngFile).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
}
} finally {
bitmap?.recycle()
}
}
private val HEX_ARRAY = "0123456789ABCDEF".toCharArray()
fun bytesToHex(bytes: ByteArray): String {
val hexChars = CharArray(bytes.size * 2)
for (j in bytes.indices) {
val v = bytes[j].toInt() and 0xFF
hexChars[j * 2] = HEX_ARRAY[v ushr 4]
hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F]
}
return String(hexChars)
}
}

View File

@ -0,0 +1,84 @@
package com.android.grape.sai.apksource
import android.content.Context
import com.android.grape.sai.IOUtils
import com.android.grape.sai.inter.ApkSource
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
/**
* An ApkSource implementation that copies APK files from the wrapped ApkSource to a temp file. Used to fix unknown APK sizes when necessary
*/
class CopyToFileApkSource(context: Context, wrappedApkSource: ApkSource) :
ApkSource {
private val mContext: Context = context.applicationContext
private val mWrappedApkSource: ApkSource = wrappedApkSource
private var mTempDir: File? = null
private var mCurrentApkFile: File? = null
@Throws(Exception::class)
override fun nextApk(): Boolean {
if (!mWrappedApkSource.nextApk()) return false
if (mTempDir == null) mTempDir = createTempDir()
mCurrentApkFile?.let {
IOUtils.deleteRecursively(it)
}
mCurrentApkFile = File(mTempDir, mWrappedApkSource.apkName)
mWrappedApkSource.openApkInputStream().use { `in` ->
FileOutputStream(mCurrentApkFile).use { out ->
`in`?.let {
IOUtils.copyStream(`in`, out)
}
}
}
return true
}
@Throws(Exception::class)
override fun openApkInputStream(): InputStream {
return FileInputStream(mCurrentApkFile)
}
override val apkLength: Long
get() = mCurrentApkFile?.length()?:0
@get:Throws(Exception::class)
override val apkName: String?
get() = mWrappedApkSource.apkName
@get:Throws(Exception::class)
override val apkLocalPath: String?
get() = mWrappedApkSource.apkLocalPath
@Throws(Exception::class)
override fun close() {
var suppressedException: Exception? = null
try {
mWrappedApkSource.close()
} catch (e: Exception) {
suppressedException = e
}
mTempDir?.let {
IOUtils.deleteRecursively(it)
}
if (suppressedException != null) throw suppressedException
}
override val appName: String?
get() = mWrappedApkSource.apkName
private fun createTempDir(): File {
var tempDir = File(mContext.filesDir, "CopyToFileApkSource")
tempDir = File(tempDir, System.currentTimeMillis().toString())
tempDir.mkdirs()
return tempDir
}
}

View File

@ -0,0 +1,56 @@
package com.android.grape.sai.apksource
import com.android.grape.sai.inter.ApkSource
import java.io.InputStream
/**
* An ApkSource that can filter out APK files from the backing ZipBackedApkSource
*/
class FilterApkSource(apkSource: ApkSource, filteredEntries: Set<String>, blacklist: Boolean) :
ApkSource {
private val mWrappedApkSource: ApkSource = apkSource
private val mFilteredEntries = filteredEntries
private val mBlacklist = blacklist
@Throws(Exception::class)
override fun nextApk(): Boolean {
if (!mWrappedApkSource.nextApk()) return false
while (shouldSkip(apkLocalPath?:"")) {
if (!mWrappedApkSource.nextApk()) return false
}
return true
}
private fun shouldSkip(localPath: String): Boolean {
return if (mBlacklist) mFilteredEntries.contains(localPath)
else !mFilteredEntries.contains(localPath)
}
@Throws(Exception::class)
override fun openApkInputStream(): InputStream? {
return mWrappedApkSource.openApkInputStream()
}
@get:Throws(Exception::class)
override val apkLength: Long
get() = mWrappedApkSource.apkLength
@get:Throws(Exception::class)
override val apkName: String?
get() = mWrappedApkSource.apkName
@get:Throws(Exception::class)
override val apkLocalPath: String?
get() = mWrappedApkSource.apkLocalPath
@Throws(Exception::class)
override fun close() {
mWrappedApkSource.close()
}
override val appName: String?
get() = mWrappedApkSource.appName
}

View File

@ -0,0 +1,108 @@
package com.android.grape.sai.apksource
import android.content.Context
import android.util.Log
import com.android.grape.pseudo.PseudoApkSigner
import com.android.grape.sai.IOUtils
import com.android.grape.sai.inter.ApkSource
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
class SignerApkSource(private val mContext: Context, apkSource: ApkSource) : ApkSource {
private val mWrappedApkSource: ApkSource = apkSource
private var mIsPrepared = false
private var mApkSigner: PseudoApkSigner? = null
private var mTempDir: File? = null
private var mCurrentSignedApkFile: File? = null
@Throws(Exception::class)
override fun nextApk(): Boolean {
if (!mWrappedApkSource.nextApk()) {
return false
}
if (!mIsPrepared) {
checkAndPrepareSigningEnvironment()
createTempDir()
mApkSigner = PseudoApkSigner(
File(signingEnvironmentDir, FILE_NAME_PAST), File(
signingEnvironmentDir, FILE_NAME_PRIVATE_KEY
)
)
}
mCurrentSignedApkFile = File(mTempDir, apkName)
mWrappedApkSource.openApkInputStream()?.let {
mApkSigner?.sign(
it,
FileOutputStream(mCurrentSignedApkFile)
)
}
return true
}
@Throws(Exception::class)
override fun openApkInputStream(): InputStream {
return FileInputStream(mCurrentSignedApkFile)
}
override val apkLength: Long
get() = mCurrentSignedApkFile!!.length()
@get:Throws(Exception::class)
override val apkName: String?
get() = mWrappedApkSource.apkName
@get:Throws(Exception::class)
override val apkLocalPath: String?
get() = mWrappedApkSource.apkLocalPath
@Throws(Exception::class)
override fun close() {
mTempDir?.let {
IOUtils.deleteRecursively(it)
}
mWrappedApkSource.close()
}
override val appName: String?
get() = mWrappedApkSource.appName
@Throws(Exception::class)
private fun checkAndPrepareSigningEnvironment() {
val signingEnvironment = signingEnvironmentDir
val pastFile = File(signingEnvironment, FILE_NAME_PAST)
val privateKeyFile = File(signingEnvironment, FILE_NAME_PRIVATE_KEY)
if (pastFile.exists() && privateKeyFile.exists()) {
mIsPrepared = true
return
}
Log.d(TAG, "Preparing signing environment...")
signingEnvironment.mkdir()
IOUtils.copyFileFromAssets(mContext, FILE_NAME_PAST, pastFile)
IOUtils.copyFileFromAssets(mContext, FILE_NAME_PRIVATE_KEY, privateKeyFile)
mIsPrepared = true
}
private val signingEnvironmentDir: File
get() = File(mContext.filesDir, "signing")
private fun createTempDir() {
mTempDir = File(mContext.filesDir, System.currentTimeMillis().toString())
mTempDir!!.mkdirs()
}
companion object {
private const val TAG = "SignerApkSource"
private const val FILE_NAME_PAST = "testkey.past"
private const val FILE_NAME_PRIVATE_KEY = "testkey.pk8"
}
}

View File

@ -0,0 +1,40 @@
package com.android.grape.sai.filedescriptor
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import com.android.grape.sai.SafUtils
import java.io.InputStream
class ContentUriFileDescriptor(c: Context, private val mContentUri: Uri) :
FileDescriptor {
private val mContentResolver: ContentResolver = c.contentResolver
private val mDocumentFile: DocumentFile? = SafUtils.docFileFromSingleUriOrFileUri(c, mContentUri)
@Throws(Exception::class)
override fun name(): String {
val name = mDocumentFile?.name
?: throw BadContentProviderException("DISPLAY_NAME column is null")
return name
}
@Throws(Exception::class)
override fun length(): Long {
val length = mDocumentFile?.length()?:0
if (length == 0L) throw BadContentProviderException("SIZE column is 0")
return length
}
@Throws(Exception::class)
override fun open(): InputStream? {
return mContentResolver.openInputStream(mContentUri)
}
private class BadContentProviderException(message: String?) : Exception(message)
}

View File

@ -0,0 +1,44 @@
package com.android.grape.sai.filedescriptor
import android.util.Log
import com.android.grape.sai.inter.ApkSource
import java.io.InputStream
class DefaultApkSource(private val mApkFileDescriptors: MutableList<FileDescriptor>) :
ApkSource {
private var mCurrentApk: FileDescriptor? = null
override fun nextApk(): Boolean {
if (mApkFileDescriptors.size == 0) return false
mCurrentApk = mApkFileDescriptors.removeAt(0)
return true
}
@Throws(Exception::class)
override fun openApkInputStream(): InputStream? {
return mCurrentApk?.open()
}
@get:Throws(Exception::class)
override val apkLength: Long
get() = mCurrentApk?.length()?: 0
@get:Throws(Exception::class)
override val apkName: String?
get() = mCurrentApk?.name()
@get:Throws(Exception::class)
override val apkLocalPath: String?
get() = mCurrentApk?.name()
override val appName: String?
get() {
try {
return if (mApkFileDescriptors.size == 1) mApkFileDescriptors[0].name() else null
} catch (e: Exception) {
Log.w("DefaultApkSource", "Unable to get app name", e)
return null
}
}
}

View File

@ -0,0 +1,15 @@
package com.android.grape.sai.filedescriptor
import java.io.InputStream
interface FileDescriptor {
@Throws(Exception::class)
fun name(): String?
@Throws(Exception::class)
fun length(): Long
@Throws(Exception::class)
fun open(): InputStream?
}

View File

@ -0,0 +1,52 @@
package com.android.grape.sai.filedescriptor
import android.text.TextUtils
import java.nio.charset.StandardCharsets
object FileUtils {
/**
* Mutate the given filename to make it valid for a FAT filesystem,
* replacing any invalid characters with "_".
*/
fun buildValidFatFilename(name: String): String {
if (TextUtils.isEmpty(name) || "." == name || ".." == name) {
return "(invalid)"
}
val res = StringBuilder(name.length)
for (element in name) {
val c = element
if (isValidFatFilenameChar(c)) {
res.append(c)
} else {
res.append('_')
}
}
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
// ext4 through a FUSE layer, so use that limit.
trimFilename(res, 255)
return res.toString()
}
private fun isValidFatFilenameChar(c: Char): Boolean {
if ((c.code in 0x00..0x1f)) {
return false
}
return when (c) {
'"', '*', '/', ':', '<', '>', '?', '\\', '|', 0x7F.toChar() -> false
else -> true
}
}
private fun trimFilename(res: StringBuilder, maxBytes: Int) {
var maxBytes = maxBytes
var raw = res.toString().toByteArray(StandardCharsets.UTF_8)
if (raw.size > maxBytes) {
maxBytes -= 3
while (raw.size > maxBytes) {
res.deleteCharAt(res.length / 2)
raw = res.toString().toByteArray(StandardCharsets.UTF_8)
}
res.insert(res.length / 2, "...")
}
}
}

View File

@ -0,0 +1,21 @@
package com.android.grape.sai.filedescriptor
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
class NormalFileDescriptor(private val mFile: File) : FileDescriptor {
override fun name(): String {
return mFile.name
}
override fun length(): Long {
return mFile.length()
}
@Throws(Exception::class)
override fun open(): InputStream {
return FileInputStream(mFile)
}
}

View File

@ -0,0 +1,124 @@
package com.android.grape.sai.filedescriptor
import android.content.Context
import android.util.Log
import com.android.grape.R
import com.android.grape.sai.Utils
import java.io.IOException
import java.io.InputStream
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipException
import java.util.zip.ZipInputStream
class ZipApkSource(private val mContext: Context, private val mZipFileDescriptor: FileDescriptor) :
ZipBackedApkSource {
private var mIsOpen = false
private var mSeenApkFiles = 0
private var mZipInputStream: ZipInputStream? = null
override var entry: ZipEntry? = null
private set
private var mWrappedStream: ZipInputStreamWrapper? = null
@Throws(Exception::class)
override fun nextApk(): Boolean {
if (!mIsOpen) {
mZipInputStream = ZipInputStream(mZipFileDescriptor.open())
mWrappedStream = ZipInputStreamWrapper(mZipInputStream!!)
mIsOpen = true
}
do {
try {
entry = mZipInputStream!!.nextEntry
} catch (e: ZipException) {
if (e.message == "only DEFLATED entries can have EXT descriptor") {
throw ZipException("only DEFLATED entries can have EXT descriptor")
}
throw e
}
} while (entry != null && (entry!!.isDirectory || !entry!!.name.lowercase(Locale.getDefault())
.endsWith(".apk"))
)
if (entry == null) {
mZipInputStream?.close()
require(mSeenApkFiles != 0) { mContext.getString(R.string.installer_error_zip_contains_no_apks) }
return false
}
mSeenApkFiles++
return true
}
override fun openApkInputStream(): InputStream? {
return mWrappedStream
}
override val apkLength: Long
get() = entry?.size?:0
override val apkName: String
get() = entry?.let { Utils.getFileNameFromZipEntry(it) }?:""
@get:Throws(Exception::class)
override val apkLocalPath: String
get() = entry?.name?:""
@Throws(Exception::class)
override fun close() {
mZipInputStream?.let {
try {
it.close()
} catch (e: IOException) {
Log.w("ZipApkSource", "Unable to close ZipInputStream", e)
}
}
}
override val appName: String?
get() {
try {
return mZipFileDescriptor.name()
} catch (e: Exception) {
Log.w("ZipApkSource", "Unable to get app name", e)
return null
}
}
/**
* Wraps ZipInputStream so it can be used as seemingly multiple InputStreams that represent each file in the archive.
* Basically just calls closeEntry instead of close, so ZipInputStream itself won't be closed
*/
private class ZipInputStreamWrapper(private val mWrappedStream: ZipInputStream) :
InputStream() {
@Throws(IOException::class)
override fun available(): Int {
return mWrappedStream.available()
}
@Throws(IOException::class)
override fun read(): Int {
return mWrappedStream.read()
}
@Throws(IOException::class)
override fun read(b: ByteArray): Int {
return mWrappedStream.read(b)
}
@Throws(IOException::class)
override fun read(b: ByteArray, off: Int, len: Int): Int {
return mWrappedStream.read(b, off, len)
}
@Throws(IOException::class)
override fun close() {
mWrappedStream.closeEntry()
}
}
}

View File

@ -0,0 +1,15 @@
package com.android.grape.sai.filedescriptor
import com.android.grape.sai.inter.ApkSource
import java.util.zip.ZipEntry
/**
* An ApkSource backed by a zip archive
*/
interface ZipBackedApkSource : ApkSource {
/**
* @return ZipEntry for the current APK
*/
val entry: ZipEntry?
}

View File

@ -0,0 +1,110 @@
package com.android.grape.sai.filedescriptor
import android.content.Context
import com.android.grape.R
import com.android.grape.sai.IOUtils
import com.android.grape.sai.Utils
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.util.Enumeration
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
/**
* An ApkSource implementation that copies given zip file FileDescriptor to a temp file and uses [ZipFile] API to read APKs from it.
* Used to read zip archives that are not compatible with ZipInputStream.
*/
class ZipFileApkSource(context: Context, private val mZipFileDescriptor: FileDescriptor) :
ZipBackedApkSource {
private val mContext: Context = context.applicationContext
private var mTempFile: File? = null
private var mZipFile: ZipFile? = null
private var mZipEntries: Enumeration<out ZipEntry>? = null
override var entry: ZipEntry? = null
private set
private var mSeenApkFile = false
@Throws(Exception::class)
override fun nextApk(): Boolean {
if (mZipFile == null) copyAndOpenZip()
entry = null
while (entry == null && mZipEntries!!.hasMoreElements()) {
val nextEntry = mZipEntries!!.nextElement()
if (!nextEntry.isDirectory && nextEntry.name.lowercase(Locale.getDefault())
.endsWith(".apk")
) {
entry = nextEntry
mSeenApkFile = true
}
}
if (entry == null) {
require(mSeenApkFile) { mContext.getString(R.string.installer_error_zip_contains_no_apks) }
return false
}
return true
}
@Throws(Exception::class)
private fun copyAndOpenZip() {
mTempFile = createTempFile()
mZipFileDescriptor.open().use { `in` ->
FileOutputStream(mTempFile).use { out ->
`in`?.let {
IOUtils.copyStream(`in`, out)
} ?: throw NullPointerException()
}
}
mZipFile = ZipFile(mTempFile)
mZipEntries = mZipFile?.entries()
}
@Throws(Exception::class)
override fun openApkInputStream(): InputStream? {
return mZipFile?.getInputStream(entry)
}
override val apkLength: Long
get() = entry?.size?:0
override val apkName: String
get() = entry?.let { Utils.getFileNameFromZipEntry(it) }?:""
@get:Throws(Exception::class)
override val apkLocalPath: String
get() = entry?.name?:""
@Throws(Exception::class)
override fun close() {
mZipFile?.close()
mTempFile?.let {
IOUtils.deleteRecursively(it)
}
}
override val appName: String?
get() {
return try {
mZipFileDescriptor.name()
} catch (e: Exception) {
null
}
}
private fun createTempFile(): File {
var tempFile = File(mContext.filesDir, "ZipFileApkSource")
tempFile.mkdir()
tempFile = File(tempFile, System.currentTimeMillis().toString() + ".zip")
return tempFile
}
}

View File

@ -0,0 +1,32 @@
package com.android.grape.sai.inter
import java.io.InputStream
interface ApkSource : AutoCloseable {
@Throws(Exception::class)
fun nextApk(): Boolean
@Throws(Exception::class)
fun openApkInputStream(): InputStream?
@get:Throws(Exception::class)
val apkLength: Long
@get:Throws(Exception::class)
val apkName: String?
@get:Throws(Exception::class)
val apkLocalPath: String?
@Throws(Exception::class)
override fun close() {
}
val appName: String?
/**
* Returns the name of the app this ApkSource will install or null if unknown
*
* @return name of the app this ApkSource will install or null if unknown
*/
get() = null
}

View File

@ -0,0 +1,17 @@
package com.android.grape.sai.inter
import com.android.grape.sai.param.SaiPiSessionParams
import com.android.grape.sai.param.SaiPiSessionState
interface SaiPackageInstaller {
fun createSession(params: SaiPiSessionParams): String?
fun enqueueSession(sessionId: String)
fun registerSessionObserver(observer: SaiPiSessionObserver)
fun unregisterSessionObserver(observer: SaiPiSessionObserver)
fun getSessions():List<SaiPiSessionState>
}

View File

@ -0,0 +1,7 @@
package com.android.grape.sai.inter
import com.android.grape.sai.param.SaiPiSessionState
interface SaiPiSessionObserver {
fun onSessionStateChanged(state: SaiPiSessionState?)
}

View File

@ -0,0 +1,154 @@
package com.android.grape.sai.param
import android.content.ContentResolver
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import com.android.grape.sai.Utils
class PackageMeta : Parcelable {
var packageName: String?
var label: String?
var hasSplits: Boolean = false
var isSystemApp: Boolean = false
var versionCode: Long = 0
var versionName: String? = null
var iconUri: Uri? = null
var installTime: Long = 0
var updateTime: Long = 0
constructor(packageName: String?, label: String?) {
this.packageName = packageName
this.label = label
}
private constructor(`in`: Parcel) {
packageName = `in`.readString()
label = `in`.readString()
hasSplits = `in`.readInt() == 1
isSystemApp = `in`.readInt() == 1
versionCode = `in`.readLong()
versionName = `in`.readString()
iconUri = `in`.readParcelable(Uri::class.java.classLoader)
installTime = `in`.readLong()
updateTime = `in`.readLong()
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(packageName)
dest.writeString(label)
dest.writeInt(if (hasSplits) 1 else 0)
dest.writeInt(if (isSystemApp) 1 else 0)
dest.writeLong(versionCode)
dest.writeString(versionName)
dest.writeParcelable(iconUri, 0)
dest.writeLong(installTime)
dest.writeLong(updateTime)
}
class Builder(packageName: String?) {
private val mPackageMeta = PackageMeta(packageName, "?")
fun setLabel(label: String?): Builder {
mPackageMeta.label = label
return this
}
fun setHasSplits(hasSplits: Boolean): Builder {
mPackageMeta.hasSplits = hasSplits
return this
}
fun setIsSystemApp(isSystemApp: Boolean): Builder {
mPackageMeta.isSystemApp = isSystemApp
return this
}
fun setVersionCode(versionCode: Long): Builder {
mPackageMeta.versionCode = versionCode
return this
}
fun setVersionName(versionName: String?): Builder {
mPackageMeta.versionName = versionName
return this
}
fun setIcon(iconResId: Int): Builder {
if (iconResId == 0) {
mPackageMeta.iconUri = null
return this
}
mPackageMeta.iconUri = Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.authority(mPackageMeta.packageName)
.path(iconResId.toString())
.build()
return this
}
fun setIconUri(iconUri: Uri?): Builder {
mPackageMeta.iconUri = iconUri
return this
}
fun setInstallTime(installTime: Long): Builder {
mPackageMeta.installTime = installTime
return this
}
fun setUpdateTime(updateTime: Long): Builder {
mPackageMeta.updateTime = updateTime
return this
}
fun build(): PackageMeta {
return mPackageMeta
}
}
companion object CREATOR : Parcelable.Creator<PackageMeta> {
override fun createFromParcel(parcel: Parcel): PackageMeta {
return PackageMeta(parcel)
}
override fun newArray(size: Int): Array<PackageMeta?> {
return arrayOfNulls(size)
}
fun forPackage(context: Context, packageName: String): PackageMeta? {
try {
val pm = context.packageManager
val applicationInfo = pm.getApplicationInfo(packageName, 0)
val packageInfo = pm.getPackageInfo(packageName, 0)
return Builder(applicationInfo.packageName)
.setLabel(applicationInfo.loadLabel(pm).toString())
.setHasSplits(applicationInfo.splitPublicSourceDirs != null && applicationInfo.splitPublicSourceDirs!!.size > 0)
.setIsSystemApp((applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0)
.setVersionCode(if (Utils.apiIsAtLeast(Build.VERSION_CODES.P)) packageInfo.longVersionCode else packageInfo.versionCode.toLong())
.setVersionName(packageInfo.versionName)
.setIcon(applicationInfo.icon)
.setInstallTime(packageInfo.firstInstallTime)
.setUpdateTime(packageInfo.lastUpdateTime)
.build()
} catch (e: PackageManager.NameNotFoundException) {
return null
}
}
}
}

View File

@ -0,0 +1,12 @@
package com.android.grape.sai.param
import com.android.grape.sai.inter.ApkSource
class SaiPiSessionParams(apkSource: ApkSource) {
private val mApkSource: ApkSource = apkSource
fun apkSource(): ApkSource {
return mApkSource
}
}

View File

@ -0,0 +1,143 @@
package com.android.grape.sai.param
import android.content.Context
import android.util.Log
import com.android.grape.sai.Stopwatch
class SaiPiSessionState private constructor(
private val mSessionId: String,
status: SaiPiSessionStatus
) :
Comparable<SaiPiSessionState> {
private val mStatus: SaiPiSessionStatus = status
private var mPackageName: String? = null
private var mAppTempName: String? = null
private var mPackageMeta: PackageMeta? = null
private var mLastUpdate: Long
private var mShortError: String? = null
private var mFullError: String? = null
init {
mLastUpdate = System.currentTimeMillis()
}
fun sessionId(): String {
return mSessionId
}
fun status(): SaiPiSessionStatus {
return mStatus
}
fun packageName(): String? {
return mPackageName
}
fun appTempName(): String? {
if (mAppTempName != null) return mAppTempName
if (mPackageName != null) return mPackageName
return null
}
fun packageMeta(): PackageMeta? {
return mPackageMeta
}
/**
* @return user-readable error description
*/
fun shortError(): String? {
return mShortError
}
/**
* @return full error info for debugging and stuff. May be same as [.shortError] if there's no better info
*/
fun fullError(): String? {
return mFullError
}
fun lastUpdate(): Long {
return mLastUpdate
}
fun newBuilder(): Builder {
return Builder(mSessionId, mStatus)
.packageName(packageName())
.appTempName(appTempName())
.packageMeta(packageMeta())
.error(shortError(), fullError())
}
override fun hashCode(): Int {
return sessionId().hashCode()
}
override fun equals(obj: Any?): Boolean {
return obj is SaiPiSessionState && obj.sessionId() == sessionId()
}
override fun toString(): String {
val sb = StringBuilder()
sb.append(
String.format(
"SaiPiSessionState: sessionId=%s, status=%s",
sessionId(),
status()
)
)
return sb.toString()
}
override fun compareTo(o: SaiPiSessionState): Int {
return java.lang.Long.compare(o.lastUpdate(), lastUpdate())
}
class Builder(sessionId: String, status: SaiPiSessionStatus) {
private val mState = SaiPiSessionState(sessionId, status)
fun packageName(packageName: String?): Builder {
mState.mPackageName = packageName
return this
}
fun appTempName(tempAppName: String?): Builder {
mState.mAppTempName = tempAppName
return this
}
fun resolvePackageMeta(c: Context): Builder {
if (mState.mPackageName == null) return this
val packageName = mState.mPackageName
val sw = Stopwatch()
mState.mPackageMeta = PackageMeta.forPackage(c, packageName?:"")
Log.d(
"SaiPiSessionState",
java.lang.String.format("Got PackageMeta in %d ms.", sw.millisSinceStart())
)
return this
}
fun packageMeta(packageMeta: PackageMeta?): Builder {
mState.mPackageMeta = packageMeta
return this
}
fun error(shortError: String?, fullError: String?): Builder {
var fullError = fullError
mState.mShortError = shortError
if (fullError == null) fullError = shortError
mState.mFullError = fullError
return this
}
fun build(): SaiPiSessionState {
mState.mLastUpdate = System.currentTimeMillis()
return mState
}
}
}

View File

@ -0,0 +1,18 @@
package com.android.grape.sai.param
enum class SaiPiSessionStatus {
CREATED, QUEUED, INSTALLING, INSTALLATION_SUCCEED, INSTALLATION_FAILED;
// fun getReadableName(c: Context): String {
// return when (this) {
// CREATED -> c.getString(R.string.installer_state_created)
// QUEUED -> c.getString(R.string.installer_state_queued)
// INSTALLING -> c.getString(R.string.installer_state_installing)
// INSTALLATION_SUCCEED -> c.getString(R.string.installer_state_installed)
// INSTALLATION_FAILED -> c.getString(R.string.installer_state_failed)
// }
//
// throw IllegalStateException("wtf")
// }
}

View File

@ -0,0 +1,32 @@
package com.android.grape.sai.prefers
import android.content.Context
import android.content.SharedPreferences
import com.android.grape.MainApplication
object DbgPreferencesHelper {
const val DONT_REPLACE_DOTS: String = "dbg_dont_replace_dots"
const val CUSTOM_INSTALL_CREATE: String = "dbg_custom_install_create"
const val ADD_FAKE_TIMESTAMP_TO_BACKUPS: String = "dbg_fake_backup_timestamp"
private val mPrefs: SharedPreferences =
MainApplication.instance.getSharedPreferences("com.android.grape.sai.prefers", Context.MODE_PRIVATE)
fun shouldReplaceDots(): Boolean {
return !mPrefs.getBoolean(DONT_REPLACE_DOTS, false)
}
val customInstallCreateCommand: String?
get() {
val command =
mPrefs.getString(CUSTOM_INSTALL_CREATE, "null")!!
if ("null".equals(command, ignoreCase = true)) return null
return command
}
fun addFakeTimestampToBackups(): Boolean {
return mPrefs.getBoolean(ADD_FAKE_TIMESTAMP_TO_BACKUPS, false)
}
}

View File

@ -0,0 +1,145 @@
package com.android.grape.sai.prefers
import android.content.Context
import android.content.SharedPreferences
import android.os.Environment
class PreferencesHelper private constructor(c: Context) {
val prefs: SharedPreferences = c.getSharedPreferences(
"com.android.grape.sai.prefers",
Context.MODE_PRIVATE
)
init {
sInstance = this
}
var homeDirectory: String?
get() = prefs.getString(
PreferencesKeys.HOME_DIRECTORY,
Environment.getExternalStorageDirectory().absolutePath
)
set(homeDirectory) {
prefs.edit().putString(PreferencesKeys.HOME_DIRECTORY, homeDirectory).apply()
}
var filePickerRawSort: Int
get() = prefs.getInt(PreferencesKeys.FILE_PICKER_SORT_RAW, 0)
set(rawSort) {
prefs.edit().putInt(PreferencesKeys.FILE_PICKER_SORT_RAW, rawSort).apply()
}
// var filePickerSortBy: Int
// get() = prefs.getInt(PreferencesKeys.FILE_PICKER_SORT_BY, DialogConfigs.SORT_BY_NAME)
// set(sortBy) {
// prefs.edit().putInt(PreferencesKeys.FILE_PICKER_SORT_BY, sortBy).apply()
// }
//
// var filePickerSortOrder: Int
// get() = prefs.getInt(
// PreferencesKeys.FILE_PICKER_SORT_ORDER,
// DialogConfigs.SORT_ORDER_NORMAL
// )
// set(sortOrder) {
// prefs.edit().putInt(PreferencesKeys.FILE_PICKER_SORT_ORDER, sortOrder).apply()
// }
fun shouldSignApks(): Boolean {
return prefs.getBoolean(PreferencesKeys.SIGN_APKS, false)
}
fun setShouldSignApks(signApks: Boolean) {
prefs.edit().putBoolean(PreferencesKeys.SIGN_APKS, signApks).apply()
}
fun shouldExtractArchives(): Boolean {
return prefs.getBoolean(PreferencesKeys.EXTRACT_ARCHIVES, false)
}
fun shouldUseZipFileApi(): Boolean {
return prefs.getBoolean(PreferencesKeys.USE_ZIPFILE, false)
}
var installer: Int
get() = prefs.getInt(PreferencesKeys.INSTALLER, PreferencesValues.INSTALLER_ROOTED)
set(installer) {
prefs.edit().putInt(PreferencesKeys.INSTALLER, installer).apply()
}
var backupFileNameFormat: String?
get() = prefs.getString(
PreferencesKeys.BACKUP_FILE_NAME_FORMAT,
PreferencesValues.BACKUP_FILE_NAME_FORMAT_DEFAULT
)
set(format) {
prefs.edit().putString(PreferencesKeys.BACKUP_FILE_NAME_FORMAT, format).apply()
}
var installLocation: Int
get() {
val rawInstallLocation =
prefs.getString(PreferencesKeys.INSTALL_LOCATION, "0")!!
return try {
rawInstallLocation.toInt()
} catch (e: NumberFormatException) {
0
}
}
set(installLocation) {
prefs.edit().putString(PreferencesKeys.INSTALL_LOCATION, installLocation.toString())
.apply()
}
fun useOldInstaller(): Boolean {
return prefs.getBoolean(PreferencesKeys.USE_OLD_INSTALLER, false)
}
fun showInstallerDialogs(): Boolean {
return prefs.getBoolean(PreferencesKeys.SHOW_INSTALLER_DIALOGS, true)
}
fun shouldShowAppFeatures(): Boolean {
return prefs.getBoolean(PreferencesKeys.SHOW_APP_FEATURES, true)
}
fun shouldShowSafTip(): Boolean {
return !prefs.getBoolean(PreferencesKeys.SAF_TIP_SHOWN, false)
}
fun setSafTipShown() {
prefs.edit().putBoolean(PreferencesKeys.SAF_TIP_SHOWN, true).apply()
}
val isInstallerXEnabled: Boolean
get() = prefs.getBoolean(PreferencesKeys.USE_INSTALLERX, true)
val isBruteParserEnabled: Boolean
get() = prefs.getBoolean(PreferencesKeys.USE_BRUTE_PARSER, true)
var isAnalyticsEnabled: Boolean
get() = prefs.getBoolean(PreferencesKeys.ENABLE_ANALYTICS, true)
set(enabled) {
prefs.edit().putBoolean(PreferencesKeys.ENABLE_ANALYTICS, enabled).apply()
}
var isInitialIndexingDone: Boolean
get() = prefs.getBoolean(PreferencesKeys.INITIAL_INDEXING_RUN, false)
set(done) {
prefs.edit().putBoolean(PreferencesKeys.INITIAL_INDEXING_RUN, done).apply()
}
var isSingleApkExportEnabled: Boolean
get() = prefs.getBoolean(PreferencesKeys.BACKUP_APK_EXPORT, false)
set(enabled) {
prefs.edit().putBoolean(PreferencesKeys.BACKUP_APK_EXPORT, enabled).apply()
}
companion object {
private lateinit var sInstance: PreferencesHelper
fun getInstance(c: Context): PreferencesHelper {
return sInstance ?: PreferencesHelper(c)
}
}
}

View File

@ -0,0 +1,31 @@
package com.android.grape.sai.prefers
object PreferencesKeys {
const val CURRENT_THEME: String = "current_theme"
const val THEME_MODE: String = "theme_mode"
const val HOME_DIRECTORY: String = "home_directory"
const val FILE_PICKER_SORT_RAW: String = "file_picker_sort_raw"
const val FILE_PICKER_SORT_BY: String = "file_picker_sort_by"
const val FILE_PICKER_SORT_ORDER: String = "file_picker_sort_order"
const val SIGN_APKS: String = "sign_apks"
const val MIUI_WARNING_SHOWN: String = "miui_warning_shown"
const val EXTRACT_ARCHIVES: String = "extract_archives"
const val USE_ZIPFILE: String = "use_zipfile"
const val INSTALLER: String = "installer"
const val BACKUP_FILE_NAME_FORMAT: String = "backup_file_name_format"
const val INSTALL_LOCATION: String = "install_location"
const val USE_OLD_INSTALLER: String = "use_old_installer"
const val SHOW_INSTALLER_DIALOGS: String = "show_installer_dialogs"
const val SHOW_APP_FEATURES: String = "show_app_features"
const val THEME: String = "theme"
const val SAF_TIP_SHOWN: String = "saf_tip_shown"
const val AUTO_THEME: String = "auto_theme"
const val AUTO_THEME_PICKER: String = "auto_theme_picker"
const val USE_INSTALLERX: String = "use_installerx"
const val USE_BRUTE_PARSER: String = "use_brute_parser"
const val ENABLE_ANALYTICS: String = "firebase_enabled"
const val INITIAL_INDEXING_RUN: String = "initial_indexing_run"
const val BACKUP_SETTINGS: String = "backup_settings"
const val BACKUP_APK_EXPORT: String = "single_apk_export"
const val ENABLE_APK_ACTION_VIEW: String = "enable_apk_action_view"
}

View File

@ -0,0 +1,9 @@
package com.android.grape.sai.prefers
object PreferencesValues {
const val INSTALLER_ROOTLESS: Int = 0
const val INSTALLER_ROOTED: Int = 1
const val INSTALLER_SHIZUKU: Int = 2
const val BACKUP_FILE_NAME_FORMAT_DEFAULT: String = "NAME_PACKAGE_VERSION"
}

View File

@ -0,0 +1,270 @@
package com.android.grape.sai.rootless
import android.content.Context
import androidx.annotation.StringRes
import com.android.grape.R
enum class AndroidPackageInstallerError(
/**
* @return error name (maybe more like a string code lol)
*/
val error: String,
/**
* @return "legacy" error code used in Android\'s PackageManager
*/
val legacyErrorCode: Int, @field:StringRes @param:StringRes private val mDescription: Int
) {
UNKNOWN("UNKNOWN", 1337, R.string.installer_rootless_error2_unknown),
INSTALL_FAILED_ALREADY_EXISTS(
"INSTALL_FAILED_ALREADY_EXISTS",
-1,
R.string.installer_rootless_error2_install_failed_already_exists
),
INSTALL_FAILED_INVALID_APK(
"INSTALL_FAILED_INVALID_APK",
-2,
R.string.installer_rootless_error2_install_failed_invalid_apk
),
INSTALL_FAILED_INVALID_URI(
"INSTALL_FAILED_INVALID_URI",
-3,
R.string.installer_rootless_error2_install_failed_invalid_uri
),
INSTALL_FAILED_INSUFFICIENT_STORAGE(
"INSTALL_FAILED_INSUFFICIENT_STORAGE",
-4,
R.string.installer_rootless_error2_install_failed_insufficient_storage
),
INSTALL_FAILED_DUPLICATE_PACKAGE(
"INSTALL_FAILED_DUPLICATE_PACKAGE",
-5,
R.string.installer_rootless_error2_install_failed_duplicate_package
),
INSTALL_FAILED_NO_SHARED_USER(
"INSTALL_FAILED_NO_SHARED_USER",
-6,
R.string.installer_rootless_error2_install_failed_no_shared_user
),
INSTALL_FAILED_UPDATE_INCOMPATIBLE(
"INSTALL_FAILED_UPDATE_INCOMPATIBLE",
-7,
R.string.installer_rootless_error2_install_failed_update_incompatible
),
INSTALL_FAILED_SHARED_USER_INCOMPATIBLE(
"INSTALL_FAILED_SHARED_USER_INCOMPATIBLE",
-8,
R.string.installer_rootless_error2_install_failed_shared_user_incompatible
),
INSTALL_FAILED_MISSING_SHARED_LIBRARY(
"INSTALL_FAILED_MISSING_SHARED_LIBRARY",
-9,
R.string.installer_rootless_error2_install_failed_missing_shared_library
),
INSTALL_FAILED_REPLACE_COULDNT_DELETE(
"INSTALL_FAILED_REPLACE_COULDNT_DELETE",
-10,
R.string.installer_rootless_error2_install_failed_replace_couldnt_delete
),
INSTALL_FAILED_DEXOPT(
"INSTALL_FAILED_DEXOPT",
-11,
R.string.installer_rootless_error2_install_failed_dexopt
),
INSTALL_FAILED_OLDER_SDK(
"INSTALL_FAILED_OLDER_SDK",
-12,
R.string.installer_rootless_error2_install_failed_older_sdk
),
INSTALL_FAILED_CONFLICTING_PROVIDER(
"INSTALL_FAILED_CONFLICTING_PROVIDER",
-13,
R.string.installer_rootless_error2_install_failed_conflicting_provider
),
INSTALL_FAILED_NEWER_SDK(
"INSTALL_FAILED_NEWER_SDK",
-14,
R.string.installer_rootless_error2_install_failed_newer_sdk
),
INSTALL_FAILED_TEST_ONLY(
"INSTALL_FAILED_TEST_ONLY",
-15,
R.string.installer_rootless_error2_install_failed_test_only
),
INSTALL_FAILED_CPU_ABI_INCOMPATIBLE(
"INSTALL_FAILED_CPU_ABI_INCOMPATIBLE",
-16,
R.string.installer_rootless_error2_install_failed_cpu_abi_incompatible
),
INSTALL_FAILED_MISSING_FEATURE(
"INSTALL_FAILED_MISSING_FEATURE",
-17,
R.string.installer_rootless_error2_install_failed_missing_feature
),
INSTALL_FAILED_CONTAINER_ERROR(
"INSTALL_FAILED_CONTAINER_ERROR",
-18,
R.string.installer_rootless_error2_install_failed_container_error
),
INSTALL_FAILED_INVALID_INSTALL_LOCATION(
"INSTALL_FAILED_INVALID_INSTALL_LOCATION",
-19,
R.string.installer_rootless_error2_install_failed_invalid_install_location
),
INSTALL_FAILED_MEDIA_UNAVAILABLE(
"INSTALL_FAILED_MEDIA_UNAVAILABLE",
-20,
R.string.installer_rootless_error2_install_failed_media_unavailable
),
INSTALL_FAILED_VERIFICATION_TIMEOUT(
"INSTALL_FAILED_VERIFICATION_TIMEOUT",
-21,
R.string.installer_rootless_error2_install_failed_verification_timeout
),
INSTALL_FAILED_VERIFICATION_FAILURE(
"INSTALL_FAILED_VERIFICATION_FAILURE",
-22,
R.string.installer_rootless_error2_install_failed_verification_failure
),
INSTALL_FAILED_PACKAGE_CHANGED(
"INSTALL_FAILED_PACKAGE_CHANGED",
-23,
R.string.installer_rootless_error2_install_failed_package_changed
),
INSTALL_FAILED_UID_CHANGED(
"INSTALL_FAILED_UID_CHANGED",
-24,
R.string.installer_rootless_error2_install_failed_uid_changed
),
INSTALL_FAILED_VERSION_DOWNGRADE(
"INSTALL_FAILED_VERSION_DOWNGRADE",
-25,
R.string.installer_rootless_error2_install_failed_version_downgrade
),
INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE(
"INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE",
-26,
R.string.installer_rootless_error2_install_failed_permission_model_downgrade
),
INSTALL_FAILED_SANDBOX_VERSION_DOWNGRADE(
"INSTALL_FAILED_SANDBOX_VERSION_DOWNGRADE",
-27,
R.string.installer_rootless_error2_install_failed_sandbox_version_downgrade
),
INSTALL_FAILED_MISSING_SPLIT(
"INSTALL_FAILED_MISSING_SPLIT",
-28,
R.string.installer_rootless_error2_install_failed_missing_split
),
INSTALL_PARSE_FAILED_NOT_APK(
"INSTALL_PARSE_FAILED_NOT_APK",
-100,
R.string.installer_rootless_error2_install_parse_failed_not_apk
),
INSTALL_PARSE_FAILED_BAD_MANIFEST(
"INSTALL_PARSE_FAILED_BAD_MANIFEST",
-101,
R.string.installer_rootless_error2_install_parse_failed_bad_manifest
),
INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION(
"INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION",
-102,
R.string.installer_rootless_error2_install_parse_failed_unexpected_exception
),
INSTALL_PARSE_FAILED_NO_CERTIFICATES(
"INSTALL_PARSE_FAILED_NO_CERTIFICATES",
-103,
R.string.installer_rootless_error2_install_parse_failed_no_certificates
),
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES(
"INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES",
-104,
R.string.installer_rootless_error2_install_parse_failed_inconsistent_certificates
),
INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING(
"INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING",
-105,
R.string.installer_rootless_error2_install_parse_failed_certificate_encoding
),
INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME(
"INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME",
-106,
R.string.installer_rootless_error2_install_parse_failed_bad_package_name
),
INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID(
"INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID",
-107,
R.string.installer_rootless_error2_install_parse_failed_bad_shared_user_id
),
INSTALL_PARSE_FAILED_MANIFEST_MALFORMED(
"INSTALL_PARSE_FAILED_MANIFEST_MALFORMED",
-108,
R.string.installer_rootless_error2_install_parse_failed_manifest_malformed
),
INSTALL_PARSE_FAILED_MANIFEST_EMPTY(
"INSTALL_PARSE_FAILED_MANIFEST_EMPTY",
-109,
R.string.installer_rootless_error2_install_parse_failed_manifest_empty
),
INSTALL_FAILED_INTERNAL_ERROR(
"INSTALL_FAILED_INTERNAL_ERROR",
-110,
R.string.installer_rootless_error2_install_failed_internal_error
),
INSTALL_FAILED_USER_RESTRICTED(
"INSTALL_FAILED_USER_RESTRICTED",
-111,
R.string.installer_rootless_error2_install_failed_user_restricted
),
INSTALL_FAILED_DUPLICATE_PERMISSION(
"INSTALL_FAILED_DUPLICATE_PERMISSION",
-112,
R.string.installer_rootless_error2_install_failed_duplicate_permission
),
INSTALL_FAILED_NO_MATCHING_ABIS(
"INSTALL_FAILED_NO_MATCHING_ABIS",
-113,
R.string.installer_rootless_error2_install_failed_no_matching_abis
),
INSTALL_FAILED_ABORTED(
"INSTALL_FAILED_ABORTED",
-115,
R.string.installer_rootless_error2_install_failed_aborted
),
INSTALL_FAILED_INSTANT_APP_INVALID(
"INSTALL_FAILED_INSTANT_APP_INVALID",
-116,
R.string.installer_rootless_error2_install_failed_instant_app_invalid
),
INSTALL_FAILED_BAD_DEX_METADATA(
"INSTALL_FAILED_BAD_DEX_METADATA",
-117,
R.string.installer_rootless_error2_install_failed_bad_dex_metadata
),
INSTALL_FAILED_BAD_SIGNATURE(
"INSTALL_FAILED_BAD_SIGNATURE",
-118,
R.string.installer_rootless_error2_install_failed_bad_signature
),
INSTALL_FAILED_OTHER_STAGED_SESSION_IN_PROGRESS(
"INSTALL_FAILED_OTHER_STAGED_SESSION_IN_PROGRESS",
-119,
R.string.installer_rootless_error2_install_failed_other_staged_session_in_progress
),
INSTALL_FAILED_MULTIPACKAGE_INCONSISTENCY(
"INSTALL_FAILED_MULTIPACKAGE_INCONSISTENCY",
-120,
R.string.installer_rootless_error2_install_failed_multipackage_inconsistency
),
INSTALL_FAILED_WRONG_INSTALLED_VERSION(
"INSTALL_FAILED_WRONG_INSTALLED_VERSION",
-121,
R.string.installer_rootless_error2_install_failed_wrong_installed_version
);
/**
* @return human readable error description
*/
fun getDescription(context: Context): String {
return context.getString(mDescription)
}
}

View File

@ -0,0 +1,78 @@
package com.android.grape.sai.rootless
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.android.grape.sai.RootlessSaiPiBroadcastReceiver
class ConfirmationIntentWrapperActivity2 : AppCompatActivity() {
private var mFinishedProperly = false
private var mSessionId = 0
private var mConfirmationIntent: Intent? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = intent
mSessionId = intent.getIntExtra(EXTRA_SESSION_ID, -1)
mConfirmationIntent = intent.getParcelableExtra(EXTRA_CONFIRMATION_INTENT)
if (savedInstanceState == null) {
try {
startActivityForResult(mConfirmationIntent!!, REQUEST_CODE_CONFIRM_INSTALLATION)
} catch (e: Exception) {
sendErrorBroadcast(mSessionId, RootlessSaiPiBroadcastReceiver.STATUS_BAD_ROM)
finish()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_CONFIRM_INSTALLATION) {
mFinishedProperly = true
finish()
}
}
override fun onDestroy() {
super.onDestroy()
if (isFinishing && !mFinishedProperly) start(
this, mSessionId, mConfirmationIntent
) //Because if user doesn't confirm/cancel the installation, PackageInstaller session will hang
}
private fun sendErrorBroadcast(sessionID: Int, status: Int) {
val statusIntent: Intent = Intent(RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT)
statusIntent.putExtra(PackageInstaller.EXTRA_STATUS, status)
statusIntent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionID)
sendBroadcast(statusIntent)
}
companion object {
private const val EXTRA_CONFIRMATION_INTENT = "confirmation_intent"
const val EXTRA_SESSION_ID: String = "session_id"
private const val REQUEST_CODE_CONFIRM_INSTALLATION = 322
fun start(c: Context, sessionId: Int, confirmationIntent: Intent?) {
val intent = Intent(
c,
ConfirmationIntentWrapperActivity2::class.java
)
intent.putExtra(EXTRA_CONFIRMATION_INTENT, confirmationIntent)
intent.putExtra(EXTRA_SESSION_ID, sessionId)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
c.startActivity(intent)
}
}
}

View File

@ -0,0 +1,174 @@
package com.android.grape.sai.rootless
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import com.android.grape.sai.BaseSaiPackageInstaller
import com.android.grape.sai.IOUtils
import com.android.grape.sai.RootlessSaiPiBroadcastReceiver
import com.android.grape.sai.Utils
import com.android.grape.sai.param.SaiPiSessionParams
import com.android.grape.sai.param.SaiPiSessionState
import com.android.grape.sai.param.SaiPiSessionStatus
import com.android.grape.sai.prefers.PreferencesHelper
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@SuppressLint("UnspecifiedRegisterReceiverFlag")
class RootlessSaiPackageInstaller private constructor(c: Context) :
BaseSaiPackageInstaller(c), RootlessSaiPiBroadcastReceiver.EventObserver {
private val mPackageInstaller: PackageInstaller =
context.packageManager.packageInstaller
private val mExecutor: ExecutorService = Executors.newFixedThreadPool(4)
private val mWorkerThread = HandlerThread("RootlessSaiPi Worker")
private val mWorkerHandler: Handler
private val mAndroidPiSessionIdToSaiPiSessionId = ConcurrentHashMap<Int, String>()
private val mSessionIdToAppTempName = ConcurrentHashMap<String, String?>()
private val mBroadcastReceiver: RootlessSaiPiBroadcastReceiver
init {
mWorkerThread.start()
mWorkerHandler = Handler(mWorkerThread.looper)
mBroadcastReceiver = RootlessSaiPiBroadcastReceiver(context)
mBroadcastReceiver.addEventObserver(this)
context.registerReceiver(
mBroadcastReceiver,
IntentFilter(RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT),
"",
mWorkerHandler
)
sInstance = this
}
override fun enqueueSession(sessionId: String) {
val params: SaiPiSessionParams? = takeCreatedSession(sessionId)?.apply {
setSessionState(
sessionId,
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.QUEUED).appTempName(
apkSource().appName
).build()
)
mExecutor.submit { install(sessionId, this) }
}
}
private fun install(sessionId: String, params: SaiPiSessionParams) {
var session: PackageInstaller.Session? = null
var appTempName: String? = null
try {
params.apkSource().use { apkSource ->
appTempName = apkSource.appName
if (appTempName != null) mSessionIdToAppTempName[sessionId] = appTempName!!
setSessionState(
sessionId,
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLING).appTempName(appTempName)
.build()
)
val sessionParams =
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
sessionParams.setInstallLocation(
PreferencesHelper.getInstance(context).installLocation
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) sessionParams.setInstallReason(
PackageManager.INSTALL_REASON_USER
)
val androidSessionId = mPackageInstaller.createSession(sessionParams)
mAndroidPiSessionIdToSaiPiSessionId[androidSessionId] = sessionId
session = mPackageInstaller.openSession(androidSessionId)
var currentApkFile = 0
while (apkSource.nextApk()) {
apkSource.openApkInputStream().use { inputStream ->
session!!.openWrite(
String.format("%d.apk", currentApkFile++),
0,
apkSource.apkLength
).use { outputStream ->
if (inputStream != null) {
IOUtils.copyStream(inputStream, outputStream)
}
session!!.fsync(outputStream)
}
}
}
val callbackIntent: Intent =
Intent(RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT)
val pendingIntent = PendingIntent.getBroadcast(context, 0, callbackIntent,
0)
session!!.commit(pendingIntent.intentSender)
}
} catch (e: Exception) {
Log.w(TAG, e)
if (session != null) session!!.abandon()
setSessionState(
sessionId,
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName(appTempName)
.error(e.localizedMessage, Utils.throwableToString(e)).build()
)
} finally {
if (session != null) session!!.close()
}
}
override fun onInstallationSucceeded(androidSessionId: Int, packageName: String?) {
val sessionId = mAndroidPiSessionIdToSaiPiSessionId[androidSessionId] ?: return
setSessionState(
sessionId,
SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_SUCCEED).packageName(packageName)
.resolvePackageMeta(context).build()
)
}
override fun onInstallationFailed(
androidSessionId: Int,
shortError: String?,
fullError: String?,
exception: Exception?
) {
val sessionId = mAndroidPiSessionIdToSaiPiSessionId[androidSessionId] ?: return
setSessionState(
sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED)
.appTempName(mSessionIdToAppTempName.remove(sessionId))
.error(shortError, fullError)
.build()
)
}
protected override fun tag(): String {
return TAG
}
companion object {
private const val TAG = "RootlessSaiPi"
@SuppressLint("StaticFieldLeak")
private var sInstance: RootlessSaiPackageInstaller? = null
fun getInstance(c: Context): RootlessSaiPackageInstaller {
synchronized(RootlessSaiPackageInstaller::class.java) {
return sInstance
?: RootlessSaiPackageInstaller(c)
}
}
}
}

View File

@ -0,0 +1,85 @@
package com.android.grape.sai.shell
import android.annotation.SuppressLint
import android.os.Build
import android.text.TextUtils
import com.android.grape.sai.Utils
import java.util.Objects
object MiuiUtils {
val isMiui: Boolean
get() = !TextUtils.isEmpty(Utils.getSystemProperty("ro.miui.ui.version.name"))
val miuiVersionName: String
get() {
val versionName: String? = Utils.getSystemProperty("ro.miui.ui.version.name")
return versionName?:"???"
}
val miuiVersionCode: Int
get() {
return try {
Objects.requireNonNull(Utils.getSystemProperty("ro.miui.ui.version.code"))?.toInt()?:0
} catch (e: Exception) {
-1
}
}
val actualMiuiVersion: String
get() = Build.VERSION.INCREMENTAL
private fun parseVersionIntoParts(version: String): IntArray {
try {
val versionParts =
version.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val intVersionParts = IntArray(versionParts.size)
for (i in versionParts.indices) intVersionParts[i] = versionParts[i].toInt()
return intVersionParts
} catch (e: Exception) {
return intArrayOf(-1)
}
}
/**
* @return 0 if versions are equal, values less than 0 if ver1 is lower than ver2, value more than 0 if ver1 is higher than ver2
*/
private fun compareVersions(version1: String, version2: String): Int {
if (version1 == version2) return 0
val version1Parts = parseVersionIntoParts(version1)
val version2Parts = parseVersionIntoParts(version2)
for (i in version2Parts.indices) {
if (i >= version1Parts.size) return -1
if (version1Parts[i] < version2Parts[i]) return -1
if (version1Parts[i] > version2Parts[i]) return 1
}
return 1
}
fun isActualMiuiVersionAtLeast(targetVer: String): Boolean {
return compareVersions(actualMiuiVersion, targetVer) >= 0
}
@get:SuppressLint("PrivateApi")
val isMiuiOptimizationDisabled: Boolean
get() {
if ("0" == Utils.getSystemProperty("persist.sys.miui_optimization")) return true
return try {
Class.forName("android.miui.AppOpsUtils")
.getDeclaredMethod("isXOptMode")
.invoke(null) as Boolean
} catch (e: Exception) {
false
}
}
val isFixedMiui: Boolean
get() = isActualMiuiVersionAtLeast("20.2.20") || isMiuiOptimizationDisabled
}

View File

@ -0,0 +1,78 @@
package com.android.grape.sai.shell
import android.annotation.SuppressLint
import java.io.InputStream
interface Shell {
fun isAvailable(): Boolean
fun exec(command: Command): Result
fun exec(command: Command, inputPipe: InputStream): Result
fun makeLiteral(arg: String): String
class Command(command: String, vararg args: String) {
private val mArgs = ArrayList<String>()
init {
mArgs.add(command)
mArgs.addAll(listOf(*args))
}
fun toStringArray(): Array<String?> {
val array = arrayOfNulls<String>(mArgs.size)
for (i in mArgs.indices) array[i] = mArgs[i]
return array
}
override fun toString(): String {
val sb = StringBuilder()
for (i in mArgs.indices) {
val arg = mArgs[i]
sb.append(arg)
if (i < mArgs.size - 1) sb.append(" ")
}
return sb.toString()
}
class Builder(command: String, vararg args: String) {
private val mCommand = Command(command, *args)
fun addArg(argument: String): Builder {
mCommand.mArgs.add(argument)
return this
}
fun build(): Command {
return mCommand
}
}
}
class Result (
var cmd: Command,
var exitCode: Int,
var out: String,
var err: String
) {
val isSuccessful: Boolean
get() = exitCode == 0
@SuppressLint("DefaultLocale")
override fun toString(): String {
return String.format(
"Command: %s\nExit code: %d\nOut:\n%s\n=============\nErr:\n%s",
cmd,
exitCode,
out,
err
)
}
}
}

View File

@ -0,0 +1,110 @@
package com.android.grape.sai.shell
import android.os.Build
import android.util.Log
import com.android.grape.sai.IOUtils
import com.android.grape.sai.Utils
import rikka.shizuku.Shizuku
import rikka.shizuku.ShizukuRemoteProcess
import java.io.InputStream
class ShizukuShell private constructor() : Shell {
init {
sInstance = this
}
override fun isAvailable(): Boolean {
if (!Shizuku.pingBinder()) return false
try {
return exec(Shell.Command("echo", "test")).isSuccessful
} catch (e: Exception) {
Log.w(TAG, "Unable to access shizuku: ")
Log.w(TAG, e)
return false
}
}
override fun exec(command: Shell.Command): Shell.Result {
return execInternal(command, null)
}
override fun exec(command: Shell.Command, inputPipe: InputStream): Shell.Result {
return execInternal(command, inputPipe)
}
override fun makeLiteral(arg: String): String {
return "'" + arg.replace("'", "'\\''") + "'"
}
private fun execInternal(command: Shell.Command, inputPipe: InputStream?): Shell.Result {
val stdOutSb = StringBuilder()
val stdErrSb = StringBuilder()
try {
val shCommand: Shell.Command.Builder =
Shell.Command.Builder("sh", "-c", command.toString())
val process: ShizukuRemoteProcess =
Shizuku.newProcess(shCommand.build().toStringArray(), null, null)
val stdOutD: Thread =
IOUtils.writeStreamToStringBuilder(stdOutSb, process.getInputStream())
val stdErrD: Thread =
IOUtils.writeStreamToStringBuilder(stdErrSb, process.getErrorStream())
if (inputPipe != null) {
try {
process.getOutputStream().use { outputStream ->
inputPipe.use { inputStream ->
IOUtils.copyStream(inputStream, outputStream)
}
}
} catch (e: Exception) {
stdOutD.interrupt()
stdErrD.interrupt()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) process.destroyForcibly()
else process.destroy()
throw RuntimeException(e)
}
}
process.waitFor()
stdOutD.join()
stdErrD.join()
return Shell.Result(
command,
process.exitValue(),
stdOutSb.toString().trim { it <= ' ' },
stdErrSb.toString().trim { it <= ' ' })
} catch (e: Exception) {
Log.w(TAG, "Unable execute command: ")
Log.w(TAG, e)
return Shell.Result(
command, -1, stdOutSb.toString().trim { it <= ' ' }, """
$stdErrSb
<!> SAI ShizukuShell Java exception: ${Utils.throwableToString(e)}
""".trimIndent()
)
}
}
companion object {
private const val TAG = "ShizukuShell"
private var sInstance: ShizukuShell? = null
val instance: ShizukuShell
get() {
synchronized(ShizukuShell::class.java) {
return sInstance
?: ShizukuShell()
}
}
}
}

View File

@ -0,0 +1,73 @@
package com.android.grape.sai.shell
import android.util.Log
import com.android.grape.sai.Utils
import com.android.grape.util.MockTools
import java.io.InputStream
class SuShell private constructor() : Shell {
init {
sInstance = this
}
fun requestRoot(): Boolean {
try {
return exec(Shell.Command("exit")).isSuccessful
} catch (e: Exception) {
Log.w(TAG, "Unable to acquire root access: ")
Log.w(TAG, e)
return false
}
}
override fun isAvailable(): Boolean {
return requestRoot()
}
override fun exec(command: Shell.Command): Shell.Result {
return execInternal(command, null)
}
override fun exec(command: Shell.Command, inputPipe: InputStream): Shell.Result {
return execInternal(command, inputPipe)
}
override fun makeLiteral(arg: String): String {
return "'" + arg.replace("'", "'\\''") + "'"
}
private fun execInternal(command: Shell.Command, inputPipe: InputStream?): Shell.Result {
val stdOutSb = StringBuilder()
val stdErrSb = StringBuilder()
try {
val res: String = MockTools.execRead(command.toString())
var result_code = -1
if (res !== "") result_code = 0
return Shell.Result(command, result_code, res, stdErrSb.toString().trim { it <= ' ' })
} catch (e: Exception) {
Log.w(TAG, "Unable execute command: ")
Log.w(TAG, e)
return Shell.Result(
command, -1, stdOutSb.toString().trim { it <= ' ' }, """
$stdErrSb
<!> SAI SuShell Java exception: ${Utils.throwableToString(e)}
""".trimIndent()
)
}
}
companion object {
private const val TAG = "SuShell"
private var sInstance: SuShell? = null
val instance: SuShell
get() {
synchronized(SuShell::class.java) {
return sInstance
?: SuShell()
}
}
}
}

View File

@ -0,0 +1,99 @@
package com.android.grape.service
import android.Manifest
import android.accessibilityservice.AccessibilityService
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.view.accessibility.AccessibilityEvent
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.android.grape.MainActivity
import com.android.grape.R
class MyAccessibilityService:AccessibilityService(){
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
}
override fun onInterrupt() {
}
override fun onServiceConnected() {
super.onServiceConnected()
startForegroundService()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"2",
"无障碍服务",
NotificationManager.IMPORTANCE_LOW
)
channel.description = "此服务在后台运行,提供无障碍支持。"
val manager: NotificationManager = getSystemService<NotificationManager>(
NotificationManager::class.java
)
if (manager != null) {
manager.createNotificationChannel(channel)
}
}
}
private fun startForegroundService() {
createNotificationChannel()
val notificationIntent: Intent = Intent(
this,
MainActivity::class.java
)
val pendingIntentFlags =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
val pendingIntent =
PendingIntent.getActivity(this, 0, notificationIntent, pendingIntentFlags)
val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(this, "2")
.setContentTitle("无障碍服务运行中")
.setContentText("此服务正在持续运行")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(pendingIntent)
val notification = notificationBuilder.build()
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION
)
!= PackageManager.PERMISSION_GRANTED
) {
Toast.makeText(this, "请授予前台服务权限以启动服务", Toast.LENGTH_SHORT).show()
return
}
val requiredPermissions = arrayOf(
Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION,
"android.permission.CAPTURE_VIDEO_OUTPUT"
)
for (permission in requiredPermissions) {
if (ContextCompat.checkSelfPermission(
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
return
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
} else {
startForeground(1, notification)
}
}
}

View File

@ -0,0 +1,119 @@
package com.android.grape.util
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.IInterface
import android.os.Looper
import android.os.Parcel
import android.os.RemoteException
import android.util.Log
import java.util.concurrent.LinkedBlockingQueue
/**
* date:2020-11-13
* desc:
*/
object AdvertisingIdClient {
private const val TAG = "AdvertisingIdClient"
private fun logd(msg: String) {
Log.d(TAG, msg)
}
/**
* 这个方法是耗时的不能在主线程调用
*/
@Throws(Exception::class)
fun getGoogleAdId(context: Context): String? {
if (Looper.getMainLooper() == Looper.myLooper()) {
logd("getGoogleAdId not running in main thread#########")
return null
}
val pm = context.packageManager
pm.getPackageInfo("com.android.vending", 0)
val connection = AdvertisingConnection()
val intent = Intent(
"com.google.android.gms.ads.identifier.service.START"
)
intent.setPackage("com.google.android.gms")
if (context.bindService(intent, connection, Context.BIND_AUTO_CREATE)) {
try {
val adInterface = AdvertisingInterface(
connection.binder
)
return adInterface.getId()
} finally {
context.unbindService(connection)
}
}
return null
}
private class AdvertisingConnection : ServiceConnection {
var retrieved: Boolean = false
private val queue = LinkedBlockingQueue<IBinder>(1)
override fun onServiceConnected(name: ComponentName, service: IBinder) {
try {
queue.put(service)
} catch (localInterruptedException: InterruptedException) {
}
}
override fun onServiceDisconnected(name: ComponentName) {
}
@get:Throws(InterruptedException::class)
val binder: IBinder
get() {
check(!this.retrieved)
this.retrieved = true
return queue.take()
}
}
private class AdvertisingInterface(private val binder: IBinder) : IInterface {
override fun asBinder(): IBinder {
return binder
}
@Throws(RemoteException::class)
fun getId(): String? {
val data = Parcel.obtain()
val reply = Parcel.obtain()
var id: String?
try {
data.writeInterfaceToken("com.google.android.gms.ads.identifier.internal.IAdvertisingIdService")
binder.transact(1, data, reply, 0)
reply.readException()
id = reply.readString()
} finally {
reply.recycle()
data.recycle()
}
return id
}
@Throws(RemoteException::class)
fun isLimitAdTrackingEnabled(paramBoolean: Boolean): Boolean {
val data = Parcel.obtain()
val reply = Parcel.obtain()
var limitAdTracking: Boolean
try {
data.writeInterfaceToken("com.google.android.gms.ads.identifier.internal.IAdvertisingIdService")
data.writeInt(if (paramBoolean) 1 else 0)
binder.transact(2, data, reply, 0)
reply.readException()
limitAdTracking = 0 != reply.readInt()
} finally {
reply.recycle()
data.recycle()
}
return limitAdTracking
}
}
}

View File

@ -0,0 +1,435 @@
package com.android.grape.util
import android.content.ContentResolver
import android.content.Context
import android.util.Log
import com.android.grape.data.AfInfo
import com.android.grape.data.BigoInfo
import com.android.grape.data.DeviceInfo
import com.blankj.utilcode.util.LogUtils
import org.json.JSONObject
import java.lang.reflect.InvocationTargetException
object ChangeDeviceInfoUtil {
fun changeDeviceInfo(
current_pkg_name: String,
context: Context?,
bigoDeviceObject: JSONObject?,
afDeviceObject: JSONObject?
) {
var bigoDeviceObject = bigoDeviceObject
var afDeviceObject = afDeviceObject
try {
val bigoDevice: BigoInfo
if (bigoDeviceObject != null) {
// BIGO
val cpuClockSpeed = bigoDeviceObject.optString("cpu_clock_speed")
val gaid = bigoDeviceObject.optString("gaid")
val userAgent = bigoDeviceObject.optString("User-Agent")
val osLang = bigoDeviceObject.optString("os_lang")
val osVer = bigoDeviceObject.optString("os_ver")
val tz = bigoDeviceObject.optString("tz")
val systemCountry = bigoDeviceObject.optString("system_country")
val simCountry = bigoDeviceObject.optString("sim_country")
val romFreeIn = bigoDeviceObject.optLong("rom_free_in")
val resolution = bigoDeviceObject.optString("resolution")
val vendor = bigoDeviceObject.optString("vendor")
val batteryScale = bigoDeviceObject.optInt("bat_scale")
// String model = deviceObject.optString("model");
val net = bigoDeviceObject.optString("net")
val dpi = bigoDeviceObject.optLong("dpi")
val romFreeExt = bigoDeviceObject.optLong("rom_free_ext")
val dpiF = bigoDeviceObject.optString("dpi_f")
val cpuCoreNum = bigoDeviceObject.optLong("cpu_core_num")
bigoDevice = BigoInfo()
bigoDevice.cpuClockSpeed = cpuClockSpeed
bigoDevice.gaid = gaid
bigoDevice.userAgent = userAgent
bigoDevice.osLang = osLang
bigoDevice.osVer = osVer
bigoDevice.tz = tz
bigoDevice.systemCountry = systemCountry
bigoDevice.simCountry = simCountry
bigoDevice.romFreeIn = romFreeIn
bigoDevice.resolution = resolution
bigoDevice.vendor = vendor
bigoDevice.batteryScale = batteryScale
bigoDevice.net = net
bigoDevice.dpi = dpi
bigoDevice.romFreeExt = romFreeExt
bigoDevice.dpiF = dpiF
bigoDevice.cpuCoreNum = cpuCoreNum
// TaskUtil.setBigoDevice(bigoDevice)
try {
callVCloudSettings_put(
"$current_pkg_name.system_country",
systemCountry,
context
)
callVCloudSettings_put("$current_pkg_name.sim_country", simCountry, context)
callVCloudSettings_put(
"$current_pkg_name.rom_free_in",
romFreeIn.toString(),
context
)
callVCloudSettings_put("$current_pkg_name.resolution", resolution, context)
callVCloudSettings_put("$current_pkg_name.vendor", vendor, context)
callVCloudSettings_put(
"$current_pkg_name.battery_scale",
batteryScale.toString(),
context
)
callVCloudSettings_put("$current_pkg_name.os_lang", osLang, context)
// callVCloudSettings_put(current_pkg_name + ".model", model, context);
callVCloudSettings_put("$current_pkg_name.net", net, context)
callVCloudSettings_put("$current_pkg_name.dpi", dpi.toString(), context)
callVCloudSettings_put(
"$current_pkg_name.rom_free_ext",
romFreeExt.toString(),
context
)
callVCloudSettings_put("$current_pkg_name.dpi_f", dpiF, context)
callVCloudSettings_put(
"$current_pkg_name.cpu_core_num",
cpuCoreNum.toString(),
context
)
callVCloudSettings_put(
"$current_pkg_name.cpu_clock_speed",
cpuClockSpeed,
context
)
callVCloudSettings_put(current_pkg_name + "_gaid", gaid, context)
// **User-Agent**
callVCloudSettings_put(current_pkg_name + "_user_agent", userAgent, context)
// **os_lang**系统语言
callVCloudSettings_put(current_pkg_name + "_os_lang", osLang, context)
// **os_ver**
callVCloudSettings_put(current_pkg_name + "_os_ver", osVer, context)
// **tz** (时区)
callVCloudSettings_put(current_pkg_name + "_tz", tz, context)
} catch (e: Throwable) {
LogUtils.e(
Log.ERROR,
"ChangeDeviceInfoUtil",
"Error occurred while changing device info",
e
)
throw RuntimeException("Error occurred in changeDeviceInfo", e)
}
bigoDeviceObject = null
}
val deviceInfo: DeviceInfo
val afDevice: AfInfo
if (afDeviceObject != null) {
val advertiserId = afDeviceObject.optString(".advertiserId")
val model = afDeviceObject.optString(".model")
val brand = afDeviceObject.optString(".brand")
val androidId = afDeviceObject.optString(".android_id")
val xPixels = afDeviceObject.optInt(".deviceData.dim.x_px")
val yPixels = afDeviceObject.optInt(".deviceData.dim.y_px")
val densityDpi = afDeviceObject.optInt(".deviceData.dim.d_dpi")
val country = afDeviceObject.optString(".country")
val batteryLevel = afDeviceObject.optString(".batteryLevel")
val stackInfo = Thread.currentThread().stackTrace[2].toString()
val product = afDeviceObject.optString(".product")
val network = afDeviceObject.optString(".network")
val langCode = afDeviceObject.optString(".lang_code")
val cpuAbi = afDeviceObject.optString(".deviceData.cpu_abi")
val yDp = afDeviceObject.optLong(".deviceData.dim.ydp")
afDevice = AfInfo()
afDevice.advertiserId = advertiserId
afDevice.model = model
afDevice.brand = brand
afDevice.androidId = androidId
afDevice.xPixels = xPixels
afDevice.yPixels = yPixels
afDevice.densityDpi = densityDpi
afDevice.country = country
afDevice.batteryLevel = batteryLevel
afDevice.stackInfo = stackInfo
afDevice.product = product
afDevice.network = network
afDevice.langCode = langCode
afDevice.cpuAbi = cpuAbi
afDevice.yDp = yDp
// TaskUtil.setAfDevice(afDevice)
val lang = afDeviceObject.optString(".lang")
val ro_product_brand = afDeviceObject.optString("ro.product.brand", "")
val ro_product_model = afDeviceObject.optString("ro.product.model", "")
val ro_product_manufacturer =
afDeviceObject.optString("ro.product.manufacturer", "")
val ro_product_device = afDeviceObject.optString("ro.product.device", "")
val ro_product_name = afDeviceObject.optString("ro.product.name", "")
val ro_build_version_incremental =
afDeviceObject.optString("ro.build.version.incremental", "")
val ro_build_fingerprint = afDeviceObject.optString("ro.build.fingerprint", "")
val ro_odm_build_fingerprint =
afDeviceObject.optString("ro.odm.build.fingerprint", "")
val ro_product_build_fingerprint =
afDeviceObject.optString("ro.product.build.fingerprint", "")
val ro_system_build_fingerprint =
afDeviceObject.optString("ro.system.build.fingerprint", "")
val ro_system_ext_build_fingerprint =
afDeviceObject.optString("ro.system_ext.build.fingerprint", "")
val ro_vendor_build_fingerprint =
afDeviceObject.optString("ro.vendor.build.fingerprint", "")
val ro_build_platform = afDeviceObject.optString("ro.board.platform", "")
val persist_sys_cloud_drm_id =
afDeviceObject.optString("persist.sys.cloud.drm.id", "")
val persist_sys_cloud_battery_capacity =
afDeviceObject.optInt("persist.sys.cloud.battery.capacity", -1)
val persist_sys_cloud_gpu_gl_vendor =
afDeviceObject.optString("persist.sys.cloud.gpu.gl_vendor", "")
val persist_sys_cloud_gpu_gl_renderer =
afDeviceObject.optString("persist.sys.cloud.gpu.gl_renderer", "")
val persist_sys_cloud_gpu_gl_version =
afDeviceObject.optString("persist.sys.cloud.gpu.gl_version", "")
val persist_sys_cloud_gpu_egl_vendor =
afDeviceObject.optString("persist.sys.cloud.gpu.egl_vendor", "")
val persist_sys_cloud_gpu_egl_version =
afDeviceObject.optString("persist.sys.cloud.gpu.egl_version", "")
val global_android_id = afDeviceObject.optString(".android_id", "")
val anticheck_pkgs = afDeviceObject.optString(".anticheck_pkgs", "")
val pm_list_features = afDeviceObject.optString(".pm_list_features", "")
val pm_list_libraries = afDeviceObject.optString(".pm_list_libraries", "")
val system_http_agent = afDeviceObject.optString("system.http.agent", "")
val webkit_http_agent = afDeviceObject.optString("webkit.http.agent", "")
val com_fk_tools_pkgInfo = afDeviceObject.optString(".pkg_info", "")
val appsflyerKey = afDeviceObject.optString(".appsflyerKey", "")
val appUserId = afDeviceObject.optString(".appUserId", "")
val disk = afDeviceObject.optString(".disk", "")
val operator = afDeviceObject.optString(".operator", "")
val cell_mcc = afDeviceObject.optString(".cell.mcc", "")
val cell_mnc = afDeviceObject.optString(".cell.mnc", "")
val date1 = afDeviceObject.optString(".date1", "")
val date2 = afDeviceObject.optString(".date2", "")
val bootId = afDeviceObject.optString("BootId", "")
deviceInfo = DeviceInfo()
deviceInfo.lang = lang
deviceInfo.roProductBrand = ro_product_brand
deviceInfo.roProductModel = ro_product_model
deviceInfo.roProductManufacturer = ro_product_manufacturer
deviceInfo.roProductDevice = ro_product_device
deviceInfo.roProductName = ro_product_name
deviceInfo.roBuildVersionIncremental = ro_build_version_incremental
deviceInfo.roBuildFingerprint = ro_build_fingerprint
deviceInfo.roOdmBuildFingerprint = ro_odm_build_fingerprint
deviceInfo.roProductBuildFingerprint = ro_product_build_fingerprint
deviceInfo.roSystemBuildFingerprint = ro_system_build_fingerprint
deviceInfo.roSystemExtBuildFingerprint = ro_system_ext_build_fingerprint
deviceInfo.roVendorBuildFingerprint = ro_vendor_build_fingerprint
deviceInfo.roBuildPlatform = ro_build_platform
deviceInfo.persistSysCloudDrmId = persist_sys_cloud_drm_id
deviceInfo.persistSysCloudBatteryCapacity = persist_sys_cloud_battery_capacity
deviceInfo.persistSysCloudGpuGlVendor = persist_sys_cloud_gpu_gl_vendor
deviceInfo.persistSysCloudGpuGlRenderer = persist_sys_cloud_gpu_gl_renderer
deviceInfo.persistSysCloudGpuGlVersion = persist_sys_cloud_gpu_gl_version
deviceInfo.persistSysCloudGpuEglVendor = persist_sys_cloud_gpu_egl_vendor
deviceInfo.persistSysCloudGpuEglVersion = persist_sys_cloud_gpu_egl_version
// TaskUtil.setDeviceInfo(deviceInfo)
try {
callVCloudSettings_put("$current_pkg_name.advertiserId", advertiserId, context)
callVCloudSettings_put("$current_pkg_name.model", model, context)
callVCloudSettings_put("$current_pkg_name.brand", brand, context)
callVCloudSettings_put("$current_pkg_name.android_id", androidId, context)
callVCloudSettings_put("$current_pkg_name.lang", lang, context)
callVCloudSettings_put("$current_pkg_name.country", country, context)
callVCloudSettings_put("$current_pkg_name.batteryLevel", batteryLevel, context)
callVCloudSettings_put(
current_pkg_name + "_screen.optMetrics.stack",
stackInfo,
context
)
callVCloudSettings_put("$current_pkg_name.product", product, context)
callVCloudSettings_put("$current_pkg_name.network", network, context)
callVCloudSettings_put("$current_pkg_name.cpu_abi", cpuAbi, context)
callVCloudSettings_put("$current_pkg_name.lang_code", langCode, context)
// **广告标识符 (advertiserId)** 及 **启用状态**
val isAdIdEnabled = true // 默认启用广告 ID
callVCloudSettings_put(
"$current_pkg_name.advertiserIdEnabled",
isAdIdEnabled.toString(),
context
)
val displayMetrics = JSONObject()
displayMetrics.put("widthPixels", xPixels)
displayMetrics.put("heightPixels", yPixels)
displayMetrics.put("densityDpi", densityDpi)
displayMetrics.put("yDp", yDp)
callVCloudSettings_put(
"screen.device.displayMetrics",
displayMetrics.toString(),
context
)
if (!ShellUtils.hasRootAccess()) {
LogUtils.d(
"ERROR",
"ChangeDeviceInfoUtil",
"Root access is required to execute system property changes"
)
}
// 设置机型, 直接设置属性
ShellUtils.execRootCmd("setprop ro.product.brand $ro_product_brand")
ShellUtils.execRootCmd("setprop ro.product.model $ro_product_model")
ShellUtils.execRootCmd("setprop ro.product.manufacturer $ro_product_manufacturer")
ShellUtils.execRootCmd("setprop ro.product.device $ro_product_device")
ShellUtils.execRootCmd("setprop ro.product.name $ro_product_name")
ShellUtils.execRootCmd("setprop ro.build.version.incremental $ro_build_version_incremental")
ShellUtils.execRootCmd("setprop ro.build.fingerprint $ro_build_fingerprint")
ShellUtils.execRootCmd("setprop ro.odm.build.fingerprint $ro_odm_build_fingerprint")
ShellUtils.execRootCmd("setprop ro.product.build.fingerprint $ro_product_build_fingerprint")
ShellUtils.execRootCmd("setprop ro.system.build.fingerprint $ro_system_build_fingerprint")
ShellUtils.execRootCmd("setprop ro.system_ext.build.fingerprint $ro_system_ext_build_fingerprint")
ShellUtils.execRootCmd("setprop ro.vendor.build.fingerprint $ro_vendor_build_fingerprint")
ShellUtils.execRootCmd("setprop ro.board.platform $ro_build_platform")
// Native.setBootId(bootId);
// 修改drm id
ShellUtils.execRootCmd("setprop persist.sys.cloud.drm.id $persist_sys_cloud_drm_id")
// 电量模拟需要大于1000
ShellUtils.execRootCmd("setprop persist.sys.cloud.battery.capacity $persist_sys_cloud_battery_capacity")
ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_vendor $persist_sys_cloud_gpu_gl_vendor")
ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_renderer $persist_sys_cloud_gpu_gl_renderer")
// 这个值不能随便改 必须是 OpenGL ES %d.%d 这个格式
ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_version $persist_sys_cloud_gpu_gl_version")
ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_vendor $persist_sys_cloud_gpu_egl_vendor")
ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_version $persist_sys_cloud_gpu_egl_version")
} catch (e: Throwable) {
LogUtils.e(
Log.ERROR,
"ChangeDeviceInfoUtil",
"Error occurred in changeDeviceInfo",
e
)
throw RuntimeException("Error occurred in changeDeviceInfo", e)
}
afDeviceObject = null
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun callVCloudSettings_put(key: String?, value: String?, context: Context?) {
if (context == null) {
LogUtils.e(Log.ERROR, "ChangeDeviceInfoUtil", "Context cannot be null", null)
throw IllegalArgumentException("Context cannot be null")
}
if (key == null || key.isEmpty()) {
LogUtils.e(Log.ERROR, "ChangeDeviceInfoUtil", "Key cannot be null or empty", null)
throw IllegalArgumentException("Key cannot be null or empty")
}
if (value == null) {
LogUtils.e(Log.ERROR, "ChangeDeviceInfoUtil", "Value cannot be null", null)
throw IllegalArgumentException("Value cannot be null")
}
try {
// 获取类对象
val clazz = Class.forName("android.provider.VCloudSettings\$Global")
val putStringMethod = clazz.getDeclaredMethod(
"putString",
ContentResolver::class.java,
String::class.java,
String::class.java
)
putStringMethod.isAccessible = true
// 调用方法
putStringMethod.invoke(null, context.contentResolver, key, value)
Log.d("Debug", "putString executed successfully.")
} catch (e: ClassNotFoundException) {
LogUtils.e(
Log.WARN,
"ChangeDeviceInfoUtil",
"Class not found: android.provider.VCloudSettings\$Global. This may not be supported on this device.",
e
)
} catch (e: NoSuchMethodException) {
LogUtils.e(
Log.WARN,
"ChangeDeviceInfoUtil",
"Method not found: android.provider.VCloudSettings\$Global.putString. This may not be supported on this",
e
)
} catch (e: InvocationTargetException) {
val cause = e.targetException
if (cause is SecurityException) {
LogUtils.e(
Log.ERROR,
"ChangeDeviceInfoUtil",
"Error occurred in changeDeviceInfo",
cause
)
} else {
LogUtils.e(
Log.ERROR,
"ChangeDeviceInfoUtil",
"Error occurred in changeDeviceInfo",
cause
)
}
} catch (e: Exception) {
LogUtils.e(
Log.ERROR,
"ChangeDeviceInfoUtil",
"Unexpected error during putString invocation",
e
)
}
}
// fun resetChangedDeviceInfo(current_pkg_name: String, context: Context?) {
// try {
// Native.setBootId("00000000000000000000000000000000")
// } catch (e: Exception) {
// LogUtils.e(Log.ERROR, "ChangeDeviceInfoUtil", "Error occurred in reset", e)
// }
//
// if (!ShellUtils.hasRootAccess()) {
// LogUtils.e(
// Log.ERROR,
// "ChangeDeviceInfoUtil",
// "Root access is required to execute system property changes",
// null
// )
// return
// }
// ShellUtils.execRootCmd("cmd settings2 delete global global_android_id")
// ShellUtils.execRootCmd("cmd settings2 delete global pm_list_features")
// ShellUtils.execRootCmd("cmd settings2 delete global pm_list_libraries")
// ShellUtils.execRootCmd("cmd settings2 delete global anticheck_pkgs")
// ShellUtils.execRootCmd("cmd settings2 delete global " + current_pkg_name + "_android_id")
// ShellUtils.execRootCmd("cmd settings2 delete global " + current_pkg_name + "_adb_enabled")
// ShellUtils.execRootCmd("cmd settings2 delete global " + current_pkg_name + "_development_settings_enabled")
//
// ShellUtils.execRootCmd("setprop persist.sys.cloud.drm.id \"\"")
//
// ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_vendor \"\"")
// ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_renderer \"\"")
// // 这个值不能随便改 必须是 OpenGL ES %d.%d 这个格式
// ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.gl_version \"\"")
//
// ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_vendor \"\"")
// ShellUtils.execRootCmd("setprop persist.sys.cloud.gpu.egl_version \"\"")
//
// ShellUtils.execRootCmd("setprop ro.product.brand Vortex")
// ShellUtils.execRootCmd("setprop ro.product.model HD65_Select")
// ShellUtils.execRootCmd("setprop ro.product.manufacturer Vortex")
// ShellUtils.execRootCmd("setprop ro.product.device HD65_Select")
// ShellUtils.execRootCmd("setprop ro.product.name HD65_Select")
// ShellUtils.execRootCmd("setprop ro.build.version.incremental 20240306")
// ShellUtils.execRootCmd("setprop ro.build.fingerprint \"Vortex/HD65_Select/HD65_Select:13/TP1A.220624.014/20240306:user/release-keys\"")
// ShellUtils.execRootCmd("setprop ro.board.platform sm8150p")
// }
}

View File

@ -0,0 +1,161 @@
package com.android.grape.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import androidx.core.content.ContextCompat
import com.blankj.utilcode.util.LogUtils
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.CountDownLatch
object ClashUtil {
fun startProxy(context: Context) {
val intent = Intent("com.github.kr328.clash.intent.action.SESSION_CREATE")
intent.putExtra("profile", "default") // 可选择您在 Clash 中配置的 Profile
context.sendBroadcast(intent)
}
fun stopProxy(context: Context) {
Thread {
val intent = Intent("com.github.kr328.clash.intent.action.SESSION_DESTROY")
context.sendBroadcast(intent)
}.start()
}
var isRunning: Boolean = false
val clashStatusReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
isRunning = intent.getBooleanExtra("isRunning", false)
Log.d("ClashUtil", "Clash Status: $isRunning")
}
}
fun checkProxy(context: Context): Boolean {
val latch = CountDownLatch(1)
try {
checkClashStatus(context, latch)
latch.await() // 等待广播接收器更新状态
} catch (e: InterruptedException) {
LogUtils.log(Log.ERROR, "ClashUtil", "checkProxy: Waiting interrupted", e)
Thread.currentThread().interrupt() // 重新设置中断状态
return false // 返回默认状态或尝试重试
}
return isRunning
}
fun checkClashStatus(context: Context, latch: CountDownLatch) {
val intentFilter = IntentFilter("com.github.kr328.clash.intent.action.SESSION_STATE")
val clashStatusReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
isRunning = intent.getBooleanExtra("isRunning", false)
Log.d("ClashUtil", "Clash Status: $isRunning")
latch.countDown() // 状态更新完成,释放锁
}
}
ContextCompat.registerReceiver(
context,
clashStatusReceiver,
intentFilter,
ContextCompat.RECEIVER_NOT_EXPORTED
)
val queryIntent = Intent("com.github.kr328.clash.intent.action.SESSION_QUERY")
context.sendBroadcast(queryIntent)
}
fun unregisterReceiver(context: Context) {
context.unregisterReceiver(clashStatusReceiver)
}
fun switchProxyGroup(groupName: String?, proxyName: String?, controllerUrl: String) {
if (groupName == null || groupName.trim { it <= ' ' }
.isEmpty() || proxyName == null || proxyName.trim { it <= ' ' }.isEmpty()) {
LogUtils.log(
Log.ERROR,
"ClashUtil",
"switchProxyGroup: Invalid arguments",
null
)
throw IllegalArgumentException("Group name and proxy name must not be empty")
}
if (!controllerUrl.matches("^https?://.*".toRegex())) {
LogUtils.log(
Log.ERROR,
"ClashUtil",
"switchProxyGroup: Invalid controller URL",
null
)
throw IllegalArgumentException("Invalid controller URL")
}
val client = OkHttpClient()
val json = JSONObject()
try {
json.put("name", proxyName)
} catch (e: JSONException) {
LogUtils.log(Log.ERROR, "ClashUtil", "switchProxyGroup: JSON error", e)
}
val jsonBody = json.toString()
val JSON: MediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = jsonBody.toRequestBody(JSON)
val url = controllerUrl.toHttpUrl()
.newBuilder()
.addPathSegments("proxies/$groupName")
.build()
val request = Request.Builder()
.url(url)
.put(requestBody)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
LogUtils.log(
Log.ERROR,
"ClashUtil",
"switchProxyGroup: Failed to switch proxy",
e
)
println("Failed to switch proxy: " + e.message)
}
@Throws(IOException::class)
override fun onResponse(call: Call, response: Response) {
try {
if (response.body != null) {
LogUtils.log(
Log.INFO,
"ClashUtil",
"switchProxyGroup: Switch proxy response",
null
)
} else {
LogUtils.log(
Log.ERROR,
"ClashUtil",
"switchProxyGroup: Response body is null",
null
)
}
} finally {
response.close()
}
}
})
}
}

View File

@ -0,0 +1,247 @@
package com.android.grape.util
import android.util.Log
import org.json.JSONException
import org.json.JSONObject
import java.util.Random
import java.util.UUID
object DeviceConvertUtil {
@Throws(JSONException::class)
fun MGConvert(temple: JSONObject): JSONObject {
val result = JSONObject()
result.put("sensor_size", temple.getInt("sensor_size"))
//result.put("pr",temple.getJSONObject("pr").getJSONObject("nameValuePairs"));
result.put("sensor", temple.getJSONArray("sensor"))
result.put("networkType", getNetworkType()) //
result.put("brand", temple.getString("brand"))
if (temple.has("cpu_abi")) {
result.put("ABI", temple.getString("cpu_abi"))
}
if (temple.has("cpu_abi2")) {
result.put("ABI2", temple.getString("cpu_abi2"))
}
val drmId = UUID.randomUUID().toString().replace("-", "")
val sysBootId = UUID.randomUUID().toString()
result.put("drmId", drmId)
result.put("sysBootId", sysBootId)
result.put("screenBrightness", Random().nextInt(100))
result.put("device", temple.getString("device"))
result.put("display", temple.getString("build_display_id"))
result.put("model", temple.getString("model"))
result.put("product", temple.getString("product"))
result.put("rawProduct", temple.getString("rawProduct"))
result.put("rawDevice", temple.getString("rawDevice"))
result.put("API", temple.getString("sdk"))
result.put("AndroidID", temple.getString("androidId"))
result.put("Carrier", temple.getString("carrier")) //
result.put("simopename", temple.getString("operator")) //
result.put("gaid", temple.getString("advertiserId")) //
result.put("regionid", temple["deviceCity"]) //timezoneJo.get("deviceCity"));
result.put("lang", temple.getString("lang_code")) //Locale.lang
result.put("displayLang", temple.getString("lang")) //Locale.displayLang
result.put("country", temple.getString("country")) //Locale.country
result.put("vendingVersionCode", DeviceConvertUtil.getLong(temple, "vendingVersionCode"))
result.put("vendingVersionName", DeviceConvertUtil.getString(temple, "vendingVersionName"))
result.put("build_type", temple.getString("deviceType"))
result.put("osArch", DeviceConvertUtil.getString(temple, "arch"))
val dim = temple.getJSONObject("dim")
result.put("width", dim.getInt("x_px"))
result.put("height", dim.getInt("y_px"))
result.put("DPI", dim.getInt("d_dpi"))
result.put("xdpi", dim.getDouble("xdp"))
result.put("ydpi", dim.getDouble("xdp"))
result.put("screenLayout", dim.getInt("size"))
result.put("wvua", temple.getString("sys_ua"))
/**web_ua */
var forwardIp = ""
if (temple.has("ip") && !temple.isNull("ip")) {
forwardIp = temple.getString("ip")
}
result.put("forwardIp", forwardIp)
val pr = temple.getJSONObject("pr").getJSONObject("nameValuePairs")
//result.put("armVariant", temple.getString("armVariant"));
result.put("roArch", getString(pr, "aa"))
result.put("chipName", getString(pr, "ab"))
result.put("nativeBridge", getString(pr, "ac"))
result.put("sysNativeBridge", getString(pr, "ad"))
result.put("nativeBridgeExec", getString(pr, "ae"))
result.put("x86Features", getString(pr, "af"))
result.put("x86Variant", getString(pr, "ag"))
result.put("zygote", getString(pr, "ah"))
result.put("mockLocation", getString(pr, "ai"))
result.put("isaArm", getString(pr, "aj"))
result.put("armFeatures", getString(pr, "ak"))
result.put("armVariant", getString(pr, "al"))
result.put("armFeatures", getString(pr, "am")) //am
result.put("armVariant", getString(pr, "an")) //an
result.put("", getString(pr, "ao")) //ao
result.put("buildUser", getString(pr, "ap")) //ap
result.put("", getString(pr, "aq")) //aq
result.put("NAME", getString(pr, "ar")) //ar
result.put("ABI", getString(pr, "as")) //as
result.put("abiList", getString(pr, "at")) //at
result.put("abiList32", getString(pr, "au")) //au
result.put("abiList64", getString(pr, "av")) //av
//as
val `as` = temple.getJSONObject("as").getJSONObject("nameValuePairs")
result.put("InstallerPackageCountOfNone", getInt(`as`, "null"))
result.put("InstallerPackageCountOfOther", getInt(`as`, "other"))
result.put("InstallerPackageCountOfVending", getInt(`as`, "cav"))
//disk
val disk = temple.getString("disk")
val disk_arr = disk.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
result.put("diskAvailableBlocks", disk_arr[0].toLong())
result.put("diskBlockSize", disk_arr[1].toLong())
result.put("diskBlockCount", 1)
//batteryLevel
result.put("batteryLevel", temple.getInt("batteryLevel")) //
//battery
result.put(
"batteryTempratureMinValue",
(getInt(temple, "battery") * 0.9).toInt()
)
result.put(
"batteryTempratureMaxValue",
(getInt(temple, "battery") * 1.1).toInt()
)
//telephony
result.put("mcc", temple.getInt("mcc"))
result.put("mnc", temple.getInt("mnc"))
result.put("firstInstallTime", 0L)
if (temple.has("sensors")) {
result.put("sensors", temple.getString("sensors"))
}
if (temple.has("expand")) {
val expand = temple.getJSONObject("expand")
// result.put("expand", expand);
result.put("board", expand.getString("board"))
result.put("longtitude", expand.getDouble("longtitude"))
result.put("latitude", expand.getDouble("latitude"))
Log.d("IOSTQ:longtitude", expand.getDouble("longtitude").toString() + "")
result.put("fingerprint", expand.getString("fingerprint"))
result.put("Manufacture", expand.getString("Manufacture"))
result.put("ID", expand.getString("ID"))
result.put("roBuildDescription", expand.getString("roBuildDescription"))
result.put("WifiMAC", expand.getString("WifiMAC"))
result.put("WifiName", expand.getString("WifiName"))
result.put("BSSID", expand.getString("BSSID"))
result.put("tmdisplayname", expand.getString("tmdisplayname"))
result.put("timerawoff", expand.getString("timerawoff"))
result.put("displayLang", expand.getString("displayLang"))
result.put("cpuinfostr", expand.getString("cpuinfostr"))
result.put("cpuinfobuf", expand.getString("cpuinfobuf"))
result.put("pmlib", expand.getString("pmlib"))
result.put("pmfea", expand.getString("pmfea"))
result.put("soRomRam", expand.getString("soRomRam"))
result.put("roHardware", expand.getString("roHardware"))
result.put("AndroidVer", expand.getString("AndroidVer"))
result.put("WifiMAC", expand.getString("WifiMAC"))
result.put("ratioVersion", expand.getString("ratioVersion"))
result.put("sdcardCreateTime", expand.getString("sdcardCreateTime"))
result.put("localip", expand.getString("localip"))
result.put("incremental", expand.getString("incremental"))
result.put("amGetConfig", expand.getString("amGetConfig"))
result.put("roBuildDescription", expand.getString("roBuildDescription"))
result.put("rootStatfs", expand.getString("rootStatfs"))
result.put("downloadCacheStatfs", expand.getString("downloadCacheStatfs"))
result.put("dataStatfs", expand.getString("dataStatfs"))
result.put("sdStatfs", expand.getString("sdStatfs"))
result.put("baseband", expand.getString("baseband"))
result.put("procVersion", expand.getString("procVersion"))
result.put("procStat", expand.getString("procStat"))
result.put("glRenderer", expand.getString("glRenderer"))
result.put("glVendor", expand.getString("glVendor"))
result.put("glVersion", expand.getString("glVersion"))
result.put("glExtensions", expand.getString("glExtensions"))
}
return result
}
fun getDouble(jo: JSONObject, name: String): Double {
if (jo.has(name) && !jo.isNull(name)) {
try {
return jo.getDouble(name)
} catch (e: Exception) {
}
}
return 0.0
}
fun getBoolean(jo: JSONObject, name: String): Boolean {
if (jo.has(name) && !jo.isNull(name)) {
try {
return jo.getBoolean(name)
} catch (e: Exception) {
}
}
return false
}
fun getInt(jo: JSONObject, name: String): Int {
if (jo.has(name) && !jo.isNull(name)) {
try {
return jo.getInt(name)
} catch (e: Exception) {
}
}
return 0
}
fun getLong(jo: JSONObject, name: String): Long {
if (jo.has(name) && !jo.isNull(name)) {
try {
return jo.getLong(name)
} catch (e: Exception) {
}
}
return 0L
}
fun getString(jo: JSONObject, name: String): String {
if (jo.has(name) && !jo.isNull(name)) {
try {
return jo.getString(name)
} catch (e: Exception) {
}
}
return ""
}
val networkTypeArr: IntArray = intArrayOf(1, 3, 4)
private fun getNetworkType(): Int {
return networkTypeArr[Random().nextInt(networkTypeArr.size)]
}
}

View File

@ -0,0 +1,390 @@
package com.android.grape.util
import android.util.Log
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.FileWriter
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.io.OutputStream
import java.io.RandomAccessFile
import java.io.UnsupportedEncodingException
import java.util.Enumeration
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
/**
* @author litianxiang
* @description:
* @date :2021/9/28 15:44
*/
object FileUtils {
private const val TAG = "IOSTQ:FileUtils"
private const val BUFFER_SIZE = 1024 * 1024 //1M Byte
//创建文件夹
fun createDir(destDirName: String): Boolean {
var destDirName = destDirName
val dir = File(destDirName)
if (dir.exists()) {
println("创建目录" + destDirName + "失败,目标目录已经存在")
return false
}
if (!destDirName.endsWith(File.separator)) {
destDirName = destDirName + File.separator
}
//创建目录
if (dir.mkdirs()) {
Log.e("createDir", "创建目录" + destDirName + "成功!")
return true
} else {
Log.e("createDir", "创建目录" + destDirName + "失败!")
return false
}
}
/**
* 创建文件
*
* @param filePath 文件地址
* @param fileName 文件名
* @return
*/
fun createFile(filePath: String, fileName: String): Boolean {
val strFilePath = filePath + fileName
val file = File(filePath)
if (!file.exists()) {
/** 注意这里是 mkdirs()方法 可以创建多个文件夹 */
file.mkdirs()
}
val subfile = File(strFilePath)
if (!subfile.exists()) {
try {
val b = subfile.createNewFile()
return b
} catch (e: IOException) {
e.printStackTrace()
}
} else {
return true
}
return false
}
/**
* 遍历文件夹下的文件
*
* @param file 地址
*/
fun getFile(file: File): List<File>? {
val list: MutableList<File> = mutableListOf()
val fileArray = file.listFiles()
if (fileArray == null) {
return null
} else {
for (f in fileArray) {
if (f.isFile) {
list.add(0, f)
} else {
getFile(f)
}
}
}
return list
}
/**
* 删除文件
*
* @param filePath 文件地址
* @return
*/
fun deleteFiles(filePath: String): Boolean {
val files = getFile(File(filePath))
if (files!!.size != 0) {
for (i in files.indices) {
val file = files[i]
/** 如果是文件则删除 如果都删除可不必判断 */
if (file.isFile) {
file.delete()
}
}
}
return true
}
//删除文件夹和文件夹里面的文件
fun deleteDirWihtFile(dir: File?) {
if (dir == null || !dir.exists() || !dir.isDirectory) return
for (file in dir.listFiles()) {
if (file.isFile) file.delete() // 删除所有文件
else if (file.isDirectory) deleteDirWihtFile(file) // 递规的方式删除文件夹
}
dir.delete() // 删除目录本身
}
/**
* 向文件中添加内容
*
* @param strcontent 内容
* @param filePath 地址
* @param fileName 文件名
*/
fun writeToFile(strcontent: String, filePath: String, fileName: String) {
//生成文件夹之后,再生成文件,不然会出错
val strFilePath = filePath + fileName
// 每次写入时,都换行写
val subfile = File(strFilePath)
var raf: RandomAccessFile? = null
try {
/** 构造函数 第二个是读写方式 */
raf = RandomAccessFile(subfile, "rw")
/** 将记录指针移动到该文件的最后 */
raf.seek(subfile.length())
/** 向文件末尾追加内容 */
raf.write(strcontent.toByteArray())
raf.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
/**
* 修改文件内容覆盖或者添加
*
* @param path 文件地址
* @param content 覆盖内容
* @param append 指定了写入的方式是覆盖写还是追加写(true=追加)(false=覆盖)
*/
fun modifyFile(path: String?, content: String?, append: Boolean) {
try {
val fileWriter = FileWriter(path, append)
val writer = BufferedWriter(fileWriter)
writer.append(content)
writer.flush()
writer.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
/**
* 读取文件内容
*
* @param filePath 地址
* @param filename 名称
* @return 返回内容
*/
fun getString(filePath: String, filename: String): String {
var inputStream: FileInputStream? = null
try {
inputStream = FileInputStream(File(filePath + filename))
} catch (e: FileNotFoundException) {
e.printStackTrace()
}
var inputStreamReader: InputStreamReader? = null
try {
inputStreamReader = InputStreamReader(inputStream, "UTF-8")
} catch (e1: UnsupportedEncodingException) {
e1.printStackTrace()
}
val reader = BufferedReader(inputStreamReader)
val sb = StringBuffer("")
var line: String?
try {
while ((reader.readLine().also { line = it }) != null) {
sb.append(line)
sb.append("\n")
}
} catch (e: IOException) {
e.printStackTrace()
}
return sb.toString()
}
/**
* 重命名文件
*
* @param oldPath 原来的文件地址
* @param newPath 新的文件地址
*/
fun renameFile(oldPath: String, newPath: String) {
val oleFile = File(oldPath)
val newFile = File(newPath)
//执行重命名
oleFile.renameTo(newFile)
}
/**
* 复制文件
*
* @param fromFile 要复制的文件目录
* @param toFile 要粘贴的文件目录
* @return 是否复制成功
*/
fun copy(fromFile: String, toFile: String): Boolean {
//要复制的文件目录
val currentFiles: Array<File>
val root = File(fromFile)
//如同判断SD卡是否存在或者文件是否存在
//如果不存在则 return出去
if (!root.exists()) {
return false
}
//如果存在则获取当前目录下的全部文件 填充数组
currentFiles = root.listFiles()
//目标目录
val targetDir = File(toFile)
//创建目录
if (!targetDir.exists()) {
targetDir.mkdirs()
}
//遍历要复制该目录下的全部文件
for (i in currentFiles.indices) {
if (currentFiles[i].isDirectory) //如果当前项为子目录 进行递归
{
copy(currentFiles[i].path + "/", toFile + currentFiles[i].name + "/")
} else //如果当前项为文件则进行文件拷贝
{
CopySdcardFile(currentFiles[i].path, toFile + currentFiles[i].name)
}
}
return true
}
//文件拷贝
//要复制的目录下的所有非子目录(文件夹)文件拷贝
fun CopySdcardFile(fromFile: String?, toFile: String?): Boolean {
try {
val fosfrom: InputStream = FileInputStream(fromFile)
val fosto: OutputStream = FileOutputStream(toFile)
val bt = ByteArray(1024)
var c: Int
while ((fosfrom.read(bt).also { c = it }) > 0) {
fosto.write(bt, 0, c)
}
fosfrom.close()
fosto.close()
return true
} catch (ex: Exception) {
return false
}
}
/**
* 获取目录下所有文件
* @param path 指定目录路径
* @return
*/
fun getFilesAllName(path: String): List<String>? {
val file = File(path)
val files = file.listFiles()
if (files == null) {
Log.e("error", "空目录")
return null
}
val s: MutableList<String> = ArrayList()
for (i in files.indices) {
s.add(files[i].absolutePath)
}
return s
}
/**
* 解压缩一个文件
*
* @param zipFile 压缩文件
* @param folderPath 解压缩的目标目录
* @return
* @throws IOException 当解压缩过程出错时抛出
*/
@Throws(IOException::class)
fun upZipFile(zipFile: File?, folderPath: String): ArrayList<File> {
val fileList = ArrayList<File>()
val desDir = File(folderPath)
if (!desDir.exists()) {
desDir.mkdirs()
}
val zf = ZipFile(zipFile)
val entries: Enumeration<*> = zf.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement() as ZipEntry
if (entry.isDirectory) {
continue
}
val `is` = zf.getInputStream(entry)
var str = folderPath + File.separator + entry.name
str = String(str.toByteArray(charset("8859_1")), charset("UTF-8"))
val desFile = File(str)
if (!desFile.exists()) {
val fileParentDir = desFile.parentFile
if (!fileParentDir!!.exists()) {
fileParentDir.mkdirs()
}
desFile.createNewFile()
}
val os: OutputStream = FileOutputStream(desFile)
val buffer = ByteArray(BUFFER_SIZE)
var length: Int
while ((`is`.read(buffer).also { length = it }) > 0) {
os.write(buffer, 0, length)
}
os.flush()
os.close()
`is`.close()
fileList.add(desFile)
}
return fileList
}
//复制文件
@Throws(IOException::class)
fun copyFile(sourceFile: File?, targetFile: File?) {
// 新建文件输入流并对它进行缓冲
val input = FileInputStream(sourceFile)
val inBuff = BufferedInputStream(input)
// 新建文件输出流并对它进行缓冲
val output = FileOutputStream(targetFile)
val outBuff = BufferedOutputStream(output)
// 缓冲数组
val b = ByteArray(1024 * 5)
var len: Int
while ((inBuff.read(b).also { len = it }) != -1) {
outBuff.write(b, 0, len)
}
// 刷新此缓冲的输出流
outBuff.flush()
//关闭流
inBuff.close()
outBuff.close()
output.close()
input.close()
}
}

View File

@ -0,0 +1,67 @@
package com.android.grape.util
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.InetSocketAddress
import java.net.Socket
object MockTools {
fun exec(cmd: String): String {
var retString = ""
try {
//创建socket
val myCmd = "SU|$cmd"
val mSocket = Socket()
val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345)
mSocket.connect(inetSocketAddress)
val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream()))
val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream()))
bufferedWriter.write(myCmd + "\r\n")
bufferedWriter.flush()
val stringBuilder = StringBuilder()
var line: String? = null
while ((bufferedReader.readLine().also { line = it }) != null) {
stringBuilder.append(line + "\n")
}
//retString = bufferedReader.readLine();
retString = stringBuilder.toString()
bufferedReader.close()
bufferedWriter.close()
} catch (eeeee: Exception) {
eeeee.printStackTrace()
}
return retString
}
fun execRead(cmd: String): String {
var retString = ""
try {
//创建socket
val myCmd = "SU_1|$cmd"
val mSocket = Socket()
val inetSocketAddress = InetSocketAddress("127.0.0.1", 12345)
mSocket.connect(inetSocketAddress)
val bufferedWriter = BufferedWriter(OutputStreamWriter(mSocket.getOutputStream()))
val bufferedReader = BufferedReader(InputStreamReader(mSocket.getInputStream()))
bufferedWriter.write(myCmd + "\r\n")
bufferedWriter.flush()
val stringBuilder = StringBuilder()
var line: String? = null
while ((bufferedReader.readLine().also { line = it }) != null) {
stringBuilder.append(line + "\n")
}
retString = stringBuilder.toString()
//retString = bufferedReader.readLine();
bufferedReader.close()
bufferedWriter.close()
} catch (eeeee: Exception) {
eeeee.printStackTrace()
}
return retString
}
}

View File

@ -0,0 +1,61 @@
package com.android.grape.util
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
object ReflectionHelper {
/**
* 安全地调用私有方法
* @param instance 目标对象实例
* @param methodName 方法名
* @param paramTypes 参数类型列表
* @param args 实际参数
* @return 调用结果
*/
fun <T> callPrivateMethod(
instance: Any,
methodName: String,
paramTypes: Array<Class<*>>,
vararg args: Any?
): T? {
return try {
val method = instance.javaClass.getDeclaredMethod(methodName, *paramTypes)
method.isAccessible = true
method.invoke(instance, *args) as? T
} catch (e: Exception) {
handleReflectionError(e)
null
}
}
private fun handleReflectionError(e: Exception) {
when (e) {
is NoSuchMethodException ->
Log.w("Reflection", "Method not found", e)
is IllegalAccessException ->
Log.w("Reflection", "Access denied", e)
else ->
Log.e("Reflection", "Reflection error", e)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Android 9+ 反射限制检查
reportNonSdkApiUsage()
}
}
@RequiresApi(Build.VERSION_CODES.P)
private fun reportNonSdkApiUsage() {
// 使用 Android 的 API 报告工具
// VMRuntime.getRuntime().setHiddenApiExemptions(listOf("Lcom/example/").toTypedArray())
}
}
// 使用示例
val result = ReflectionHelper.callPrivateMethod<String>(
instance = "",
methodName = "getHiddenData",
paramTypes = arrayOf(),
args = arrayOf()
)

View File

@ -0,0 +1,25 @@
package com.android.grape.util
import android.content.Context
import android.content.Intent
import android.util.Log
object ScriptUtil {
const val TAG: String = "LU_SCRIPT"
const val START: Int = 1
const val STOP: Int = 2
const val SRC_FILES: String = "sc_file"
const val SC_ACTION: String = "com.ak.lu.SCRIPT"
const val EVENT: String = "event"
fun execScript(context: Context, src: String?) {
val intent = Intent(SC_ACTION)
// todo 需要替换为调度app的pkg
// intent.setComponent(new ComponentName(TARGET_PKG, ScriptReceiver.class.getName()));
intent.putExtra(EVENT, START)
intent.putExtra(SRC_FILES, src)
context.sendBroadcast(intent)
Util.script_status = 0
Log.i(TAG, context.packageName + " send broadcast:" + SC_ACTION + ":" + EVENT)
}
}

View File

@ -0,0 +1,114 @@
package com.android.grape.util
import android.app.IMikRom
import android.os.IBinder
import android.util.Log
object ServiceUtils {
private var iMikRom: IMikRom? = null
fun getiMikRom(): IMikRom? {
if (iMikRom == null) {
try {
val localClass = Class.forName("android.os.ServiceManager")
val getService = localClass.getMethod(
"getService", *arrayOf<Class<*>>(
String::class.java
)
)
if (getService != null) {
val objResult = getService.invoke(localClass, *arrayOf<Any>("mikrom"))
if (objResult != null) {
val binder = objResult as IBinder
iMikRom = IMikRom.Stub.asInterface(binder)
}
}
} catch (e: Exception) {
Log.d("MikManager", e.message!!)
e.printStackTrace()
}
}
return iMikRom
}
/**
* 文件读取
* @param path
* @return
*/
fun readFile(path: String?): String {
var retMsg = ""
val iMikRom: IMikRom? = getiMikRom()
if (iMikRom != null) {
try {
retMsg = iMikRom.readFile(path)
} catch (eee: Exception) {
eee.printStackTrace()
}
}
return retMsg
}
/**
* 文件保存
* @param path
* @param data
*/
fun writeFile(path: String?, data: String?) {
val iMikRom: IMikRom? = getiMikRom()
if (iMikRom != null) {
try {
iMikRom.writeFile(path, data)
} catch (eee: Exception) {
eee.printStackTrace()
}
}
}
fun shellExec(cmd: String?): String {
var retMsg = ""
val iMikRom: IMikRom? = getiMikRom()
if (iMikRom != null) {
try {
retMsg = iMikRom.shellExec(cmd)
} catch (eee: Exception) {
eee.printStackTrace()
}
}
return retMsg
}
/**
* 判断App是否已经开启注入
* @param callingPkg
* @return
*/
fun isEnableApp(callingPkg: String): Boolean {
var retMsg = false
val iMikRom: IMikRom? = getiMikRom()
if (iMikRom != null) {
try {
retMsg = iMikRom.isEnableApp(callingPkg)
} catch (eee: Exception) {
eee.printStackTrace()
}
}
return retMsg
}
/**
* 设置App是否需要打开注入
* @param pkgName
* @param isEnable
*/
fun setEnableApp(pkgName: String, isEnable: Boolean) {
val iMikRom: IMikRom? = getiMikRom()
if (iMikRom != null) {
try {
iMikRom.setEnableApp(pkgName, isEnable)
} catch (eee: Exception) {
eee.printStackTrace()
}
}
}
}

View File

@ -0,0 +1,382 @@
package com.android.grape.util;
import static java.security.AccessController.getContext;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import com.blankj.utilcode.util.LogUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ShellUtils {
public static String getPackagePath(Context context, String packageName) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0);
return appInfo.sourceDir; // 返回 APK 的完整路径
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return "";
}
}
public static void exec(String cmd) {
try {
LogUtils.e(Log.INFO, "ShellUtils", "Executing command: " + cmd, null);
Process process = Runtime.getRuntime().exec(cmd);
process.waitFor();
} catch (Exception e) {
LogUtils.e(Log.ERROR, "ShellUtils", "Error executing command: " + e.getMessage(), e);
}
}
public static int getPid(Process p) {
int pid = -1;
try {
Field f = p.getClass().getDeclaredField("pid");
f.setAccessible(true);
pid = f.getInt(p);
f.setAccessible(false);
} catch (Throwable e) {
pid = -1;
}
return pid;
}
public static boolean hasBin(String binName) {
// 验证 binName 是否符合规则
if (binName == null || binName.isEmpty()) {
LogUtils.e(Log.ERROR, "ShellUtils", "Invalid bin name",null);
throw new IllegalArgumentException("Bin name cannot be null or empty");
}
for (char c : binName.toCharArray()) {
if (!Character.isLetterOrDigit(c) && c != '.' && c != '_' && c != '-') {
throw new IllegalArgumentException("Invalid bin name");
}
}
// 获取 PATH 环境变量
String pathEnv = System.getenv("PATH");
if (pathEnv == null) {
LogUtils.e(Log.ERROR, "ShellUtils", "PATH environment variable is not available", null);
return false;
}
// 使用适合当前系统的路径分隔符分割路径
String[] paths = pathEnv.split(File.pathSeparator);
for (String path : paths) {
File file = new File(path, binName); // 使用 File 构造完整路径
try {
// 检查文件是否可执行
if (file.canExecute()) {
return true;
}
} catch (SecurityException e) {
LogUtils.e(Log.ERROR, "ShellUtils", "Security exception occurred while checking: " + file.getAbsolutePath(), e);
}
}
// 如果未找到可执行文件返回 false
return false;
}
public static String execRootCmdAndGetResult(String cmd) {
Log.d("ShellUtils", "execRootCmdAndGetResult - Started execution for command: " + cmd);
if (cmd == null || cmd.trim().isEmpty()) {
LogUtils.e(Log.ERROR, "ShellUtils", "Unsafe or empty command. Aborting execution.", null);
throw new IllegalArgumentException("Unsafe or empty command.");
}
// if (!isCommandSafe(cmd)) { // 检查命令的合法性
// Log.e("ShellUtils", "Detected unsafe command. Aborting execution.");
// throw new IllegalArgumentException("Detected unsafe command.");
// }
Process process = null;
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Log.d("ShellUtils", "Determining appropriate shell for execution...");
if (hasBin("su")) {
Log.d("ShellUtils", "'su' binary found, using 'su' shell.");
process = Runtime.getRuntime().exec("su");
} else if (hasBin("xu")) {
Log.d("ShellUtils", "'xu' binary found, using 'xu' shell.");
process = Runtime.getRuntime().exec("xu");
} else if (hasBin("vu")) {
Log.d("ShellUtils", "'vu' binary found, using 'vu' shell.");
process = Runtime.getRuntime().exec("vu");
} else {
Log.d("ShellUtils", "No specific binary found, using 'sh' shell.");
process = Runtime.getRuntime().exec("sh");
}
try (OutputStream os = new BufferedOutputStream(process.getOutputStream());
InputStream is = process.getInputStream();
InputStream errorStream = process.getErrorStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream, StandardCharsets.UTF_8))) {
Log.d("ShellUtils", "Starting separate thread to process error stream...");
executor.submit(() -> {
String line;
try {
while ((line = errorReader.readLine()) != null) {
LogUtils.e(Log.ERROR, "ShellUtils", "Shell Error: " + line, null);
}
} catch (IOException e) {
LogUtils.e(Log.ERROR, "ShellUtils", "Error while reading process error stream: " + e.getMessage(), e);
}
});
Log.d("ShellUtils", "Writing the command to the shell...");
os.write((cmd + "\n").getBytes());
os.write("exit\n".getBytes());
os.flush();
Log.d("ShellUtils", "Command written to shell. Waiting for process to complete.");
StringBuilder output = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
Log.d("ShellUtils", "Shell Output: " + line);
output.append(line).append("\n");
}
Log.d("ShellUtils", "Awaiting process termination...");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!process.waitFor(10, TimeUnit.SECONDS)) {
LogUtils.e(Log.ERROR, "ShellUtils", "Process execution timed out. Destroying process.", null);
process.destroyForcibly();
throw new RuntimeException("Shell command execution timeout.");
}
} else {
Log.d("ShellUtils", "Using manual time tracking method for process termination (API < 26).");
long startTime = System.currentTimeMillis();
while (true) {
try {
process.exitValue();
break;
} catch (IllegalThreadStateException e) {
if (System.currentTimeMillis() - startTime > 10000) { // 10 seconds
LogUtils.e(Log.ERROR, "ShellUtils", "Process execution timed out (manual tracking). Destroying process.", null);
process.destroy();
throw new RuntimeException("Shell command execution timeout.");
}
Thread.sleep(100); // Sleep briefly before re-checking
}
}
}
Log.d("ShellUtils", "Process terminated successfully. Returning result.");
return output.toString().trim();
}
} catch (IOException | InterruptedException e) {
LogUtils.e(Log.ERROR, "ShellUtils", "Command execution failed: " + e.getMessage(), e);
Thread.currentThread().interrupt();
return "Error: " + e.getMessage();
} finally {
if (process != null) {
Log.d("ShellUtils", "Finalizing process. Attempting to destroy it.");
process.destroy();
}
executor.shutdown();
Log.d("ShellUtils", "Executor service shut down.");
}
}
public static void execRootCmd(String cmd) {
// 校验命令是否安全
if (!isCommandSafe(cmd)) {
LogUtils.e(Log.ERROR, "ShellUtils", "Unsafe command, aborting.", null);
return;
}
List<String> cmds = new ArrayList<>();
cmds.add(cmd);
// 使用同步锁保护线程安全
synchronized (ShellUtils.class) {
try {
List<String> results = execRootCmds(cmds);
// 判断是否需要打印输出仅用于开发调试阶段
for (String result : results) {
Log.d("ShellUtils", "Command Result: " + result);
}
} catch (Exception e) {
LogUtils.e(Log.ERROR, "ShellUtils", "Unexpected error: " + e.getMessage(), e);
}
}
}
private static boolean isCommandSafe(String cmd) {
// 检查空值和空字符串
if (cmd == null || cmd.trim().isEmpty()) {
LogUtils.e(Log.ERROR, "ShellUtils", "Rejected command: empty or null value.", null);
return false;
}
// 检查非法字符
if (!cmd.matches("^[a-zA-Z0-9._/:\\-~`'\" *|]+$")) {
LogUtils.e(Log.ERROR, "ShellUtils", "Rejected command due to illegal characters: " + cmd, null);
return false;
}
// 检查多命令逻辑运算符限制
if (cmd.contains("&&") || cmd.contains("||")) {
Log.d("ShellUtils", "Command contains logical operators.");
if (!isExpectedMultiCommand(cmd)) {
LogUtils.e(Log.ERROR, "ShellUtils", "Rejected command due to prohibited structure: " + cmd, null);
return false;
}
}
// 路径遍历保护
if (cmd.contains("../") || cmd.contains("..\\")) {
LogUtils.e(Log.ERROR, "ShellUtils", "Rejected command due to path traversal attempt: " + cmd, null);
return false;
}
// 命令长度限制
if (cmd.startsWith("tar") && cmd.length() > 800) { // 特定命令支持更长长度
LogUtils.e(Log.ERROR, "ShellUtils", "Command rejected due to excessive length.", null);
return false;
} else if (cmd.length() > 500) {
LogUtils.e("ShellUtils", "Command rejected due to excessive length.", null);
return false;
}
Log.d("ShellUtils", "Command passed safety checks: " + cmd);
return true;
}
// 附加方法检查多命令是否符合预期
private static boolean isExpectedMultiCommand(String cmd) {
// 判断是否为允许的命令组合比如 `cd` `tar` 组合命令
return cmd.matches("^cd .+ && (tar|zip|cp).+");
}
public static List<String> execRootCmds(List<String> cmds) {
List<String> results = new ArrayList<>();
Process process = null;
try {
// 初始化 Shell 环境
process = hasBin("su") ? Runtime.getRuntime().exec("su") : Runtime.getRuntime().exec("sh");
// 启动读取线程
Process stdProcess = process;
Thread stdThread = new Thread(() -> {
try (BufferedReader stdReader = new BufferedReader(new InputStreamReader(stdProcess.getInputStream()))) {
List<String> localResults = new ArrayList<>();
String line;
while ((line = stdReader.readLine()) != null) {
localResults.add(line);
Log.d("ShellUtils", "Stdout: " + line);
}
synchronized (results) {
results.addAll(localResults);
}
} catch (IOException ioException) {
LogUtils.e(Log.ERROR, "ShellUtils", "Error reading stdout", ioException);
}
});
Process finalProcess = process;
Thread errThread = new Thread(() -> {
try (BufferedReader errReader = new BufferedReader(new InputStreamReader(finalProcess.getErrorStream()))) {
String line;
while ((line = errReader.readLine()) != null) {
LogUtils.e(Log.ERROR, "ShellUtils", "Stderr: " + line, null);
}
} catch (IOException ioException) {
LogUtils.e(Log.ERROR, "ShellUtils", "Error reading stderr", ioException);
}
});
// 启动子线程
stdThread.start();
errThread.start();
try (OutputStream os = process.getOutputStream()) {
for (String cmd : cmds) {
// if (!isCommandSafe(cmd)) {
// Log.w("ShellUtils", "Skipping unsafe command: " + cmd);
// continue;
// }
os.write((cmd + "\n").getBytes());
Log.d("ShellUtils", "Executing command: " + cmd);
}
os.write("exit\n".getBytes());
os.flush();
}
try {
// 执行命令等待解决
process.waitFor();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断
LogUtils.e(Log.ERROR, "ShellUtils", "Error executing commands", e);
}
// 等待子线程完成
stdThread.join();
errThread.join();
} catch (InterruptedIOException e) {
LogUtils.e(Log.ERROR, "ShellUtils", "Error reading stdout: Interrupted", e);
Thread.currentThread().interrupt(); // 恢复线程的中断状态
} catch (Exception e) {
LogUtils.e(Log.ERROR, "ShellUtils", "Error executing commands", e);
} finally {
if (process != null) {
process.destroy();
}
}
return results;
}
public static boolean hasRootAccess() {
// 记录是否出现安全异常
boolean hasSecurityError = false;
// 检查二进制文件
String[] binariesToCheck = {"su", "xu", "vu"};
for (String bin : binariesToCheck) {
try {
if (hasBin(bin)) {
return true;
}
} catch (SecurityException e) {
hasSecurityError = true;
LogUtils.e(Log.ERROR, "ShellUtils", "Security exception while checking: " + bin, e);
}
}
// 判断如果发生安全异常则反馈问题
if (hasSecurityError) {
Log.w("ShellUtils", "Potential security error detected while checking root access.");
}
// 没有找到合法的二进制文件则认为无root权限
return false;
}
}

View File

@ -0,0 +1,248 @@
package com.android.grape.util
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
object StoragePermissionHelper {
// 权限请求代码常量
private const val REQUEST_CODE_BASIC_STORAGE_PERMISSION = 1001
private const val REQUEST_CODE_MANAGE_ALL_FILES_PERMISSION = 1002
/**
* 检查是否已授予所有必要权限
*/
fun hasFullStoragePermission(context: Context): Boolean {
return when {
// Android 10 及以下
Build.VERSION.SDK_INT < Build.VERSION_CODES.R ->
hasBasicStoragePermission(context)
// Android 11+ 管理所有文件权限
else -> Environment.isExternalStorageManager()
}
}
/**
* 请求文件存储权限兼容所有版本
*
* @param activity 当前活动上下文
* @param onGranted 所有权限已授予的回调
* @param onDenied 权限被拒绝的回调
*/
fun requestFullStoragePermission(activity: Activity, onGranted: () -> Unit, onDenied: () -> Unit) {
// 检查是否已有权限
if (hasFullStoragePermission(activity)) {
onGranted()
return
}
when {
// Android 11+ - 请求管理所有文件权限
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
requestManageExternalStorage(activity, onGranted, onDenied)
// Android 10 - 只需要读取权限
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q ->
requestBasicPermissions(activity,
permissions = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
onGranted = onGranted,
onDenied = onDenied
)
// Android 6-9 - 需要读写权限
else ->
requestBasicPermissions(activity,
permissions = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
),
onGranted = onGranted,
onDenied = onDenied
)
}
}
/**
* 处理权限请求结果
*
* @param activity 当前活动
* @param requestCode 请求代码
* @param grantResults 授权结果数组
* @param onGranted 所有权限已授予的回调
* @param onDenied 权限被拒绝的回调
*/
fun handlePermissionResult(
activity: Activity,
requestCode: Int,
grantResults: IntArray,
onGranted: () -> Unit,
onDenied: () -> Unit
) {
when (requestCode) {
REQUEST_CODE_BASIC_STORAGE_PERMISSION -> {
handleBasicPermissionResult(grantResults, onGranted, onDenied)
}
// 管理权限结果在 onActivityResult 中处理
}
}
/**
* 处理从设置返回的结果用于 MANAGE_EXTERNAL_STORAGE
*
* @param activity 当前活动
* @param requestCode 请求代码
* @param onGranted 权限已授予的回调
* @param onDenied 权限被拒绝的回调
*/
@RequiresApi(Build.VERSION_CODES.R)
fun handleActivityResult(
activity: Activity,
requestCode: Int,
onGranted: () -> Unit,
onDenied: () -> Unit
) {
if (requestCode == REQUEST_CODE_MANAGE_ALL_FILES_PERMISSION) {
if (Environment.isExternalStorageManager()) {
onGranted()
} else {
onDenied()
}
}
}
/**
* 显示权限解释对话框
*/
fun showPermissionRationale(
activity: Activity,
message: String = "需要文件访问权限来执行此操作",
onContinue: () -> Unit
) {
AlertDialog.Builder(activity)
.setTitle("权限需要")
.setMessage(message)
.setPositiveButton("继续") { _, _ -> onContinue() }
.setNegativeButton("取消", null)
.show()
}
/**
* 打开应用设置页面
*/
fun openAppSettings(activity: Activity) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${activity.packageName}")
}
activity.startActivity(intent)
}
// 私有方法 ------------------------------------------------------------
private fun hasBasicStoragePermission(context: Context): Boolean {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ->
ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
else ->
ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
}
}
private fun requestBasicPermissions(
activity: Activity,
permissions: Array<String>,
onGranted: () -> Unit,
onDenied: () -> Unit
) {
if (permissions.all {
ActivityCompat.shouldShowRequestPermissionRationale(activity, it)
}) {
// 显示解释
showPermissionRationale(activity, "这些权限对于应用功能是必需的") {
ActivityCompat.requestPermissions(
activity,
permissions,
REQUEST_CODE_BASIC_STORAGE_PERMISSION
)
}
} else {
// 直接请求
ActivityCompat.requestPermissions(
activity,
permissions,
REQUEST_CODE_BASIC_STORAGE_PERMISSION
)
}
}
private fun handleBasicPermissionResult(
grantResults: IntArray,
onGranted: () -> Unit,
onDenied: () -> Unit
) {
val allGranted = grantResults.isNotEmpty() &&
grantResults.all { it == PackageManager.PERMISSION_GRANTED }
if (allGranted) {
onGranted()
} else {
onDenied()
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun requestManageExternalStorage(
activity: Activity,
onGranted: () -> Unit,
onDenied: () -> Unit
) {
// 检查是否已经有权限
if (Environment.isExternalStorageManager()) {
onGranted()
return
}
try {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
data = Uri.parse("package:${activity.packageName}")
}
activity.startActivityForResult(intent, REQUEST_CODE_MANAGE_ALL_FILES_PERMISSION)
} catch (e: Exception) {
// 处理不支持 ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION 的设备
try {
val fallbackIntent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION).apply {
data = Uri.parse("package:${activity.packageName}")
}
activity.startActivityForResult(fallbackIntent, REQUEST_CODE_MANAGE_ALL_FILES_PERMISSION)
} catch (ex: Exception) {
// 最终回退到应用设置页面
showPermissionRationale(activity, "请授予管理所有文件权限") {
openAppSettings(activity)
onDenied()
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,69 @@
package com.android.grape.work
import android.accessibilityservice.AccessibilityService
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.provider.Settings
import android.text.TextUtils
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.android.grape.service.MyAccessibilityService
class CheckAccessibilityWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
if (!isAccessibilityServiceEnabled(
applicationContext,
MyAccessibilityService::class.java
)
) {
// 判断是否已经提示过用户引导开启
val sharedPreferences = applicationContext
.getSharedPreferences("my_app_prefs", Context.MODE_PRIVATE)
val hasPrompted = sharedPreferences.getBoolean("accessibility_prompted", false)
if (!hasPrompted) {
// 引导用户打开辅助功能服务
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
applicationContext.startActivity(intent)
// 更新状态
sharedPreferences.edit().putBoolean("accessibility_prompted", true).apply()
}
return Result.retry()
}
return Result.success()
}
companion object{
fun isAccessibilityServiceEnabled(
context: Context,
service: Class<out AccessibilityService?>
): Boolean {
val enabledServices = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
)
if (TextUtils.isEmpty(enabledServices)) {
return false
}
val components =
enabledServices.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val expectedComponentName =
ComponentName(context.packageName, service.canonicalName).flattenToString()
for (componentName in components) {
if (expectedComponentName.equals(componentName, ignoreCase = true)) {
return true
}
}
return false
}
}
}

Some files were not shown because too many files have changed in this diff Show More