Compare commits

...

No commits in common. "main" and "master" have entirely different histories.
main ... master

180 changed files with 16714 additions and 29 deletions

15
.gitignore vendored Normal file
View File

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

3
.idea/.gitignore vendored
View File

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

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
AndroidGrape

View File

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

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AppInsightsSettings">
<option name="tabSettings">
<map>
<entry key="Firebase Crashlytics">
<value>
<InsightsFilterSettings>
<option name="connection">
<ConnectionSetting>
<option name="appId" value="PLACEHOLDER" />
<option name="mobileSdkAppId" value="" />
<option name="projectId" value="" />
<option name="projectNumber" value="" />
</ConnectionSetting>
</option>
<option name="signal" value="SIGNAL_UNSPECIFIED" />
<option name="timeIntervalDays" value="THIRTY_DAYS" />
<option name="visibilityType" value="ALL" />
</InsightsFilterSettings>
</value>
</entry>
</map>
</option>
</component>
</project>

6
.idea/compiler.xml Normal file
View File

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

574
.idea/dbnavigator.xml Normal file
View File

@ -0,0 +1,574 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DDLFileAttachmentManager">
<mappings />
<preferences />
</component>
<component name="DBNavigator.Project.DataEditorManager">
<record-view-column-sorting-type value="BY_INDEX" />
<value-preview-text-wrapping value="true" />
<value-preview-pinned value="false" />
</component>
<component name="DBNavigator.Project.DatabaseAssistantManager">
<assistants>
<assistant-state connection-id="96c06034-88db-4cab-9c16-73c5d99a2c0a" default-profile-name="" selected-profile-name="" selected-model-name="" assistant-type="GENERIC" selected-action="SHOW_SQL" availability="UNAVAILABLE" acknowledgement="NONE">
<messages />
</assistant-state>
</assistants>
</component>
<component name="DBNavigator.Project.DatabaseBrowserManager">
<autoscroll-to-editor value="false" />
<autoscroll-from-editor value="true" />
<show-object-properties value="true" />
<loaded-nodes />
</component>
<component name="DBNavigator.Project.DatabaseConsoleManager">
<connection id="96c06034-88db-4cab-9c16-73c5d99a2c0a">
<console name="Connection" type="STANDARD" schema="device_info" session="Main" />
</connection>
</component>
<component name="DBNavigator.Project.DatabaseEditorStateManager">
<last-used-providers />
</component>
<component name="DBNavigator.Project.DatabaseFileManager">
<open-files />
</component>
<component name="DBNavigator.Project.DatabaseSessionManager">
<connection id="96c06034-88db-4cab-9c16-73c5d99a2c0a" />
</component>
<component name="DBNavigator.Project.DatasetFilterManager">
<filter-actions connection-id="96c06034-88db-4cab-9c16-73c5d99a2c0a" dataset="device_info.device_info" active-filter-id="EMPTY_FILTER" />
</component>
<component name="DBNavigator.Project.ExecutionManager">
<retain-sticky-names value="false" />
</component>
<component name="DBNavigator.Project.ObjectQuickFilterManager">
<last-used-operator value="EQUAL" />
<filters />
</component>
<component name="DBNavigator.Project.Settings">
<connections>
<connection id="96c06034-88db-4cab-9c16-73c5d99a2c0a" active="true" signed="true">
<database>
<name value="Connection" />
<description value="" />
<database-type value="SQLITE" />
<config-type value="BASIC" />
<database-version value="3.49" />
<driver-source value="BUNDLED" />
<driver-library value="" />
<driver value="" />
<url-type value="FILE" />
<host value="" />
<port value="" />
<database value="" />
<tns-folder value="" />
<tns-profile value="" />
<files>
<file path="D:\Desktop\device_info.db" schema="device_info" />
</files>
<type value="NONE" />
<user value="" />
<token-type value="" />
<token-config-file value="" />
<token-profile value="" />
<session-user value="" />
</database>
<properties>
<auto-commit value="false" />
</properties>
<ssh-settings>
<active value="false" />
<proxy-host value="" />
<proxy-port value="22" />
<proxy-user value="" />
<auth-type value="PASSWORD" />
<key-file value="" />
</ssh-settings>
<ssl-settings>
<active value="false" />
<certificate-authority-file value="" />
<client-certificate-file value="" />
<client-key-file value="" />
</ssl-settings>
<details>
<charset value="UTF-8" />
<session-management value="true" />
<ddl-file-binding value="true" />
<database-logging value="true" />
<connect-automatically value="true" />
<restore-workspace value="true" />
<restore-workspace-deep value="false" />
<environment-type value="default" />
<connectivity-timeout value="30" />
<idle-time-to-disconnect value="30" />
<idle-time-to-disconnect-pool value="5" />
<credential-expiry-time value="10" />
<max-connection-pool-size value="7" />
<alternative-statement-delimiter value="" />
</details>
<debugger>
<compile-dependencies value="true" />
<tcp-driver-tunneling value="false" />
<tcp-host-address value="" />
<tcp-port-from value="4000" />
<tcp-port-to value="4999" />
<debugger-type value="ASK" />
</debugger>
<object-filters hide-empty-schemas="false" hide-pseudo-columns="false" hide-audit-columns="false">
<object-filters />
<object-type-filter use-master-settings="true">
<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>
</object-filters>
</connection>
</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>
<component name="DBNavigator.Project.StatementExecutionManager">
<execution-variables />
<execution-variable-types />
</component>
</project>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-08-02T03:09:35.609697900Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=8.217.74.194:8924;connection=5f91b786" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

19
.idea/gradle.xml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<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>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

10
.idea/migrations.xml Normal file
View File

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

View File

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

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/grape.iml" filepath="$PROJECT_DIR$/.idea/grape.iml" />
</modules>
</component>
</project>

View File

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

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="NONE" />
</component>
</project>

View File

@ -1,2 +0,0 @@
# grape

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

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

@ -0,0 +1,99 @@
import com.android.build.gradle.internal.api.ApkVariantOutputImpl
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.android.grape"
compileSdk = 35
signingConfigs {
create("release") {
storeFile = file("key.jks")
storePassword = "androidgrape"
keyAlias = "key0"
keyPassword = "androidgrape"
}
}
defaultConfig {
applicationId = "com.android.grape"
minSdk = 23
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
buildConfigField("int", "TAG", "119")
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
buildConfigField("int", "TAG", "120")
isMinifyEnabled = false
}
create("home") {
buildConfigField("int", "TAG", "120")
signingConfig = signingConfigs.getByName("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
}
applicationVariants.all {
val variant = this
variant.outputs.all {
val versionName = variant.versionName
val versionCode = variant.versionCode
val outputFileName = "Grape-${variant.name}-v${versionName}-${versionCode}.apk"
if (this is ApkVariantOutputImpl) {
this.outputFileName = outputFileName
}
}
}
}
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")
}

BIN
app/key.jks Normal file

Binary file not shown.

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

@ -0,0 +1,48 @@
-keep class com.google.gson.** { *; }
-keep class com.google.gson.stream.** { *; }
# 保留所有注解
-keepattributes *Annotation*
-keepattributes Signature
# 保留枚举类
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留所有模型类(根据你的包结构调整)
-keep class com.android.grape.pad.** { *; }
-keep class com.android.grape.data.** { *; }
-keep class com.android.grape.net.ApiResponse{ *; }
-keep class com.android.grape.net.ApiResponseList{ *; }
# 保留所有使用 @SerializedName 注解的字段
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# 保留所有模型类的无参构造函数
-keepclassmembers class com.android.grape.pad.** {
public <init>();
}
# 保留类型适配器
-keep class * extends com.google.gson.TypeAdapter {
public com.google.gson.TypeAdapter create(com.google.gson.Gson, com.google.gson.reflect.TypeToken);
}
# 保留 Gson 创建的类
-keep class com.google.gson.examples.android.model.** { *; }
-keepattributes Signature
# 保留 TypeToken 类及其子类
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
-keep class sun.misc.Unsafe { *; }
# 保留注解信息
-keepattributes *Annotation*
# 保留 Kotlin 元数据(如果使用 Kotlin
-keepclassmembers class **$TypeToken { *; }

View File

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

View File

@ -0,0 +1,135 @@
<?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.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VPN_SERVICE" />
<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:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidGrape"
tools:targetApi="31">
<provider
android:name=".provider.DeviceInfoProvider"
android:authorities="com.android.grape.deviceinfo.provider"
android:enabled="true"
android:exported="true">
</provider>
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
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>
<service android:name="com.android.grape.job.StartVpnPortJobService"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE"/>
</application>
</manifest>

View File

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

View File

@ -0,0 +1,207 @@
package com.android.grape
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import com.android.grape.databinding.ActivityMainBinding
import com.android.grape.job.MonitorService
import com.android.grape.util.BackupUtils
import com.android.grape.util.BackupUtils.killRecordProcess
import com.android.grape.util.ClashUtil
import com.android.grape.util.FileUtils
import com.android.grape.util.MockTools
import com.android.grape.util.NotificationPermissionHandler
import com.android.grape.util.ScriptUtils.registerScriptResultReceiver
import com.android.grape.util.ScriptUtils.unregisterScriptResultReceiver
import com.android.grape.util.ShellUtil
import com.android.grape.util.StoragePermissionHelper
import com.android.grape.util.TaskUtils
import kotlinx.coroutines.launch
/**
* public class MainActivity extends AppCompatActivity
*/
class MainActivity : AppCompatActivity() {
/**
private MainViewModel viewModel = new ViewModelProvider(this).get(MainViewModel.class);
*/
private val viewModel by viewModels<MainViewModel>()
/**
* private ActivityMainBinding viewBinding;
*/
private lateinit var viewBinding: ActivityMainBinding
private lateinit var permissionHandler: NotificationPermissionHandler
/**
* 基础设置调用 super.onCreate() enableEdgeToEdge() 启用全面屏支持
* 绑定视图使用 ViewBinding 加载布局
*
* 权限检查调用 checkPermission() 请求通知和存储权限
* 服务与监听注册脚本接收器设置按钮点击监听器以启动或停止监控服务
*
* Socket 服务器初始化创建并配置本地 Socket 服务器注释部分为通信逻辑
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
init()
/**
viewBinding = ActivityMainBinding.inflate(layoutInflater)
使用 ActivityMainBinding 对当前 Activity 的布局文件进行绑定生成可访问 UI 元素的绑定对象
setContentView(viewBinding.root) 将绑定对象中的根视图设置为 Activity 的内容视图完成界面加载
*/
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
/**
* 这段代码用于适配系统窗口如状态栏导航栏的内边距确保内容不被遮挡
* 设置窗口边距监听器通过 ViewCompat.setOnApplyWindowInsetsListener ID main 的根视图设置监听
* 获取系统栏边距 insets 中提取状态栏和导航栏的边距信息
* 设置视图内边距将视图的内边距设为系统栏的边距使内容避开系统栏区域
*/
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()
registerScriptResultReceiver()
viewBinding.start.setOnClickListener {
MonitorService.onEvent(MainApplication.instance)
}
viewBinding.stop.setOnClickListener {
killRecordProcess(this, packageName)
// lifecycleScope.launch {
// BackupUtils.backUp(this@MainActivity, "com.policybazaar")
// BackupUtils.recoverRecordData(this@MainActivity)
// }
}
}
fun init() {
MockTools.exec("pm grant com.android.grape android.permission.INTERACT_ACROSS_USERS")
MockTools.exec("pm grant com.android.grape android.permission.WRITE_SECURE_SETTINGS")
MockTools.exec("pm setenforce 0")
}
override fun onDestroy() {
super.onDestroy()
ClashUtil.unregisterReceiver(this)
unregisterScriptResultReceiver()
}
private fun checkPermission() {
permissionHandler = NotificationPermissionHandler(this) { result ->
handlePermissionResult(result)
}
checkNotificationPermission()
StoragePermissionHelper.requestFullStoragePermission(
activity = this,
onGranted = { performFileOperation() },
onDenied = { showPermissionDeniedDialog() }
)
}
private fun checkNotificationPermission() {
when (val result = NotificationPermissionHandler.checkPermissionState(this)) {
NotificationPermissionHandler.PermissionResult.GRANTED,
NotificationPermissionHandler.PermissionResult.NOT_NEEDED -> {
showNotification()
}
NotificationPermissionHandler.PermissionResult.DENIED,
NotificationPermissionHandler.PermissionResult.NEEDS_SETTINGS -> {
// 请求权限
permissionHandler.requestNotificationPermission()
}
}
}
/**
* 处理权限结果回调
*/
private fun handlePermissionResult(result: NotificationPermissionHandler.PermissionResult) {
when (result) {
NotificationPermissionHandler.PermissionResult.GRANTED -> {
// 权限已授予,显示通知
showNotification()
Toast.makeText(this, "通知权限已开启", Toast.LENGTH_SHORT).show()
}
NotificationPermissionHandler.PermissionResult.DENIED -> {
// 权限被拒绝
Toast.makeText(this, "通知权限被拒绝", Toast.LENGTH_SHORT).show()
}
NotificationPermissionHandler.PermissionResult.NEEDS_SETTINGS -> {
Toast.makeText(this, "通知权限被拒绝", Toast.LENGTH_SHORT).show()
}
NotificationPermissionHandler.PermissionResult.NOT_NEEDED -> {
// 不需要特殊权限(旧设备)
showNotification()
}
}
}
private fun showNotification() {
viewModel.checkAccessibilityService()
}
private fun performFileOperation() {
// 执行文件读写操作
// Toast.makeText(this, "文件访问权限已授予", Toast.LENGTH_SHORT).show()
// MonitorService.onEvent(this)
}
private fun showPermissionDeniedDialog() {
AlertDialog.Builder(this)
.setTitle("权限被拒绝")
.setMessage("您拒绝了文件访问权限")
.setPositiveButton("设置") { _, _ ->
StoragePermissionHelper.openAppSettings(this)
}
.setNegativeButton("稍后", null)
.show()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
StoragePermissionHelper.handlePermissionResult(
activity = this,
requestCode = requestCode,
grantResults = grantResults,
onGranted = { performFileOperation() },
onDenied = { showPermissionDeniedDialog() }
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
StoragePermissionHelper.handleActivityResult(
activity = this,
requestCode = requestCode,
onGranted = { performFileOperation() },
onDenied = { showPermissionDeniedDialog() }
)
}
}
}

View File

@ -0,0 +1,43 @@
package com.android.grape
import android.app.Application
import com.blankj.utilcode.util.LogUtils
/**
* Android应用的主入口MainApplication类其功能如下
* 单例实例通过伴生对象的instance属性实现全局访问点
* 初始化日志记录在onCreate()中启用日志写入文件功能
* 环境初始化initEvn()方法设置本地库路径确保JNI等组件能正确加载
*/
class MainApplication : Application() {
/**
private static MainApplication instance;
public static MainApplication getInstance() {
return instance;
}
*/
companion object {
lateinit var instance: MainApplication
}
/**
* 调用父类方法super.onCreate() 确保系统完成基础初始化
* 设置全局实例instance = this 将当前实例赋值给伴生对象中的 instance
* 启用日志写入文件通过 LogUtils 开启日志记录到文件功能
* 初始化环境配置调用 initEvn() 设置本地库路径
*/
override fun onCreate() {
super.onCreate()
instance = this
LogUtils.getConfig()
.setBorderSwitch(false)
.setLog2FileSwitch(true)
initEvn()
}
fun initEvn() {
System.setProperty("java.library.path", this.applicationInfo.nativeLibraryDir)
}
}

View File

@ -0,0 +1,42 @@
package com.android.grape
import androidx.lifecycle.ViewModel
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkManager
import com.android.grape.job.MonitorService
import com.android.grape.service.MyAccessibilityService
import com.android.grape.work.CheckAccessibilityWorker
import java.util.concurrent.TimeUnit
/**
* 继承 ViewModel用于在 Activity/Fragment 生命周期中保留 UI 相关数据
* 检查无障碍服务状态调用 CheckAccessibilityWorker.isAccessibilityServiceEnabled() 检查 MyAccessibilityService 是否启用
* 定时检测机制若未启用则通过 WorkManager 提交一个每 15 分钟执行一次的周期性任务持续检查用户是否开启该服务
*/
class MainViewModel : ViewModel() {
/**
* 这段代码用于检查无障碍服务是否启用若未启用则定时检测
* 检查服务状态调用 CheckAccessibilityWorker.isAccessibilityServiceEnabled() 判断 MyAccessibilityService 是否已开启
* 未启用则提交定时任务若未启用使用 WorkManager 提交一个每 15 分钟执行一次的后台任务 CheckAccessibilityWorker用于持续检查用户是否启用了该服务
*/
fun checkAccessibilityService() {
/**
* boolean accessibilityServiceEnabled = CheckAccessibilityWorker.isAccessibilityServiceEnabled(
* MainApplication.getInstance(),
* MyAccessibilityService.class
* );
*/
val accessibilityServiceEnabled = CheckAccessibilityWorker.isAccessibilityServiceEnabled(
MainApplication.instance,
MyAccessibilityService::class.java
)
if (!accessibilityServiceEnabled) {
val workRequest = PeriodicWorkRequest.Builder(
CheckAccessibilityWorker::class.java, 15, TimeUnit.MINUTES
)
.build()
WorkManager.getInstance(MainApplication.instance).enqueue(workRequest)
}
}
}

View File

@ -0,0 +1,139 @@
package com.android.grape.data
import android.util.Log
import org.json.JSONObject
/**
* @Time: 2025-15-16 16:15
* @Creator: 初屿贤
* @File: AppState
* @Project: AndroidGrape
* @Description:
*/
// AppState.kt
object AppState {
/**
* 任务json
*/
var taskJson: JSONObject? = null
set
var paramsJson: JSONObject? = null
set
var recordPackageName: String? = null
var recordFileName: String? = null
var trackingLink: String? = null
var recordId: Long = 0L
var proxyIp: String? = null
var proxyPort: Int = 0
var proxyCountry: String? = null
var lang: String? = null
var ua: String? = null
var clickTime: Long = 0L
var installTime: Long = 0L
var installServerTimeFromGP: Long = 0L
var clickServerTimeFromGP: Long = 0L
var lastUpdateTime: Long = 0L
var instalTimeFromGp: Long = 0L
val country: String? = null
var reloginRecordId: Long = 0L
var videoProxy: String = ""
var forwardIp: String? = ""
var afApp: String = ""
var preClickRecordId: Long = 0
var backFileName: String? = null
set
var backFileName1: String? = null
set
var backFileName2: String? = null
set
var isClickRet: Boolean = false
var clickErrReason: String = ""
var delegateIp: String? = null
var isNeedReg: Boolean = false
var isNeedBackup: Boolean = false
var regEmailJson: JSONObject? = null
var isCanAuto: Boolean = false
var canAutoLc: String = ""
var canAutoAtc: String = ""
var backupResult: JSONObject? = null
var backUpServerIp: String = ""
var isCanceled: Boolean = false
var isPaused: Boolean = false
var ctit: Int = 0
set
var keepOpen: Int = 0
var script_path: String = "/sdcard/apks/script.zip"
var fuzzy_domain: String = ""
var fuzzy_proxy: String? = ""
var script_status: Int = 0
var scriptOpenApp: Int = 0
var isNeedRestored: Boolean = false
var logBuffer: StringBuffer = StringBuffer()
set
const val monitorDir = "monitor"
const val apkDir = "apks"
var recordExtraFileName: String? = null
var mainUserAndGroup: String? = null
var nRandom = 0
const val TAG = "IOSTQ:Util"
const val tag = "3"
const val sendRefer = true
const val AUTO_PACKAGENAME: String = "com.play4u.luabox"
const val AUTO_JSPACKAGENAME: String = "org.autojs.autojs6"
const val proxy_packagename: String = "com.tunnelworkshop.postern"
const val AUTO_CLASSNAME: String = "com.cyjh.elfin.activity.SplashActivity"
const val hookPackageName = "com.affsystem.androidhooker"
const val hookAppMainClass = "com.affsystem.androidhooker.MainActivity"
const val monitorFile = "monitor.txt"
const val taskFile = "task.txt"
var defaultAppJo = JSONObject()
var defaultPRoxyJo = JSONObject()
var appVer: String? = null
var appVerCode: Long? = null
val cacheJson: JSONObject? = null
var startInstallTime = 0L
var referer: String? = null
var appDataUrl = ""
var afLog = ""
var installRet: Boolean? = null
var appAfVer = ""
const val TYPE_SUCCESS: Int = 0
const val TYPE_FAILED: Int = 1
const val TYPE_PAUSED: Int = 2
const val TYPE_CANCELED: Int = 3
var tags: String? = null
const val isCollectURL = false
const val testForSetInfo = false
var oldpath: String? = null
var newpath: String? = null
const val apk_path = "/sdcard/Android/data/sperixlabs.proxyme/files/monitor/apks"
var zip_name: String? = null
var baoming: String? = null
var appVersion: String?
get() = appVer
set(appAfVerV) {
Log.i("TaskUtils", "setAppAfV: $appAfVerV")
appVer = appAfVerV
}
var appVersionCode: Long?
get() = appVerCode
set(appVerCode) {
Log.i("TaskUtils", "setAppAfVerCode: $appVerCode")
AppState.appVerCode = appVerCode
}
}

View File

@ -0,0 +1,8 @@
package com.android.grape.data
import com.google.gson.annotations.SerializedName
data class As(
var nameValuePairs: NameValuePairs = NameValuePairs()
)

View File

@ -0,0 +1,11 @@
package com.android.grape.data
import com.google.gson.annotations.SerializedName
data class DdlInfo(
var fromfg: Int = 0,
var net0: Int = 0,
var rfrwait: Int = 0,
var status: String = ""
)

View File

@ -0,0 +1,145 @@
package com.android.grape.data
import com.google.gson.annotations.SerializedName
import org.json.JSONArray
import org.json.JSONObject
data class Device(
var advertiserId: String = "",
@SerializedName("af_preinstalled")
var afPreinstalled: Boolean = false,
var androidId: String = "",
var arch: String = "",
@SerializedName("as")
var asX: As = As(),
var battery: Int = 0,
var battery1: Int = 0,
var batteryLevel: Int = 0,
var batteryType: Int = 0,
var bn: String = "",
var brand: String = "",
var btch: String = "",
@SerializedName("build_display_id")
var buildDisplayId: String = "",
var carrier: String = "",
@SerializedName("click_ts")
var clickTs: Int = 0,
var clk: String = "",
var country: String = "",
@SerializedName("cpu_abi")
var cpuAbi: String = "",
@SerializedName("cpu_abi2")
var cpuAbi2: String = "",
var ddlInfo: DdlInfo = DdlInfo(),
var debug: Boolean = false,
var device: String = "",
var deviceCity: String = "",
var deviceType: String = "",
var dim: Dim = Dim(),
var dimId: Int = 0,
var dimReset: Int = 0,
var dimType: Int = 0,
var disk: String = "",
var diskRate: Double = 0.0,
var expand: Expand = Expand(),
var fetchAdIdLatency: Int = 0,
@SerializedName("from_fg")
var fromFg: Int = 0,
@SerializedName("google_custom")
var googleCustom: GoogleCustom = GoogleCustom(),
var hook: String = "",
@SerializedName("init_to_fg")
var initToFg: Int = 0,
var install: String = "",
@SerializedName("install_begin_ts")
var installBeginTs: Int = 0,
@SerializedName("installer_package")
var installerPackage: String = "",
var instant: Boolean = false,
var ip: String = "",
var ivc: Boolean = false,
var lang: String = "",
@SerializedName("lang_code")
var langCode: String = "",
@SerializedName("last_boot_time")
var lastBootTime: Long = 0,
var lastBootTimeOff: Int = 0,
var latency: Int = 0,
var locale: Locale = Locale(),
var manufactor: String = "",
var mcc: Int = 0,
var mnc: Int = 0,
var model: String = "",
@SerializedName("native_dir")
var nativeDir: Boolean = false,
var network: String = "",
var noRcLatency: Boolean = false,
// @SerializedName("open_referrer")//todo string or object
// var openReferrer: String = "",
@SerializedName("open_referrerReset")
var openReferrerReset: Int = 0,
var opener: String = "",
var `operator`: String = "",
var platformextension: String = "",
var pr: Pr = Pr(),
var product: String = "",
var rawDevice: String = "",
var rawProduct: String = "",
@SerializedName("rc.delay")
var rcDelay: Int = 0,
@SerializedName("rc.latency")
var rcLatency: Int = 0,
var referrer: String = "",
@SerializedName("sc_o")
var scO: String = "",
var sdk: String = "",
var sdkVer: String = "",
@Transient
var sensor: JSONArray = JSONArray(),
var sensorReset: Int = 0,
@SerializedName("sensor_size")
var sensorSize: Int = 0,
@SerializedName("sig_n")
var sigN: String = "",
@SerializedName("sys_ua")
var sysUa: String = "",
var timepassedsincelastlaunch: Int = 0,
var tzDisplayName: String = "",
var tzOffTime: Int = 0,
@SerializedName("val")
var valX: String = "",
var vendingVersionCode: String = "",
var vendingVersionName: String = "",
@SerializedName("web_ua")
var webUa: String = ""
){
fun toJson(): String {
return JSONObject().apply {
put("sensors", sensor)
put("arch", arch)
put("cpu_abi", cpuAbi)
put("cpu_abi2", cpuAbi2)
put("size", dim.size)
put("xdp", dim.xdp)
put("ydp", dim.ydp)
put("y_px", dim.yPx)
put("x_px", dim.xPx)
put("d_dpi", dim.dDpi)
put("btch", btch)
put("btl", batteryLevel)
put("disk", disk)
put("sdk", sdk)
put("network", network)
put("api_ver", vendingVersionCode)
put("api_ver_name", vendingVersionName)
put("carrier", carrier)
put("product", product)
put("last_boot_time", lastBootTime)
put("install_source_info", installerPackage)
put("advertiserId", advertiserId)
put("opener", opener)
}.toString()
}
}

View File

@ -0,0 +1,16 @@
package com.android.grape.data
import com.google.gson.annotations.SerializedName
data class Dim(
@SerializedName("d_dpi")
var dDpi: String = "",
var size: String = "",
@SerializedName("x_px")
var xPx: String = "",
var xdp: String = "",
@SerializedName("y_px")
var yPx: String = "",
var ydp: String = ""
)

View File

@ -0,0 +1,87 @@
package com.android.grape.data
import com.google.gson.annotations.SerializedName
data class Expand(
@SerializedName("ABI")
var aBI: String = "",
@SerializedName("API")
var aPI: String = "",
var amGetConfig: String = "",
@SerializedName("AndroidID")
var androidID: String = "",
@SerializedName("AndroidVer")
var androidVer: String = "",
@SerializedName("BSSID")
var bSSID: String = "",
var baseband: String = "",
var batteryLevel: Int = 0,
var board: String = "",
var bootLoader: String = "",
var brand: String = "",
var country: String = "",
@SerializedName("CountryCode")
var countryCode: String = "",
var cpuinfobuf: String = "",
var cpuinfostr: String = "",
@SerializedName("DPI")
var dPI: Int = 0,
var dataStatfs: Long = 0,
var device: String = "",
var display: String = "",
var displayLang: String = "",
var downloadCacheStatfs: Long = 0,
var fingerprint: String = "",
var firstInstallTime: Int = 0,
var gaid: String = "",
var glExtensions: String = "",
var glRenderer: String = "",
var glVendor: String = "",
var glVersion: String = "",
var height: Int = 0,
@SerializedName("ID")
var iD: String = "",
var incremental: String = "",
var lang: String = "",
var lastUpdateTime: Int = 0,
var latitude: Double = 0.0,
var localip: String = "",
var longtitude: Double = 0.0,
var lyMAC: String = "",
@SerializedName("Manufacture")
var manufacture: String = "",
var model: String = "",
var network: String = "",
var phoneBootTime: Int = 0,
var phoneUsedTime: Int = 0,
var pmfea: String = "",
var pmlib: String = "",
var procStat: String = "",
var procVersion: String = "",
var ratioVersion: String = "",
var referrerClickTime: Long = 0,
var referrerFromGP: String = "",
var regionid: String = "",
var roBoardPlatform: String = "",
var roBootimageBuildFingerprint: String = "",
var roBuildDateUtc: Int = 0,
var roBuildDescription: String = "",
var roHardware: String = "",
var roProductCpuAbilist: String = "",
var roProductName: String = "",
var rootStatfs: Int = 0,
var screenBrightness: Int = 0,
var sdStatfs: Long = 0,
var sdcardCreateTime: Int = 0,
var soRomRam: String = "",
var timerawoff: Int = 0,
var tmdisplayname: String = "",
var uname: String = "",
var width: Int = 0,
@SerializedName("WifiMAC")
var wifiMAC: String = "",
@SerializedName("WifiName")
var wifiName: String = "",
var wvua: String = ""
)

View File

@ -0,0 +1,14 @@
package com.android.grape.data
import com.google.gson.annotations.SerializedName
data class GoogleCustom(
@SerializedName("click_server_ts")
var clickServerTs: Int = 0,
@SerializedName("install_begin_server_ts")
var installBeginServerTs: Int = 0,
@SerializedName("install_version")
var installVersion: String = "",
var instant: Boolean = false
)

View File

@ -0,0 +1,10 @@
package com.android.grape.data
import com.google.gson.annotations.SerializedName
data class Locale(
var country: String = "",
var displayLang: String = "",
var lang: String = ""
)

View File

@ -0,0 +1,11 @@
package com.android.grape.data
import com.google.gson.annotations.SerializedName
data class NameValuePairs(
var cav: Int = 0,
@SerializedName("null")
var nullX: Int = 0,
var other: Int = 0
)

View File

@ -0,0 +1,22 @@
package com.android.grape.data
import com.google.gson.annotations.SerializedName
data class NameValuePairsX(
var ac: String = "",
var ah: String = "",
var ai: String = "",
var ak: String = "",
var al: String = "",
var am: String = "",
var an: String = "",
var ap: String = "",
var aq: String = "",
var ar: String = "",
@SerializedName("as")
var asX: String = "",
var at: String = "",
var au: String = "",
var av: String = ""
)

View File

@ -0,0 +1,8 @@
package com.android.grape.data
import com.google.gson.annotations.SerializedName
data class Pr(
var nameValuePairs: NameValuePairsX = NameValuePairsX()
)

View File

@ -0,0 +1,14 @@
package com.android.grape.data
import com.google.gson.annotations.SerializedName
data class Sensor(
var sN: String = "",
var sT: Int = 0,
var sV: String = "",
var sVE: List<Double> = listOf(),
var sVS: List<Double> = listOf()
) {
companion object
}

View File

@ -0,0 +1,46 @@
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.data.AppState.recordPackageName
import com.android.grape.util.ScriptUtils.execScript
import com.android.grape.util.TaskUtils
class AutoJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
val openZzzj = true
if (openZzzj) {
Handler(Looper.getMainLooper()).postDelayed({
execScript(
this@AutoJobService,
"/sdcard/script/${recordPackageName}/main.js"
)
}, 2000L)
} else {
Handler(Looper.getMainLooper()).postDelayed({
onEvent(
this@AutoJobService
)
}, 5000L)
}
}
companion object {
private const val TAG = "AutoJobService"
private const val jobId = 731
fun onEvent(context: Context) {
enqueueWork(
context,
AutoJobService::class.java, jobId, Intent(
context,
AutoJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,76 @@
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.data.AppState.clickErrReason
import com.android.grape.data.AppState.delegateIp
import com.android.grape.data.AppState.isClickRet
import com.android.grape.data.AppState.proxyCountry
import com.android.grape.data.AppState.ua
import com.android.grape.net.MyGet
import com.android.grape.util.InstallUtils.setInstallRet
import com.android.grape.util.TaskUtils
class CheckIpJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
if (checkIp()) {
InstallService.onEvent(this)
} else {
isClickRet = false
setInstallRet(false)
clickErrReason = "networkErr"
TaskUtils.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 + " ; " + ua)
val resp: String? = MyGet.getData(url, 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 && proxyCountry.equals(conn[1])) {
val ip = conn[0]
delegateIp = ip
return true
}
} catch (e: Exception) {
}
}
Thread.sleep(2000)
} while (nRetryCount++ < 3)
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
companion object {
private const val TAG = "IOSTQ:CheckIpJobService"
private const val tryNum = 0
private const val maxTryNum = 5
private const val jobId = 509
fun onEvent(context: Context) {
enqueueWork(
context,
CheckIpJobService::class.java, jobId, Intent(
context,
CheckIpJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,53 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import androidx.core.app.JobIntentService
import com.android.grape.data.AppState.clickErrReason
import com.android.grape.data.AppState.isClickRet
import com.android.grape.util.InstallUtils.setInstallRet
import com.android.grape.util.TaskUtils
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 = TaskUtils.execDownload(this)
} catch (e: Exception) {
e.printStackTrace()
}
LogUtils.i(TAG, "download app : $succ")
if (succ) {
errTime = 0L
// InstallService.onEvent(this)
StartVpnPortJobService.onEvent(this)
} else {
isClickRet = false
setInstallRet(false)
clickErrReason = "downloadErr"
TaskUtils.setFinish(this)
}
}
companion object {
private const val TAG = "IOSTQ:DownloadJobService"
private var errTime = 0L
private const val jobId = 103
fun onEvent(context: Context) {
enqueueWork(
context,
DownloadAppJobService::class.java, jobId, Intent(
context,
DownloadAppJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,71 @@
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.data.AppState.isNeedRestored
import com.android.grape.data.AppState.recordPackageName
import com.android.grape.util.AppUtils.installRecord
import com.android.grape.util.InstallUtils.setInstallRet
import com.android.grape.util.TaskUtils
class InstallService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
Log.i(TAG, "onHandleWork ...")
if (installRecord(this)) {
Log.i(TAG, "installRecord succ")
tryNum = 0
setInstallRet(true)
println("IOSTQ:isNeedRestored == " + isNeedRestored)
if (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 : " + recordPackageName + " ; " + tryNum)
TaskUtils.setFinish(this)
}
}
}
companion object {
private const val TAG = "IOSTQ:InstallService"
private var tryNum = 0
private const val jobId = 107
fun onEvent(context: Context) {
enqueueWork(
context,
InstallService::class.java, jobId, Intent(
context,
InstallService::class.java
)
)
}
}
}

View File

@ -0,0 +1,106 @@
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.data.AppState.clickTime
import com.android.grape.util.ClashUtil
import com.android.grape.util.MockTools
import com.android.grape.util.TaskUtils
/**
这段 Kotlin 代码定义了 MonitorService 继承自 JobIntentService用于执行异步后台任务
onHandleWork 入口方法防止重复执行调用核心逻辑 exec()
伴生对象Companion Object 提供静态方法 onEvent 启动服务任务setRunning 控制任务状态
*/
class MonitorService : JobIntentService() {
/**
* 这段代码是 MonitorService 中的 onHandleWork 方法用于处理后台任务
* 日志记录打印当前任务开始执行的日志信息
* 防止重复执行通过 running 标志判断是否已在运行若未运行则将其设为 true 并执行 exec() 方法
*/
override fun onHandleWork(intent: Intent) {
Log.i(TAG, "onHandleWork ... $running")
if (!running) {
running = true
exec()
}
}
/**
* 这段代码定义了 exec() 方法用于执行核心监控任务
* 设置 APK 权限通过 chmod 777 修改 APK 文件权限
* 切换 VPN 代理调用 ClashUtil.switchProxyGroup 切换代理组为直连
* 判断任务类型
* 如果 clickTime > 0触发回调任务 SendCallbackJobService.onEvent
* 否则执行默认任务 Util.execTask
*/
protected fun exec() {
try {
val apkRoot = "chmod 777 $packageCodePath"
MockTools.exec(apkRoot)
Log.i(TAG, "auto stop vpn")
ClashUtil.switchProxyGroup("PROXY", "DIRECT", "http://127.0.0.1:6170")
Thread.sleep(3000)
if (clickTime > 0L) {
clickTime = 0L
SendCallbackJobService.onEvent(this)
} else {
TaskUtils.execTask(applicationContext)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* private static final String TAG = "IOSTQ:MonitorService";
* private static final int JOB_ID = 101;
*
* private static boolean running = false;
*
* public static void onEvent(Context context) {
* Intent intent = new Intent(context, MonitorService.class);
* enqueueWork(context, MonitorService.class, JOB_ID, intent);
* }
*
* public static void setRunning(boolean runningV) {
* running = runningV;
* }
*/
companion object {
private const val TAG = "IOSTQ:MonitorService"
private const val jobId = 101
private var running = false
/**
* 这段代码定义了一个静态方法 onEvent用于启动一个后台任务
* 封装 Intent创建一个用于启动 MonitorService Intent
* 提交异步任务通过 enqueueWork() 将该服务作为后台任务入队由系统调度执行
*
* public static void onEvent(Context context) {
* Intent intent = new Intent(context, MonitorService.class);
* enqueueWork(context, MonitorService.class, jobId, intent);
* }
*/
fun onEvent(context: Context) {
enqueueWork(
context,
MonitorService::class.java, jobId, Intent(
context,
MonitorService::class.java
)
)
}
fun setRunning(runningV: Boolean) {
running = runningV
}
}
}

View File

@ -0,0 +1,75 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import androidx.core.app.JobIntentService
import com.android.grape.data.AppState.AUTO_JSPACKAGENAME
import com.android.grape.data.AppState.canAutoLc
import com.android.grape.data.AppState.isCanAuto
import com.android.grape.data.AppState.recordPackageName
import com.android.grape.net.ChangeCallBack
import com.android.grape.provider.DeviceDataAccessor
import com.android.grape.provider.DeviceInfoHelper
import com.android.grape.util.ChangeDeviceInfoUtil
import com.android.grape.util.FileUtils
import com.android.grape.util.MockTools
import com.android.grape.util.ScriptUtils.doScript
import com.android.grape.util.ServiceUtils
import com.android.grape.util.TaskUtils
import com.android.grape.util.TaskUtils.setFinish
import java.io.IOException
class OpenAppService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
println("IOSTQ:isCanAuto() == " + isCanAuto)
println("IOSTQ:getCanAutoLc() == " + canAutoLc)
try {
ChangeDeviceInfoUtil.changeDevice(callBack = object : ChangeCallBack {
override fun changeSuccess() {
runCatching {
if (isCanAuto && canAutoLc.isNotEmpty()) {
val deviceInfo = DeviceDataAccessor.getDeviceInfo(applicationContext, DeviceInfoHelper.getDeviceId())
// FileUtils.writePackageName(recordPackageName?:"")
FileUtils.writeDevice(recordPackageName?:"", deviceInfo?: "")
FileUtils.runPlugin(recordPackageName?:"")
MockTools.exec("pm grant ${AUTO_JSPACKAGENAME} android.permission.SYSTEM_ALERT_WINDOW") //悬浮窗权限
MockTools.exec("settings put secure enabled_accessibility_services ${AUTO_JSPACKAGENAME}/${AUTO_JSPACKAGENAME}.core.accessibility.AccessibilityService")
doScript(this@OpenAppService, AUTO_JSPACKAGENAME) //autojs
}else {
setFinish(this@OpenAppService)
}
}.onFailure {
it.printStackTrace()
setFinish(this@OpenAppService)
}
}
override fun changeFailed() {
setFinish(this@OpenAppService)
}
})
} catch (e: IOException) {
e.printStackTrace()
setFinish(this@OpenAppService)
}
// Util.hookOpenApp(this);
}
companion object {
private const val TAG = "IOSTQ:OpenAppService"
private const val jobId = 111
fun onEvent(context: Context) {
enqueueWork(
context,
OpenAppService::class.java, jobId, Intent(
context,
OpenAppService::class.java
)
)
}
}
}

View File

@ -0,0 +1,53 @@
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.BackupUtils.recoverRecordData
import com.android.grape.util.TaskUtils
class RecoverJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
Log.i(TAG, "onHandleWork")
var succ = false
try {
succ = recoverRecordData(this)
} catch (e: Exception) {
e.printStackTrace()
}
if (succ) {
Handler(Looper.getMainLooper()).postDelayed(
{ OpenAppService.onEvent(this@RecoverJobService) },
3000L
)
} else {
Handler(Looper.getMainLooper()).postDelayed({
onEvent(
this@RecoverJobService
)
}, 3000L)
}
}
companion object {
private const val TAG = "IOSTQ:RecoverJobService"
private const val jobId = 109
fun onEvent(context: Context) {
enqueueWork(
context,
RecoverJobService::class.java, jobId, Intent(
context,
RecoverJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,345 @@
package com.android.grape.job
import android.content.Context
import android.content.Intent
import android.os.Environment
import android.os.Handler
import android.os.Looper
import androidx.core.app.JobIntentService
import com.android.grape.data.AppState.afApp
import com.android.grape.data.AppState.backFileName
import com.android.grape.data.AppState.backFileName1
import com.android.grape.data.AppState.backFileName2
import com.android.grape.data.AppState.backupResult
import com.android.grape.data.AppState.clickErrReason
import com.android.grape.data.AppState.clickServerTimeFromGP
import com.android.grape.data.AppState.clickTime
import com.android.grape.data.AppState.delegateIp
import com.android.grape.data.AppState.instalTimeFromGp
import com.android.grape.data.AppState.installServerTimeFromGP
import com.android.grape.data.AppState.installTime
import com.android.grape.data.AppState.isClickRet
import com.android.grape.data.AppState.isNeedBackup
import com.android.grape.data.AppState.isNeedRestored
import com.android.grape.data.AppState.lastUpdateTime
import com.android.grape.data.AppState.logBuffer
import com.android.grape.data.AppState.preClickRecordId
import com.android.grape.data.AppState.recordId
import com.android.grape.data.AppState.recordPackageName
import com.android.grape.data.AppState.reloginRecordId
import com.android.grape.net.AfClient.postData
import com.android.grape.util.AppUtils.getAppAfVer
import com.android.grape.util.DeviceUtils.getMainUserAndGroup
import com.android.grape.util.InstallUtils.isInstallRet
import com.android.grape.util.MockTools
import com.android.grape.util.MyPost
import com.android.grape.util.ShellUtils.chownSh
import com.android.grape.util.ShellUtils.delFileSh
import com.android.grape.util.TaskUtils
import com.android.grape.util.TaskUtils.HkVer
import com.android.grape.util.TaskUtils.getAfLog
import com.android.grape.util.TaskUtils.setFinish
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() {
/**
* 日志记录打印任务开始执行的日志
* 判断安装状态通过 Util.isInstallRet() 判断是否需要发送安装结果
* 发送备份数据调用 SendBackup() 发送备份信息
* 根据状态选择操作
* 若需恢复则调用 sendReloginEvent()
* 否则调用 sendLogEvent() 发送日志事件
* 结束任务调用 Util.setFinish() 标记任务完成
*/
override fun onHandleWork(intent: Intent) {
LogUtils.i(TAG, "onHandleWork")
if (isInstallRet()) {
SendBackup()
if (isNeedRestored) {
sendReloginEvent()
} else {
sendLogEvent()
}
MockTools.exec("rm -rf ${Environment.getExternalStorageDirectory()}/script/${recordPackageName}/afLog.txt")
}
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 (isNeedBackup) {
val cachejson = JSONObject()
cachejson.put("firstInstallTime", installTime)
cachejson.put("lastUpdateTime", lastUpdateTime)
cachejson.put("installServerTimeFromGP", installServerTimeFromGP)
cachejson.put("clickServerTimeToGP", clickServerTimeFromGP)
cachejson.put("installTimeFromGP", instalTimeFromGp)
datajson.put("cacheJson", cachejson)
}
paramsJo.put("recordId", recordId)
paramsJo.put("preClickRecordId", preClickRecordId)
paramsJo.put("dataJson", datajson)
paramsJo.put("reloginRecordId", reloginRecordId)
val clickInfoJo = JSONObject()
clickInfoJo.put("clickRet", isClickRet)
clickInfoJo.put("clickIp", delegateIp)
clickInfoJo.put("clickTime", clickTime)
clickInfoJo.put("clickErrReason", clickErrReason)
paramsJo.put("clickInfo", clickInfoJo)
val installInfoJo = JSONObject()
installInfoJo.put("installRet", isInstallRet())
installInfoJo.put("installTime", installTime)
paramsJo.put("installInfo", installInfoJo)
if (null != backupResult) {
paramsJo.put("backUpFiles", backupResult)
}
val logInfoJo = JSONObject()
logInfoJo.put("afLog", getAfLog() + "\r\n" + logBuffer.toString())
// logInfoJo.put("setConfLog", Util.getParamsJson());
paramsJo.put("logInfo", logInfoJo)
var params: String? = null
params = paramsJo.toString()
var nRetryCount = 0
do {
val ret: String? = 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 (isNeedBackup) {
val cachejson = JSONObject()
cachejson.put("firstInstallTime", installTime)
cachejson.put("lastUpdateTime", lastUpdateTime)
cachejson.put("installServerTimeFromGP", installServerTimeFromGP)
cachejson.put("clickServerTimeToGP", clickServerTimeFromGP)
cachejson.put("installTimeFromGP", instalTimeFromGp)
datajson.put("cacheJson", cachejson)
}
paramsJo.put("recordId", recordId)
paramsJo.put("preClickRecordId", preClickRecordId)
paramsJo.put("dataJson", datajson)
val clickInfoJo = JSONObject()
clickInfoJo.put("clickRet", isClickRet)
clickInfoJo.put("clickIp", delegateIp)
clickInfoJo.put("clickTime", clickTime)
clickInfoJo.put("clickErrReason", clickErrReason)
paramsJo.put("clickInfo", clickInfoJo)
val installInfoJo = JSONObject()
installInfoJo.put("installRet", isInstallRet())
installInfoJo.put("installTime", installTime)
paramsJo.put("installInfo", installInfoJo)
val logInfoJo = JSONObject()
logInfoJo.put("afLog", getAfLog() + "\r\n" + logBuffer.toString())
// logInfoJo.put("setConfLog", Util.getParamsJson());
logInfoJo.put("hookVer", HkVer())
logInfoJo.put("afVer", getAppAfVer())
logInfoJo.put("afApp", afApp)
paramsJo.put("logInfo", logInfoJo)
if (null != backupResult) {
paramsJo.put("backUpFiles", backupResult)
}
var params: String? = null
params = paramsJo.toString()
var nRetryCount = 0
do {
val ret: String = 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? = backFileName
val fileName1: String = backFileName1 ?: ""
val fileName2: String = 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) {
delFileSh(fileName)
delFileSh(fileName1)
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)
}
chownSh(fileName, getMainUserAndGroup(this))
var url = "http://47.83.1.116/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", "${recordId}")
builder.addFormDataPart("afVer", 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= " + 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")) {
backupResult = retJo.getJSONObject("backUpFiles")
}
result = true
}
}
} catch (e: Exception) {
LogUtils.e(TAG, e.toString())
result = false
}
return result
}
/**
* class PostRet {
* int succ = 0;
* }
*/
internal class PostRet {
var succ: Int = 0
}
companion object {
private const val TAG = "IOSTQ:SendCallbackJobService"
private const val jobId = 700
/**
* 封装 Intent创建一个用于启动 SendCallbackJobService Intent
* 提交异步任务通过 enqueueWork() 将该服务作为后台任务入队由系统调度执行
*/
fun onEvent(context: Context) {
enqueueWork(
context,
SendCallbackJobService::class.java, jobId, Intent(
context,
SendCallbackJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,75 @@
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.data.AppState.isClickRet
import com.android.grape.data.AppState.proxyCountry
import com.android.grape.net.MyGet
import com.android.grape.util.ClashUtil
import com.android.grape.util.ClashUtil.getProxyPort
import com.android.grape.util.InstallUtils.setInstallRet
import com.android.grape.util.TaskUtils
import com.android.grape.util.TaskUtils.setFinish
import java.util.Locale
class StartVpnPortJobService : JobIntentService() {
override fun onHandleWork(intent: Intent) {
Log.i(TAG, "start to handle work")
ClashUtil.switchProxyGroup("PROXY", "DIRECT", "http://127.0.0.1:6170")
if (exec()) {
Handler(Looper.getMainLooper()).postDelayed({
StartVpnServerJobService.onEvent(
this@StartVpnPortJobService
)
}, 1000L)
} else {
setInstallRet(false)
isClickRet = false
setFinish(this)
}
}
private fun exec(): Boolean {
try {
val port = getProxyPort()
var nRetryCount = 0
do {
val url =
"http://39.103.73.250/tt/test/testProxy.jsp?port=$port&country=" + proxyCountry
?.uppercase(Locale.getDefault())
val result: String = MyGet.get(url)
Log.d(TAG, "request url == $url result$result")
if (result.contains("ok")) {
return true
}
} while (nRetryCount++ < 3)
return false
} catch (err: Exception) {
err.printStackTrace()
return false
}
}
companion object {
private const val TAG = "IOSTQ:StartVpnPort"
private const val jobId = 503
fun onEvent(context: Context) {
enqueueWork(
context,
StartVpnPortJobService::class.java, jobId, Intent(
context,
StartVpnPortJobService::class.java
)
)
}
}
}

View File

@ -0,0 +1,51 @@
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
/**
* 本地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
)
}, 2000L)
}
}
private fun exec(): Boolean {
return ClashUtil.switchProxyGroup("PROXY", "my-socks5-proxy", "http://127.0.0.1:6170")
}
companion object {
private const val TAG = "IOSTQ:StartVpnServer"
private const val jobId = 505
fun onEvent(context: Context) {
enqueueWork(
context,
StartVpnServerJobService::class.java, jobId, Intent(
context,
StartVpnServerJobService::class.java
)
)
}
}
}

View File

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

View File

@ -0,0 +1,189 @@
package com.android.grape.manager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.res.AssetManager
import com.android.grape.data.AppState.afApp
import com.android.grape.data.AppState.afLog
import com.android.grape.data.AppState.appAfVer
import com.android.grape.data.AppState.appDataUrl
import com.android.grape.data.AppState.backFileName
import com.android.grape.data.AppState.backFileName1
import com.android.grape.data.AppState.backFileName2
import com.android.grape.data.AppState.backUpServerIp
import com.android.grape.data.AppState.backupResult
import com.android.grape.data.AppState.canAutoAtc
import com.android.grape.data.AppState.canAutoLc
import com.android.grape.data.AppState.clickErrReason
import com.android.grape.data.AppState.clickTime
import com.android.grape.data.AppState.ctit
import com.android.grape.data.AppState.defaultAppJo
import com.android.grape.data.AppState.defaultPRoxyJo
import com.android.grape.data.AppState.delegateIp
import com.android.grape.data.AppState.forwardIp
import com.android.grape.data.AppState.fuzzy_domain
import com.android.grape.data.AppState.fuzzy_proxy
import com.android.grape.data.AppState.instalTimeFromGp
import com.android.grape.data.AppState.installRet
import com.android.grape.data.AppState.installTime
import com.android.grape.data.AppState.isCanAuto
import com.android.grape.data.AppState.isClickRet
import com.android.grape.data.AppState.isNeedBackup
import com.android.grape.data.AppState.isNeedReg
import com.android.grape.data.AppState.isNeedRestored
import com.android.grape.data.AppState.keepOpen
import com.android.grape.data.AppState.lang
import com.android.grape.data.AppState.logBuffer
import com.android.grape.data.AppState.proxyCountry
import com.android.grape.data.AppState.proxyIp
import com.android.grape.data.AppState.proxyPort
import com.android.grape.data.AppState.recordExtraFileName
import com.android.grape.data.AppState.recordFileName
import com.android.grape.data.AppState.recordId
import com.android.grape.data.AppState.recordPackageName
import com.android.grape.data.AppState.referer
import com.android.grape.data.AppState.regEmailJson
import com.android.grape.data.AppState.scriptOpenApp
import com.android.grape.data.AppState.startInstallTime
import com.android.grape.data.AppState.taskJson
import com.android.grape.data.AppState.trackingLink
import com.android.grape.data.AppState.ua
import com.android.grape.data.AppState.videoProxy
import org.json.JSONObject
import java.io.IOException
import java.io.InputStream
import java.util.Properties
/**
* @Time: 2025-27-16 17:27
* @Creator: 初屿贤
* @File: ConfigManager
* @Project: AndroidGrape
* @Description:
*/
object ConfigManager {
fun initDefaultAppJo() {
try {
defaultAppJo = JSONObject(
"{\"packname\":\"luxury.best.mycamerafilter\",\"minsdk\":19,\"appname\":\"Super Fast Filter\",\"sig_sha256\":\"a2a087582d4b08db38bb50a5a4a7e588513688d81c044e5ae6aa7569dd18c454\",\"vercode\":7,\"apkmd5\":\"81d97721c197506795bd4697da916edc\",\"sig_sha1\":\"80cd0ca6eb908870d89f2ee7b9ff06d080512562\",\"sig\":\"MIIDSzCCAjOgAwIBAgIEKbAxNjANBgkqhkiG9w0BAQsFADBVMQswCQYDVQQGEwJLUjEMMAoGA1UECBMDUHJvMQ0wCwYDVQQHEwRDaXR5MQwwCgYDVQQKEwNPcmcxCzAJBgNVBAsTAk9VMQ4wDAYDVQQDEwVZb3VuZzAgFw0yMDEyMzExNTE5MTNaGA8yMDc1MTAwNDE1MTkxM1owVTELMAkGA1UEBhMCS1IxDDAKBgNVBAgTA1BybzENMAsGA1UEBxMEQ2l0eTEMMAoGA1UEChMDT3JnMQswCQYDVQQLEwJPVTEOMAwGA1UEAxMFWW91bmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCnkvDWPAkmp0RriiRp0pdKWvG3EvYP+EQ3cKUBz2IEygZRK4rH6dfHnzG0AbqZE/eTV1ISiA8sOotlCIAukq3DdBpg+2WFmO7RO//0lhxc2L+QJJsSyrweeE7jCgyTTRCTf/NUqNhCsscGaFMa2JYU/FVDQDDGNXS1rPk6Tx+D+B+XWu7nZBioH0Bcp6rEJ+FD23+tHKEwXiw7znKUojuJ+wtZjWqFNI6L0PkI5rI7Ckg/NwBqGIT9xEL8TDZATdIIOb4c3QbqhEdxA42/PFnXTmWG8W3AcYKivvdRCXmFPhz++e4NRxZHnbQGvRfbl0yKs8U2ViHPV9YMooIaVMAnAgMBAAGjITAfMB0GA1UdDgQWBBQ5Bpf8zg0LB3RTfMr7tKo+e7+RujANBgkqhkiG9w0BAQsFAAOCAQEAl/JZliWRvfKGP46L4vF3PYvvg+61Iho6giPq+zYLmFFhtw67Vc5bUrBqn5t+rAZ5EO871pqnB326SJW+Q0Iy2W/z05MK5QS302R8RYB+9DyQQHCi9c7NVsYoNoEQ3BCh/K01qtYfkE+xoKSMsiWhTtrpoNIkxh2pVjxAcos0MZcOjN0htP6xkXL24uEMSZTkmfv3Wyty1OFvdguncRJJksPVb+Xwt3gCOzsFbuPtG06NlwfN3R4PhfaeIapil5zMhbzWnx7OPoNXYYJhk+rAiVI8wPcQEo4+MGXEyCKma6OZzPFSNoRZBOdEsFAe21/D/0heRHJ2BveHwtA6jcx6zg==\",\"vername\":\"1.0.4\",\"apksize\":3609050,\"sig_md5\":\"6c6e8954878832e065f2a12bf21aa40c\",\"appid\":\"5133078\",\"slotid\":\"945726662\",\"adType\":1}"
)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun initDefaultProxyJo() {
try {
defaultPRoxyJo = JSONObject("{\"proxyPort\": 0,\"proxyIp\": \"\"}")
} catch (e: Exception) {
}
}
fun init() {
scriptOpenApp = 0
fuzzy_proxy = null
backupResult = null
backUpServerIp = ""
fuzzy_domain = ""
referer = ""
isNeedBackup = false
isNeedRestored = false
taskJson = null
installRet = false
recordPackageName = null
recordFileName = null
recordExtraFileName = null
trackingLink = null
proxyIp = null
proxyPort = 0
proxyCountry = null
lang = null
delegateIp = null
ua = null
recordId = 0L
videoProxy = ""
forwardIp = null
referer = null
appDataUrl = ""
installTime = 0L
instalTimeFromGp = 0L
startInstallTime = 0L
clickTime = 0L
installTime = 0L
afLog = ""
backFileName = null
backFileName1 = null
backFileName2 = null
installRet = null
isClickRet = false
clickErrReason = ""
appAfVer = ""
afApp = ""
ctit = 0
keepOpen = 0
isNeedReg = false
isNeedBackup = false
regEmailJson = null
isCanAuto = false
canAutoAtc = ""
canAutoLc = ""
logBuffer = StringBuffer()
}
fun getPropertiesFromAssets(context: Context, fileName: String): Properties {
val am: AssetManager = context.assets
var `is`: InputStream? = null
val prop = Properties()
try {
`is` = am.open(fileName)
prop.load(`is`)
} catch (e: IOException) {
e.printStackTrace()
} finally {
if (`is` != null) {
try {
`is`.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
return prop
}
fun getMetaDataValue(context: Context, name: String): String {
var value: Any? = null
val packageManager: PackageManager = context.packageManager
val applicationInfo: ApplicationInfo
try {
applicationInfo =
packageManager.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
if (applicationInfo.metaData != null) {
value = applicationInfo.metaData[name]
}
} catch (e: PackageManager.NameNotFoundException) {
throw RuntimeException("Could not read the name in the manifest file.", e)
}
if (value == null) {
throw RuntimeException("The name '$name' is not defined in the manifest file's meta data.")
}
return value.toString()
}
}

View File

@ -0,0 +1,54 @@
package com.android.grape.manager
import com.android.grape.data.AppState.clickTime
import com.android.grape.data.AppState.instalTimeFromGp
import com.android.grape.data.AppState.installTime
import java.util.Random
/**
* @Time: 2025-30-16 17:30
* @Creator: 初屿贤
* @File: TrackingManager
* @Project: AndroidGrape
* @Description:
*/
object TrackingManager {
fun genInstallTimeFromGP(ct: Long, it: Long): LongArray {
var clickServerTimeToGP: Long
var installServerTimeFromGP: Long
val timeOff = it - ct
if (timeOff < 180000L) {
installTime = it - 1000 * (5 + Random().nextInt(10))
instalTimeFromGp = (installTime / 1000) - (installTime % 7) - 3
} else if (timeOff < 600000L) {
installTime = it - 1000 * (20 + Random().nextInt(40))
instalTimeFromGp = (installTime / 1000) - (installTime % 30) - 30
} else {
// firstInstallTime = it - 1000 * (40+new Random().nextInt(140));
installTime = it - 1000 * (180 + Random().nextInt((timeOff * 0.3 / 1000).toInt()))
instalTimeFromGp = (installTime / 1000) - (installTime % 120) - 180
}
clickServerTimeToGP = (instalTimeFromGp * 1000 - clickTime)
if (clickServerTimeToGP > 0) {
val randomNum =
if (clickServerTimeToGP.toInt() / 1000 >= 20) Random().nextInt(20) else Random().nextInt(
clickServerTimeToGP.toInt() / 1000
)
clickServerTimeToGP = clickTime + ((1 + Random().nextInt(1 + randomNum)) * 1000)
}
installServerTimeFromGP = (installTime - instalTimeFromGp * 1000)
if (installServerTimeFromGP > 0) {
val randomNum =
if (installServerTimeFromGP.toInt() / 1000 >= 20) Random().nextInt(20) else Random().nextInt(
installServerTimeFromGP.toInt() / 1000
)
installServerTimeFromGP = instalTimeFromGp + (1 + Random().nextInt(1 + randomNum))
}
return longArrayOf(clickServerTimeToGP, installServerTimeFromGP)
}
}

View File

@ -0,0 +1,215 @@
package com.android.grape.net
import android.util.Log
import com.android.grape.util.FileUtils
import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.UnsupportedEncodingException
import java.net.HttpURLConnection
import java.net.URL
/**
* @Time: 2025-07-16 16:07
* @Creator: 初屿贤
* @File: AfClient
* @Project: AndroidGrape
* @Description:
*/
object AfClient {
fun downloadFile(httpUrl: String, fileName: String): Boolean {
Log.i(
"AfClient",
"start to downloadFile : $httpUrl ; $fileName"
)
val file = File(fileName)
if (file.exists() && file.length() >= 1024 * 1024 * 2) { //文件已经存在就不下载 且 大小超过3M
return true
}
val create_dir = file.parentFile
if (create_dir != null && !create_dir.exists()) {
FileUtils.forceMakeDir(create_dir)
} else if (create_dir == null) {
return false
}
val fileLength = 0L
try {
var conn: HttpURLConnection? = null
var `is`: InputStream? = null
var fos: FileOutputStream? = null
val url = URL(httpUrl)
try {
conn = url.openConnection() as HttpURLConnection
`is` = conn.inputStream
fos = FileOutputStream(file)
val buf = ByteArray(256)
conn.connect()
if (conn.responseCode >= 400) {
Log.i("AfClient", "connection timeout")
} else {
//System.out.println("文件大小:"+fileLength);
while (true) {
if (`is` != null) {
val numRead = `is`.read(buf)
if (numRead <= 0) {
return true
} else {
fos.write(buf, 0, numRead)
}
} else {
break
}
}
if (file.length() >= 1024 * 100) {
return true
}
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
conn?.disconnect()
} catch (e: Exception) {
}
try {
fos?.close()
} catch (e: Exception) {
}
try {
`is`?.close()
} catch (e: Exception) {
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
fun postData(byt: ByteArray?, url: String): String? {
val bytes: ByteArray? = postDataBytes(byt, url)
if (bytes != null && bytes.isNotEmpty()) {
try {
return String(bytes, charset("UTF-8"))
} catch (e: UnsupportedEncodingException) {
e.printStackTrace()
}
}
return null
}
fun postDataBytes(byt: ByteArray?, url: String): ByteArray? {
var result: String? = null
var httpUrlConnection: HttpURLConnection? = null
var inStrm: InputStream? = null
var baos: ByteArrayOutputStream? = null
var bis: BufferedInputStream? = null
try {
httpUrlConnection = getHttpURLConnection(url)
httpUrlConnection.allowUserInteraction = true
httpUrlConnection.doOutput = true
httpUrlConnection.doInput = true
httpUrlConnection.useCaches = false
httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428
httpUrlConnection.setRequestProperty(
"Content-type",
"application/x-java-serialized-object"
)
// httpUrlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
httpUrlConnection.requestMethod = "POST"
httpUrlConnection.connectTimeout = 20000
var outStrm: OutputStream? = null
outStrm = httpUrlConnection.outputStream
outStrm.write(byt)
outStrm.flush()
outStrm.close()
inStrm = httpUrlConnection.inputStream
baos = ByteArrayOutputStream()
bis = BufferedInputStream(inStrm)
val buf = ByteArray(1024)
var readSize = -1
while ((bis.read(buf).also { readSize = it }) != -1) {
baos.write(buf, 0, readSize)
}
val data = baos.toByteArray()
return data
} catch (e: Exception) {
e.printStackTrace()
result = null
} finally {
if (baos != null) {
try {
baos.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (bis != null) {
try {
bis.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (inStrm != null) {
try {
inStrm.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (httpUrlConnection != null) {
httpUrlConnection.disconnect()
httpUrlConnection = null
}
System.gc()
}
return null
}
const val dynamicPort0: Int = 20380
const val dynamicPort: Int = dynamicPort0
const val dynamicPort1: Int = 30379
fun getHttpURLConnection(
_url: String
): HttpURLConnection {
val url = URL(_url)
var httpUrlConnection: HttpURLConnection? = null
httpUrlConnection = url.openConnection() as HttpURLConnection
return httpUrlConnection
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,233 @@
package com.android.grape.net
import android.util.Log
import com.blankj.utilcode.util.LogUtils
import com.google.gson.Gson
import okhttp3.*
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Level
import okhttp3.logging.HttpLoggingInterceptor.Logger
import java.io.IOException
import java.util.concurrent.TimeUnit
object HttpUtils {
const val HOST: String = "https://openapi-hk.armcloud.net"
// OkHttp客户端配置
private val client: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor().apply {
level = Level.BODY
})
.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?)
}
/**
* GET 请求异步
*
* @param url 请求URL
* @param params 查询参数可选
* @param headers 请求头可选
* @param callback 回调接口
*/
fun getAsync(
url: String,
params: Map<String, String>? = null,
callback: HttpCallback
) {
val request = buildGetRequest(url, params)
executeRequestAsync(request, callback)
}
/**
* POST 请求异步
*
* @param url 请求URL
* @param body 请求体JSON格式
* @param headers 请求头可选
* @param callback 回调接口
*/
fun postAsync(
url: String,
body: String? = null,
callback: HttpCallback
) {
val request = buildPostRequest(url, body)
executeRequestAsync(request, callback)
}
/**
* GET 请求同步
*
* @param url 请求URL
* @param params 查询参数可选
* @param headers 请求头可选
* @return Pair<响应内容, 状态码>
*/
fun getSync(
url: String,
params: Map<String, String>? = null,
): Pair<String?, Int> {
val request = buildGetRequest(url, params)
return executeRequestSync(request)
}
/**
* POST 请求同步
*
* @param url 请求URL
* @param body 请求体JSON格式
* @param headers 请求头可选
* @return Pair<响应内容, 状态码>
*/
fun postSync(
url: String,
body: String? = null
): Pair<String?, Int> {
val request = buildPostRequest(url, body)
return executeRequestSync(request)
}
/**
* 取消所有请求
*/
fun cancelAllRequests() {
client.dispatcher.cancelAll()
}
// 内部方法 --------------------------------
private fun buildGetRequest(
path: String ,
params: Map<String, String>?,
): Request {
val url = "$HOST$path"
val httpUrlBuilder = url.toHttpUrlOrNull()?.newBuilder()
?: throw IllegalArgumentException("Invalid URL: $url")
var param = ""
params?.forEach { (key, value) ->
httpUrlBuilder.addQueryParameter(key, value)
param += "&${key}=${value}"
}
if (param.isNotEmpty()){
param = param.substring(1)
}
val requestBuilder = Request.Builder().url(httpUrlBuilder.build())
val authver = "2.0"
val timestamp = System.currentTimeMillis()
val sign =
ArmCloudSignatureV2.calculateSignature(timestamp.toString(), path, param)
addHeaders(
requestBuilder,
mapOf(
"authver" to authver,
"x-ak" to ArmCloudSignatureV2.SECRET_KEY,
"x-timestamp" to timestamp.toString(),
"x-sign" to sign
)
)
return requestBuilder.build()
}
private fun buildPostRequest(
path: String,
body: String? = null
): Request {
val url = "$HOST$path"
val bodyString = body?:""
val requestBuilder = Request.Builder()
.url(url)
.post(bodyString.toRequestBody(JSON_MEDIA_TYPE))
val authver = "2.0"
val timestamp = System.currentTimeMillis()
val sign =
ArmCloudSignatureV2.calculateSignature(timestamp.toString(), path, body)
addHeaders(
requestBuilder,
mapOf(
"authver" to authver,
"x-ak" to ArmCloudSignatureV2.SECRET_KEY,
"x-timestamp" to timestamp.toString(),
"x-sign" to sign
)
)
return requestBuilder.build()
}
private fun addHeaders(
builder: Request.Builder,
headers: Map<String, String>
) {
headers.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.peekBody(Long.MAX_VALUE).string()
val code = response.code
Log.d("TAG", "executeRequestSync: $responseBody")
if (response.isSuccessful && responseBody.isNotEmpty()) {
Pair(responseBody, code)
} else {
Pair(null, -1)
}
} catch (e: IOException) {
Pair(null, -1) // 网络错误状态码
}
}
}

View File

@ -0,0 +1,273 @@
package com.android.grape.net
import android.content.Context
import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.TimeUnit
object MyGet {
private const val TAG = "MyGet"
val affHttpClient: OkHttpClient = OkHttpClient().apply {
dispatcher.maxRequests = 1000
dispatcher.maxRequestsPerHost = 1000
}
fun get(url_: String): String {
var response: Response? = null
val client: OkHttpClient = affHttpClient.newBuilder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build()
val request = Request.Builder()
.url(url_)
.addHeader("User-Agent", "PostmanRuntime/7.29.0")
.addHeader("Accept", "*/*")
.addHeader("Accept-Encoding", "gzip, deflate, br")
.addHeader("Connection", "keep-alive")
.build()
try {
response = client.newCall(request).execute()
return response.body?.string()?:""
} catch (e: java.lang.Exception) {
e.printStackTrace()
} finally {
response?.close()
}
return ""
}
fun getData(url_: String, ua: String?): String? {
return getData(url_, ua, 0)
}
fun getData( url_: String, ua: String?, num: Int): String? {
Log.i(TAG, "start getData : $url_")
var result: String? = null
var httpUrlConnection: HttpURLConnection? = null
var inStrm: InputStream? = null
var baos: ByteArrayOutputStream? = null
var bis: BufferedInputStream? = null
try {
val url = URL(url_)
httpUrlConnection = url.openConnection() as HttpURLConnection
httpUrlConnection.allowUserInteraction = true
httpUrlConnection.doOutput = false
httpUrlConnection.doInput = true
httpUrlConnection.useCaches = false
httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428
httpUrlConnection.setRequestProperty("Content-Type", "text/html;charset=utf-8")
httpUrlConnection.setRequestProperty("User-Agent", ua)
httpUrlConnection.instanceFollowRedirects = false
httpUrlConnection.requestMethod = "GET"
httpUrlConnection.connectTimeout = 60000
httpUrlConnection.readTimeout = 60000
val statusCode = httpUrlConnection.responseCode
Log.i(TAG, "getData status code : $statusCode")
val location = httpUrlConnection.getHeaderField("Location")
if (location != null && location.length > 0) {
return if (location.startsWith("http")) {
if (num < 6) {
getData(location, ua, num + 1)
} else {
"jumpOver"
}
} else {
"unknowSchema__$location"
}
} else if (statusCode >= 200 && statusCode < 300) {
inStrm = httpUrlConnection.inputStream
baos = ByteArrayOutputStream()
bis = BufferedInputStream(inStrm)
val buf = ByteArray(1024)
var readSize = -1
while ((bis.read(buf).also { readSize = it }) != -1) {
baos.write(buf, 0, readSize)
}
val data = baos.toByteArray()
return String(data, charset("UTF-8"))
} else {
return "errorOccur"
}
} catch (e: Exception) {
e.printStackTrace()
result = null
} finally {
if (baos != null) {
try {
baos.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (bis != null) {
try {
bis.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (inStrm != null) {
try {
inStrm.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (httpUrlConnection != null) {
httpUrlConnection.disconnect()
httpUrlConnection = null
}
System.gc()
}
return null
}
fun getData(context: Context?, url_: String, ua: String?, forwardIp: String?): String? {
return getData(context, url_, ua, forwardIp, 0)
}
fun getData(
context: Context?,
url_: String,
ua: String?,
forwardIp: String?,
num: Int
): String? {
var url_ = url_
url_ = url_.replace(" ", "")
Log.i(TAG, "start getData : $url_")
var result: String? = null
var httpUrlConnection: HttpURLConnection? = null
var inStrm: InputStream? = null
var baos: ByteArrayOutputStream? = null
var bis: BufferedInputStream? = null
try {
val url = URL(url_)
httpUrlConnection = url.openConnection() as HttpURLConnection
httpUrlConnection.allowUserInteraction = true
httpUrlConnection.doOutput = false
httpUrlConnection.doInput = true
httpUrlConnection.useCaches = false
httpUrlConnection.instanceFollowRedirects = false
httpUrlConnection.setRequestProperty("Connection", "close") //add 20200428
httpUrlConnection.setRequestProperty("Content-Type", "text/html;charset=utf-8")
httpUrlConnection.setRequestProperty("User-Agent", ua)
httpUrlConnection.setRequestProperty("X-Forwarded-For", forwardIp)
httpUrlConnection.requestMethod = "GET"
httpUrlConnection.connectTimeout = 60000
httpUrlConnection.readTimeout = 60000
val statusCode = httpUrlConnection.responseCode
Log.i(TAG, "getData status code : $statusCode")
val location = httpUrlConnection.getHeaderField("Location")
if (location != null && location.length > 0) {
if (location.startsWith("http")) {
return if (num >= 6) {
"jumpOver"
} else {
getData(context, location, ua, forwardIp, num + 1)
}
} else if (location.contains("://")) {
return "unknowSchema__$location"
} else {
if (num >= 6) {
return "jumpOver"
} else {
val arr =
url_.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
var newloc = ""
if (location.startsWith("/")) {
newloc = arr[0] + "//" + arr[2] + "/" + location
} else {
val pos = url_.lastIndexOf("/")
newloc = url_.substring(0, pos) + "/" + location
}
return getData(context, newloc, ua, forwardIp, num + 1)
}
}
} else if (statusCode >= 200 && statusCode < 300) {
inStrm = httpUrlConnection.inputStream
baos = ByteArrayOutputStream()
bis = BufferedInputStream(inStrm)
val buf = ByteArray(1024)
var readSize = -1
while ((bis.read(buf).also { readSize = it }) != -1) {
baos.write(buf, 0, readSize)
}
val data = baos.toByteArray()
return String(data, charset("UTF-8"))
} else {
return "errorOccur"
}
} catch (e: Exception) {
e.printStackTrace()
result = null
} finally {
if (baos != null) {
try {
baos.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (bis != null) {
try {
bis.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (inStrm != null) {
try {
inStrm.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (httpUrlConnection != null) {
httpUrlConnection.disconnect()
httpUrlConnection = null
}
System.gc()
}
return null
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,29 @@
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 com.android.grape.data.AppState.script_status
import com.android.grape.job.UnInstallService
import com.android.grape.util.ScriptUtils.SCRIPT_RESULT_KEY
import com.android.grape.util.TaskUtils
import com.blankj.utilcode.util.LogUtils
class ScriptReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
LogUtils.i("TAG", context.packageName + " Receive action:" + action)
val scriptResult = intent.getStringExtra(SCRIPT_RESULT_KEY)
script_status = 0
LogUtils.i(
"IOSTQ:脚本结束",
"result----> $scriptResult"
)
Handler(Looper.getMainLooper()).postDelayed(
{ UnInstallService.onEvent(context) },
3000
)
}
}

View File

@ -0,0 +1,144 @@
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
)
mApkFiles?.let {
for (apkFile in it) apkFileDescriptors.add(NormalFileDescriptor(apkFile))
}
apkSource = DefaultApkSource(apkFileDescriptors)
} else if (mZipFile != null) {
mZipFile?.let {
val zipBackedApkSource: ZipBackedApkSource
if (mReadZipViaZipFileEnabled) zipBackedApkSource =
ZipFileApkSource(mContext, NormalFileDescriptor(it))
else zipBackedApkSource = ZipApkSource(mContext, NormalFileDescriptor(it))
apkSource = zipBackedApkSource
sourceIsZip = true
}
} else if (mZipUri != null) {
mZipUri?.let {
val zipBackedApkSource: ZipBackedApkSource
if (mReadZipViaZipFileEnabled) zipBackedApkSource =
ZipFileApkSource(mContext, ContentUriFileDescriptor(mContext, it))
else zipBackedApkSource =
ZipApkSource(mContext, ContentUriFileDescriptor(mContext, it))
apkSource = zipBackedApkSource
sourceIsZip = true
}
} else if (mApkUris != null) {
val apkUriDescriptors: MutableList<FileDescriptor> = ArrayList<FileDescriptor>(
mApkUris?.size?: 0
)
for (apkUri in mApkUris!!) apkUriDescriptors.add(
ContentUriFileDescriptor(
mContext,
apkUri
)
)
apkSource = DefaultApkSource(apkUriDescriptors)
} else {
throw IllegalStateException("No source set")
}
apkSource?.let {
if (mSigningEnabled) apkSource = SignerApkSource(mContext, it)
//Signing already uses temp files, so there's not reason to use CopyToFileApkSource with it
if (mZipExtractionEnabled && sourceIsZip && !mSigningEnabled) {
apkSource = CopyToFileApkSource(mContext, it)
}
if (mFilteredApks != null) apkSource = FilterApkSource(it, mFilteredApks?:setOf(), mBlacklist)
}
return apkSource?: throw IllegalStateException("No source set")
}
private fun ensureSourceSetOnce() {
check(!mSourceSet) { "Source can be only be set once" }
mSourceSet = true
}
}

View File

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

View File

@ -0,0 +1,115 @@
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 {
mDefaultInstaller?.let {
return createSessionOnInstaller(it, params)
}
return ""
}
override fun enqueueSession(sessionId: String) {
val installer = mSessionIdToInstaller.remove(sessionId)
requireNotNull(installer) { "Unknown sessionId" }
installer.enqueueSession(sessionId)
}
override fun registerSessionObserver(observer: SaiPiSessionObserver) {
mObservers.add(observer)
}
override fun unregisterSessionObserver(observer: SaiPiSessionObserver) {
mObservers.remove(observer)
}
override fun getSessions(): List<SaiPiSessionState>
{
val sessions = ArrayList<SaiPiSessionState>()
for (installer in mInstallers.values) sessions.addAll(installer.getSessions())
sessions.sort()
return sessions
}
override fun onSessionStateChanged(state: SaiPiSessionState?) {
for (observer in mObservers) observer.onSessionStateChanged(state)
}
companion object {
@SuppressLint("StaticFieldLeak")
private var sInstance: FlexSaiPackageInstaller? = null
fun getInstance(): FlexSaiPackageInstaller {
synchronized(FlexSaiPackageInstaller::class.java) {
return sInstance
?: FlexSaiPackageInstaller(MainApplication.instance)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,361 @@
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.data.AppState.isClickRet
import com.android.grape.data.AppState.recordPackageName
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.FileUtils
import com.android.grape.util.TaskUtils
import com.android.grape.util.TaskUtils.setInstallRet
import com.blankj.utilcode.util.LogUtils
import java.io.File
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.packageManager.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();
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()
setInstallRet(false)
return
}
androidSessionId = createSession()
//todo params.apkSource().apkLocalPath?
val path = "${FileUtils.CACHE_PATH}${recordPackageName}"
val file = File(path)
val files = file.listFiles()
var currentApkFile = 0
files?.let {
for (f in files) {
if (f.length() <= 0) {
setSessionState(sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.INSTALLATION_FAILED).appTempName(appTempName).error(MainApplication.instance.getString(R.string.installer_error_unknown_apk_size), null).build())
unlockInstallation()
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()
setInstallRet(false)
} else {
isClickRet = true
}
}
} catch (e: Exception) {
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()
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(
listOf(
*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.entries) {
if (installCommitResult.out.contains(error.error)) {
matchedError = error
break
}
}
return matchedError.getDescription(MainApplication.instance)
}
protected abstract val shell: Shell
protected abstract val installerName: String?
protected abstract val shellUnavailableMessage: String?
abstract override fun tag(): String?
companion object {
private val mSharedSemaphore = Semaphore(1)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,111 @@
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() == true) {
mZipEntries?.nextElement()?.let { nextEntry ->
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
}
}

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