init
This commit is contained in:
commit
890539cbc0
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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")
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
|
@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) // 网络错误状态码
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.android.grape.net
|
||||
|
||||
class NetworkHelper {
|
||||
|
||||
}
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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`)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.android.grape.sai
|
||||
|
||||
class Stopwatch {
|
||||
private val mStart = System.currentTimeMillis()
|
||||
|
||||
fun millisSinceStart(): Long {
|
||||
return System.currentTimeMillis() - mStart
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
}
|
|
@ -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, "...")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.android.grape.sai.inter
|
||||
|
||||
import com.android.grape.sai.param.SaiPiSessionState
|
||||
|
||||
interface SaiPiSessionObserver {
|
||||
fun onSessionStateChanged(state: SaiPiSessionState?)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
// }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
// }
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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
Loading…
Reference in New Issue