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">
|
<project version="4">
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<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>
|
</component>
|
||||||
</project>
|
</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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</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