Compare commits
No commits in common. "main" and "master" have entirely different histories.
|
@ -0,0 +1,15 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
|
@ -0,0 +1 @@
|
|||
AndroidGrape
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,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>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
|
@ -1,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>
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="NONE" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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")
|
||||
}
|
Binary file not shown.
|
@ -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 { *; }
|
|
@ -0,0 +1,24 @@
|
|||
package com.android.grape
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.android.grape", appContext.packageName)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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>
|
|
@ -0,0 +1,12 @@
|
|||
// IMikRom.aidl
|
||||
package android.app;
|
||||
|
||||
// Declare any non-default types here with import statements
|
||||
|
||||
interface IMikRom {
|
||||
String shellExec(String cmd);
|
||||
String readFile(String path);
|
||||
void writeFile(String path,String data);
|
||||
void setEnableApp(String pkgName,boolean isEnable);
|
||||
boolean isEnableApp(String pkgName);
|
||||
}
|
|
@ -0,0 +1,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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.android.grape.data
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class As(
|
||||
var nameValuePairs: NameValuePairs = NameValuePairs()
|
||||
)
|
|
@ -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 = ""
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = ""
|
||||
)
|
|
@ -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 = ""
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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 = ""
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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 = ""
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package com.android.grape.data
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Pr(
|
||||
var nameValuePairs: NameValuePairsX = NameValuePairsX()
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package com.android.grape.net
|
||||
|
||||
import android.util.Log
|
||||
import com.android.grape.pad.Pad
|
||||
import com.android.grape.pad.PadTask
|
||||
import com.android.grape.pad.TaskDetail
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
object Api {
|
||||
private const val TAG = "Api"
|
||||
const val UPDATE_PAD: String = "/openapi/open/pad/updatePadProperties"
|
||||
const val PAD_DETAIL: String = "/openapi/open/pad/padDetails"
|
||||
const val PAD_TASK: String = "/task-center/open/task/padTaskDetail"
|
||||
|
||||
|
||||
fun padDetail(): ApiResponse<Pad>{
|
||||
val param = Gson().toJson(mapOf(
|
||||
"page" to 1,
|
||||
"rows" to 10,
|
||||
"padCode" to listOf("ACP250702PWJCTLF")
|
||||
))
|
||||
val (response, code) = HttpUtils.postSync(PAD_DETAIL, param)
|
||||
if (code == 200) {
|
||||
Log.d("padDetail", response.toString())
|
||||
var apiResponse: ApiResponse<Pad> = Gson().fromJson(response, object : TypeToken<ApiResponse<Pad>>() {}.type)
|
||||
return apiResponse
|
||||
} else {
|
||||
Log.d("padDetail", "error: $code")
|
||||
return ApiResponse(code, "error", 0, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePad(params: String): ApiResponseList<PadTask>{
|
||||
val (response, code) = HttpUtils.postSync(UPDATE_PAD, params)
|
||||
if (code == 200) {
|
||||
Log.d("updatePad", response.toString())
|
||||
var apiResponse: ApiResponseList<PadTask> = Gson().fromJson(response, object : TypeToken<ApiResponseList<PadTask>>() {}.type)
|
||||
return apiResponse
|
||||
} else {
|
||||
Log.d("updatePad", "error: $code")
|
||||
return ApiResponseList(code, "error", 0, emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
fun padTaskDetail(taskId: Int): Int{
|
||||
val jsonString = JSONObject().apply {
|
||||
put("taskIds", JSONArray().apply {
|
||||
put(taskId)
|
||||
})
|
||||
}.toString()
|
||||
Log.d(TAG, "padTaskDetail: $jsonString")
|
||||
val (response, code) = HttpUtils.postSync(PAD_TASK, jsonString)
|
||||
if (code == 200) {
|
||||
Log.d("padTaskDetail", response.toString())
|
||||
var apiResponse: ApiResponseList<TaskDetail> = Gson().fromJson(response, object : TypeToken<ApiResponseList<TaskDetail>>() {}.type)
|
||||
val taskList = apiResponse.data
|
||||
if (apiResponse.isSuccess() && taskList!= null && taskList.isNotEmpty()){
|
||||
val task = taskList[0]
|
||||
return task.taskStatus
|
||||
}
|
||||
return -1
|
||||
}else {
|
||||
Log.d("padTaskDetail", "error: $code")
|
||||
return -1
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.android.grape.net
|
||||
|
||||
data class ApiResponse<T>(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val ts: Long,
|
||||
var data: T? = null
|
||||
){
|
||||
fun isSuccess(): Boolean {
|
||||
return code == 200
|
||||
}
|
||||
}
|
||||
|
||||
data class ApiResponseList<T>(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val ts: Long,
|
||||
var data: List<T>? = null
|
||||
){
|
||||
fun isSuccess(): Boolean {
|
||||
return code == 200
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package com.android.grape.net
|
||||
|
||||
import android.util.Log
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
||||
object ArmCloudSignatureV2 {
|
||||
const val ALGORITHM: String = "HmacSHA256"
|
||||
const val SECRET_KEY: String = "gz8f1u0t63byzdu6ozbx8r5qs3e5lipt"
|
||||
const val SECRET_ID: String = "3yc8c8bg1dym0zaiwjh867al"
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun calculateSignature(
|
||||
timestamp: String?,
|
||||
path: String,
|
||||
body: String?,
|
||||
secretKey: String = SECRET_ID
|
||||
): String {
|
||||
val stringToSign = timestamp + path + (body ?: "")
|
||||
Log.d("TAG", "calculateSignature: $stringToSign")
|
||||
val hmacSha256 = Mac.getInstance(ALGORITHM)
|
||||
val secretKeySpec = SecretKeySpec(secretKey.toByteArray(StandardCharsets.UTF_8), ALGORITHM)
|
||||
hmacSha256.init(secretKeySpec)
|
||||
val hash = hmacSha256.doFinal(stringToSign.toByteArray(StandardCharsets.UTF_8))
|
||||
return bytesToHex(hash)
|
||||
}
|
||||
|
||||
private fun bytesToHex(bytes: ByteArray): String {
|
||||
val hexString = StringBuilder()
|
||||
for (b in bytes) {
|
||||
val hex = Integer.toHexString(0xff and b.toInt())
|
||||
if (hex.length == 1) hexString.append('0')
|
||||
hexString.append(hex)
|
||||
}
|
||||
return hexString.toString()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.android.grape.net
|
||||
|
||||
interface ChangeCallBack {
|
||||
fun changeSuccess()
|
||||
fun changeFailed()
|
||||
}
|
|
@ -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) // 网络错误状态码
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.android.grape.pad
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class DcInfo(
|
||||
var area: String = "",
|
||||
var dcCode: String = "",
|
||||
var dcName: String = "",
|
||||
var ossEndpoint: String = "",
|
||||
var ossEndpointInternal: String = "",
|
||||
var ossFileEndpoint: String = "",
|
||||
var ossScreenshotEndpoint: String = ""
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
package com.android.grape.pad
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Pad(
|
||||
var page: Int = 0,
|
||||
var pageData: List<PageData> = listOf(),
|
||||
var rows: Int = 0,
|
||||
var size: Int = 0,
|
||||
var total: Int = 0,
|
||||
var totalPage: Int = 0
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
package com.android.grape.pad
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class PadTask(
|
||||
var padCode: String = "",
|
||||
var taskId: Int = 0,
|
||||
var vmStatus: Int = 0
|
||||
)
|
|
@ -0,0 +1,17 @@
|
|||
package com.android.grape.pad
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class PageData(
|
||||
var dataSize: Long = 0,
|
||||
var dataSizeUsed: Long = 0,
|
||||
var dcInfo: DcInfo = DcInfo(),
|
||||
var deviceLevel: String = "",
|
||||
var deviceStatus: Int = 0,
|
||||
var imageId: String = "",
|
||||
var online: Int = 0,
|
||||
var padCode: String = "",
|
||||
var padStatus: Int = 0,
|
||||
var streamStatus: Int = 0
|
||||
)
|
|
@ -0,0 +1,14 @@
|
|||
package com.android.grape.pad
|
||||
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class TaskDetail(
|
||||
var endTime: Long = 0,
|
||||
var errorMsg: String = "",
|
||||
var padCode: String = "",
|
||||
var taskContent: Any? = Any(),
|
||||
var taskId: Int = 0,
|
||||
var taskResult: Any? = Any(),
|
||||
var taskStatus: Int = 0
|
||||
)
|
|
@ -0,0 +1,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"
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.android.grape.provider
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
|
||||
object DeviceInfoContract {
|
||||
const val AUTHORITY = "com.android.grape.deviceinfo.provider"
|
||||
val CONTENT_URI: Uri = "content://$AUTHORITY/device_info".toUri()
|
||||
|
||||
object DeviceInfoEntry {
|
||||
const val TABLE_NAME = "device_info"
|
||||
const val COLUMN_DEVICE_ID = "device_id"
|
||||
const val COLUMN_DATA = "data"
|
||||
const val COLUMN_TIMESTAMP = "timestamp"
|
||||
|
||||
// JSON 键名
|
||||
const val KEY_CPU_ABI = "cpu_abi"
|
||||
const val KEY_DIM = "dim"
|
||||
const val KEY_BTCH = "btch"
|
||||
const val KEY_ARCH = "arch"
|
||||
const val KEY_BTL = "btl"
|
||||
const val KEY_CPU_ABI2 = "cpu_abi2"
|
||||
const val KEY_DISK = "disk"
|
||||
const val KEY_SDK = "sdk"
|
||||
const val KEY_NETWORK = "network"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package com.android.grape.provider
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.provider.BaseColumns
|
||||
|
||||
class DeviceInfoDbHelper(context: Context) : SQLiteOpenHelper(
|
||||
context, DATABASE_NAME, null, DATABASE_VERSION
|
||||
) {
|
||||
companion object {
|
||||
const val DATABASE_NAME = "device_info.db"
|
||||
const val DATABASE_VERSION = 1
|
||||
|
||||
const val SQL_CREATE_ENTRIES = """
|
||||
CREATE TABLE ${DeviceInfoContract.DeviceInfoEntry.TABLE_NAME} (
|
||||
${BaseColumns._ID} INTEGER PRIMARY KEY,
|
||||
${DeviceInfoContract.DeviceInfoEntry.COLUMN_DEVICE_ID} TEXT UNIQUE,
|
||||
${DeviceInfoContract.DeviceInfoEntry.COLUMN_DATA} TEXT NOT NULL,
|
||||
${DeviceInfoContract.DeviceInfoEntry.COLUMN_TIMESTAMP} INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.execSQL(SQL_CREATE_ENTRIES)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("DROP TABLE IF EXISTS ${DeviceInfoContract.DeviceInfoEntry.TABLE_NAME}")
|
||||
onCreate(db)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.android.grape.provider
|
||||
|
||||
object DeviceInfoHelper {
|
||||
// 获取设备唯一ID
|
||||
fun getDeviceId(): String {
|
||||
return "d1b3e7f8c9a04b8e9f2c1d5e6f7a8b9c"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
package com.android.grape.provider
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.UriMatcher
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteQueryBuilder
|
||||
import android.net.Uri
|
||||
import android.provider.BaseColumns
|
||||
import java.sql.SQLException
|
||||
|
||||
// DeviceInfoProvider.kt
|
||||
class DeviceInfoProvider : ContentProvider() {
|
||||
private lateinit var dbHelper: DeviceInfoDbHelper
|
||||
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
|
||||
addURI(DeviceInfoContract.AUTHORITY, "device_info", 1)
|
||||
addURI(DeviceInfoContract.AUTHORITY, "device_info/#", 2)
|
||||
addURI(DeviceInfoContract.AUTHORITY, "device_info/device/*", 3)
|
||||
}
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
dbHelper = DeviceInfoDbHelper(context!!)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<String>?,
|
||||
sortOrder: String?
|
||||
): Cursor? {
|
||||
val db = dbHelper.readableDatabase
|
||||
val qb = SQLiteQueryBuilder().apply {
|
||||
tables = DeviceInfoContract.DeviceInfoEntry.TABLE_NAME
|
||||
}
|
||||
|
||||
when (uriMatcher.match(uri)) {
|
||||
1 -> {} // 查询所有
|
||||
2 -> qb.appendWhere("${BaseColumns._ID} = ${uri.lastPathSegment}")
|
||||
3 -> qb.appendWhere("${DeviceInfoContract.DeviceInfoEntry.COLUMN_DEVICE_ID} = '${uri.lastPathSegment}'")
|
||||
else -> throw IllegalArgumentException("Unknown URI: $uri")
|
||||
}
|
||||
|
||||
return qb.query(db, projection, selection, selectionArgs, null, null, sortOrder).apply {
|
||||
setNotificationUri(context!!.contentResolver, uri)
|
||||
}
|
||||
}
|
||||
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? {
|
||||
val db = dbHelper.writableDatabase
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
|
||||
// 添加时间戳
|
||||
values?.put(DeviceInfoContract.DeviceInfoEntry.COLUMN_TIMESTAMP, now)
|
||||
|
||||
val rowId = db.insert(DeviceInfoContract.DeviceInfoEntry.TABLE_NAME, null, values)
|
||||
|
||||
if (rowId > 0) {
|
||||
val newUri = ContentUris.withAppendedId(DeviceInfoContract.CONTENT_URI, rowId)
|
||||
context?.contentResolver?.notifyChange(newUri, null)
|
||||
return newUri
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun update(
|
||||
uri: Uri,
|
||||
values: ContentValues?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<String>?
|
||||
): Int {
|
||||
val db = dbHelper.writableDatabase
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
|
||||
// 更新时间戳
|
||||
values?.put(DeviceInfoContract.DeviceInfoEntry.COLUMN_TIMESTAMP, now)
|
||||
|
||||
val count = when (uriMatcher.match(uri)) {
|
||||
1 -> db.update(
|
||||
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
|
||||
values, selection, selectionArgs
|
||||
)
|
||||
2 -> {
|
||||
val id = uri.lastPathSegment
|
||||
db.update(
|
||||
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
|
||||
values,
|
||||
"${BaseColumns._ID} = ?",
|
||||
arrayOf(id)
|
||||
)
|
||||
}
|
||||
3 -> {
|
||||
val deviceId = uri.lastPathSegment
|
||||
db.update(
|
||||
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
|
||||
values,
|
||||
"${DeviceInfoContract.DeviceInfoEntry.COLUMN_DEVICE_ID} = ?",
|
||||
arrayOf(deviceId)
|
||||
)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unknown URI: $uri")
|
||||
}
|
||||
|
||||
context?.contentResolver?.notifyChange(uri, null)
|
||||
return count
|
||||
}
|
||||
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
|
||||
val db = dbHelper.writableDatabase
|
||||
|
||||
val count = when (uriMatcher.match(uri)) {
|
||||
1 -> db.delete(
|
||||
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
|
||||
selection, selectionArgs
|
||||
)
|
||||
2 -> {
|
||||
val id = uri.lastPathSegment
|
||||
db.delete(
|
||||
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
|
||||
"${BaseColumns._ID} = ?",
|
||||
arrayOf(id)
|
||||
)
|
||||
}
|
||||
3 -> {
|
||||
val deviceId = uri.lastPathSegment
|
||||
db.delete(
|
||||
DeviceInfoContract.DeviceInfoEntry.TABLE_NAME,
|
||||
"${DeviceInfoContract.DeviceInfoEntry.COLUMN_DEVICE_ID} = ?",
|
||||
arrayOf(deviceId)
|
||||
)
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unknown URI: $uri")
|
||||
}
|
||||
|
||||
context?.contentResolver?.notifyChange(uri, null)
|
||||
return count
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String? {
|
||||
return when (uriMatcher.match(uri)) {
|
||||
1 -> "${DeviceInfoContract.AUTHORITY}/device_info"
|
||||
2, 3 -> "vnd.android.cursor.item/vnd.example.deviceinfo"
|
||||
else -> throw IllegalArgumentException("Unknown URI: $uri")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,819 @@
|
|||
package com.android.grape.pseudo
|
||||
|
||||
import java.io.UnsupportedEncodingException
|
||||
|
||||
|
||||
/**
|
||||
* Utilities for encoding and decoding the Base64 representation of
|
||||
* binary data. See RFCs [2045](http://www.ietf.org/rfc/rfc2045.txt) and [3548](http://www.ietf.org/rfc/rfc3548.txt).
|
||||
*/
|
||||
object Base64 {
|
||||
/**
|
||||
* Default values for encoder/decoder flags.
|
||||
*/
|
||||
const val DEFAULT: Int = 0
|
||||
|
||||
/**
|
||||
* Encoder flag bit to omit the padding '=' characters at the end
|
||||
* of the output (if any).
|
||||
*/
|
||||
const val NO_PADDING: Int = 1
|
||||
|
||||
/**
|
||||
* Encoder flag bit to omit all line terminators (i.e., the output
|
||||
* will be on one long line).
|
||||
*/
|
||||
const val NO_WRAP: Int = 2
|
||||
|
||||
/**
|
||||
* Encoder flag bit to indicate lines should be terminated with a
|
||||
* CRLF pair instead of just an LF. Has no effect if `NO_WRAP` is specified as well.
|
||||
*/
|
||||
const val CRLF: Int = 4
|
||||
|
||||
/**
|
||||
* Encoder/decoder flag bit to indicate using the "URL and
|
||||
* filename safe" variant of Base64 (see RFC 3548 section 4) where
|
||||
* `-` and `_` are used in place of `+` and
|
||||
* `/`.
|
||||
*/
|
||||
const val URL_SAFE: Int = 8
|
||||
|
||||
// --------------------------------------------------------
|
||||
// decoding
|
||||
// --------------------------------------------------------
|
||||
/**
|
||||
* Decode the Base64-encoded data in input and return the data in
|
||||
* a new byte array.
|
||||
*
|
||||
*
|
||||
* The padding '=' characters at the end are considered optional, but
|
||||
* if any are present, there must be the correct number of them.
|
||||
*
|
||||
* @param str the input String to decode, which is converted to
|
||||
* bytes using the default charset
|
||||
* @param flags controls certain features of the decoded output.
|
||||
* Pass `DEFAULT` to decode standard Base64.
|
||||
* @throws IllegalArgumentException if the input contains
|
||||
* incorrect padding
|
||||
*/
|
||||
fun decode(str: String, flags: Int): ByteArray {
|
||||
return decode(str.toByteArray(), flags)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the Base64-encoded data in input and return the data in
|
||||
* a new byte array.
|
||||
*
|
||||
*
|
||||
* The padding '=' characters at the end are considered optional, but
|
||||
* if any are present, there must be the correct number of them.
|
||||
*
|
||||
* @param input the input array to decode
|
||||
* @param flags controls certain features of the decoded output.
|
||||
* Pass `DEFAULT` to decode standard Base64.
|
||||
* @throws IllegalArgumentException if the input contains
|
||||
* incorrect padding
|
||||
*/
|
||||
fun decode(input: ByteArray, flags: Int): ByteArray {
|
||||
return decode(input, 0, input.size, flags)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the Base64-encoded data in input and return the data in
|
||||
* a new byte array.
|
||||
*
|
||||
*
|
||||
* The padding '=' characters at the end are considered optional, but
|
||||
* if any are present, there must be the correct number of them.
|
||||
*
|
||||
* @param input the data to decode
|
||||
* @param offset the position within the input array at which to start
|
||||
* @param len the number of bytes of input to decode
|
||||
* @param flags controls certain features of the decoded output.
|
||||
* Pass `DEFAULT` to decode standard Base64.
|
||||
* @throws IllegalArgumentException if the input contains
|
||||
* incorrect padding
|
||||
*/
|
||||
fun decode(input: ByteArray, offset: Int, len: Int, flags: Int): ByteArray {
|
||||
// Allocate space for the most data the input could represent.
|
||||
// (It could contain less if it contains whitespace, etc.)
|
||||
val decoder = Decoder(flags, ByteArray(len * 3 / 4))
|
||||
|
||||
require(decoder.process(input, offset, len, true)) { "bad base-64" }
|
||||
|
||||
// Maybe we got lucky and allocated exactly enough output space.
|
||||
if (decoder.op == (decoder.output?.size ?: 0)) {
|
||||
return decoder.output?: ByteArray(0)
|
||||
}
|
||||
|
||||
// Need to shorten the array, so allocate a new one of the
|
||||
// right size and copy.
|
||||
val temp = ByteArray(decoder.op)
|
||||
decoder.output?.let { System.arraycopy(it, 0, temp, 0, decoder.op) }
|
||||
return temp
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// encoding
|
||||
// --------------------------------------------------------
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* String with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing `DEFAULT` results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
fun encodeToString(input: ByteArray, flags: Int): String {
|
||||
try {
|
||||
return String(encode(input, flags), charset("US-ASCII"))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
// US-ASCII is guaranteed to be available.
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* String with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param offset the position within the input array at which to
|
||||
* start
|
||||
* @param len the number of bytes of input to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing `DEFAULT` results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
fun encodeToString(input: ByteArray, offset: Int, len: Int, flags: Int): String {
|
||||
try {
|
||||
return String(encode(input, offset, len, flags), charset("US-ASCII"))
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
// US-ASCII is guaranteed to be available.
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* byte[] with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing `DEFAULT` results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
fun encode(input: ByteArray, flags: Int): ByteArray {
|
||||
return encode(input, 0, input.size, flags)
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64-encode the given data and return a newly allocated
|
||||
* byte[] with the result.
|
||||
*
|
||||
* @param input the data to encode
|
||||
* @param offset the position within the input array at which to
|
||||
* start
|
||||
* @param len the number of bytes of input to encode
|
||||
* @param flags controls certain features of the encoded output.
|
||||
* Passing `DEFAULT` results in output that
|
||||
* adheres to RFC 2045.
|
||||
*/
|
||||
fun encode(input: ByteArray, offset: Int, len: Int, flags: Int): ByteArray {
|
||||
val encoder = Encoder(flags, null)
|
||||
|
||||
// Compute the exact length of the array we will produce.
|
||||
var output_len = len / 3 * 4
|
||||
|
||||
// Account for the tail of the data and the padding bytes, if any.
|
||||
if (encoder.do_padding) {
|
||||
if (len % 3 > 0) {
|
||||
output_len += 4
|
||||
}
|
||||
} else {
|
||||
when (len % 3) {
|
||||
0 -> {}
|
||||
1 -> output_len += 2
|
||||
2 -> output_len += 3
|
||||
}
|
||||
}
|
||||
|
||||
// Account for the newlines, if any.
|
||||
if (encoder.do_newline && len > 0) {
|
||||
output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) *
|
||||
(if (encoder.do_cr) 2 else 1)
|
||||
}
|
||||
|
||||
encoder.output = ByteArray(output_len)
|
||||
encoder.process(input, offset, len, true)
|
||||
|
||||
assert(encoder.op == output_len)
|
||||
|
||||
return encoder.output?: ByteArray(0)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// shared code
|
||||
// --------------------------------------------------------
|
||||
/* package */
|
||||
internal abstract class Coder {
|
||||
var output: ByteArray? = null
|
||||
var op: Int = 0
|
||||
|
||||
/**
|
||||
* Encode/decode another block of input data. this.output is
|
||||
* provided by the caller, and must be big enough to hold all
|
||||
* the coded data. On exit, this.opwill be set to the length
|
||||
* of the coded data.
|
||||
*
|
||||
* @param finish true if this is the final call to process for
|
||||
* this object. Will finalize the coder state and
|
||||
* include any final bytes in the output.
|
||||
* @return true if the input so far is good; false if some
|
||||
* error has been detected in the input stream..
|
||||
*/
|
||||
abstract fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean
|
||||
|
||||
/**
|
||||
* @return the maximum number of bytes a call to process()
|
||||
* could produce for the given number of input bytes. This may
|
||||
* be an overestimate.
|
||||
*/
|
||||
abstract fun maxOutputSize(len: Int): Int
|
||||
}
|
||||
|
||||
/* package */
|
||||
internal class Decoder(flags: Int, output: ByteArray?) : Coder() {
|
||||
/**
|
||||
* States 0-3 are reading through the next input tuple.
|
||||
* State 4 is having read one '=' and expecting exactly
|
||||
* one more.
|
||||
* State 5 is expecting no more data or padding characters
|
||||
* in the input.
|
||||
* State 6 is the error state; an error has been detected
|
||||
* in the input and no future input can "fix" it.
|
||||
*/
|
||||
private var state: Int // state number (0 to 6)
|
||||
private var value: Int
|
||||
|
||||
private val alphabet: IntArray
|
||||
|
||||
init {
|
||||
this.output = output
|
||||
|
||||
alphabet = if ((flags and URL_SAFE) == 0) DECODE else DECODE_WEBSAFE
|
||||
state = 0
|
||||
value = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an overestimate for the number of bytes `len` bytes could decode to.
|
||||
*/
|
||||
override fun maxOutputSize(len: Int): Int {
|
||||
return len * 3 / 4 + 10
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode another block of input data.
|
||||
*
|
||||
* @return true if the state machine is still healthy. false if
|
||||
* bad base-64 data has been detected in the input stream.
|
||||
*/
|
||||
override fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean {
|
||||
var len = len
|
||||
if (this.state == 6) return false
|
||||
|
||||
var p = offset
|
||||
len += offset
|
||||
|
||||
// Using local variables makes the decoder about 12%
|
||||
// faster than if we manipulate the member variables in
|
||||
// the loop. (Even alphabet makes a measurable
|
||||
// difference, which is somewhat surprising to me since
|
||||
// the member variable is final.)
|
||||
var state = this.state
|
||||
var value = this.value
|
||||
var op = 0
|
||||
val output = this.output
|
||||
val alphabet = this.alphabet
|
||||
|
||||
while (p < len) {
|
||||
// Try the fast path: we're starting a new tuple and the
|
||||
// next four bytes of the input stream are all data
|
||||
// bytes. This corresponds to going through states
|
||||
// 0-1-2-3-0. We expect to use this method for most of
|
||||
// the data.
|
||||
//
|
||||
// If any of the next four bytes of input are non-data
|
||||
// (whitespace, etc.), value will end up negative. (All
|
||||
// the non-data values in decode are small negative
|
||||
// numbers, so shifting any of them up and or'ing them
|
||||
// together will result in a value with its top bit set.)
|
||||
//
|
||||
// You can remove this whole block and the output should
|
||||
// be the same, just slower.
|
||||
if (state == 0) {
|
||||
while (p + 4 <= len &&
|
||||
(((alphabet[input[p].toInt() and 0xff] shl 18) or
|
||||
(alphabet[input[p + 1].toInt() and 0xff] shl 12) or
|
||||
(alphabet[input[p + 2].toInt() and 0xff] shl 6) or
|
||||
(alphabet[input[p + 3].toInt() and 0xff])).also { value = it }) >= 0
|
||||
) {
|
||||
output?.set(op + 2, value.toByte())
|
||||
output?.set(op + 1, (value shr 8).toByte())
|
||||
output?.set(op, (value shr 16).toByte())
|
||||
op += 3
|
||||
p += 4
|
||||
}
|
||||
if (p >= len) break
|
||||
}
|
||||
|
||||
// The fast path isn't available -- either we've read a
|
||||
// partial tuple, or the next four input bytes aren't all
|
||||
// data, or whatever. Fall back to the slower state
|
||||
// machine implementation.
|
||||
val d = alphabet[input[p++].toInt() and 0xff]
|
||||
|
||||
when (state) {
|
||||
0 -> if (d >= 0) {
|
||||
value = d
|
||||
++state
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6
|
||||
return false
|
||||
}
|
||||
|
||||
1 -> if (d >= 0) {
|
||||
value = (value shl 6) or d
|
||||
++state
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6
|
||||
return false
|
||||
}
|
||||
|
||||
2 -> if (d >= 0) {
|
||||
value = (value shl 6) or d
|
||||
++state
|
||||
} else if (d == EQUALS) {
|
||||
// Emit the last (partial) output tuple;
|
||||
// expect exactly one more padding character.
|
||||
output?.set(op++, (value shr 4).toByte())
|
||||
state = 4
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6
|
||||
return false
|
||||
}
|
||||
|
||||
3 -> if (d >= 0) {
|
||||
// Emit the output triple and return to state 0.
|
||||
value = (value shl 6) or d
|
||||
output?.set(op + 2, value.toByte())
|
||||
output?.set(op + 1, (value shr 8).toByte())
|
||||
output?.set(op, (value shr 16).toByte())
|
||||
op += 3
|
||||
state = 0
|
||||
} else if (d == EQUALS) {
|
||||
// Emit the last (partial) output tuple;
|
||||
// expect no further data or padding characters.
|
||||
output?.set(op + 1, (value shr 2).toByte())
|
||||
output?.set(op, (value shr 10).toByte())
|
||||
op += 2
|
||||
state = 5
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6
|
||||
return false
|
||||
}
|
||||
|
||||
4 -> if (d == EQUALS) {
|
||||
++state
|
||||
} else if (d != SKIP) {
|
||||
this.state = 6
|
||||
return false
|
||||
}
|
||||
|
||||
5 -> if (d != SKIP) {
|
||||
this.state = 6
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!finish) {
|
||||
// We're out of input, but a future call could provide
|
||||
// more.
|
||||
this.state = state
|
||||
this.value = value
|
||||
this.op = op
|
||||
return true
|
||||
}
|
||||
|
||||
// Done reading input. Now figure out where we are left in
|
||||
// the state machine and finish up.
|
||||
when (state) {
|
||||
0 -> {}
|
||||
1 -> {
|
||||
// Read one extra input byte, which isn't enough to
|
||||
// make another output byte. Illegal.
|
||||
this.state = 6
|
||||
return false
|
||||
}
|
||||
|
||||
2 -> // Read two extra input bytes, enough to emit 1 more
|
||||
// output byte. Fine.
|
||||
output?.set(op++, (value shr 4).toByte())
|
||||
|
||||
3 -> {
|
||||
// Read three extra input bytes, enough to emit 2 more
|
||||
// output bytes. Fine.
|
||||
output?.set(op++, (value shr 10).toByte())
|
||||
output?.set(op++, (value shr 2).toByte())
|
||||
}
|
||||
|
||||
4 -> {
|
||||
// Read one padding '=' when we expected 2. Illegal.
|
||||
this.state = 6
|
||||
return false
|
||||
}
|
||||
|
||||
5 -> {}
|
||||
}
|
||||
|
||||
this.state = state
|
||||
this.op = op
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Lookup table for turning bytes into their position in the
|
||||
* Base64 alphabet.
|
||||
*/
|
||||
private val DECODE = intArrayOf(
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
|
||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
|
||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
)
|
||||
|
||||
/**
|
||||
* Decode lookup table for the "web safe" variant (RFC 3548
|
||||
* sec. 4) where - and _ replace + and /.
|
||||
*/
|
||||
private val DECODE_WEBSAFE = intArrayOf(
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
|
||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
|
||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
)
|
||||
|
||||
/**
|
||||
* Non-data values in the DECODE arrays.
|
||||
*/
|
||||
private const val SKIP = -1
|
||||
private const val EQUALS = -2
|
||||
}
|
||||
}
|
||||
|
||||
/* package */
|
||||
internal class Encoder(flags: Int, output: ByteArray?) : Coder() {
|
||||
private val tail: ByteArray
|
||||
|
||||
/* package */
|
||||
var tailLen: Int
|
||||
private var count: Int
|
||||
|
||||
val do_padding: Boolean
|
||||
val do_newline: Boolean
|
||||
val do_cr: Boolean
|
||||
private val alphabet: ByteArray
|
||||
|
||||
init {
|
||||
this.output = output
|
||||
|
||||
do_padding = (flags and NO_PADDING) == 0
|
||||
do_newline = (flags and NO_WRAP) == 0
|
||||
do_cr = (flags and CRLF) != 0
|
||||
alphabet = if ((flags and URL_SAFE) == 0) ENCODE else ENCODE_WEBSAFE
|
||||
|
||||
tail = ByteArray(2)
|
||||
tailLen = 0
|
||||
|
||||
count = if (do_newline) LINE_GROUPS else -1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an overestimate for the number of bytes `len` bytes could encode to.
|
||||
*/
|
||||
override fun maxOutputSize(len: Int): Int {
|
||||
return len * 8 / 5 + 10
|
||||
}
|
||||
|
||||
override fun process(input: ByteArray, offset: Int, len: Int, finish: Boolean): Boolean {
|
||||
// Using local variables makes the encoder about 9% faster.
|
||||
var len = len
|
||||
val alphabet = this.alphabet
|
||||
val output = this.output
|
||||
var op = 0
|
||||
var count = this.count
|
||||
|
||||
var p = offset
|
||||
len += offset
|
||||
var v = -1
|
||||
|
||||
// First we need to concatenate the tail of the previous call
|
||||
// with any input bytes available now and see if we can empty
|
||||
// the tail.
|
||||
when (tailLen) {
|
||||
0 -> {}
|
||||
1 -> {
|
||||
if (p + 2 <= len) {
|
||||
// A 1-byte tail with at least 2 bytes of
|
||||
// input available now.
|
||||
v = ((tail[0].toInt() and 0xff) shl 16) or
|
||||
((input[p++].toInt() and 0xff) shl 8) or
|
||||
(input[p++].toInt() and 0xff)
|
||||
tailLen = 0
|
||||
}
|
||||
}
|
||||
|
||||
2 -> if (p + 1 <= len) {
|
||||
// A 2-byte tail with at least 1 byte of input.
|
||||
v = ((tail[0].toInt() and 0xff) shl 16) or
|
||||
((tail[1].toInt() and 0xff) shl 8) or
|
||||
(input[p++].toInt() and 0xff)
|
||||
tailLen = 0
|
||||
}
|
||||
}
|
||||
|
||||
if (v != -1) {
|
||||
output?.set(op++, alphabet[(v shr 18) and 0x3f])
|
||||
output?.set(op++, alphabet[(v shr 12) and 0x3f])
|
||||
output?.set(op++, alphabet[(v shr 6) and 0x3f])
|
||||
output?.set(op++, alphabet[v and 0x3f])
|
||||
if (--count == 0) {
|
||||
if (do_cr) output?.set(op++, '\r'.code.toByte())
|
||||
output?.set(op++, '\n'.code.toByte())
|
||||
count = LINE_GROUPS
|
||||
}
|
||||
}
|
||||
|
||||
// At this point either there is no tail, or there are fewer
|
||||
// than 3 bytes of input available.
|
||||
|
||||
// The main loop, turning 3 input bytes into 4 output bytes on
|
||||
// each iteration.
|
||||
while (p + 3 <= len) {
|
||||
v = ((input[p].toInt() and 0xff) shl 16) or
|
||||
((input[p + 1].toInt() and 0xff) shl 8) or
|
||||
(input[p + 2].toInt() and 0xff)
|
||||
output?.set(op, alphabet[(v shr 18) and 0x3f])
|
||||
output?.set(op + 1, alphabet[(v shr 12) and 0x3f])
|
||||
output?.set(op + 2, alphabet[(v shr 6) and 0x3f])
|
||||
output?.set(op + 3, alphabet[v and 0x3f])
|
||||
p += 3
|
||||
op += 4
|
||||
if (--count == 0) {
|
||||
if (do_cr) output?.set(op++, '\r'.code.toByte())
|
||||
output?.set(op++, '\n'.code.toByte())
|
||||
count = LINE_GROUPS
|
||||
}
|
||||
}
|
||||
|
||||
if (finish) {
|
||||
// Finish up the tail of the input. Note that we need to
|
||||
// consume any bytes in tail before any bytes
|
||||
// remaining in input; there should be at most two bytes
|
||||
// total.
|
||||
|
||||
if (p - tailLen == len - 1) {
|
||||
var t = 0
|
||||
v = ((if (tailLen > 0) tail[t++] else input[p++]).toInt() and 0xff) shl 4
|
||||
tailLen -= t
|
||||
output?.set(op++, alphabet[(v shr 6) and 0x3f])
|
||||
output?.set(op++, alphabet[v and 0x3f])
|
||||
if (do_padding) {
|
||||
output?.set(op++, '='.code.toByte())
|
||||
output?.set(op++, '='.code.toByte())
|
||||
}
|
||||
if (do_newline) {
|
||||
if (do_cr) output?.set(op++, '\r'.code.toByte())
|
||||
output?.set(op++, '\n'.code.toByte())
|
||||
}
|
||||
} else if (p - tailLen == len - 2) {
|
||||
var t = 0
|
||||
v = (((if (tailLen > 1) tail[t++] else input[p++]).toInt() and 0xff) shl 10) or
|
||||
(((if (tailLen > 0) tail[t++] else input[p++]).toInt() and 0xff) shl 2)
|
||||
tailLen -= t
|
||||
output?.set(op++, alphabet[(v shr 12) and 0x3f])
|
||||
output?.set(op++, alphabet[(v shr 6) and 0x3f])
|
||||
output?.set(op++, alphabet[v and 0x3f])
|
||||
if (do_padding) {
|
||||
output?.set(op++, '='.code.toByte())
|
||||
}
|
||||
if (do_newline) {
|
||||
if (do_cr) output?.set(op++, '\r'.code.toByte())
|
||||
output?.set(op++, '\n'.code.toByte())
|
||||
}
|
||||
} else if (do_newline && op > 0 && count != LINE_GROUPS) {
|
||||
if (do_cr) output?.set(op++, '\r'.code.toByte())
|
||||
output?.set(op++, '\n'.code.toByte())
|
||||
}
|
||||
|
||||
assert(tailLen == 0)
|
||||
assert(p == len)
|
||||
} else {
|
||||
// Save the leftovers in tail to be consumed on the next
|
||||
// call to encodeInternal.
|
||||
|
||||
if (p == len - 1) {
|
||||
tail[tailLen++] = input[p]
|
||||
} else if (p == len - 2) {
|
||||
tail[tailLen++] = input[p]
|
||||
tail[tailLen++] = input[p + 1]
|
||||
}
|
||||
}
|
||||
|
||||
this.op = op
|
||||
this.count = count
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Emit a new line every this many output tuples. Corresponds to
|
||||
* a 76-character line length (the maximum allowable according to
|
||||
* [RFC 2045](http://www.ietf.org/rfc/rfc2045.txt)).
|
||||
*/
|
||||
const val LINE_GROUPS: Int = 19
|
||||
|
||||
/**
|
||||
* Lookup table for turning Base64 alphabet positions (6 bits)
|
||||
* into output bytes.
|
||||
*/
|
||||
private val ENCODE = byteArrayOf(
|
||||
'A'.code.toByte(),
|
||||
'B'.code.toByte(),
|
||||
'C'.code.toByte(),
|
||||
'D'.code.toByte(),
|
||||
'E'.code.toByte(),
|
||||
'F'.code.toByte(),
|
||||
'G'.code.toByte(),
|
||||
'H'.code.toByte(),
|
||||
'I'.code.toByte(),
|
||||
'J'.code.toByte(),
|
||||
'K'.code.toByte(),
|
||||
'L'.code.toByte(),
|
||||
'M'.code.toByte(),
|
||||
'N'.code.toByte(),
|
||||
'O'.code.toByte(),
|
||||
'P'.code.toByte(),
|
||||
'Q'.code.toByte(),
|
||||
'R'.code.toByte(),
|
||||
'S'.code.toByte(),
|
||||
'T'.code.toByte(),
|
||||
'U'.code.toByte(),
|
||||
'V'.code.toByte(),
|
||||
'W'.code.toByte(),
|
||||
'X'.code.toByte(),
|
||||
'Y'.code.toByte(),
|
||||
'Z'.code.toByte(),
|
||||
'a'.code.toByte(),
|
||||
'b'.code.toByte(),
|
||||
'c'.code.toByte(),
|
||||
'd'.code.toByte(),
|
||||
'e'.code.toByte(),
|
||||
'f'.code.toByte(),
|
||||
'g'.code.toByte(),
|
||||
'h'.code.toByte(),
|
||||
'i'.code.toByte(),
|
||||
'j'.code.toByte(),
|
||||
'k'.code.toByte(),
|
||||
'l'.code.toByte(),
|
||||
'm'.code.toByte(),
|
||||
'n'.code.toByte(),
|
||||
'o'.code.toByte(),
|
||||
'p'.code.toByte(),
|
||||
'q'.code.toByte(),
|
||||
'r'.code.toByte(),
|
||||
's'.code.toByte(),
|
||||
't'.code.toByte(),
|
||||
'u'.code.toByte(),
|
||||
'v'.code.toByte(),
|
||||
'w'.code.toByte(),
|
||||
'x'.code.toByte(),
|
||||
'y'.code.toByte(),
|
||||
'z'.code.toByte(),
|
||||
'0'.code.toByte(),
|
||||
'1'.code.toByte(),
|
||||
'2'.code.toByte(),
|
||||
'3'.code.toByte(),
|
||||
'4'.code.toByte(),
|
||||
'5'.code.toByte(),
|
||||
'6'.code.toByte(),
|
||||
'7'.code.toByte(),
|
||||
'8'.code.toByte(),
|
||||
'9'.code.toByte(),
|
||||
'+'.code.toByte(),
|
||||
'/'.code.toByte(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Lookup table for turning Base64 alphabet positions (6 bits)
|
||||
* into output bytes.
|
||||
*/
|
||||
private val ENCODE_WEBSAFE = byteArrayOf(
|
||||
'A'.code.toByte(),
|
||||
'B'.code.toByte(),
|
||||
'C'.code.toByte(),
|
||||
'D'.code.toByte(),
|
||||
'E'.code.toByte(),
|
||||
'F'.code.toByte(),
|
||||
'G'.code.toByte(),
|
||||
'H'.code.toByte(),
|
||||
'I'.code.toByte(),
|
||||
'J'.code.toByte(),
|
||||
'K'.code.toByte(),
|
||||
'L'.code.toByte(),
|
||||
'M'.code.toByte(),
|
||||
'N'.code.toByte(),
|
||||
'O'.code.toByte(),
|
||||
'P'.code.toByte(),
|
||||
'Q'.code.toByte(),
|
||||
'R'.code.toByte(),
|
||||
'S'.code.toByte(),
|
||||
'T'.code.toByte(),
|
||||
'U'.code.toByte(),
|
||||
'V'.code.toByte(),
|
||||
'W'.code.toByte(),
|
||||
'X'.code.toByte(),
|
||||
'Y'.code.toByte(),
|
||||
'Z'.code.toByte(),
|
||||
'a'.code.toByte(),
|
||||
'b'.code.toByte(),
|
||||
'c'.code.toByte(),
|
||||
'd'.code.toByte(),
|
||||
'e'.code.toByte(),
|
||||
'f'.code.toByte(),
|
||||
'g'.code.toByte(),
|
||||
'h'.code.toByte(),
|
||||
'i'.code.toByte(),
|
||||
'j'.code.toByte(),
|
||||
'k'.code.toByte(),
|
||||
'l'.code.toByte(),
|
||||
'm'.code.toByte(),
|
||||
'n'.code.toByte(),
|
||||
'o'.code.toByte(),
|
||||
'p'.code.toByte(),
|
||||
'q'.code.toByte(),
|
||||
'r'.code.toByte(),
|
||||
's'.code.toByte(),
|
||||
't'.code.toByte(),
|
||||
'u'.code.toByte(),
|
||||
'v'.code.toByte(),
|
||||
'w'.code.toByte(),
|
||||
'x'.code.toByte(),
|
||||
'y'.code.toByte(),
|
||||
'z'.code.toByte(),
|
||||
'0'.code.toByte(),
|
||||
'1'.code.toByte(),
|
||||
'2'.code.toByte(),
|
||||
'3'.code.toByte(),
|
||||
'4'.code.toByte(),
|
||||
'5'.code.toByte(),
|
||||
'6'.code.toByte(),
|
||||
'7'.code.toByte(),
|
||||
'8'.code.toByte(),
|
||||
'9'.code.toByte(),
|
||||
'-'.code.toByte(),
|
||||
'_'.code.toByte(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.android.grape.pseudo
|
||||
|
||||
internal object Constants {
|
||||
const val LINE_ENDING: String = "\r\n"
|
||||
val GENERATOR_NAME: String =
|
||||
java.lang.String.format("PseudoApkSigner %s", "1.0")
|
||||
const val UTF8: String = "UTF-8"
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package com.android.grape.pseudo
|
||||
|
||||
|
||||
internal class ManifestBuilder {
|
||||
private val mEntries: ArrayList<ManifestEntry>
|
||||
|
||||
private var mVersion: Long = 0
|
||||
private var mCachedManifest: String = ""
|
||||
private var mCachedVersion: Long = -1
|
||||
|
||||
init {
|
||||
mEntries = ArrayList()
|
||||
}
|
||||
|
||||
fun build(): String {
|
||||
if (mVersion == mCachedVersion) return mCachedManifest
|
||||
|
||||
val stringBuilder = StringBuilder()
|
||||
|
||||
stringBuilder.append(generateHeader().toString())
|
||||
for (entry in mEntries) {
|
||||
stringBuilder.append(entry.toString())
|
||||
}
|
||||
|
||||
mCachedVersion = mVersion
|
||||
mCachedManifest = stringBuilder.toString()
|
||||
|
||||
return mCachedManifest
|
||||
}
|
||||
|
||||
private fun generateHeader(): ManifestEntry {
|
||||
val header = ManifestEntry()
|
||||
header.setAttribute("Manifest-Version", "1.0")
|
||||
header.setAttribute("Created-By", Constants.GENERATOR_NAME)
|
||||
return header
|
||||
}
|
||||
|
||||
internal class ManifestEntry {
|
||||
private val mAttributes = LinkedHashMap<String, String>()
|
||||
|
||||
fun setAttribute(attribute: String, value: String) {
|
||||
mAttributes[attribute] = value
|
||||
}
|
||||
|
||||
fun getAttribute(attribute: String): String? {
|
||||
return mAttributes[attribute]
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
|
||||
for (key in mAttributes.keys) stringBuilder.append(
|
||||
String.format(
|
||||
"%s: %s" + Constants.LINE_ENDING, key,
|
||||
mAttributes[key]
|
||||
)
|
||||
)
|
||||
stringBuilder.append(Constants.LINE_ENDING)
|
||||
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
fun addEntry(entry: ManifestEntry) {
|
||||
mEntries.add(entry)
|
||||
mVersion++
|
||||
}
|
||||
|
||||
val entries: List<ManifestEntry>
|
||||
get() = mEntries
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package com.android.grape.pseudo
|
||||
|
||||
import android.provider.SyncStateContract
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.DigestInputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.interfaces.RSAPrivateKey
|
||||
import java.util.Locale
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
|
||||
class PseudoApkSigner(private val mTemplateFile: File, privateKey: File) {
|
||||
private val mPrivateKey: RSAPrivateKey = Utils.readPrivateKey(privateKey)
|
||||
|
||||
private var mSignerName = "CERT"
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun sign(apkFile: File, output: File) {
|
||||
sign(FileInputStream(apkFile), FileOutputStream(output))
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun sign(apkInputStream: InputStream, output: OutputStream) {
|
||||
val manifest: ManifestBuilder = ManifestBuilder()
|
||||
val signature: SignatureFileGenerator = SignatureFileGenerator(manifest, HASHING_ALGORITHM)
|
||||
|
||||
val apkZipInputStream = ZipInputStream(apkInputStream)
|
||||
|
||||
val zipOutputStream: ZipAlignZipOutputStream = ZipAlignZipOutputStream.create(output, 4)
|
||||
val messageDigest = MessageDigest.getInstance(HASHING_ALGORITHM)
|
||||
var zipEntry: ZipEntry
|
||||
OUTER@ while ((apkZipInputStream.nextEntry.also { zipEntry = it }) != null) {
|
||||
if (zipEntry.isDirectory) continue
|
||||
|
||||
if (zipEntry.name.lowercase(Locale.getDefault()).startsWith("meta-inf/")) {
|
||||
for (fileToSkipEnding in META_INF_FILES_TO_SKIP_ENDINGS) {
|
||||
if (zipEntry.name.lowercase(Locale.getDefault()).endsWith(
|
||||
fileToSkipEnding
|
||||
)
|
||||
) continue@OUTER
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
messageDigest.reset()
|
||||
val entryInputStream = DigestInputStream(apkZipInputStream, messageDigest)
|
||||
|
||||
val newZipEntry = ZipEntry(zipEntry.name)
|
||||
newZipEntry.method = zipEntry.method
|
||||
if (zipEntry.method == ZipEntry.STORED) {
|
||||
newZipEntry.size = zipEntry.size
|
||||
newZipEntry.compressedSize = zipEntry.size
|
||||
newZipEntry.crc = zipEntry.crc
|
||||
}
|
||||
|
||||
zipOutputStream.alignment = if (newZipEntry.name.endsWith(".so")) 4096 else 4
|
||||
zipOutputStream.putNextEntry(newZipEntry)
|
||||
Utils.copyStream(entryInputStream, zipOutputStream)
|
||||
zipOutputStream.closeEntry()
|
||||
apkZipInputStream.closeEntry()
|
||||
|
||||
val manifestEntry: ManifestBuilder.ManifestEntry = ManifestBuilder.ManifestEntry()
|
||||
manifestEntry.setAttribute("Name", zipEntry.name)
|
||||
manifestEntry.setAttribute(
|
||||
HASHING_ALGORITHM + "-Digest",
|
||||
Utils.base64Encode(messageDigest.digest())
|
||||
)
|
||||
manifest.addEntry(manifestEntry)
|
||||
}
|
||||
|
||||
zipOutputStream.putNextEntry(ZipEntry("META-INF/MANIFEST.MF"))
|
||||
zipOutputStream.write(manifest.build().toByteArray())
|
||||
zipOutputStream.closeEntry()
|
||||
|
||||
zipOutputStream.putNextEntry(ZipEntry(String.format("META-INF/%s.SF", mSignerName)))
|
||||
zipOutputStream.write(signature.generate().toByteArray())
|
||||
zipOutputStream.closeEntry()
|
||||
|
||||
zipOutputStream.putNextEntry(ZipEntry(String.format("META-INF/%s.RSA", mSignerName)))
|
||||
zipOutputStream.write(Utils.readFile(mTemplateFile))
|
||||
zipOutputStream.write(
|
||||
Utils.sign(
|
||||
HASHING_ALGORITHM,
|
||||
mPrivateKey,
|
||||
signature.generate().toByteArray()
|
||||
)
|
||||
)
|
||||
zipOutputStream.closeEntry()
|
||||
|
||||
apkZipInputStream.close()
|
||||
zipOutputStream.close()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets name of the .SF and .RSA file in META-INF
|
||||
*
|
||||
* @param signerName desired .SF and .RSA files name
|
||||
*/
|
||||
fun setSignerName(signerName: String) {
|
||||
mSignerName = signerName
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val META_INF_FILES_TO_SKIP_ENDINGS =
|
||||
arrayOf("manifest.mf", ".sf", ".rsa", ".dsa", ".ec")
|
||||
private const val HASHING_ALGORITHM = "SHA1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package com.android.grape.pseudo
|
||||
|
||||
import com.android.grape.pseudo.Utils.base64Encode
|
||||
import com.android.grape.pseudo.Utils.hash
|
||||
|
||||
internal class SignatureFileGenerator(
|
||||
private val mManifest: ManifestBuilder,
|
||||
private val mHashingAlgorithm: String
|
||||
) {
|
||||
@Throws(Exception::class)
|
||||
fun generate(): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
stringBuilder.append(generateHeader().toString())
|
||||
|
||||
for (manifestEntry in mManifest.entries) {
|
||||
val sfEntry = ManifestBuilder.ManifestEntry()
|
||||
sfEntry.setAttribute("Name", manifestEntry.getAttribute("Name")!!)
|
||||
sfEntry.setAttribute(
|
||||
"$mHashingAlgorithm-Digest", base64Encode(
|
||||
hash(
|
||||
manifestEntry.toString().toByteArray(
|
||||
charset(Constants.UTF8)
|
||||
), mHashingAlgorithm
|
||||
)
|
||||
)
|
||||
)
|
||||
stringBuilder.append(sfEntry.toString())
|
||||
}
|
||||
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun generateHeader(): ManifestBuilder.ManifestEntry {
|
||||
val header = ManifestBuilder.ManifestEntry()
|
||||
header.setAttribute("Signature-Version", "1.0")
|
||||
header.setAttribute("Created-By", Constants.GENERATOR_NAME)
|
||||
header.setAttribute(
|
||||
"$mHashingAlgorithm-Digest-Manifest", base64Encode(
|
||||
hash(
|
||||
mManifest.build()!!.toByteArray(charset(Constants.UTF8)), mHashingAlgorithm
|
||||
)
|
||||
)
|
||||
)
|
||||
return header
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package com.android.grape.pseudo
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.KeyFactory
|
||||
import java.security.MessageDigest
|
||||
import java.security.PrivateKey
|
||||
import java.security.Signature
|
||||
import java.security.interfaces.RSAPrivateKey
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
|
||||
|
||||
object Utils {
|
||||
@Throws(Exception::class)
|
||||
fun getFileHash(file: File?, hashingAlgorithm: String): ByteArray {
|
||||
return getFileHash(FileInputStream(file), hashingAlgorithm)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getFileHash(fileInputStream: InputStream, hashingAlgorithm: String): ByteArray {
|
||||
val messageDigest = MessageDigest.getInstance(hashingAlgorithm)
|
||||
|
||||
val buffer = ByteArray(1024 * 1024)
|
||||
|
||||
var read: Int
|
||||
while ((fileInputStream.read(buffer).also { read = it }) > 0) messageDigest.update(
|
||||
buffer,
|
||||
0,
|
||||
read
|
||||
)
|
||||
|
||||
fileInputStream.close()
|
||||
|
||||
return messageDigest.digest()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun hash(bytes: ByteArray, hashingAlgorithm: String): ByteArray {
|
||||
val messageDigest = MessageDigest.getInstance(hashingAlgorithm)
|
||||
messageDigest.update(bytes)
|
||||
return messageDigest.digest()
|
||||
}
|
||||
|
||||
fun base64Encode(bytes: ByteArray): String {
|
||||
return Base64.encodeToString(bytes, 0)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun copyStream(from: InputStream, to: OutputStream) {
|
||||
val buf = ByteArray(1024 * 1024)
|
||||
var len: Int
|
||||
while ((from.read(buf).also { len = it }) > 0) {
|
||||
to.write(buf, 0, len)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun sign(hashingAlgorithm: String, privateKey: PrivateKey?, message: ByteArray?): ByteArray {
|
||||
val sign = Signature.getInstance(hashingAlgorithm + "withRSA")
|
||||
sign.initSign(privateKey)
|
||||
sign.update(message)
|
||||
return sign.sign()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun readPrivateKey(file: File): RSAPrivateKey {
|
||||
val keySpec = PKCS8EncodedKeySpec(readFile(file))
|
||||
return KeyFactory.getInstance("RSA").generatePrivate(keySpec) as RSAPrivateKey
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun readFile(file: File): ByteArray {
|
||||
val fileBytes = ByteArray(file.length().toInt())
|
||||
|
||||
val inputStream = FileInputStream(file)
|
||||
inputStream.read(fileBytes)
|
||||
inputStream.close()
|
||||
|
||||
return fileBytes
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package com.android.grape.pseudo
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
|
||||
class ZipAlignZipOutputStream private constructor(
|
||||
outputStream: BytesCounterOutputStream,
|
||||
var alignment: Int
|
||||
) :
|
||||
ZipOutputStream(outputStream) {
|
||||
private var mBytesCounter: BytesCounterOutputStream? = null
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun putNextEntry(zipEntry: ZipEntry) {
|
||||
if (zipEntry.method == ZipEntry.STORED) {
|
||||
var headerSize = 30
|
||||
headerSize += zipEntry.name.toByteArray().size
|
||||
|
||||
val temp = ((mBytesCounter?.bytesWritten?.plus(headerSize))?.rem(alignment))?:0
|
||||
val requiredPadding =
|
||||
(alignment - temp).toInt()
|
||||
zipEntry.extra = ByteArray(requiredPadding)
|
||||
}
|
||||
|
||||
super.putNextEntry(zipEntry)
|
||||
}
|
||||
|
||||
private class BytesCounterOutputStream(private val mWrappedOutputStream: OutputStream) :
|
||||
OutputStream() {
|
||||
var bytesWritten: Long = 0
|
||||
private set
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(b: ByteArray) {
|
||||
mWrappedOutputStream.write(b)
|
||||
bytesWritten += b.size.toLong()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(b: Int) {
|
||||
mWrappedOutputStream.write(b)
|
||||
bytesWritten++
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(b: ByteArray, off: Int, len: Int) {
|
||||
mWrappedOutputStream.write(b, off, len)
|
||||
bytesWritten += len.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(outputStream: OutputStream, alignment: Int): ZipAlignZipOutputStream {
|
||||
val bytesCounterOutputStream = BytesCounterOutputStream(outputStream)
|
||||
val zipAlignZipOutputStream =
|
||||
ZipAlignZipOutputStream(bytesCounterOutputStream, alignment)
|
||||
zipAlignZipOutputStream.mBytesCounter = bytesCounterOutputStream
|
||||
return zipAlignZipOutputStream
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package com.android.grape.sai
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import com.android.grape.sai.inter.SaiPackageInstaller
|
||||
import com.android.grape.sai.inter.SaiPiSessionObserver
|
||||
import com.android.grape.sai.param.SaiPiSessionParams
|
||||
import com.android.grape.sai.param.SaiPiSessionState
|
||||
import com.android.grape.sai.param.SaiPiSessionStatus
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentSkipListMap
|
||||
|
||||
@SuppressLint("UseSparseArrays")
|
||||
abstract class BaseSaiPackageInstaller protected constructor(c: Context) :
|
||||
SaiPackageInstaller {
|
||||
protected val context: Context = c.applicationContext
|
||||
private var mLastSessionId: Long = 0
|
||||
|
||||
private val mCreatedSessions: ConcurrentHashMap<String, SaiPiSessionParams> =
|
||||
ConcurrentHashMap<String, SaiPiSessionParams>()
|
||||
|
||||
private val mSessionStates: ConcurrentSkipListMap<String, SaiPiSessionState> =
|
||||
ConcurrentSkipListMap<String, SaiPiSessionState>()
|
||||
|
||||
private val mObservers: MutableSet<SaiPiSessionObserver> =
|
||||
Collections.newSetFromMap<SaiPiSessionObserver>(
|
||||
ConcurrentHashMap<SaiPiSessionObserver, Boolean>()
|
||||
)
|
||||
|
||||
override fun createSession(params: SaiPiSessionParams): String {
|
||||
val sessionId = newSessionId()
|
||||
mCreatedSessions[sessionId] = params
|
||||
setSessionState(sessionId, SaiPiSessionState.Builder(sessionId, SaiPiSessionStatus.CREATED).build())
|
||||
return sessionId
|
||||
}
|
||||
|
||||
override fun registerSessionObserver(observer: SaiPiSessionObserver) {
|
||||
mObservers.add(observer)
|
||||
}
|
||||
|
||||
override fun unregisterSessionObserver(observer: SaiPiSessionObserver) {
|
||||
mObservers.remove(observer)
|
||||
}
|
||||
|
||||
override fun getSessions():List<SaiPiSessionState> {
|
||||
return Collections.unmodifiableList<SaiPiSessionState>(
|
||||
ArrayList<SaiPiSessionState>(
|
||||
mSessionStates.values
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
protected fun setSessionState(sessionId: String, state: SaiPiSessionState) {
|
||||
LogUtils.d(
|
||||
tag(),
|
||||
String.format("%s->setSessionState(%s, %s)", javaClass.simpleName, sessionId, state)
|
||||
)
|
||||
mSessionStates[sessionId] = state
|
||||
Utils.onMainThread {
|
||||
for (observer in mObservers) observer.onSessionStateChanged(state)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun takeCreatedSession(sessionId: String): SaiPiSessionParams? {
|
||||
return mCreatedSessions.remove(sessionId)
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
protected fun newSessionId(): String {
|
||||
val sessionId = mLastSessionId++
|
||||
return String.format("%d@%s", sessionId, javaClass.name)
|
||||
}
|
||||
|
||||
protected abstract fun tag(): String?
|
||||
}
|
|
@ -0,0 +1,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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`)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package com.android.grape.sai
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
|
||||
|
||||
/**
|
||||
* @author litianxiang
|
||||
* @description:
|
||||
* @date :2021/6/21 18:23
|
||||
*/
|
||||
class MyBroadcastReceiver : BroadcastReceiver() {
|
||||
private var mReceiver: MyReceiver? = null
|
||||
private var fruit: String? = null
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
//接收广播消息
|
||||
fruit = intent.getStringExtra("fruit")
|
||||
//调用接口MyReceiver里面的interFruit方法传入接收的内容
|
||||
mReceiver?.interFruit(fruit)
|
||||
//使用Toast显示广播消息
|
||||
Toast.makeText(context, fruit, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
//创建一个接口把接收到的广播内容传递回MainActivity
|
||||
interface MyReceiver {
|
||||
fun interFruit(fruit: String?)
|
||||
}
|
||||
|
||||
fun MyThis(mr: MyReceiver?) {
|
||||
mReceiver = mr
|
||||
}
|
||||
|
||||
|
||||
fun sendSuccess(intent: Intent, context: Context) {
|
||||
// 发送广播
|
||||
var intent = intent
|
||||
intent = Intent("myBroadCast")
|
||||
//android版本为8以上的,静态声明广播注册需要设置包名
|
||||
intent.setPackage("com.aefyr.sai.fdroid")
|
||||
intent.putExtra("fruit", "Installation success")
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
fun sendFailed(intent: Intent, context: Context) {
|
||||
// 发送广播
|
||||
|
||||
var intent = intent
|
||||
intent = Intent("myBroadCast")
|
||||
//android版本为8以上的,静态声明广播注册需要设置包名
|
||||
intent.setPackage("com.aefyr.sai.fdroid")
|
||||
intent.putExtra("fruit", "Installation failed")
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package com.android.grape.sai
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import com.android.grape.MainApplication
|
||||
import com.android.grape.R
|
||||
import com.android.grape.sai.shell.Shell
|
||||
import com.android.grape.sai.shell.SuShell
|
||||
|
||||
|
||||
class RootedSaiPackageInstaller private constructor(c: Context) :
|
||||
ShellSaiPackageInstaller(c) {
|
||||
init {
|
||||
sInstance = this
|
||||
}
|
||||
|
||||
override val shell: Shell
|
||||
get() = SuShell.instance
|
||||
|
||||
override val installerName: String
|
||||
get() = "Rooted"
|
||||
|
||||
override val shellUnavailableMessage: String
|
||||
get() = MainApplication.instance.getString(R.string.installer_error_root_no_root)
|
||||
|
||||
override fun tag(): String {
|
||||
return "RootedSaiPi"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private var sInstance: RootedSaiPackageInstaller? = null
|
||||
|
||||
fun getInstance(c: Context): RootedSaiPackageInstaller {
|
||||
synchronized(RootedSaiPackageInstaller::class.java) {
|
||||
return sInstance
|
||||
?: RootedSaiPackageInstaller(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
package com.android.grape.sai
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.android.grape.R
|
||||
import com.android.grape.sai.rootless.AndroidPackageInstallerError
|
||||
import com.android.grape.sai.rootless.ConfirmationIntentWrapperActivity2
|
||||
|
||||
class RootlessSaiPiBroadcastReceiver(c: Context) : BroadcastReceiver() {
|
||||
private val mContext: Context = c.applicationContext
|
||||
|
||||
private val mObservers = HashSet<EventObserver>()
|
||||
|
||||
private var myBroadcastReceiver: MyBroadcastReceiver? = null
|
||||
|
||||
fun addEventObserver(observer: EventObserver) {
|
||||
mObservers.add(observer)
|
||||
}
|
||||
|
||||
fun removeEventObserver(observer: EventObserver) {
|
||||
mObservers.remove(observer)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
|
||||
when (status) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
Log.d(TAG, "Requesting user confirmation for installation")
|
||||
dispatchOnConfirmationPending(
|
||||
intent.getIntExtra(
|
||||
PackageInstaller.EXTRA_SESSION_ID,
|
||||
-1
|
||||
), intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||
)
|
||||
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||
|
||||
ConfirmationIntentWrapperActivity2.start(
|
||||
context,
|
||||
intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1),
|
||||
confirmationIntent
|
||||
)
|
||||
}
|
||||
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
Log.d(TAG, "Installation succeed")
|
||||
dispatchOnInstallationSucceeded(
|
||||
intent.getIntExtra(
|
||||
PackageInstaller.EXTRA_SESSION_ID,
|
||||
-1
|
||||
), intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||
)
|
||||
myBroadcastReceiver = MyBroadcastReceiver().apply {
|
||||
sendSuccess(intent, context)
|
||||
}
|
||||
Toast.makeText(context, "Installation succeed", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.d(TAG, "Installation failed")
|
||||
dispatchOnInstallationFailed(
|
||||
intent.getIntExtra(
|
||||
PackageInstaller.EXTRA_SESSION_ID,
|
||||
-1
|
||||
), parseError(intent), getRawError(intent), null
|
||||
)
|
||||
myBroadcastReceiver = MyBroadcastReceiver().apply {
|
||||
sendFailed(intent, context)
|
||||
}
|
||||
Toast.makeText(context, "Installation failed", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchOnConfirmationPending(sessionId: Int, packageName: String?) {
|
||||
for (observer in mObservers) observer.onConfirmationPending(sessionId, packageName)
|
||||
}
|
||||
|
||||
private fun dispatchOnInstallationSucceeded(sessionId: Int, packageName: String?) {
|
||||
for (observer in mObservers) observer.onInstallationSucceeded(sessionId, packageName)
|
||||
}
|
||||
|
||||
private fun dispatchOnInstallationFailed(
|
||||
sessionId: Int,
|
||||
shortError: String,
|
||||
fullError: String?,
|
||||
exception: Exception?
|
||||
) {
|
||||
for (observer in mObservers) observer.onInstallationFailed(
|
||||
sessionId,
|
||||
shortError,
|
||||
fullError,
|
||||
exception
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRawError(intent: Intent): String? {
|
||||
return intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
}
|
||||
|
||||
private fun parseError(intent: Intent): String {
|
||||
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
|
||||
val otherPackage = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)
|
||||
val error = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
val errorCode = intent.getIntExtra(
|
||||
ANDROID_PM_EXTRA_LEGACY_STATUS,
|
||||
AndroidPackageInstallerError.UNKNOWN.legacyErrorCode
|
||||
)
|
||||
|
||||
if (status == STATUS_BAD_ROM) {
|
||||
return mContext.getString(R.string.installer_error_lidl_rom)
|
||||
}
|
||||
|
||||
val androidPackageInstallerError: AndroidPackageInstallerError =
|
||||
getAndroidPmError(errorCode, error)
|
||||
if (androidPackageInstallerError != AndroidPackageInstallerError.UNKNOWN) {
|
||||
return androidPackageInstallerError.getDescription(mContext)
|
||||
}
|
||||
|
||||
return getSimplifiedErrorDescription(status, otherPackage)
|
||||
}
|
||||
|
||||
fun getSimplifiedErrorDescription(status: Int, blockingPackage: String?): String {
|
||||
when (status) {
|
||||
PackageInstaller.STATUS_FAILURE_ABORTED -> return mContext.getString(R.string.installer_error_aborted)
|
||||
|
||||
PackageInstaller.STATUS_FAILURE_BLOCKED -> {
|
||||
var blocker = mContext.getString(R.string.installer_error_blocked_device)
|
||||
if (blockingPackage != null) {
|
||||
val appLabel = Utils.getAppLabel(mContext, blockingPackage)
|
||||
if (appLabel != null) blocker = appLabel
|
||||
}
|
||||
return mContext.getString(R.string.installer_error_blocked, blocker)
|
||||
}
|
||||
|
||||
PackageInstaller.STATUS_FAILURE_CONFLICT -> return mContext.getString(R.string.installer_error_conflict)
|
||||
|
||||
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> return mContext.getString(R.string.installer_error_incompatible)
|
||||
|
||||
PackageInstaller.STATUS_FAILURE_INVALID -> return mContext.getString(R.string.installer_error_bad_apks)
|
||||
|
||||
PackageInstaller.STATUS_FAILURE_STORAGE -> return mContext.getString(R.string.installer_error_storage)
|
||||
|
||||
STATUS_BAD_ROM -> return mContext.getString(R.string.installer_error_lidl_rom)
|
||||
}
|
||||
return mContext.getString(R.string.installer_error_generic)
|
||||
}
|
||||
|
||||
fun getAndroidPmError(legacyErrorCode: Int, error: String?): AndroidPackageInstallerError {
|
||||
for (androidPackageInstallerError in AndroidPackageInstallerError.entries) {
|
||||
if (androidPackageInstallerError.legacyErrorCode == legacyErrorCode || (error != null && error.startsWith(
|
||||
androidPackageInstallerError.error
|
||||
))
|
||||
) return androidPackageInstallerError
|
||||
}
|
||||
return AndroidPackageInstallerError.UNKNOWN
|
||||
}
|
||||
|
||||
interface EventObserver {
|
||||
fun onConfirmationPending(sessionId: Int, packageName: String?) {
|
||||
}
|
||||
|
||||
fun onInstallationSucceeded(sessionId: Int, packageName: String?) {
|
||||
}
|
||||
|
||||
fun onInstallationFailed(
|
||||
sessionId: Int,
|
||||
shortError: String?,
|
||||
fullError: String?,
|
||||
exception: Exception?
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "RootlessSaiPiBR"
|
||||
|
||||
const val ANDROID_PM_EXTRA_LEGACY_STATUS: String = "android.content.pm.extra.LEGACY_STATUS"
|
||||
|
||||
val ACTION_DELIVER_PI_EVENT: String = "com.android.grape.action.RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT"
|
||||
|
||||
const val STATUS_BAD_ROM: Int = -322
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package com.android.grape.sai
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.android.grape.sai.filedescriptor.FileUtils
|
||||
import java.io.File
|
||||
|
||||
|
||||
object SafUtils {
|
||||
private const val PATH_TREE = "tree"
|
||||
|
||||
fun getRootForPath(docUri: Uri): String {
|
||||
val path = DocumentsContract.getTreeDocumentId(docUri)
|
||||
|
||||
val indexOfLastColon = path.lastIndexOf(':')
|
||||
require(indexOfLastColon != -1) { "Given uri does not contain a colon: $docUri" }
|
||||
|
||||
return path.substring(0, indexOfLastColon)
|
||||
}
|
||||
|
||||
fun getPathWithoutRoot(docUri: Uri): String {
|
||||
val path = DocumentsContract.getTreeDocumentId(docUri)
|
||||
|
||||
val indexOfLastColon = path.lastIndexOf(':')
|
||||
require(indexOfLastColon != -1) { "Given uri does not contain a colon: $docUri" }
|
||||
|
||||
return path.substring(indexOfLastColon + 1)
|
||||
}
|
||||
|
||||
fun buildChildDocumentUri(directoryUri: Uri, childDisplayName: String): Uri {
|
||||
require(isTreeUri(directoryUri)) { "directoryUri must be a tree uri" }
|
||||
|
||||
val rootPath = getRootForPath(directoryUri)
|
||||
val directoryPath = getPathWithoutRoot(directoryUri)
|
||||
|
||||
val childPath =
|
||||
rootPath + ":" + directoryPath + "/" + FileUtils.buildValidFatFilename(childDisplayName)
|
||||
|
||||
return DocumentsContract.buildDocumentUriUsingTree(directoryUri, childPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the given URI represents a [DocumentsContract.Document] tree.
|
||||
*/
|
||||
fun isTreeUri(uri: Uri): Boolean {
|
||||
val paths = uri.pathSegments
|
||||
return (paths.size >= 2 && PATH_TREE == paths[0])
|
||||
}
|
||||
|
||||
fun docFileFromSingleUriOrFileUri(context: Context, contentUri: Uri): DocumentFile? {
|
||||
if (ContentResolver.SCHEME_FILE == contentUri.scheme) {
|
||||
val path = contentUri.path ?: return null
|
||||
|
||||
val file = File(path)
|
||||
if (file.isDirectory) return null
|
||||
|
||||
return DocumentFile.fromFile(file)
|
||||
} else {
|
||||
return DocumentFile.fromSingleUri(context, contentUri)
|
||||
}
|
||||
}
|
||||
|
||||
fun docFileFromTreeUriOrFileUri(context: Context, contentUri: Uri): DocumentFile? {
|
||||
if (ContentResolver.SCHEME_FILE == contentUri.scheme) {
|
||||
val path = contentUri.path ?: return null
|
||||
|
||||
val file = File(path)
|
||||
if (!file.isDirectory) return null
|
||||
|
||||
return DocumentFile.fromFile(file)
|
||||
} else {
|
||||
return DocumentFile.fromTreeUri(context, contentUri)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileNameFromContentUri(context: Context, contentUri: Uri): String? {
|
||||
val documentFile = docFileFromSingleUriOrFileUri(context, contentUri) ?: return null
|
||||
|
||||
return documentFile.name
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context
|
||||
* @param contentUri
|
||||
* @return file length or 0 if it's unknown
|
||||
*/
|
||||
fun getFileLengthFromContentUri(context: Context, contentUri: Uri): Long {
|
||||
val documentFile = docFileFromSingleUriOrFileUri(context, contentUri) ?: return 0
|
||||
|
||||
return documentFile.length()
|
||||
}
|
||||
|
||||
fun parcelFdToFile(fd: ParcelFileDescriptor): File {
|
||||
return File("/proc/self/fd/" + fd.fd)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package com.android.grape.sai
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import com.android.grape.MainApplication
|
||||
import com.android.grape.R
|
||||
import com.android.grape.sai.shell.Shell
|
||||
import com.android.grape.sai.shell.ShizukuShell
|
||||
|
||||
|
||||
class ShizukuSaiPackageInstaller private constructor(c: Context) :
|
||||
ShellSaiPackageInstaller(c) {
|
||||
init {
|
||||
sInstance = this
|
||||
}
|
||||
|
||||
override val shell: Shell
|
||||
get() = ShizukuShell.instance
|
||||
|
||||
override val installerName: String
|
||||
get() = "Shizuku"
|
||||
|
||||
override val shellUnavailableMessage: String
|
||||
get() = MainApplication.instance.getString(R.string.installer_error_shizuku_unavailable)
|
||||
|
||||
override fun tag(): String {
|
||||
return "ShizukuSaiPi"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private var sInstance: ShizukuSaiPackageInstaller? = null
|
||||
|
||||
fun getInstance(): ShizukuSaiPackageInstaller {
|
||||
synchronized(ShizukuSaiPackageInstaller::class.java) {
|
||||
return sInstance
|
||||
?: ShizukuSaiPackageInstaller(MainApplication.instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.android.grape.sai
|
||||
|
||||
class Stopwatch {
|
||||
private val mStart = System.currentTimeMillis()
|
||||
|
||||
fun millisSinceStart(): Long {
|
||||
return System.currentTimeMillis() - mStart
|
||||
}
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
package com.android.grape.sai
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.app.UiModeManager
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
import java.util.zip.ZipEntry
|
||||
import kotlin.math.pow
|
||||
|
||||
object Utils {
|
||||
private const val TAG = "SAIUtils"
|
||||
|
||||
fun getAppLabel(c: Context, packageName: String): String? {
|
||||
try {
|
||||
val pm = c.packageManager
|
||||
val appInfo = pm.getApplicationInfo(packageName, 0)
|
||||
return pm.getApplicationLabel(appInfo).toString()
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun throwableToString(throwable: Throwable): String {
|
||||
val sw = StringWriter(1024)
|
||||
val pw = PrintWriter(sw)
|
||||
|
||||
throwable.printStackTrace(pw)
|
||||
pw.close()
|
||||
|
||||
return sw.toString()
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
fun getSystemProperty(key: String?): String? {
|
||||
try {
|
||||
return Class.forName("android.os.SystemProperties")
|
||||
.getDeclaredMethod("get", String::class.java)
|
||||
.invoke(null, key) as String
|
||||
} catch (e: Exception) {
|
||||
Log.w("SAIUtils", "Unable to use SystemProperties.get", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun copyTextToClipboard(c: Context, text: CharSequence?) {
|
||||
val clipboardManager = c.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("text", text))
|
||||
}
|
||||
|
||||
fun getFileNameFromZipEntry(zipEntry: ZipEntry): String {
|
||||
val path = zipEntry.name
|
||||
val lastIndexOfSeparator = path.lastIndexOf("/")
|
||||
if (lastIndexOfSeparator == -1) return path
|
||||
return path.substring(lastIndexOfSeparator + 1)
|
||||
}
|
||||
|
||||
fun apiIsAtLeast(sdkInt: Int): Boolean {
|
||||
return Build.VERSION.SDK_INT >= sdkInt
|
||||
}
|
||||
|
||||
fun hideKeyboard(activity: Activity) {
|
||||
val inputMethodManager =
|
||||
activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(activity.window.decorView.windowToken, 0)
|
||||
}
|
||||
|
||||
fun hideKeyboard(fragment: Fragment) {
|
||||
val activity: Activity? = fragment.activity
|
||||
if (activity != null) {
|
||||
hideKeyboard(activity)
|
||||
return
|
||||
}
|
||||
|
||||
val inputMethodManager = fragment.requireContext()
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(fragment.requireView().windowToken, 0)
|
||||
}
|
||||
|
||||
fun escapeFileName(name: String): String {
|
||||
return name.replace("[\\\\/:*?\"<>|]".toRegex(), "_")
|
||||
}
|
||||
|
||||
private var sSizeDecimalFormat: DecimalFormat? = null
|
||||
|
||||
fun getThemeColor(c: Context, @AttrRes attribute: Int): Int {
|
||||
val typedValue = TypedValue()
|
||||
c.theme.resolveAttribute(attribute, typedValue, true)
|
||||
return typedValue.data
|
||||
}
|
||||
|
||||
private val sMainThreadHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun onMainThread(r: Runnable) {
|
||||
sMainThreadHandler.post(r)
|
||||
}
|
||||
|
||||
fun getExtension(fileName: String): String? {
|
||||
val lastDotIndex = fileName.lastIndexOf('.')
|
||||
if (lastDotIndex == -1) return null
|
||||
|
||||
return fileName.substring(lastDotIndex + 1)
|
||||
}
|
||||
|
||||
fun getFileNameWithoutExtension(fileName: String): String? {
|
||||
val lastDotIndex = fileName.lastIndexOf('.')
|
||||
if (lastDotIndex == -1) return null
|
||||
|
||||
return fileName.substring(0, lastDotIndex)
|
||||
}
|
||||
|
||||
fun softRestartApp(c: Context) {
|
||||
val activityManager = c.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
for (task in activityManager.appTasks) task.finishAndRemoveTask()
|
||||
|
||||
val intent = c.packageManager.getLaunchIntentForPackage(c.packageName)
|
||||
c.startActivity(intent)
|
||||
}
|
||||
|
||||
fun hardRestartApp(c: Context) {
|
||||
val activityManager = c.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
for (task in activityManager.appTasks) task.finishAndRemoveTask()
|
||||
|
||||
val intent = c.packageManager.getLaunchIntentForPackage(c.packageName)
|
||||
c.startActivity(intent)
|
||||
System.exit(0)
|
||||
}
|
||||
|
||||
fun dpToPx(c: Context, dp: Int): Int {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
dp.toFloat(),
|
||||
c.resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
|
||||
fun spToPx(c: Context, dp: Int): Int {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_SP,
|
||||
dp.toFloat(),
|
||||
c.resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
|
||||
fun isTv(c: Context): Boolean {
|
||||
val uiModeManager = c.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
return uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
|
||||
}
|
||||
|
||||
fun <T> getParentAs(fragment: Fragment, asClass: Class<T>): T? {
|
||||
var parent: Any? = fragment.parentFragment
|
||||
if (parent == null) parent = fragment.activity
|
||||
|
||||
if (asClass.isInstance(parent)) return asClass.cast(parent)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file within `dir` directory in app's cache directory. File will have a random name.
|
||||
* Even though this method is called createTEMPfile, created file won't be deleted automatically
|
||||
*
|
||||
* @param context
|
||||
* @param dir
|
||||
* @param extension
|
||||
* @return
|
||||
*/
|
||||
fun createTempFileInCache(context: Context, dir: String, extension: String): File? {
|
||||
val directory = File(context.cacheDir, dir)
|
||||
if (!directory.exists() && !directory.mkdir()) return null
|
||||
|
||||
return createUniqueFileInDirectory(directory, extension)
|
||||
}
|
||||
|
||||
fun createUniqueFileInDirectory(dir: File, extension: String): File? {
|
||||
if (!dir.exists() && !dir.mkdirs() && !dir.exists()) return null
|
||||
|
||||
if (!dir.canWrite()) return null
|
||||
|
||||
var file: File? = null
|
||||
while (file == null || file.exists()) file =
|
||||
File(dir, UUID.randomUUID().toString() + "." + extension)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun saveDrawableAsPng(drawable: Drawable, pngFile: File?) {
|
||||
var bitmap: Bitmap? = null
|
||||
try {
|
||||
bitmap = Bitmap.createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
|
||||
drawable.draw(canvas)
|
||||
|
||||
FileOutputStream(pngFile).use { outputStream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||
}
|
||||
} finally {
|
||||
bitmap?.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
private val HEX_ARRAY = "0123456789ABCDEF".toCharArray()
|
||||
|
||||
fun bytesToHex(bytes: ByteArray): String {
|
||||
val hexChars = CharArray(bytes.size * 2)
|
||||
for (j in bytes.indices) {
|
||||
val v = bytes[j].toInt() and 0xFF
|
||||
hexChars[j * 2] = HEX_ARRAY[v ushr 4]
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F]
|
||||
}
|
||||
return String(hexChars)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package com.android.grape.sai.apksource
|
||||
|
||||
import android.content.Context
|
||||
import com.android.grape.sai.IOUtils
|
||||
import com.android.grape.sai.inter.ApkSource
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* An ApkSource implementation that copies APK files from the wrapped ApkSource to a temp file. Used to fix unknown APK sizes when necessary
|
||||
*/
|
||||
class CopyToFileApkSource(context: Context, wrappedApkSource: ApkSource) :
|
||||
ApkSource {
|
||||
private val mContext: Context = context.applicationContext
|
||||
private val mWrappedApkSource: ApkSource = wrappedApkSource
|
||||
|
||||
private var mTempDir: File? = null
|
||||
private var mCurrentApkFile: File? = null
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun nextApk(): Boolean {
|
||||
if (!mWrappedApkSource.nextApk()) return false
|
||||
|
||||
if (mTempDir == null) mTempDir = createTempDir()
|
||||
mCurrentApkFile?.let {
|
||||
IOUtils.deleteRecursively(it)
|
||||
}
|
||||
|
||||
mCurrentApkFile = File(mTempDir, mWrappedApkSource.apkName?:"")
|
||||
|
||||
mWrappedApkSource.openApkInputStream().use { `in` ->
|
||||
FileOutputStream(mCurrentApkFile).use { out ->
|
||||
`in`?.let {
|
||||
IOUtils.copyStream(`in`, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun openApkInputStream(): InputStream {
|
||||
return FileInputStream(mCurrentApkFile)
|
||||
}
|
||||
|
||||
override val apkLength: Long
|
||||
get() = mCurrentApkFile?.length()?:0
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
override val apkName: String?
|
||||
get() = mWrappedApkSource.apkName
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
override val apkLocalPath: String?
|
||||
get() = mWrappedApkSource.apkLocalPath
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun close() {
|
||||
var suppressedException: Exception? = null
|
||||
try {
|
||||
mWrappedApkSource.close()
|
||||
} catch (e: Exception) {
|
||||
suppressedException = e
|
||||
}
|
||||
|
||||
mTempDir?.let {
|
||||
IOUtils.deleteRecursively(it)
|
||||
}
|
||||
|
||||
if (suppressedException != null) throw suppressedException
|
||||
}
|
||||
|
||||
override val appName: String?
|
||||
get() = mWrappedApkSource.apkName
|
||||
|
||||
private fun createTempDir(): File {
|
||||
var tempDir = File(mContext.filesDir, "CopyToFileApkSource")
|
||||
tempDir = File(tempDir, System.currentTimeMillis().toString())
|
||||
tempDir.mkdirs()
|
||||
return tempDir
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package com.android.grape.sai.apksource
|
||||
|
||||
import com.android.grape.sai.inter.ApkSource
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
/**
|
||||
* An ApkSource that can filter out APK files from the backing ZipBackedApkSource
|
||||
*/
|
||||
class FilterApkSource(apkSource: ApkSource, filteredEntries: Set<String>, blacklist: Boolean) :
|
||||
ApkSource {
|
||||
private val mWrappedApkSource: ApkSource = apkSource
|
||||
private val mFilteredEntries = filteredEntries
|
||||
private val mBlacklist = blacklist
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun nextApk(): Boolean {
|
||||
if (!mWrappedApkSource.nextApk()) return false
|
||||
|
||||
while (shouldSkip(apkLocalPath?:"")) {
|
||||
if (!mWrappedApkSource.nextApk()) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun shouldSkip(localPath: String): Boolean {
|
||||
return if (mBlacklist) mFilteredEntries.contains(localPath)
|
||||
else !mFilteredEntries.contains(localPath)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun openApkInputStream(): InputStream? {
|
||||
return mWrappedApkSource.openApkInputStream()
|
||||
}
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
override val apkLength: Long
|
||||
get() = mWrappedApkSource.apkLength
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
override val apkName: String?
|
||||
get() = mWrappedApkSource.apkName
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
override val apkLocalPath: String?
|
||||
get() = mWrappedApkSource.apkLocalPath
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun close() {
|
||||
mWrappedApkSource.close()
|
||||
}
|
||||
|
||||
override val appName: String?
|
||||
get() = mWrappedApkSource.appName
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package com.android.grape.sai.apksource
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.android.grape.pseudo.PseudoApkSigner
|
||||
import com.android.grape.sai.IOUtils
|
||||
import com.android.grape.sai.inter.ApkSource
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
|
||||
class SignerApkSource(private val mContext: Context, apkSource: ApkSource) : ApkSource {
|
||||
private val mWrappedApkSource: ApkSource = apkSource
|
||||
private var mIsPrepared = false
|
||||
private var mApkSigner: PseudoApkSigner? = null
|
||||
private var mTempDir: File? = null
|
||||
|
||||
private var mCurrentSignedApkFile: File? = null
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun nextApk(): Boolean {
|
||||
if (!mWrappedApkSource.nextApk()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!mIsPrepared) {
|
||||
checkAndPrepareSigningEnvironment()
|
||||
createTempDir()
|
||||
mApkSigner = PseudoApkSigner(
|
||||
File(signingEnvironmentDir, FILE_NAME_PAST), File(
|
||||
signingEnvironmentDir, FILE_NAME_PRIVATE_KEY
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
mCurrentSignedApkFile = File(mTempDir, apkName?:"")
|
||||
mWrappedApkSource.openApkInputStream()?.let {
|
||||
mApkSigner?.sign(
|
||||
it,
|
||||
FileOutputStream(mCurrentSignedApkFile)
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun openApkInputStream(): InputStream {
|
||||
return FileInputStream(mCurrentSignedApkFile)
|
||||
}
|
||||
|
||||
override val apkLength: Long
|
||||
get() = mCurrentSignedApkFile!!.length()
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
override val apkName: String?
|
||||
get() = mWrappedApkSource.apkName
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
override val apkLocalPath: String?
|
||||
get() = mWrappedApkSource.apkLocalPath
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun close() {
|
||||
mTempDir?.let {
|
||||
IOUtils.deleteRecursively(it)
|
||||
}
|
||||
mWrappedApkSource.close()
|
||||
}
|
||||
|
||||
override val appName: String?
|
||||
get() = mWrappedApkSource.appName
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun checkAndPrepareSigningEnvironment() {
|
||||
val signingEnvironment = signingEnvironmentDir
|
||||
val pastFile = File(signingEnvironment, FILE_NAME_PAST)
|
||||
val privateKeyFile = File(signingEnvironment, FILE_NAME_PRIVATE_KEY)
|
||||
|
||||
if (pastFile.exists() && privateKeyFile.exists()) {
|
||||
mIsPrepared = true
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Preparing signing environment...")
|
||||
signingEnvironment.mkdir()
|
||||
|
||||
IOUtils.copyFileFromAssets(mContext, FILE_NAME_PAST, pastFile)
|
||||
IOUtils.copyFileFromAssets(mContext, FILE_NAME_PRIVATE_KEY, privateKeyFile)
|
||||
|
||||
mIsPrepared = true
|
||||
}
|
||||
|
||||
private val signingEnvironmentDir: File
|
||||
get() = File(mContext.filesDir, "signing")
|
||||
|
||||
private fun createTempDir() {
|
||||
mTempDir = File(mContext.filesDir, System.currentTimeMillis().toString())
|
||||
mTempDir!!.mkdirs()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SignerApkSource"
|
||||
private const val FILE_NAME_PAST = "testkey.past"
|
||||
private const val FILE_NAME_PRIVATE_KEY = "testkey.pk8"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package com.android.grape.sai.filedescriptor
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.android.grape.sai.SafUtils
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
class ContentUriFileDescriptor(c: Context, private val mContentUri: Uri) :
|
||||
FileDescriptor {
|
||||
private val mContentResolver: ContentResolver = c.contentResolver
|
||||
private val mDocumentFile: DocumentFile? = SafUtils.docFileFromSingleUriOrFileUri(c, mContentUri)
|
||||
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun name(): String {
|
||||
val name = mDocumentFile?.name
|
||||
?: throw BadContentProviderException("DISPLAY_NAME column is null")
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun length(): Long {
|
||||
val length = mDocumentFile?.length()?:0
|
||||
|
||||
if (length == 0L) throw BadContentProviderException("SIZE column is 0")
|
||||
|
||||
return length
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun open(): InputStream? {
|
||||
return mContentResolver.openInputStream(mContentUri)
|
||||
}
|
||||
|
||||
private class BadContentProviderException(message: String?) : Exception(message)
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.android.grape.sai.filedescriptor
|
||||
|
||||
import android.util.Log
|
||||
import com.android.grape.sai.inter.ApkSource
|
||||
import java.io.InputStream
|
||||
|
||||
class DefaultApkSource(private val mApkFileDescriptors: MutableList<FileDescriptor>) :
|
||||
ApkSource {
|
||||
private var mCurrentApk: FileDescriptor? = null
|
||||
|
||||
override fun nextApk(): Boolean {
|
||||
if (mApkFileDescriptors.size == 0) return false
|
||||
|
||||
mCurrentApk = mApkFileDescriptors.removeAt(0)
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun openApkInputStream(): InputStream? {
|
||||
return mCurrentApk?.open()
|
||||
}
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
override val apkLength: Long
|
||||
get() = mCurrentApk?.length()?: 0
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
override val apkName: String?
|
||||
get() = mCurrentApk?.name()
|
||||
|
||||
@get:Throws(Exception::class)
|
||||
override val apkLocalPath: String?
|
||||
get() = mCurrentApk?.name()
|
||||
|
||||
override val appName: String?
|
||||
get() {
|
||||
try {
|
||||
return if (mApkFileDescriptors.size == 1) mApkFileDescriptors[0].name() else null
|
||||
} catch (e: Exception) {
|
||||
Log.w("DefaultApkSource", "Unable to get app name", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.android.grape.sai.filedescriptor
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
interface FileDescriptor {
|
||||
@Throws(Exception::class)
|
||||
fun name(): String?
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun length(): Long
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun open(): InputStream?
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package com.android.grape.sai.filedescriptor
|
||||
|
||||
import android.text.TextUtils
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
object FileUtils {
|
||||
/**
|
||||
* Mutate the given filename to make it valid for a FAT filesystem,
|
||||
* replacing any invalid characters with "_".
|
||||
*/
|
||||
fun buildValidFatFilename(name: String): String {
|
||||
if (TextUtils.isEmpty(name) || "." == name || ".." == name) {
|
||||
return "(invalid)"
|
||||
}
|
||||
val res = StringBuilder(name.length)
|
||||
for (element in name) {
|
||||
val c = element
|
||||
if (isValidFatFilenameChar(c)) {
|
||||
res.append(c)
|
||||
} else {
|
||||
res.append('_')
|
||||
}
|
||||
}
|
||||
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
|
||||
// ext4 through a FUSE layer, so use that limit.
|
||||
trimFilename(res, 255)
|
||||
return res.toString()
|
||||
}
|
||||
|
||||
private fun isValidFatFilenameChar(c: Char): Boolean {
|
||||
if ((c.code in 0x00..0x1f)) {
|
||||
return false
|
||||
}
|
||||
return when (c) {
|
||||
'"', '*', '/', ':', '<', '>', '?', '\\', '|', 0x7F.toChar() -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun trimFilename(res: StringBuilder, maxBytes: Int) {
|
||||
var 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, "...")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.android.grape.sai.filedescriptor
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
class NormalFileDescriptor(private val mFile: File) : FileDescriptor {
|
||||
override fun name(): String {
|
||||
return mFile.name
|
||||
}
|
||||
|
||||
override fun length(): Long {
|
||||
return mFile.length()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun open(): InputStream {
|
||||
return FileInputStream(mFile)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.android.grape.sai.filedescriptor
|
||||
|
||||
import com.android.grape.sai.inter.ApkSource
|
||||
import java.util.zip.ZipEntry
|
||||
|
||||
|
||||
/**
|
||||
* An ApkSource backed by a zip archive
|
||||
*/
|
||||
interface ZipBackedApkSource : ApkSource {
|
||||
/**
|
||||
* @return ZipEntry for the current APK
|
||||
*/
|
||||
val entry: ZipEntry?
|
||||
}
|
|
@ -0,0 +1,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
Loading…
Reference in New Issue