From 307dbe8859692c48f1d3594d99077c0a2638bb39 Mon Sep 17 00:00:00 2001
From: hyb1996 <946994919@qq.com>
Date: Fri, 27 Jan 2017 09:05:38 +0800
Subject: [PATCH] initial commit
---
.gitignore | 9 +
.idea/compiler.xml | 22 +
.idea/copyright/profiles_settings.xml | 3 +
.idea/encodings.xml | 6 +
.idea/gradle.xml | 18 +
.idea/inspectionProfiles/Project_Default.xml | 10 +
.../inspectionProfiles/profiles_settings.xml | 7 +
.idea/misc.xml | 61 +++
.idea/modules.xml | 9 +
.idea/runConfigurations.xml | 12 +
app/.gitignore | 1 +
app/build.gradle | 43 ++
app/proguard-rules.pro | 17 +
.../scriptdroid/ExampleInstrumentedTest.java | 26 +
app/src/main/AndroidManifest.xml | 76 +++
.../stardust/scriptdroid/BaseActivity.java | 32 ++
.../stardust/scriptdroid/MainActivity.java | 162 ++++++
.../scriptdroid/ShortcutActivity.java | 89 ++++
.../stardust/scriptdroid/action/Action.java | 466 ++++++++++++++++++
.../action/ActionPerformService.java | 184 +++++++
.../stardust/scriptdroid/data/ScriptFile.java | 52 ++
.../scriptdroid/data/ScriptFileList.java | 28 ++
.../data/SharedPrefScriptFileList.java | 86 ++++
.../com/stardust/scriptdroid/droid/Droid.java | 29 ++
.../scriptdroid/droid/Interpreter.java | 210 ++++++++
.../scriptdroid/droid/StringTool.java | 10 +
.../scriptdroid/droid/SyntaxDefinition.java | 30 ++
.../scriptdroid/file/FileChooser.java | 65 +++
.../stardust/scriptdroid/file/FileUtils.java | 79 +++
.../runningservices/RunningPackageTool.java | 78 +++
.../scriptdroid/shortcut/Shortcut.java | 114 +++++
.../scriptdroid/shortcut/ShortcutHelper.java | 418 ++++++++++++++++
.../scriptdroid/tile/TileService.java | 14 +
.../stardust/scriptdroid/tool/ClassTool.java | 22 +
.../stardust/scriptdroid/tool/ViewTool.java | 16 +
.../scriptdroid/ui/ScriptFileOperation.java | 149 ++++++
.../ui/ScriptFileOperationPopupMenu.java | 135 +++++
.../ui/ScriptListRecyclerView.java | 165 +++++++
.../scriptdroid/ui/SlidingUpPanel.java | 108 ++++
.../java/com/stardust/util/MapEntries.java | 37 ++
.../com/stardust/util/SparseArrayEntries.java | 22 +
.../stardust/util/function/ArrayOptional.java | 8 +
.../com/stardust/util/function/Domino.java | 8 +
.../stardust/util/function/FunctionTool.java | 15 +
.../com/stardust/util/function/ListTool.java | 24 +
.../AccessibilityNodeInfoReflect.java | 40 ++
app/src/main/res/anim/slide_down.xml | 9 +
app/src/main/res/anim/slide_up.xml | 9 +
.../res/drawable/ic_accessibility_service.png | Bin 0 -> 18349 bytes
.../main/res/drawable/ic_add_black_24dp.png | Bin 0 -> 119 bytes
.../main/res/drawable/ic_add_white_48dp.png | Bin 0 -> 116 bytes
.../ic_create_new_folder_black_48dp.png | Bin 0 -> 191 bytes
.../res/drawable/ic_delete_black_48dp.png | Bin 0 -> 148 bytes
.../drawable/ic_delete_forever_black_48dp.png | Bin 0 -> 227 bytes
.../drawable/ic_delete_forever_green_48dp.png | Bin 0 -> 965 bytes
.../res/drawable/ic_delete_green_48dp.png | Bin 0 -> 546 bytes
.../main/res/drawable/ic_edit_gray_48dp.png | Bin 0 -> 14872 bytes
.../main/res/drawable/ic_edit_green_48dp.png | Bin 0 -> 767 bytes
.../ic_play_circle_filled_black_48dp.png | Bin 0 -> 444 bytes
app/src/main/res/drawable/ic_play_green.png | Bin 0 -> 23800 bytes
app/src/main/res/drawable/ic_rename_green.png | Bin 0 -> 17787 bytes
.../main/res/drawable/ic_shortcut_green.png | Bin 0 -> 4236 bytes
.../navigation_show_more_vertical_512.png | Bin 0 -> 13339 bytes
.../main/res/drawable/pencil_and_paper.png | Bin 0 -> 22025 bytes
app/src/main/res/drawable/play_blue.png | Bin 0 -> 4508 bytes
.../res/drawable/popup_menu_background.xml | 6 +
app/src/main/res/drawable/rename.png | Bin 0 -> 4797 bytes
app/src/main/res/drawable/robot_head.png | Bin 0 -> 23332 bytes
.../main/res/drawable/robot_head_green.png | Bin 0 -> 32853 bytes
.../main/res/drawable/robot_icon_green.png | Bin 0 -> 27697 bytes
app/src/main/res/drawable/run.png | Bin 0 -> 15483 bytes
app/src/main/res/drawable/script_droid.png | Bin 0 -> 15875 bytes
.../main/res/drawable/script_droid_100.png | Bin 0 -> 3993 bytes
app/src/main/res/drawable/script_droid_50.png | Bin 0 -> 1896 bytes
.../main/res/drawable/sdcard_icon_black.png | Bin 0 -> 1897 bytes
app/src/main/res/drawable/shortcut_icon.png | Bin 0 -> 4214 bytes
app/src/main/res/layout/activity_main.xml | 97 ++++
app/src/main/res/layout/content_main.xml | 29 ++
...ript_file_operation_popup_menu_content.xml | 14 +
.../script_file_operation_popup_menu_item.xml | 27 +
.../layout/script_list_recycler_view_item.xml | 88 ++++
app/src/main/res/layout/sliding_up_panel.xml | 24 +
app/src/main/res/menu/menu_main.xml | 22 +
app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes
app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes
app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes
.../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes
.../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10486 bytes
app/src/main/res/values-v21/styles.xml | 9 +
app/src/main/res/values-w820dp/dimens.xml | 6 +
app/src/main/res/values/attrs.xml | 3 +
app/src/main/res/values/colors.xml | 7 +
app/src/main/res/values/dimens.xml | 6 +
app/src/main/res/values/strings.xml | 20 +
app/src/main/res/values/styles.xml | 20 +
.../res/xml/accessibility_service_config.xml | 8 +
app/src/main/res/xml/provider_paths.xml | 4 +
.../stardust/scriptdroid/ExampleUnitTest.java | 19 +
build.gradle | 23 +
gradle.properties | 20 +
gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes
gradle/wrapper/gradle-wrapper.properties | 6 +
gradlew | 160 ++++++
gradlew.bat | 90 ++++
settings.gradle | 1 +
105 files changed, 3942 insertions(+)
create mode 100644 .gitignore
create mode 100644 .idea/compiler.xml
create mode 100644 .idea/copyright/profiles_settings.xml
create mode 100644 .idea/encodings.xml
create mode 100644 .idea/gradle.xml
create mode 100644 .idea/inspectionProfiles/Project_Default.xml
create mode 100644 .idea/inspectionProfiles/profiles_settings.xml
create mode 100644 .idea/misc.xml
create mode 100644 .idea/modules.xml
create mode 100644 .idea/runConfigurations.xml
create mode 100644 app/.gitignore
create mode 100644 app/build.gradle
create mode 100644 app/proguard-rules.pro
create mode 100644 app/src/androidTest/java/com/stardust/scriptdroid/ExampleInstrumentedTest.java
create mode 100644 app/src/main/AndroidManifest.xml
create mode 100644 app/src/main/java/com/stardust/scriptdroid/BaseActivity.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/MainActivity.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/ShortcutActivity.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/action/Action.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/action/ActionPerformService.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/data/ScriptFile.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/data/ScriptFileList.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/data/SharedPrefScriptFileList.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/droid/Droid.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/droid/Interpreter.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/droid/StringTool.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/droid/SyntaxDefinition.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/file/FileChooser.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/file/FileUtils.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/runningservices/RunningPackageTool.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/shortcut/Shortcut.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/shortcut/ShortcutHelper.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/tile/TileService.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/tool/ClassTool.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/tool/ViewTool.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/ui/ScriptFileOperation.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/ui/ScriptFileOperationPopupMenu.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/ui/ScriptListRecyclerView.java
create mode 100644 app/src/main/java/com/stardust/scriptdroid/ui/SlidingUpPanel.java
create mode 100644 app/src/main/java/com/stardust/util/MapEntries.java
create mode 100644 app/src/main/java/com/stardust/util/SparseArrayEntries.java
create mode 100644 app/src/main/java/com/stardust/util/function/ArrayOptional.java
create mode 100644 app/src/main/java/com/stardust/util/function/Domino.java
create mode 100644 app/src/main/java/com/stardust/util/function/FunctionTool.java
create mode 100644 app/src/main/java/com/stardust/util/function/ListTool.java
create mode 100644 app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoReflect.java
create mode 100644 app/src/main/res/anim/slide_down.xml
create mode 100644 app/src/main/res/anim/slide_up.xml
create mode 100644 app/src/main/res/drawable/ic_accessibility_service.png
create mode 100644 app/src/main/res/drawable/ic_add_black_24dp.png
create mode 100644 app/src/main/res/drawable/ic_add_white_48dp.png
create mode 100644 app/src/main/res/drawable/ic_create_new_folder_black_48dp.png
create mode 100644 app/src/main/res/drawable/ic_delete_black_48dp.png
create mode 100644 app/src/main/res/drawable/ic_delete_forever_black_48dp.png
create mode 100644 app/src/main/res/drawable/ic_delete_forever_green_48dp.png
create mode 100644 app/src/main/res/drawable/ic_delete_green_48dp.png
create mode 100644 app/src/main/res/drawable/ic_edit_gray_48dp.png
create mode 100644 app/src/main/res/drawable/ic_edit_green_48dp.png
create mode 100644 app/src/main/res/drawable/ic_play_circle_filled_black_48dp.png
create mode 100644 app/src/main/res/drawable/ic_play_green.png
create mode 100644 app/src/main/res/drawable/ic_rename_green.png
create mode 100644 app/src/main/res/drawable/ic_shortcut_green.png
create mode 100644 app/src/main/res/drawable/navigation_show_more_vertical_512.png
create mode 100644 app/src/main/res/drawable/pencil_and_paper.png
create mode 100644 app/src/main/res/drawable/play_blue.png
create mode 100644 app/src/main/res/drawable/popup_menu_background.xml
create mode 100644 app/src/main/res/drawable/rename.png
create mode 100644 app/src/main/res/drawable/robot_head.png
create mode 100644 app/src/main/res/drawable/robot_head_green.png
create mode 100644 app/src/main/res/drawable/robot_icon_green.png
create mode 100644 app/src/main/res/drawable/run.png
create mode 100644 app/src/main/res/drawable/script_droid.png
create mode 100644 app/src/main/res/drawable/script_droid_100.png
create mode 100644 app/src/main/res/drawable/script_droid_50.png
create mode 100644 app/src/main/res/drawable/sdcard_icon_black.png
create mode 100644 app/src/main/res/drawable/shortcut_icon.png
create mode 100644 app/src/main/res/layout/activity_main.xml
create mode 100644 app/src/main/res/layout/content_main.xml
create mode 100644 app/src/main/res/layout/script_file_operation_popup_menu_content.xml
create mode 100644 app/src/main/res/layout/script_file_operation_popup_menu_item.xml
create mode 100644 app/src/main/res/layout/script_list_recycler_view_item.xml
create mode 100644 app/src/main/res/layout/sliding_up_panel.xml
create mode 100644 app/src/main/res/menu/menu_main.xml
create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png
create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png
create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png
create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png
create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
create mode 100644 app/src/main/res/values-v21/styles.xml
create mode 100644 app/src/main/res/values-w820dp/dimens.xml
create mode 100644 app/src/main/res/values/attrs.xml
create mode 100644 app/src/main/res/values/colors.xml
create mode 100644 app/src/main/res/values/dimens.xml
create mode 100644 app/src/main/res/values/strings.xml
create mode 100644 app/src/main/res/values/styles.xml
create mode 100644 app/src/main/res/xml/accessibility_service_config.xml
create mode 100644 app/src/main/res/xml/provider_paths.xml
create mode 100644 app/src/test/java/com/stardust/scriptdroid/ExampleUnitTest.java
create mode 100644 build.gradle
create mode 100644 gradle.properties
create mode 100644 gradle/wrapper/gradle-wrapper.jar
create mode 100644 gradle/wrapper/gradle-wrapper.properties
create mode 100644 gradlew
create mode 100644 gradlew.bat
create mode 100644 settings.gradle
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..39fb081a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 00000000..96cc43ef
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 00000000..e7bedf33
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 00000000..97626ba4
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 00000000..7ac24c77
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 00000000..65f3da7d
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 00000000..3b312839
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..8cdb7ebe
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..24411114
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 00000000..7f68460d
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 00000000..acf4a50f
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,43 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 25
+ buildToolsVersion "25.0.2"
+ defaultConfig {
+ applicationId "com.stardust.scriptdroid"
+ minSdkVersion 24
+ //由于不知道在API24以上如何调起其他文件编辑器应用编辑文件,只能改到API23。FileProvider貌似只能查看编辑后不能保存?
+ //参照ScriptFileOperation.Edit
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ jackOptions {
+ enabled true
+ }
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ encoding = 'utf-8'
+ }
+}
+
+dependencies {
+ compile fileTree(include: ['*.jar'], dir: 'libs')
+ androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+ compile 'com.android.support:appcompat-v7:25.0.0'
+ compile 'com.android.support:design:25.0.0'
+ testCompile 'junit:junit:4.12'
+ compile 'com.afollestad.material-dialogs:core:0.9.2.3'
+ compile 'com.google.code.gson:gson:2.8.0'
+ compile 'com.sothree.slidinguppanel:library:3.3.1'
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 00000000..fba55431
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in E:\YiBin\eclipse\Android_SDK_windows/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/app/src/androidTest/java/com/stardust/scriptdroid/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/stardust/scriptdroid/ExampleInstrumentedTest.java
new file mode 100644
index 00000000..11c68b43
--- /dev/null
+++ b/app/src/androidTest/java/com/stardust/scriptdroid/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.stardust.scriptdroid;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.stardust.scriptdroid", appContext.getPackageName());
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..2be358e3
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/stardust/scriptdroid/BaseActivity.java b/app/src/main/java/com/stardust/scriptdroid/BaseActivity.java
new file mode 100644
index 00000000..cef1e52b
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/BaseActivity.java
@@ -0,0 +1,32 @@
+package com.stardust.scriptdroid;
+
+import android.os.Build;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+
+import java.util.stream.Stream;
+
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+
+/**
+ * Created by Stardust on 2017/1/23.
+ */
+
+public class BaseActivity extends AppCompatActivity {
+
+
+ @SuppressWarnings("unchecked")
+ public T $(int resId) {
+ return (T) findViewById(resId);
+ }
+
+ protected void checkPermission(String... permissions) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ String[] requestPermissions = Stream.of(permissions).filter(permission -> checkSelfPermission(permission) == PERMISSION_DENIED).toArray(String[]::new);
+
+ if (requestPermissions.length > 0)
+ requestPermissions(requestPermissions, 0);
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/MainActivity.java b/app/src/main/java/com/stardust/scriptdroid/MainActivity.java
new file mode 100644
index 00000000..29714dc3
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/MainActivity.java
@@ -0,0 +1,162 @@
+package com.stardust.scriptdroid;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.support.v7.widget.Toolbar;
+import android.text.InputType;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.afollestad.materialdialogs.MaterialDialog;
+import com.stardust.scriptdroid.action.ActionPerformService;
+import com.stardust.scriptdroid.data.ScriptFile;
+import com.stardust.scriptdroid.data.ScriptFileList;
+import com.stardust.scriptdroid.data.SharedPrefScriptFileList;
+import com.stardust.scriptdroid.file.FileChooser;
+import com.stardust.scriptdroid.file.FileUtils;
+import com.stardust.scriptdroid.ui.ScriptFileOperation;
+import com.stardust.scriptdroid.ui.ScriptListRecyclerView;
+import com.stardust.scriptdroid.ui.SlidingUpPanel;
+import com.stardust.util.MapEntries;
+
+import java.util.Map;
+import java.util.Optional;
+
+
+public class MainActivity extends BaseActivity {
+
+ private View mView;
+ private SlidingUpPanel mSlidingUpPanel;
+ private ScriptListRecyclerView mScriptListRecyclerView;
+ private ScriptFileList mScriptFileList;
+ private FileChooser mFileChooser;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setUpUI();
+
+ setUpFileChooser();
+
+ checkPermissions();
+ }
+
+ private void checkPermissions() {
+ //checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE);
+ ActionPerformService.goToPermissionSettingIfDisabled(this);
+ }
+
+
+ private void setUpFileChooser() {
+ mFileChooser = new FileChooser(this);
+ mFileChooser.setOnFileChoseListener(inputStream -> Optional.ofNullable(FileUtils.getPath(inputStream)).ifPresent(this::addScriptFile));
+ }
+
+ private void addScriptFile(final String path) {
+ new MaterialDialog.Builder(this).title(R.string.text_name)
+ .inputType(InputType.TYPE_CLASS_TEXT)
+ .input(getString(R.string.text_please_input_name), "", (dialog, input) -> addScriptFile(input.toString(), path)).show();
+ }
+
+ private void addScriptFile(String name, String path) {
+ mScriptFileList.add(new ScriptFile(name, path));
+ mScriptListRecyclerView.getAdapter().notifyItemInserted(mScriptFileList.size() - 1);
+ }
+
+
+ private void setUpUI() {
+ mView = View.inflate(this, R.layout.activity_main, null);
+ setContentView(mView);
+ mSlidingUpPanel = $(R.id.bottom_menu);
+
+ setUpToolbar();
+ setUpScriptList();
+ setUpListener();
+ }
+
+ private void setUpToolbar() {
+ Toolbar toolbar = $(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ toolbar.setNavigationIcon(R.drawable.script_droid_50);
+ toolbar.setTitle(R.string.app_name);
+ }
+
+ private void setUpScriptList() {
+ mScriptListRecyclerView = $(R.id.script_list);
+ mScriptFileList = new SharedPrefScriptFileList(this);
+ mScriptListRecyclerView.setScriptFileList(mScriptFileList);
+ }
+
+ private void setUpListener() {
+ $(R.id.fab).setOnClickListener(view -> mSlidingUpPanel.show());
+ $(R.id.import_from_file).setOnClickListener(v -> showFileChooser());
+ $(R.id.create_new_file).setOnClickListener(v -> createScriptFile());
+ }
+
+ private void createScriptFile() {
+ new MaterialDialog.Builder(this).title(R.string.text_name)
+ .inputType(InputType.TYPE_CLASS_TEXT)
+ .input(getString(R.string.text_please_input_name), "", (dialog, input) -> {
+ String path = ScriptFile.DEFAULT_FOLDER + input + ".text";
+ createScriptFile(input.toString(), path);
+ }).show();
+ }
+
+ private void createScriptFile(String name, String path) {
+ if (FileUtils.createFileIfNotExists(path)) {
+ addScriptFile(name, path);
+ new ScriptFileOperation.Edit().operate(mScriptListRecyclerView, mScriptFileList, mScriptFileList.size() - 1);
+ } else {
+ Snackbar.make(mView, R.string.text_file_create_fail, Snackbar.LENGTH_LONG).show();
+ }
+ }
+
+ private void showFileChooser() {
+ mFileChooser.startFileManagerToChoose("*/*", (exception, mimeType) -> {
+ exception.printStackTrace();
+ Snackbar.make(mView, R.string.text_file_manager_not_found, Snackbar.LENGTH_SHORT).show();
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+
+ private final Map mOptionActionMap = new MapEntries()
+ .entry(R.id.action_exit, this::finish)
+ .entry(R.id.action_disable_service, this::disableAccessibilityService)
+ .entry(R.id.action_settings, () -> ActionPerformService.setActions(ActionPerformService.NO_ACTION))
+ .map();
+
+
+ private void startSettingActivity() {
+ //TODO create Setting Activity
+ startActivity(new Intent(this, MainActivity.class));
+ }
+
+ private void disableAccessibilityService() {
+ Optional.ofNullable(ActionPerformService.getInstance()).ifPresent(ActionPerformService::disableSelf);
+ Snackbar.make(mView, R.string.text_service_disabled, Snackbar.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ Runnable action = mOptionActionMap.get(item.getItemId());
+ if (action != null) {
+ action.run();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ mFileChooser.onActivityResult(requestCode, resultCode, data);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/stardust/scriptdroid/ShortcutActivity.java b/app/src/main/java/com/stardust/scriptdroid/ShortcutActivity.java
new file mode 100644
index 00000000..4a6bd36f
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/ShortcutActivity.java
@@ -0,0 +1,89 @@
+package com.stardust.scriptdroid;
+
+import android.app.Activity;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+import com.stardust.scriptdroid.droid.Droid;
+
+import java.io.File;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.BooleanSupplier;
+
+import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+/**
+ * Created by Stardust on 2017/1/23.
+ */
+public class ShortcutActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final String path = getIntent().getStringExtra("path");
+ if (!ensure(() -> !TextUtils.isEmpty(path), R.string.text_path_is_empty))
+ return;
+ final File scriptFile = new File(path);
+ new Domino()
+ .then(() -> ensure(scriptFile::exists, R.string.text_file_not_exists))
+ .then(() -> ensure(this::hasStorageReadPermission, R.string.text_no_file_rw_permission))
+ .then(() -> {
+ runScriptFile(path);
+ return true;
+ })
+ .fall();
+ }
+
+ private void runScriptFile(String path) {
+ try {
+ Droid.run(this, new File(path));
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private boolean ensure(BooleanSupplier bool, String message) {
+ boolean b = bool.getAsBoolean();
+ if (!b) {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+ }
+ return b;
+ }
+
+ private boolean ensure(BooleanSupplier bool, int resId) {
+ return ensure(bool, getString(resId));
+ }
+
+ private boolean hasStorageReadPermission() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+ checkSelfPermission(READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED;
+ }
+
+ @FunctionalInterface
+ interface Tile {
+ boolean fall();
+ }
+
+ public static class Domino {
+
+ private List mTiles = new LinkedList<>();
+
+ Domino then(Tile next) {
+ mTiles.add(next);
+ return this;
+ }
+
+ void fall() {
+ for (Tile tile : mTiles) {
+ if (!tile.fall())
+ break;
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/action/Action.java b/app/src/main/java/com/stardust/scriptdroid/action/Action.java
new file mode 100644
index 00000000..a2cfc429
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/action/Action.java
@@ -0,0 +1,466 @@
+package com.stardust.scriptdroid.action;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.util.SparseArray;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.lang.ref.WeakReference;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT;
+
+/**
+ * Created by Stardust on 2017/1/21.
+ */
+
+public abstract class Action {
+
+ private static WeakReference mContext;
+
+ public static void setActionContext(Context context) {
+ mContext = new WeakReference<>(context);
+ }
+
+ public abstract boolean perform(AccessibilityNodeInfo rootNodeInfo);
+
+ interface TargetFilter {
+ AccessibilityNodeInfo findTarget(AccessibilityNodeInfo nodeInfo);
+ }
+
+ public static class MultiAction extends Action {
+
+ private List mActions;
+ private boolean mDependent;
+
+ public MultiAction(List actions, boolean dependent) {
+ if (actions == null)
+ throw new NullPointerException("actions = null");
+ mDependent = dependent;
+ mActions = actions;
+ }
+
+ @Override
+ public boolean perform(AccessibilityNodeInfo rootNodeInfo) {
+ boolean succeed = true;
+ for (Action action : mActions) {
+ if (!action.perform(rootNodeInfo)) {
+ succeed = false;
+ if (!mDependent)
+ break;
+ }
+ }
+ return succeed;
+ }
+ }
+
+ public static abstract class TargetAction extends Action {
+
+ static final int TYPE_ID = 1;
+ static final int TYPE_TEXT = 2;
+ static final int TYPE_BOUNDS = 3;
+ static final int TYPE_DESCRIPTION = 4;
+
+ private int mType;
+ private String mString;
+ private Rect mBoundsInScreen;
+
+ private TargetAction(String str, int type) {
+ if (str == null)
+ throw new NullPointerException("str == null");
+ if (type != TYPE_TEXT && type != TYPE_ID && type != TYPE_DESCRIPTION)
+ throw new IllegalArgumentException("type illegal");
+ mString = str;
+ mType = type;
+ }
+
+ private TargetAction(Rect boundsInScreen) {
+ mType = TYPE_BOUNDS;
+ mBoundsInScreen = boundsInScreen;
+ }
+
+ @Override
+ public boolean perform(AccessibilityNodeInfo rootNodeInfo) {
+ List target = null;
+ switch (mType) {
+ case TYPE_TEXT:
+ target = rootNodeInfo.findAccessibilityNodeInfosByText(mString);
+ break;
+ case TYPE_ID:
+ target = rootNodeInfo.findAccessibilityNodeInfosByViewId(mString);
+ break;
+ case TYPE_BOUNDS:
+ AccessibilityNodeInfo nodeInfo = findAccessibilityNodeInfosByBounds(rootNodeInfo, mBoundsInScreen);
+ target = nodeInfo == null ? Collections.EMPTY_LIST : Collections.singletonList(nodeInfo);
+ break;
+ case TYPE_DESCRIPTION:
+ target = findAccessibilityNodeInfosByDescription(rootNodeInfo, mString);
+ }
+ return target != null && perform(target);
+ }
+
+
+ private List findAccessibilityNodeInfosByDescription(AccessibilityNodeInfo rootNodeInfo, String description) {
+ if (description.startsWith("edittext") && !description.equals("edittext")) {
+ int i = Integer.parseInt(description.substring(8));
+ return Collections.singletonList(findEditText(rootNodeInfo).get(i));
+ }
+ if (description.equals("edittext")) {
+ return findEditText(rootNodeInfo);
+ }
+ return null;
+ }
+
+ private List findEditText(AccessibilityNodeInfo rootNodeInfo) {
+ if (rootNodeInfo == null) {
+ return Collections.EMPTY_LIST;
+ }
+ if (rootNodeInfo.isEditable()) {
+ return Collections.singletonList(rootNodeInfo);
+ }
+ List list = new LinkedList<>();
+ for (int i = 0; i < rootNodeInfo.getChildCount(); i++) {
+ list.addAll(findEditText(rootNodeInfo.getChild(i)));
+ }
+ return list;
+ }
+
+ //(882, 1722 - 1036, 1876)
+ private AccessibilityNodeInfo findAccessibilityNodeInfosByBounds(AccessibilityNodeInfo root, Rect boundsInScreen) {
+ if (root == null)
+ return null;
+ Rect rect = new Rect();
+ root.getBoundsInScreen(rect);
+ if (rect.equals(boundsInScreen)) {
+ return root;
+ }
+ for (int i = 0; i < root.getChildCount(); i++) {
+ AccessibilityNodeInfo child = root.getChild(i);
+ if (child == null)
+ continue;
+ AccessibilityNodeInfo nodeInfo = findAccessibilityNodeInfosByBounds(child, boundsInScreen);
+ if (nodeInfo != null)
+ return nodeInfo;
+ else
+ child.recycle();
+ }
+ return null;
+ }
+
+ abstract boolean perform(@NonNull List nodeInfoList);
+ }
+
+ public static class SimpleAction extends TargetAction {
+
+ private int mAction;
+
+ SimpleAction(int action, String str, int type) {
+ super(str, type);
+ mAction = action;
+ }
+
+ SimpleAction(int action, Rect boundsInScreen) {
+ super(boundsInScreen);
+ mAction = action;
+ }
+
+ @Override
+ boolean perform(@NonNull List nodeInfoList) {
+ for (AccessibilityNodeInfo nodeInfo : nodeInfoList) {
+ performAction(nodeInfo);
+ }
+ return true;
+ }
+
+ boolean performAction(AccessibilityNodeInfo nodeInfo) {
+ return nodeInfo.performAction(mAction);
+ }
+
+ void setAction(int action) {
+ mAction = action;
+ }
+
+ int getAction() {
+ return mAction;
+ }
+ }
+
+ public static class SimpleFilterAction extends SimpleAction {
+
+ private TargetFilter mFilter;
+
+ SimpleFilterAction(int action, String str, int type, TargetFilter filter) {
+ super(action, str, type);
+ mFilter = filter;
+ }
+
+ SimpleFilterAction(int action, Rect boundsInScreen, TargetFilter filter) {
+ super(action, boundsInScreen);
+ mFilter = filter;
+ }
+
+ @Override
+ boolean perform(@NonNull List nodeInfoList) {
+ boolean performed = false;
+ for (AccessibilityNodeInfo node : nodeInfoList) {
+ node = mFilter.findTarget(node);
+ if (node != null) {
+ performAction(node);
+ performed = true;
+ }
+ }
+ return performed;
+ }
+
+ }
+
+ interface Able {
+ boolean isAble(AccessibilityNodeInfo node);
+ }
+
+ private static final SparseArray ACTION_ABLE_MAP = new SparseArray<>();
+
+ static {
+ ACTION_ABLE_MAP.put(ACTION_CLICK, new Able() {
+ @Override
+ public boolean isAble(AccessibilityNodeInfo node) {
+ return node.isClickable();
+ }
+ });
+ ACTION_ABLE_MAP.put(ACTION_LONG_CLICK, new Able() {
+ @Override
+ public boolean isAble(AccessibilityNodeInfo node) {
+ return node.isLongClickable();
+ }
+ });
+ ACTION_ABLE_MAP.put(ACTION_FOCUS, new Able() {
+ @Override
+ public boolean isAble(AccessibilityNodeInfo node) {
+ return node.isFocusable();
+ }
+ });
+ ACTION_ABLE_MAP.put(ACTION_SCROLL_FORWARD, new Able() {
+ @Override
+ public boolean isAble(AccessibilityNodeInfo node) {
+ return node.isScrollable();
+ }
+ });
+ ACTION_ABLE_MAP.put(ACTION_SCROLL_BACKWARD, new Able() {
+ @Override
+ public boolean isAble(AccessibilityNodeInfo node) {
+ return node.isScrollable();
+ }
+ });
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ ACTION_ABLE_MAP.put(ACTION_SET_TEXT, new Able() {
+ @Override
+ public boolean isAble(AccessibilityNodeInfo node) {
+ return node.isEditable();
+ }
+ });
+ }
+ }
+
+
+ public static class FindDownwardlyDfsFilterAction extends SimpleFilterAction {
+
+ public static FindDownwardlyDfsFilterAction createActionByBounds(int action, Rect boundInScreen) {
+ Able able = ACTION_ABLE_MAP.get(action);
+ return new FindDownwardlyDfsFilterAction(action, boundInScreen, able);
+ }
+
+ FindDownwardlyDfsFilterAction(int action, Rect boundInScreen, Able able) {
+ super(action, boundInScreen, new FindDownwardlyDfsTargetFilter(able));
+ }
+
+ FindDownwardlyDfsFilterAction(int action, String str, int type, Able able) {
+ super(action, str, type, new FindDownwardlyDfsTargetFilter(able));
+ }
+
+
+ private static class FindDownwardlyDfsTargetFilter implements TargetFilter {
+ Able mAble;
+
+ FindDownwardlyDfsTargetFilter(Able able) {
+ mAble = able;
+ }
+
+ @Override
+ public AccessibilityNodeInfo findTarget(AccessibilityNodeInfo n) {
+ if (n == null)
+ return null;
+ if (mAble.isAble(n))
+ return n;
+ for (int i = 0; i < n.getChildCount(); i++) {
+ AccessibilityNodeInfo child = n.getChild(i);
+ if (child == null)
+ continue;
+ AccessibilityNodeInfo node = findTarget(child);
+ if (node != null)
+ return node;
+ else
+ child.recycle();
+ }
+ return null;
+ }
+ }
+
+ }
+
+ public static class FindUpwardlyFilterAction extends SimpleFilterAction {
+
+
+ public static FindUpwardlyFilterAction createActionById(int action, String id) {
+ return createActionInner(action, id, TYPE_ID);
+ }
+
+ public static FindUpwardlyFilterAction createActionByText(int action, String text) {
+ return createActionInner(action, text, TYPE_TEXT);
+ }
+
+ public static Action createActionByDescription(int action, String description) {
+ return createActionInner(action, description, TYPE_DESCRIPTION);
+ }
+
+ private static FindUpwardlyFilterAction createActionInner(int action, String str, int type) {
+ Able able = ACTION_ABLE_MAP.get(action);
+ return new FindUpwardlyFilterAction(action, str, type, able);
+ }
+
+ private static class FindUpwardlyTargetFilter implements TargetFilter {
+ Able mAble;
+
+ FindUpwardlyTargetFilter(Able able) {
+ mAble = able;
+ }
+
+ @Override
+ public AccessibilityNodeInfo findTarget(AccessibilityNodeInfo n) {
+ AccessibilityNodeInfo node = n;
+ while (node != null && !mAble.isAble(node)) {
+ AccessibilityNodeInfo parent = node.getParent();
+ if (node != n) {
+ node.recycle();
+ }
+ node = parent;
+ }
+ return node;
+ }
+ }
+
+ FindUpwardlyFilterAction(int action, String str, int type, Able able) {
+ super(action, str, type, new FindUpwardlyTargetFilter(able));
+ }
+
+ FindUpwardlyFilterAction(int action, Rect boundsInScreen, Able able) {
+ super(action, boundsInScreen, new FindUpwardlyTargetFilter(able));
+ }
+ }
+
+ public static class ScrollAction extends Action {
+
+ public static final int SCROLL_FORWARD = AccessibilityNodeInfo.ACTION_SCROLL_FORWARD;
+ public static final int SCROLL_BACKWARD = AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
+
+ private int mScrollAction;
+ private int mTimes;
+
+ public ScrollAction(int scrollAction) {
+ this(scrollAction, 1);
+ }
+
+ public ScrollAction(int scrollAction, int times) {
+ if (scrollAction != SCROLL_BACKWARD && scrollAction != SCROLL_FORWARD)
+ throw new IllegalArgumentException("scrollAction illegal");
+ mScrollAction = scrollAction;
+ mTimes = times;
+ }
+
+ @Override
+ public boolean perform(AccessibilityNodeInfo rootNodeInfo) {
+ AccessibilityNodeInfo scrollableNodeInfo = findScrollableNodeInfo(rootNodeInfo);
+ if (scrollableNodeInfo == null) {
+ return false;
+ } else {
+ for (int i = 0; i < mTimes; i++) {
+ scrollableNodeInfo.performAction(mScrollAction);
+ }
+ return true;
+ }
+ }
+
+ private AccessibilityNodeInfo findScrollableNodeInfo(AccessibilityNodeInfo nodeInfo) {
+ if (nodeInfo == null)
+ return null;
+ if (nodeInfo.isScrollable()) {
+ return nodeInfo;
+ }
+ for (int i = 0; i < nodeInfo.getChildCount(); i++) {
+ AccessibilityNodeInfo node = findScrollableNodeInfo(nodeInfo.getChild(i));
+ if (node != null) {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ }
+
+ public static class IntentAction extends Action {
+
+ private final Intent mIntent;
+
+ public IntentAction(Intent intent) {
+ mIntent = intent;
+ }
+
+ @Override
+ public boolean perform(AccessibilityNodeInfo rootNodeInfo) {
+ if (mContext == null || mContext.get() == null)
+ return false;
+ mContext.get().startActivity(mIntent);
+ return true;
+ }
+
+ public Intent getIntent() {
+ return mIntent;
+ }
+ }
+
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ public static class InputAction extends FindUpwardlyFilterAction {
+
+ private static final Able EDITABLE = new Able() {
+ @Override
+ public boolean isAble(AccessibilityNodeInfo node) {
+ return node.isEditable();
+ }
+ };
+ private String mText;
+
+ public InputAction(String description, String text) {
+ super(ACTION_SET_TEXT, description, TYPE_DESCRIPTION, EDITABLE);
+ mText = text;
+ }
+
+ boolean performAction(AccessibilityNodeInfo nodeInfo) {
+ Bundle arg = new Bundle();
+ arg.putCharSequence(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, mText);
+ return nodeInfo.performAction(ACTION_SET_TEXT, arg);
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/action/ActionPerformService.java b/app/src/main/java/com/stardust/scriptdroid/action/ActionPerformService.java
new file mode 100644
index 00000000..c851a2fb
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/action/ActionPerformService.java
@@ -0,0 +1,184 @@
+package com.stardust.scriptdroid.action;
+
+import android.accessibilityservice.AccessibilityService;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Toast;
+
+import com.afollestad.materialdialogs.MaterialDialog;
+import com.stardust.scriptdroid.R;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+
+/**
+ * Created by Stardust on 2017/1/21.
+ */
+
+public class ActionPerformService extends AccessibilityService {
+
+ private static final String TAG = "SettingRunningServiceSS";
+ private static ActionPerformService instance;
+
+ public static void goToPermissionSettingIfDisabled(final Context context) {
+ if (!isAccessibilityServiceEnabled(context, ActionPerformService.class)) {
+ new MaterialDialog.Builder(context)
+ .content(R.string.explain_accessibility_permission)
+ .positiveText(R.string.text_go_to_setting)
+ .negativeText(R.string.text_cancel)
+ .onPositive((dialog, which) -> context.startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))).show();
+ }
+ }
+
+ public static boolean isAccessibilityServiceEnabled(Context context, Class> accessibilityService) {
+ ComponentName expectedComponentName = new ComponentName(context, accessibilityService);
+
+ String enabledServicesSetting = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
+ if (enabledServicesSetting == null)
+ return false;
+
+ TextUtils.SimpleStringSplitter colonSplitter = new TextUtils.SimpleStringSplitter(':');
+ colonSplitter.setString(enabledServicesSetting);
+
+ while (colonSplitter.hasNext()) {
+ String componentNameString = colonSplitter.next();
+ ComponentName enabledService = ComponentName.unflattenFromString(componentNameString);
+
+ if (enabledService != null && enabledService.equals(expectedComponentName))
+ return true;
+ }
+
+ return false;
+ }
+
+ public static final int STATE_WAIT = 0;
+ @SuppressWarnings("unchecked")
+ public static final List NO_ACTION = Collections.EMPTY_LIST;
+
+ public static final List actions = new ArrayList<>();
+ private static int state = STATE_WAIT;
+
+ public static boolean assistModeEnable = true;
+ private AccessibilityNodeInfo mLastFocus;
+
+ public static void setActions(Collection collection) {
+ synchronized (actions) {
+ actions.clear();
+ actions.addAll(collection);
+ state = STATE_WAIT;
+ }
+ }
+
+ public static ActionPerformService getInstance() {
+ return instance;
+ }
+
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ Log.v(TAG, "event type=" + event.getEventType());
+ AccessibilityNodeInfo nodeInfo = getRootInActiveWindow();
+ performAssistance(event, event.getSource());
+ Log.v(TAG, "rootInActiveWindow = " + nodeInfo);
+ if (nodeInfo == null)
+ return;
+ Log.v(TAG, "state = " + state);
+ Action action = nextAction();
+ if (action == null) {
+ reset();
+ } else if (action.perform(nodeInfo)) {
+ state++;
+ }
+ }
+
+ private void performAssistance(AccessibilityEvent event, AccessibilityNodeInfo nodeInfo) {
+ if (!assistModeEnable || nodeInfo == null)
+ return;
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED || event.getEventType() == AccessibilityEvent.TYPE_VIEW_LONG_CLICKED) {
+ nodeInfo.refresh();
+ Log.v(TAG, "click: " + nodeInfo);
+ Rect rect = new Rect();
+ nodeInfo.getBoundsInScreen(rect);
+ String str = rect.toString().replace('-', ',').replace(" ", "");
+ ClipboardManager manager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+ manager.setPrimaryClip(ClipData.newPlainText("", str));
+ Toast.makeText(this, "id=" + nodeInfo.getViewIdResourceName() + " bounds=" + str, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private int[] findAddress(AccessibilityNodeInfo nodeInfo) {
+ List address = new ArrayList<>();
+ AccessibilityNodeInfo lastNodeInfo = null;
+ while (nodeInfo.getParent() != null) {
+ address.add(findPositionInParent(nodeInfo));
+ if (lastNodeInfo != null)
+ lastNodeInfo.recycle();
+ lastNodeInfo = nodeInfo;
+ nodeInfo = nodeInfo.getParent();
+ }
+ int[] array = new int[address.size()];
+ for (int i = 0; i < address.size(); i++) {
+ array[i] = address.get(address.size() - i - 1);
+ }
+ return array;
+ }
+
+
+
+ private int findPositionInParent(AccessibilityNodeInfo nodeInfo) {
+ AccessibilityNodeInfo parent = nodeInfo.getParent();
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ AccessibilityNodeInfo child = parent.getChild(i);
+ if (child != null && child.equals(nodeInfo)) {
+ parent.recycle();
+ child.recycle();
+ return i;
+ }
+ if (child != null) {
+ child.recycle();
+ }
+ }
+ parent.recycle();
+ return -1;
+ }
+
+ private void reset() {
+ state = STATE_WAIT;
+ synchronized (actions) {
+ actions.clear();
+ }
+ }
+
+ private Action nextAction() {
+ synchronized (actions) {
+ if (state >= actions.size()) {
+ return null;
+ }
+ return actions.get(state);
+ }
+ }
+
+ @Override
+ public void onInterrupt() {
+
+ }
+
+ @Override
+ public void onServiceConnected() {
+ Log.v(TAG, "onServiceConnected");
+ Action.setActionContext(this);
+ instance = this;
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/data/ScriptFile.java b/app/src/main/java/com/stardust/scriptdroid/data/ScriptFile.java
new file mode 100644
index 00000000..ef49ddde
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/data/ScriptFile.java
@@ -0,0 +1,52 @@
+package com.stardust.scriptdroid.data;
+
+import android.content.Context;
+import android.os.Environment;
+
+import com.stardust.scriptdroid.action.Action;
+import com.stardust.scriptdroid.action.ActionPerformService;
+import com.stardust.scriptdroid.droid.Interpreter;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/1/23.
+ */
+
+public class ScriptFile {
+
+ public static final String DEFAULT_FOLDER = Environment.getExternalStorageDirectory() + "/脚本/";
+ public String name;
+
+ public String path;
+
+ public ScriptFile(String name, String path) {
+ this.name = name;
+ this.path = path;
+ }
+
+ public void run(Context context) {
+ Interpreter interpreter = new Interpreter(context);
+
+ try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
+ List actions = new ArrayList<>();
+ String line = reader.readLine();
+ while (line != null) {
+ actions.add(interpreter.interpreter(line));
+ line = reader.readLine();
+ }
+ ActionPerformService.setActions(actions);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public File toFile() {
+ return new File(path);
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/data/ScriptFileList.java b/app/src/main/java/com/stardust/scriptdroid/data/ScriptFileList.java
new file mode 100644
index 00000000..6cc46d75
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/data/ScriptFileList.java
@@ -0,0 +1,28 @@
+package com.stardust.scriptdroid.data;
+
+import java.io.File;
+
+/**
+ * Created by Stardust on 2017/1/23.
+ */
+
+public abstract class ScriptFileList {
+
+ public abstract void add(ScriptFile scriptFile);
+
+ public abstract ScriptFile get(int i);
+
+ public abstract void remove(int i);
+
+ public abstract void rename(int position, String newName);
+
+ public abstract int size();
+
+ public boolean deleteFromFileSystem(int i) {
+ File file = new File(get(i).path);
+ remove(i);
+ return file.delete();
+ }
+
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/data/SharedPrefScriptFileList.java b/app/src/main/java/com/stardust/scriptdroid/data/SharedPrefScriptFileList.java
new file mode 100644
index 00000000..694b43eb
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/data/SharedPrefScriptFileList.java
@@ -0,0 +1,86 @@
+package com.stardust.scriptdroid.data;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/1/23.
+ */
+
+public class SharedPrefScriptFileList extends ScriptFileList {
+
+ private static final Gson GSON = new Gson();
+
+ private static final String SP_KEY_SCRIPT_NAME = "script_name";
+ private static final String SP_KEY_SCRIPT_PATH = "script_path";
+
+ private SharedPreferences mSharedPreferences;
+ private List mScriptPath;
+ private List mScriptName;
+
+
+ public SharedPrefScriptFileList(Context context) {
+ mSharedPreferences = context.getSharedPreferences("SharedPrefScriptFileList", Context.MODE_PRIVATE);
+ readFromSharedPref();
+ }
+
+ private void readFromSharedPref() {
+ Type type = new TypeToken>() {
+ }.getType();
+ mScriptName = GSON.fromJson(mSharedPreferences.getString(SP_KEY_SCRIPT_NAME, ""), type);
+ mScriptPath = GSON.fromJson(mSharedPreferences.getString(SP_KEY_SCRIPT_PATH, ""), type);
+ if (mScriptName == null || mScriptPath == null || mScriptName.size() != mScriptPath.size()) {
+ reset();
+ }
+ }
+
+ private void reset() {
+ mScriptName = new ArrayList<>();
+ mScriptPath = new ArrayList<>();
+ syncWithSharedPref();
+ }
+
+ @Override
+ public void add(ScriptFile scriptFile) {
+ mScriptName.add(scriptFile.name);
+ mScriptPath.add(scriptFile.path);
+ syncWithSharedPref();
+ }
+
+ @Override
+ public ScriptFile get(int i) {
+ return new ScriptFile(mScriptName.get(i), mScriptPath.get(i));
+ }
+
+ @Override
+ public void remove(int i) {
+ mScriptName.remove(i);
+ mScriptPath.remove(i);
+ syncWithSharedPref();
+ }
+
+ @Override
+ public void rename(int position, String newName) {
+ mScriptName.set(position, newName);
+ syncWithSharedPref();
+ }
+
+ @Override
+ public int size() {
+ return mScriptPath.size();
+ }
+
+ protected void syncWithSharedPref() {
+ SharedPreferences.Editor editor = mSharedPreferences.edit();
+ editor.putString(SP_KEY_SCRIPT_NAME, GSON.toJson(mScriptName));
+ editor.putString(SP_KEY_SCRIPT_PATH, GSON.toJson(mScriptPath));
+ editor.apply();
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/Droid.java b/app/src/main/java/com/stardust/scriptdroid/droid/Droid.java
new file mode 100644
index 00000000..8b9dbfad
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/Droid.java
@@ -0,0 +1,29 @@
+package com.stardust.scriptdroid.droid;
+
+import android.content.Context;
+
+import com.stardust.scriptdroid.action.ActionPerformService;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+/**
+ * Created by Stardust on 2017/1/23.
+ */
+
+public class Droid {
+
+ public static void run(Context context, File file) {
+ Interpreter interpreter = new Interpreter(context);
+ try (FileInputStream fis = new FileInputStream(file)) {
+ byte[] bytes = new byte[fis.available()];
+ fis.read(bytes);
+ fis.close();
+ String str = new String(bytes);
+ ActionPerformService.setActions(interpreter.interpreterAll(str));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/Interpreter.java b/app/src/main/java/com/stardust/scriptdroid/droid/Interpreter.java
new file mode 100644
index 00000000..09466c8f
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/Interpreter.java
@@ -0,0 +1,210 @@
+package com.stardust.scriptdroid.droid;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Pair;
+
+import com.stardust.scriptdroid.action.Action;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD;
+import static android.view.accessibility.AccessibilityNodeInfo.ACTION_SELECT;
+
+/**
+ * Created by Stardust on 2017/1/21.
+ */
+
+public class Interpreter {
+
+ interface Filter {
+ boolean filter(String str);
+ }
+
+ private static final Filter FILTER_STRING = new Filter() {
+ @Override
+ public boolean filter(String str) {
+ return str.startsWith("\"") && str.endsWith("\"");
+ }
+ };
+
+
+ private static final Filter FILTER_ADDRESS = (str) -> str.startsWith("(") && str.endsWith(")");
+
+ private static final Filter FILTER_DESCRIPTION = new Filter() {
+ @Override
+ public boolean filter(String str) {
+ return str.startsWith("[") && str.endsWith("]");
+ }
+ };
+
+ private static final Filter FILTER_ANY = new Filter() {
+ @Override
+ public boolean filter(String str) {
+ return true;
+ }
+ };
+
+ private static class StateTree {
+
+ private List> mChildren = new LinkedList<>();
+
+ StateTree child(final String str, StateTree child) {
+ return child(new Filter() {
+ @Override
+ public boolean filter(String s) {
+ return str.equals(s);
+ }
+ }, child);
+ }
+
+ StateTree child(Filter filter, StateTree child) {
+ mChildren.add(new Pair<>(filter, child));
+ return this;
+ }
+
+ StateLeaf match(String[] command, int startIndex) {
+ for (Pair child : mChildren) {
+ if (child.first.filter(command[startIndex])) {
+ return child.second.match(command, startIndex + 1);
+ }
+ }
+ return null;
+ }
+
+ public StateTree child(String command, final int action) {
+ return child(command, new StateTree()
+ .child(FILTER_STRING, new StateLeaf() {
+ @Override
+ Action handle(String[] command) {
+ return Action.FindUpwardlyFilterAction.createActionByText(action, StringTool.removeDoubleQuotes(command[1]));
+ }
+ })
+ .child(FILTER_DESCRIPTION, new StateLeaf() {
+ @Override
+ Action handle(String[] command) {
+ return Action.FindUpwardlyFilterAction.createActionByDescription(action, StringTool.removeDoubleQuotes(command[1]));
+ }
+ })
+ .child(FILTER_ADDRESS, new StateLeaf() {
+ @Override
+ Action handle(String[] command) {
+ String[] intStrings = StringTool.removeDoubleQuotes(command[1]).split(",");
+ int[] bounds = Stream.of(intStrings).mapToInt(Integer::parseInt).toArray();
+ Rect rect = new Rect(bounds[0], bounds[1], bounds[2], bounds[3]);
+ return Action.FindDownwardlyDfsFilterAction.createActionByBounds(action, rect);
+ }
+ })
+ .child(FILTER_ANY, new StateLeaf() {
+ @Override
+ Action handle(String[] command) {
+ return Action.FindUpwardlyFilterAction.createActionById(action, StringTool.removeDoubleQuotes(command[1]));
+ }
+ }));
+ }
+ }
+
+ private abstract static class StateLeaf extends StateTree {
+
+ abstract Action handle(String[] command);
+
+ StateLeaf match(String[] command, int startIndex) {
+ return this;
+ }
+ }
+
+ private Context mContext;
+
+ private StateTree stateTree = new StateTree()
+ .child("click", ACTION_CLICK)
+ .child("longclick", ACTION_LONG_CLICK)
+ .child("focus", ACTION_FOCUS)
+ .child("select", ACTION_SELECT)
+ .child("forward", ACTION_SCROLL_FORWARD)
+ .child("backward", ACTION_SCROLL_BACKWARD)
+ .child("scrollup", new StateLeaf() {
+ @Override
+ Action handle(String[] command) {
+ return new Action.ScrollAction(Action.ScrollAction.SCROLL_BACKWARD);
+ }
+ }).child("scrolldown", new StateLeaf() {
+ @Override
+ Action handle(String[] command) {
+ return new Action.ScrollAction(Action.ScrollAction.SCROLL_FORWARD);
+ }
+ }).child("input", new StateTree().child(FILTER_DESCRIPTION, new StateLeaf() {
+ @Override
+ Action handle(String[] command) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return new Action.InputAction(StringTool.removeDoubleQuotes(command[1]), command[2]);
+ } else {
+ return null;
+ }
+ }
+ })).child("open", new StateTree()
+ .child(FILTER_STRING, new StateLeaf() {
+ @Override
+ Action handle(String[] command) {
+ String appName = StringTool.removeDoubleQuotes(command[1]);
+ List installedApplications = mContext.getPackageManager().getInstalledApplications(PackageManager.GET_META_DATA);
+ for (ApplicationInfo applicationInfo : installedApplications) {
+ if (mContext.getPackageManager().getApplicationLabel(applicationInfo).toString().equals(appName)) {
+ return new Action.IntentAction(mContext.getPackageManager().getLaunchIntentForPackage(applicationInfo.packageName));
+ }
+ }
+ return null;
+ }
+ }).child(FILTER_ANY, new StateLeaf() {
+ @Override
+ Action handle(String[] command) {
+ return new Action.IntentAction(mContext.getPackageManager().getLaunchIntentForPackage(command[1]));
+ }
+ }));
+
+
+ public Interpreter(Context context) {
+ mContext = context;
+ }
+
+ public Action interpreterInner(String line) {
+ String[] words = line.split(" ");
+ return stateTree.match(words, 0).handle(words);
+ }
+
+ public Action interpreter(String line) {
+ boolean independent = true;
+ String[] actions = line.split("\\|");
+ if (actions.length == 1) {
+ actions = line.split("\\&");
+ independent = false;
+ }
+ if (actions.length == 1)
+ return interpreterInner(line);
+ List list = new ArrayList<>(actions.length);
+ for (String action : actions) {
+ list.add(interpreterInner(action));
+ }
+ return new Action.MultiAction(list, independent);
+ }
+
+
+ public List interpreterAll(String str) {
+ String[] lines = str.split("\n");
+ List list = new ArrayList<>(lines.length);
+ for (String line : lines) {
+ list.add(interpreter(line));
+ }
+ return list;
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/StringTool.java b/app/src/main/java/com/stardust/scriptdroid/droid/StringTool.java
new file mode 100644
index 00000000..06816efe
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/StringTool.java
@@ -0,0 +1,10 @@
+package com.stardust.scriptdroid.droid;
+
+/**
+ * Created by Stardust on 2017/1/22.
+ */
+public class StringTool {
+ public static String removeDoubleQuotes(String str) {
+ return str.substring(1, str.length() - 1);
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/droid/SyntaxDefinition.java b/app/src/main/java/com/stardust/scriptdroid/droid/SyntaxDefinition.java
new file mode 100644
index 00000000..776012c8
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/droid/SyntaxDefinition.java
@@ -0,0 +1,30 @@
+package com.stardust.scriptdroid.droid;
+
+/**
+ * Created by Stardust on 2017/1/21.
+ */
+
+public class SyntaxDefinition {
+
+ /**
+ * click 文本
+ * 点击文本所在区域。e.g. click 朋友圈
+ * click 编号
+ * 点击编号对应区域
+ * longclick 文本
+ *
+ * longclick 编号
+ *
+ * scrollup
+ *
+ * scrolldown
+ *
+ * scrollup 编号 [幅度]
+ *
+ * scrolldown 编号 [幅度]
+ *
+ *
+ *
+ */
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/file/FileChooser.java b/app/src/main/java/com/stardust/scriptdroid/file/FileChooser.java
new file mode 100644
index 00000000..87f2f570
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/file/FileChooser.java
@@ -0,0 +1,65 @@
+package com.stardust.scriptdroid.file;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.net.Uri;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+import static android.app.Activity.RESULT_OK;
+
+/**
+ * Created by Stardust on 2017/1/23.
+ */
+
+public class FileChooser {
+
+ public interface FileManagerNotFoundHandler {
+ void handle(ActivityNotFoundException e, String mimeType);
+ }
+
+ public interface OnFileChoseListener {
+ void onFileChose(InputStream inputStream);
+ }
+
+ private static final int FILE_CHOOSE = 1209;
+ private Activity mActivity;
+
+ private OnFileChoseListener mOnFileChoseListener;
+
+ public FileChooser(Activity activity) {
+ mActivity = activity;
+ }
+
+ public void setOnFileChoseListener(OnFileChoseListener onFileChoseListener) {
+ mOnFileChoseListener = onFileChoseListener;
+ }
+
+
+ public void startFileManagerToChoose(String mimeType, FileManagerNotFoundHandler handler) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType(mimeType);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ try {
+ mActivity.startActivityForResult(Intent.createChooser(intent, "选择一个文件"), FILE_CHOOSE);
+ } catch (ActivityNotFoundException ex) {
+ handler.handle(ex, mimeType);
+ }
+ }
+
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == FILE_CHOOSE && resultCode == RESULT_OK) {
+ Uri uri = data.getData();
+ ContentResolver cr = mActivity.getContentResolver();
+ try {
+ InputStream inputStream = cr.openInputStream(uri);
+ mOnFileChoseListener.onFileChose(inputStream);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/file/FileUtils.java b/app/src/main/java/com/stardust/scriptdroid/file/FileUtils.java
new file mode 100644
index 00000000..eb396ec5
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/file/FileUtils.java
@@ -0,0 +1,79 @@
+package com.stardust.scriptdroid.file;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.net.URISyntaxException;
+
+/**
+ * Created by Stardust on 2017/1/23.
+ */
+
+public class FileUtils {
+
+ public static String getPath(Context context, Uri uri) throws URISyntaxException {
+ if ("content".equalsIgnoreCase(uri.getScheme())) {
+ String[] projection = {"_data"};
+ Cursor cursor = null;
+
+ try {
+ cursor = context.getContentResolver().query(uri, projection, null, null, null);
+ int column_index = cursor.getColumnIndexOrThrow("_data");
+ if (cursor.moveToFirst()) {
+ return cursor.getString(column_index);
+ }
+ } catch (Exception e) {
+ // Eat it
+ }
+ } else if ("file".equalsIgnoreCase(uri.getScheme())) {
+ return uri.getPath();
+ }
+
+ return null;
+ }
+
+ public static String getPath(InputStream inputStream) {
+ if (inputStream instanceof FileInputStream) {
+ FileInputStream fis = (FileInputStream) inputStream;
+ try {
+ Field field = fis.getClass().getDeclaredField("path");
+ field.setAccessible(true);
+ return (String) field.get(fis);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }
+
+ public static boolean createFileIfNotExists(String path) {
+ ensureFolder(path);
+ File file = new File(path);
+ if (!file.exists()) {
+ try {
+ return file.createNewFile();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return false;
+ }
+
+ private static boolean ensureFolder(String path) {
+ int i = path.lastIndexOf("\\");
+ if (i < 0)
+ i = path.lastIndexOf("/");
+ if (i >= 0) {
+ String folder = path.substring(i);
+ return new File(folder).mkdirs();
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/runningservices/RunningPackageTool.java b/app/src/main/java/com/stardust/scriptdroid/runningservices/RunningPackageTool.java
new file mode 100644
index 00000000..1e58da6f
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/runningservices/RunningPackageTool.java
@@ -0,0 +1,78 @@
+package com.stardust.scriptdroid.runningservices;
+
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.app.usage.UsageStats;
+import android.app.usage.UsageStatsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.provider.Settings;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/1/20.
+ */
+
+public class RunningPackageTool {
+
+
+ public static String getRunningPackage(Context context) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return getRunningPackageLollipop(context);
+ } else {
+ return getRunningPackageBeforeLollipop(context);
+ }
+
+ }
+
+ private static String getRunningPackageBeforeLollipop(Context context) {
+ ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ @SuppressWarnings("deprecation")
+ ComponentName runningActivity = am.getRunningTasks(1).get(0).topActivity;
+ return runningActivity.getPackageName();
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private static String getRunningPackageLollipop(Context context) {
+ List usageStats = getPastTwoSecondsUsageStats(context);
+ UsageStats latestStats = findLatestUsageStats(usageStats);
+ if (latestStats == null)
+ return null;
+ return latestStats.getPackageName();
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private static void goToUsageAccessSettings(Context context) {
+ Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
+ context.startActivity(intent);
+ }
+
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private static UsageStats findLatestUsageStats(List usageStats) {
+ UsageStats latestStats = null;
+ for (UsageStats us : usageStats) {
+ if (latestStats == null || latestStats.getLastTimeUsed() < us.getLastTimeUsed()) {
+ latestStats = us;
+ }
+ }
+ return latestStats;
+ }
+
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
+ @SuppressWarnings("unchecked")
+ private static List getPastTwoSecondsUsageStats(Context context) {
+ long ts = System.currentTimeMillis();
+ UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
+ List usageStats = usageStatsManager.queryUsageStats(UsageStatsManager.INTERVAL_BEST, ts - 2000, ts);
+ if (usageStats == null) {
+ usageStats = Collections.EMPTY_LIST;
+ }
+ return usageStats;
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/shortcut/Shortcut.java b/app/src/main/java/com/stardust/scriptdroid/shortcut/Shortcut.java
new file mode 100644
index 00000000..937cab59
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/shortcut/Shortcut.java
@@ -0,0 +1,114 @@
+package com.stardust.scriptdroid.shortcut;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Created by Stardust on 2017/1/20.
+ */
+
+public class Shortcut {
+
+ private Context mContext;
+ private String mName;
+ private String mTargetClass;
+ private String mTargetPackage;
+ private Intent.ShortcutIconResource mIcon;
+ private boolean mDuplicate = false;
+ private Intent mLaunchIntent = new Intent();
+
+ public Shortcut(Context context) {
+ mContext = context;
+ mTargetPackage = mContext.getPackageName();
+ }
+
+ public Shortcut(String name, Context context) {
+ this(context);
+ mName = name;
+ }
+
+ public Shortcut name(String name) {
+ mName = name;
+ return this;
+ }
+
+ public Shortcut targetPackage(String targetPackage) {
+ mTargetPackage = targetPackage;
+ return this;
+ }
+
+ public Shortcut targetClass(String targetClass) {
+ mTargetClass = targetClass;
+ return this;
+ }
+
+ public Shortcut targetClass(Class> targetClass) {
+ mTargetClass = targetClass.getName();
+ return this;
+ }
+
+
+ public Shortcut icon(Intent.ShortcutIconResource icon) {
+ mIcon = icon;
+ return this;
+ }
+
+ public Shortcut icon(int resId) {
+ mIcon = Intent.ShortcutIconResource.fromContext(mContext, resId);
+ return this;
+ }
+
+ public Shortcut duplicate(boolean duplicate) {
+ mDuplicate = duplicate;
+ return this;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public Intent.ShortcutIconResource getIcon() {
+ return mIcon;
+ }
+
+ public boolean isDuplicate() {
+ return mDuplicate;
+ }
+
+ private String getClassName() {
+ return mTargetClass;
+ }
+
+ private String getPackageName() {
+ return mTargetPackage;
+ }
+
+ public Intent getCreateIntent() {
+ return new Intent(Intent.ACTION_CREATE_SHORTCUT)
+ .putExtra(Intent.EXTRA_SHORTCUT_NAME, getName())
+ .putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, getIcon())
+ .putExtra(Intent.EXTRA_SHORTCUT_INTENT, getLaunchIntent())
+ .putExtra("duplicate", isDuplicate())
+ .setAction("com.android.launcher.action.INSTALL_SHORTCUT");
+ }
+
+ public Intent getLaunchIntent() {
+ mLaunchIntent.setClassName(getPackageName(), getClassName());
+ return mLaunchIntent;
+ }
+
+ public void send() {
+ mContext.sendBroadcast(getCreateIntent());
+ }
+
+ public Shortcut extras(Bundle bundle) {
+ mLaunchIntent.putExtras(bundle);
+ return this;
+ }
+
+ public Shortcut extras(Intent intent) {
+ mLaunchIntent.putExtras(intent);
+ return this;
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/shortcut/ShortcutHelper.java b/app/src/main/java/com/stardust/scriptdroid/shortcut/ShortcutHelper.java
new file mode 100644
index 00000000..e80ba7ff
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/shortcut/ShortcutHelper.java
@@ -0,0 +1,418 @@
+package com.stardust.scriptdroid.shortcut;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.stardust.scriptdroid.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ShortcutHelper {
+ public final static String TAG = ShortcutHelper.class.getSimpleName();
+ public static final String ACTION_DESKTOP_LINK = "com.scu.zqc.action.SHORTCUT";
+ public static final String READ_SETTINGS = "com.android.launcher.permission.READ_SETTINGS";
+ public static final String ACTION_INSTALL_SHORTCUT = "com.android.launcher.action.INSTALL_SHORTCUT";
+ public static final String ACTION_UNINSTALL_SHORTCUT = "com.android.launcher.action.UNINSTALL_SHORTCUT";
+ public static final String MAIN_ACTIVITY = "com.scu.shortcut.MainActivity";
+ public static final String APP_STORE_URL = "http://m.app.so.com/?src=browser";
+ public static final String EXTRA_DUPLICATE = "duplicate";
+
+ public static void addShortCut(final Context context, Bitmap src) {
+ String appStoreShortcutName = context.getString(R.string.app_name);
+ if (getIsAddShortCut(context, appStoreShortcutName)) {
+// uninstallShortcut(context, appStoreShortcutName);
+ try {
+ int size = (int) context.getResources().getDimension(android.R.dimen.app_icon_size);
+ Bitmap scaledBitmap = Bitmap.createScaledBitmap(src, size, size, true);
+ createFavLinkShortCut(context, appStoreShortcutName, APP_STORE_URL, scaledBitmap, false, true);
+ } catch (Exception e) {
+ }
+ }
+ }
+
+ public static boolean uninstallShortcut(Context context, String name, Intent shortCut) {
+ Intent intent = new Intent(ACTION_UNINSTALL_SHORTCUT);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortCut);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
+ context.sendBroadcast(intent);
+ return true;
+ }
+
+ public static boolean createFavLinkShortCut(Context context, String name, String url, Parcelable icon,
+ boolean isSendToQihooDesktop, boolean skipCheckExist) {
+ if (context == null || TextUtils.isEmpty(url) || TextUtils.isEmpty(name) || icon == null) {
+ return false;
+ }
+ // 先查询launcher页面是否已经生成其快捷方式
+ if (getIsAddShortCut(context, name) && !skipCheckExist) {
+ return false;
+ }
+
+ Intent shortcutIntent = getShortCutIntent(context, url, MAIN_ACTIVITY);
+ shortcutIntent.setAction(ACTION_DESKTOP_LINK);
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ Intent installIntent = getInstallIntent(shortcutIntent, name, icon);
+
+ // 将创建快捷方式的intent指定到桌面应用程序包来处理.
+ String deskPackageName = getLauncherPackageName(context);
+ if (null != deskPackageName && !TextUtils.isEmpty(deskPackageName)) {
+ installIntent.setPackage(deskPackageName);
+ }
+
+ if (isSendToQihooDesktop) {
+ installIntent.putExtra("from", context.getPackageName());
+ }
+
+ context.sendBroadcast(installIntent);
+ return true;
+ }
+
+ /**
+ * 获取正在运行桌面包名(注:存在多个桌面时且未指定默认桌面时,该方法返回Null,使用时需处理这个情况)
+ */
+ public static String getLauncherPackageName(Context context) {
+ try {
+ final Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory(Intent.CATEGORY_HOME);
+ final ResolveInfo res = context.getPackageManager().resolveActivity(intent, 0);
+ if (res != null && res.activityInfo == null) {
+ // should not happen. A home is always installed, isn't it?
+ return null;
+ }
+
+ if (res != null && ("android").equals(res.activityInfo.packageName)) {
+ // 有多个桌面程序存在,且未指定默认项时;
+ return null;
+ } else {
+ return res.activityInfo.packageName;
+ }
+ } catch (Exception e) {
+
+ }
+
+ return null;
+ }
+
+ private static Intent getInstallIntent(Context context, Intent shortcutIntent, int name, int res) {
+ Intent intent = getInstallIntent(shortcutIntent, context.getString(name), null);
+ Parcelable icon = Intent.ShortcutIconResource.fromContext(context, res);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon);
+ return intent;
+ }
+
+ private static Intent getInstallIntent(Intent shortcutIntent, String name, Parcelable iconResource) {
+ Intent intent = new Intent(ACTION_INSTALL_SHORTCUT);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+
+ if (TextUtils.isEmpty(name)) {
+ if (Build.VERSION.SDK_INT < 11 || "SM-N9008V".equals(Build.MODEL)) {
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, "添加");
+ } else {
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, "\t");
+ }
+ } else {
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
+ }
+ if (iconResource != null) {
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, iconResource);
+ }
+ intent.putExtra(EXTRA_DUPLICATE, false);
+ return intent;
+ }
+
+ //判断应用是否安装
+ public static boolean isPkgInstalled(Context c, String pkgName) {
+ PackageManager mPm = c.getPackageManager();
+ if (mPm == null) {
+ return false;
+ }
+ PackageInfo pkginfo = null;
+ try {
+ pkginfo = mPm.getPackageInfo(pkgName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ //e.printStackTrace();
+ }
+ return pkginfo == null ? false : true;
+ }
+
+ public static Intent getShortCutIntent(Context context, String action, String className) {
+ Intent shortcutIntent = new Intent(Intent.ACTION_MAIN);
+ if ("NX511J".equals(Build.MODEL)) {
+ shortcutIntent.setPackage(context.getPackageName());
+ } else {
+ shortcutIntent.setClassName(context, className);
+ }
+ shortcutIntent.setData(Uri.parse(action));
+ shortcutIntent.putExtra("value", "test");
+ return shortcutIntent;
+ }
+
+ public static boolean createShortCutWithIntentInSilence(Context context, String name, Intent shortCutIntent, Parcelable icon, boolean isSendToQihooDesktop) {
+ if (context == null || TextUtils.isEmpty(name) || icon == null) {
+ return false;
+ }
+ // 先查询launcher页面是否已经生成其快捷方式
+ if (getIsAddShortCut(context, name)) {
+ return false;
+ }
+ Intent installIntent = getInstallIntent(shortCutIntent, name, icon);
+ // 将创建快捷方式的intent指定到桌面应用程序包来处理.
+ String deskPackageName = getLauncherPackageName(context);
+ if (null != deskPackageName && !TextUtils.isEmpty(deskPackageName)) {
+ installIntent.setPackage(deskPackageName);
+ }
+ if (isSendToQihooDesktop) {
+ installIntent.putExtra("from", context.getPackageName());
+ }
+ context.sendBroadcast(installIntent);
+ return true;
+ }
+
+ public static boolean createFavLinkShortCutWithShortcutIntent(Context context, String name, String url, Parcelable icon,
+ boolean isSendToQihooDesktop, boolean skipCheckExist, Intent shortcutIntent) {
+
+ if (context == null || TextUtils.isEmpty(url) || TextUtils.isEmpty(name) || icon == null) {
+ return false;
+ }
+
+ // 先查询launcher页面是否已经生成其快捷方式
+ if (getIsAddShortCut(context, name) && !skipCheckExist) {
+ return false;
+ }
+
+ Intent installIntent = getInstallIntent(shortcutIntent, name, icon);
+
+ // 将创建快捷方式的intent指定到桌面应用程序包来处理.
+ String deskPackageName = getLauncherPackageName(
+ context);
+
+ if (null != deskPackageName && !TextUtils.isEmpty(deskPackageName)) {
+ installIntent.setPackage(deskPackageName);
+ }
+
+ if (isSendToQihooDesktop) {
+ installIntent.putExtra("from", context.getPackageName());
+ }
+
+ context.sendBroadcast(installIntent);
+ return true;
+ }
+
+ public static boolean getIsAddShortCut(Context context, String name) {
+ // 如果只是按照title来比较是否存在的话可能存在重名应用,所以还需要根据intent这个字段一起判断。
+ String intentTag = "com.scu.zqc";
+ Cursor c = null;
+ try {
+ final ContentResolver cr = context.getContentResolver();
+ String AUTHORITY = getAuthority(context);
+ Log.d(TAG, "AUTHORITY = " + AUTHORITY);
+
+ if (null == AUTHORITY) {
+ AUTHORITY = getSuitableAuthority(context);
+ }
+
+ final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/favorites?notify=true");
+ c = cr.query(CONTENT_URI, new String[]{"title", "iconResource"},
+ "title=? and intent like ?", new String[]{name, "%" + intentTag + "%"}, null);
+
+ if (null != c && c.getCount() > 0) {
+ Log.d(TAG, "the shortCut of" + name + "has been created!");
+ return true;
+ } else {
+ if (c != null) {
+ c.close();
+ }
+ // 有的手机上需要用下面的方法才能获取到快捷方式是否存在
+ AUTHORITY = getAuthorityFromPermission(context, READ_SETTINGS);
+ final Uri CONTENT_URI_AUTHORITY = Uri.parse("content://" + AUTHORITY +
+ "/favorites?notify=true");
+ c = cr.query(CONTENT_URI_AUTHORITY, new String[]{"title", "iconResource"},
+ "title=? and intent like ?", new String[]{name, "%" + intentTag + "%"}, null);
+
+ if (null != c && c.getCount() > 0) {
+ Log.d(TAG, "the shortCut of" + name + "has been created!");
+ return true;
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "exception: " + e.getMessage());
+ return getIsAddShortCutFromPermission(context, name);
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean getIsAddShortCutFromPermission(Context context, String name) {
+ Cursor c = null;
+ String intentTag = context.getPackageName();
+ String AUTHORITY = getAuthorityFromPermission(context, READ_SETTINGS);
+ try {
+ final ContentResolver cr = context.getContentResolver();
+ final Uri CONTENT_URI_AUTHORITY = Uri.parse("content://" + AUTHORITY +
+ "/favorites?notify=true");
+ c = cr.query(CONTENT_URI_AUTHORITY, new String[]{"title", "iconResource"},
+ "title=? and intent like ?", new String[]{name, "%" + intentTag + "%"}, null);
+ if (null != c && c.getCount() > 0) {
+ Log.d(TAG, "the shortCut of" + name + "has been created!");
+ return true;
+ }
+ } catch (Exception e) {
+ // : handle exception
+ Log.e(TAG, "exception: " + e.getMessage());
+ } finally {
+ if (null != c) {
+ c.close();
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 通过permission来找不同机型的Authority
+ *
+ * @return
+ */
+ public static String getAuthority(Context context) {
+ String authority = ".launcher.settings";
+ String authority2 = ".launcher2.settings";
+ String authority3 = ".launcher3.settings";
+ List packs = null;
+ try {
+ packs = context.getPackageManager().getInstalledPackages(
+ PackageManager.GET_PROVIDERS);
+
+ for (PackageInfo pack : packs) {
+ ProviderInfo[] providers = pack.providers;
+
+ if (null != providers) {
+ for (ProviderInfo provider : providers) {
+ if (!TextUtils.isEmpty(provider.authority) && (provider.authority.contains(authority) ||
+ provider.authority.contains(authority2) ||
+ provider.authority.contains(authority3))) {
+ return provider.authority;
+ }
+ }
+ }
+ }
+ } catch (OutOfMemoryError e) {
+ e.printStackTrace();
+ packs = null;
+ }
+ return null;
+ }
+
+ private static String getAuthorityFromPermission(Context context,
+ String permission) {
+ if (permission == null) {
+ return null;
+ }
+ try {
+ List packs = context.getPackageManager()
+ .getInstalledPackages(PackageManager.GET_PROVIDERS);
+
+ if (packs != null) {
+ for (PackageInfo pack : packs) {
+ ProviderInfo[] providers = pack.providers;
+
+ if (providers != null) {
+ for (ProviderInfo provider : providers) {
+ if (permission.equals(provider.readPermission)) {
+ return provider.authority;
+ }
+
+ if (permission.equals(provider.writePermission)) {
+ return provider.authority;
+ }
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ /**
+ * 获取合适的Authority访问launcher数据库
+ * 优先级:".launcher3.settings" > ".launcher2.settings" > ".launcher.settings" > ".settings"
+ */
+ public static String getSuitableAuthority(Context context) {
+ String AUTHORITY = null;
+ String authority = ".launcher.settings";
+ String authority2 = ".launcher2.settings";
+ String authority3 = ".launcher3.settings";
+ String authority4 = ".settings";
+
+ List packs = context.getPackageManager().getInstalledPackages(
+ PackageManager.GET_PROVIDERS);
+
+ List authorityList = new ArrayList();
+ for (PackageInfo pack : packs) {
+ ProviderInfo[] providers = pack.providers;
+
+ if (null != providers) {
+ for (ProviderInfo provider : providers) {
+ if (!TextUtils.isEmpty(provider.authority) && (provider.authority.contains(authority)
+ || provider.authority.contains(authority2) ||
+ provider.authority.contains(authority3))) {
+ authorityList.add(provider.authority);
+ }
+ }
+ }
+ }
+ String AUTHORITY_TEMP_1 = null;
+ String AUTHORITY_TEMP_2 = null;
+ String AUTHORITY_TEMP_3 = null;
+
+ for (String item : authorityList) {
+ if (item.contains(authority3) && TextUtils.isEmpty(AUTHORITY_TEMP_3)) {
+ AUTHORITY_TEMP_3 = item;
+ } else if (item.contains(authority2) && TextUtils.isEmpty(AUTHORITY_TEMP_2)) {
+ AUTHORITY_TEMP_2 = item;
+ } else if (item.contains(authority) && TextUtils.isEmpty(AUTHORITY_TEMP_1)) {
+ AUTHORITY_TEMP_1 = item;
+ }
+ }
+ if (!TextUtils.isEmpty(AUTHORITY_TEMP_3)) {
+ AUTHORITY = AUTHORITY_TEMP_3;//优先适配launcher3
+ } else if (!TextUtils.isEmpty(AUTHORITY_TEMP_2)) {
+ AUTHORITY = AUTHORITY_TEMP_2;
+ } else if (!TextUtils.isEmpty(AUTHORITY_TEMP_1)) {
+ AUTHORITY = AUTHORITY_TEMP_1;
+ }
+ if (TextUtils.isEmpty(AUTHORITY)) {
+ for (PackageInfo pack : packs) {
+ ProviderInfo[] providers = pack.providers;
+
+ if (null != providers) {
+ for (ProviderInfo provider : providers) {
+ if (!TextUtils.isEmpty(provider.authority) && provider.authority.endsWith(authority4)) {
+ return provider.authority;
+ }
+ }
+ }
+ }
+ }
+ return AUTHORITY;
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/tile/TileService.java b/app/src/main/java/com/stardust/scriptdroid/tile/TileService.java
new file mode 100644
index 00000000..ecb234b7
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/tile/TileService.java
@@ -0,0 +1,14 @@
+package com.stardust.scriptdroid.tile;
+
+import com.stardust.scriptdroid.action.ActionPerformService;
+
+/**
+ * Created by Stardust on 2017/1/26.
+ */
+
+public class TileService extends android.service.quicksettings.TileService {
+
+ public void onClick(){
+ ActionPerformService.assistModeEnable = !ActionPerformService.assistModeEnable;
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/tool/ClassTool.java b/app/src/main/java/com/stardust/scriptdroid/tool/ClassTool.java
new file mode 100644
index 00000000..b36e3619
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/tool/ClassTool.java
@@ -0,0 +1,22 @@
+package com.stardust.scriptdroid.tool;
+
+/**
+ * Created by Stardust on 2017/1/23.
+ */
+
+public class ClassTool {
+
+ public static void loadClass(Class c) {
+ try {
+ Class.forName(c.getName());
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static void loadClasses(Class... classes) {
+ for (Class c : classes) {
+ loadClass(c);
+ }
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/tool/ViewTool.java b/app/src/main/java/com/stardust/scriptdroid/tool/ViewTool.java
new file mode 100644
index 00000000..972c5a52
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/tool/ViewTool.java
@@ -0,0 +1,16 @@
+package com.stardust.scriptdroid.tool;
+
+import android.support.annotation.IdRes;
+import android.view.View;
+
+/**
+ * Created by Stardust on 2017/1/24.
+ */
+
+public class ViewTool {
+
+ @SuppressWarnings("unchecked")
+ public static V $(View view, @IdRes int resId) {
+ return (V) view.findViewById(resId);
+ }
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/ScriptFileOperation.java b/app/src/main/java/com/stardust/scriptdroid/ui/ScriptFileOperation.java
new file mode 100644
index 00000000..f6bdf138
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/ScriptFileOperation.java
@@ -0,0 +1,149 @@
+package com.stardust.scriptdroid.ui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.design.widget.Snackbar;
+
+import com.afollestad.materialdialogs.MaterialDialog;
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.ShortcutActivity;
+import com.stardust.scriptdroid.data.ScriptFile;
+import com.stardust.scriptdroid.data.ScriptFileList;
+import com.stardust.scriptdroid.shortcut.Shortcut;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/1/23.
+ */
+
+public abstract class ScriptFileOperation {
+
+ private static List operationNames = new ArrayList<>();
+ private static List operations = new ArrayList<>();
+
+ public static ScriptFileOperation getOperation(int index) {
+ return operations.get(index);
+ }
+
+ public static List getOperationNames() {
+ return operationNames;
+ }
+
+ public abstract void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position);
+
+ private static void addOperation(String name, int iconResId, ScriptFileOperation operation) {
+ operation.mName = name;
+ operation.mIconResId = iconResId;
+ operationNames.add(name);
+ operations.add(operation);
+ }
+
+ private String mName;
+ private int mIconResId;
+
+ public String getName() {
+ return mName;
+ }
+
+ public int getIconResId() {
+ return mIconResId;
+ }
+
+ public static class Run extends ScriptFileOperation {
+
+ static {
+ addOperation("运行", R.drawable.ic_play_green, new Run());
+ }
+
+ @Override
+ public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
+ Snackbar.make(recyclerView, "开始运行", Snackbar.LENGTH_SHORT).show();
+ ScriptFile scriptFile = scriptFileList.get(position);
+ scriptFile.run(recyclerView.getContext());
+ }
+ }
+
+ public static class Edit extends ScriptFileOperation {
+
+ static {
+ addOperation("编辑", R.drawable.ic_edit_green_48dp, new Edit());
+ }
+
+ @Override
+ public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
+ Context context = recyclerView.getContext();
+ ScriptFile scriptFile = scriptFileList.get(position);
+ Uri uri = Uri.parse("file://" + scriptFile.path);
+ context.startActivity(new Intent(Intent.ACTION_EDIT).setDataAndType(uri, "text/plain"));
+ }
+ }
+
+ public static class Rename extends ScriptFileOperation {
+
+ static {
+ addOperation("重命名", R.drawable.ic_rename_green, new Rename());
+ }
+
+ @Override
+ public void operate(final ScriptListRecyclerView recyclerView, final ScriptFileList scriptFileList, final int position) {
+ String oldName = scriptFileList.get(position).name;
+ new MaterialDialog.Builder(recyclerView.getContext()).title("重命名")
+ .input("输入新名称", oldName, (dialog, input) -> {
+ scriptFileList.rename(position, input.toString());
+ recyclerView.getAdapter().notifyItemChanged(position);
+ }).show();
+ }
+ }
+
+ public static class CreateShortcut extends ScriptFileOperation {
+
+ static {
+ addOperation("创建桌面快捷方式", R.drawable.ic_shortcut_green, new CreateShortcut());
+ }
+
+
+ @Override
+ public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
+ Context context = recyclerView.getContext();
+ ScriptFile scriptFile = scriptFileList.get(position);
+ new Shortcut(context).name(scriptFile.name)
+ .targetClass(ShortcutActivity.class)
+ .icon(R.drawable.script_droid)
+ .extras(new Intent().putExtra("path", scriptFile.path))
+ .send();
+ Snackbar.make(recyclerView, "已创建", Snackbar.LENGTH_SHORT).show();
+ }
+ }
+
+ public static class Remove extends ScriptFileOperation {
+
+ static {
+ addOperation("删除", R.drawable.ic_delete_green_48dp, new Remove());
+ }
+
+ @Override
+ public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
+ scriptFileList.remove(position);
+ recyclerView.getAdapter().notifyItemRemoved(position);
+ }
+ }
+
+ public static class Delete extends ScriptFileOperation {
+
+ static {
+ addOperation("彻底删除", R.drawable.ic_delete_forever_green_48dp, new Delete());
+ }
+
+ @Override
+ public void operate(ScriptListRecyclerView recyclerView, ScriptFileList scriptFileList, int position) {
+ boolean succeed = scriptFileList.deleteFromFileSystem(position);
+ Snackbar.make(recyclerView, succeed ? "已删除" : "文件删除失败", Snackbar.LENGTH_SHORT).show();
+ recyclerView.getAdapter().notifyItemRemoved(position);
+ }
+ }
+
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/ScriptFileOperationPopupMenu.java b/app/src/main/java/com/stardust/scriptdroid/ui/ScriptFileOperationPopupMenu.java
new file mode 100644
index 00000000..bffb9c3c
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/ScriptFileOperationPopupMenu.java
@@ -0,0 +1,135 @@
+package com.stardust.scriptdroid.ui;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import com.stardust.scriptdroid.R;
+
+import static com.stardust.scriptdroid.tool.ViewTool.$;
+
+/**
+ * Created by Stardust on 2017/1/24.
+ */
+
+public class ScriptFileOperationPopupMenu extends PopupWindow {
+
+
+ public interface OnItemClickListener {
+ void onClick(View view, int position);
+ }
+
+ private Context mContext;
+ private ScriptFileOperationListRecyclerView mOperationListRecyclerView;
+ private OnItemClickListener mOnItemClickListener;
+
+ private final View.OnClickListener mOnItemClickRealListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOnItemClickListener != null) {
+ int position = mOperationListRecyclerView.getChildViewHolder(v).getAdapterPosition();
+ mOnItemClickListener.onClick(v, position);
+ }
+ }
+ };
+
+ public ScriptFileOperationPopupMenu(Context context) {
+ super();
+ mContext = context;
+ init();
+ }
+
+ public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
+ mOnItemClickListener = onItemClickListener;
+ }
+
+ public void show(View anchor) {
+ super.showAsDropDown(anchor, 0, -anchor.getWidth());
+ }
+
+ private void init() {
+ setWidth(WindowManager.LayoutParams.WRAP_CONTENT);
+ setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
+ setOutsideTouchable(true);
+ setAnimationStyle(-1);
+ initContentView();
+ }
+
+ private void initContentView() {
+ View contentView = View.inflate(mContext, R.layout.script_file_operation_popup_menu_content, null);
+ setContentView(contentView);
+ mOperationListRecyclerView = $(contentView, R.id.operation_list);
+ mOperationListRecyclerView.setOnItemClickListener(mOnItemClickRealListener);
+ }
+
+
+ public static class ScriptFileOperationListRecyclerView extends RecyclerView {
+
+ private OnClickListener mOnItemClickListener;
+
+ public ScriptFileOperationListRecyclerView(Context context) {
+ super(context);
+ init();
+ }
+
+ public ScriptFileOperationListRecyclerView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public ScriptFileOperationListRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ setLayoutManager(new LinearLayoutManager(getContext()));
+ setAdapter(new Adapter() {
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View itemView = LayoutInflater.from(getContext()).inflate(R.layout.script_file_operation_popup_menu_item, parent, false);
+ return new ViewHolder(itemView);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ ScriptFileOperation operation = ScriptFileOperation.getOperation(position);
+ holder.operationName.setText(operation.getName());
+ holder.icon.setImageResource(operation.getIconResId());
+ }
+
+ @Override
+ public int getItemCount() {
+ return ScriptFileOperation.getOperationNames().size();
+ }
+ });
+ }
+
+ public void setOnItemClickListener(OnClickListener onItemClickListener) {
+ mOnItemClickListener = onItemClickListener;
+ }
+
+ private class ViewHolder extends RecyclerView.ViewHolder {
+
+ TextView operationName;
+ ImageView icon;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ itemView.setOnClickListener(mOnItemClickListener);
+ operationName = $(itemView, R.id.name);
+ icon = $(itemView, R.id.icon);
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/ScriptListRecyclerView.java b/app/src/main/java/com/stardust/scriptdroid/ui/ScriptListRecyclerView.java
new file mode 100644
index 00000000..449ecb01
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/ScriptListRecyclerView.java
@@ -0,0 +1,165 @@
+package com.stardust.scriptdroid.ui;
+
+import android.content.Context;
+import android.os.Environment;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.DividerItemDecoration;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.afollestad.materialdialogs.MaterialDialog;
+import com.stardust.scriptdroid.R;
+import com.stardust.scriptdroid.data.ScriptFile;
+import com.stardust.scriptdroid.data.ScriptFileList;
+import com.stardust.scriptdroid.tool.ClassTool;
+
+import static com.stardust.scriptdroid.tool.ViewTool.$;
+
+import static com.stardust.scriptdroid.ui.ScriptFileOperation.*;
+
+/**
+ * Created by Stardust on 2017/1/23.
+ */
+
+public class ScriptListRecyclerView extends RecyclerView {
+
+ private ScriptFileList mScriptFileList;
+
+ private final OnClickListener mOnItemClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int position = getChildViewHolder(v).getAdapterPosition();
+ new ScriptFileOperation.Run().operate(ScriptListRecyclerView.this, mScriptFileList, position);
+ }
+ };
+
+
+ private final OnClickListener mOnEditIconClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int position = getChildViewHolder((View) v.getParent()).getAdapterPosition();
+ new ScriptFileOperation.Edit().operate(ScriptListRecyclerView.this, mScriptFileList, position);
+ }
+ };
+
+ private final OnClickListener mOnMoreIconClickListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mOperateFileIndex = getChildViewHolder((View) v.getParent()).getAdapterPosition();
+ //showOperationDialog(position);
+ showOrDismissOperationPopupMenu(v);
+ }
+
+ };
+
+ private ScriptFileOperationPopupMenu mScriptFileOperationPopupMenu;
+ private int mOperateFileIndex;
+
+
+ public ScriptListRecyclerView(Context context) {
+ super(context);
+ init();
+ }
+
+
+ public ScriptListRecyclerView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public ScriptListRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ private void init() {
+ setAdapter(new Adapter());
+ setLayoutManager(new LinearLayoutManager(getContext()));
+ addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
+ initScriptFileOperationPopupMenu();
+ }
+
+ private void initScriptFileOperationPopupMenu() {
+ mScriptFileOperationPopupMenu = new ScriptFileOperationPopupMenu(getContext());
+ mScriptFileOperationPopupMenu.setOnItemClickListener((view, position) -> {
+ ScriptFileOperation.getOperation(position).operate(ScriptListRecyclerView.this, mScriptFileList, mOperateFileIndex);
+ mScriptFileOperationPopupMenu.dismiss();
+ });
+ }
+
+ public void setScriptFileList(ScriptFileList scriptFileList) {
+ mScriptFileList = scriptFileList;
+ getAdapter().notifyDataSetChanged();
+ }
+
+ private void showOperationDialog(final int position) {
+ new MaterialDialog.Builder(getContext()).items(ScriptFileOperation.getOperationNames())
+ .itemsCallback((dialog, itemView, operation, text) -> ScriptFileOperation.getOperation(operation).operate(ScriptListRecyclerView.this, mScriptFileList, position)).show();
+ }
+
+ private void showOrDismissOperationPopupMenu(View v) {
+ if (mScriptFileOperationPopupMenu.isShowing()) {
+ mScriptFileOperationPopupMenu.dismiss();
+ } else {
+ mScriptFileOperationPopupMenu.show(v);
+ }
+ }
+
+ private class Adapter extends RecyclerView.Adapter {
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View itemView = LayoutInflater.from(getContext()).inflate(R.layout.script_list_recycler_view_item, parent, false);
+ return new ViewHolder(itemView);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ ScriptFile scriptFile = mScriptFileList.get(position);
+ holder.name.setText(scriptFile.name);
+ holder.path.setText(trimFilePath(scriptFile.path));
+ }
+
+ private final String SD_CARD_PATH = Environment.getExternalStorageDirectory().toString();
+
+ private String trimFilePath(String path) {
+ if (path.startsWith(SD_CARD_PATH)) {
+ path = path.substring(SD_CARD_PATH.length());
+ }
+ return path;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mScriptFileList.size();
+ }
+ }
+
+ private class ViewHolder extends RecyclerView.ViewHolder {
+
+ TextView name, path;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ name = (TextView) itemView.findViewById(R.id.name);
+ path = (TextView) itemView.findViewById(R.id.path);
+ $(itemView, R.id.edit).setOnClickListener(mOnEditIconClickListener);
+ $(itemView, R.id.more).setOnClickListener(mOnMoreIconClickListener);
+ itemView.setOnClickListener(mOnItemClickListener);
+ }
+ }
+
+ static {
+ loadScriptFileOperations();
+ }
+
+ private static void loadScriptFileOperations() {
+ ClassTool.loadClasses(Run.class, Edit.class, Rename.class, CreateShortcut.class, Remove.class, Delete.class);
+ }
+
+}
diff --git a/app/src/main/java/com/stardust/scriptdroid/ui/SlidingUpPanel.java b/app/src/main/java/com/stardust/scriptdroid/ui/SlidingUpPanel.java
new file mode 100644
index 00000000..5da55754
--- /dev/null
+++ b/app/src/main/java/com/stardust/scriptdroid/ui/SlidingUpPanel.java
@@ -0,0 +1,108 @@
+package com.stardust.scriptdroid.ui;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+
+import com.stardust.scriptdroid.R;
+
+/**
+ * Created by Stardust on 2017/1/24.
+ */
+
+public class SlidingUpPanel extends FrameLayout {
+
+ private static final long DEFAULT_ANIMATION_DURATION = 300;
+
+ private Animation mSlideUpAnimation, mSlideDownAnimation;
+ private View mShadow;
+ private boolean mShowing = false;
+ private FrameLayout mContentContainer;
+
+ public SlidingUpPanel(Context context) {
+ super(context);
+ init();
+ }
+
+ public SlidingUpPanel(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public SlidingUpPanel(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public SlidingUpPanel(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ init();
+ }
+
+ public void show() {
+ setVisibility(VISIBLE);
+ mContentContainer.startAnimation(mSlideUpAnimation);
+ mShowing = true;
+ }
+
+ public void dismiss() {
+ mContentContainer.startAnimation(mSlideDownAnimation);
+ postDelayed(() -> setVisibility(GONE), mSlideDownAnimation.getDuration());
+ mShowing = false;
+ }
+
+ public void setAnimationDuration(long animationDuration) {
+ mSlideUpAnimation.setDuration(animationDuration);
+ mSlideDownAnimation.setDuration(animationDuration / 2);
+ }
+
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ private void init() {
+ LayoutInflater.from(getContext()).inflate(R.layout.sliding_up_panel, this, true);
+ initShadow();
+ initAnimation();
+ setVisibility(GONE);
+ mContentContainer = (FrameLayout) findViewById(R.id.content_container);
+ }
+
+ private void initAnimation() {
+ mSlideUpAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.slide_up);
+ mSlideDownAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.slide_down);
+ setAnimationDuration(DEFAULT_ANIMATION_DURATION);
+ }
+
+ private void initShadow() {
+ mShadow = findViewById(R.id.shadow);
+ mShadow.setOnClickListener(v -> dismiss());
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_UP && isShowing()) {
+ dismiss();
+ }
+ return super.onInterceptTouchEvent(ev);
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (mContentContainer != null)
+ mContentContainer.addView(child, index, params);
+ else
+ super.addView(child, index, params);
+ }
+
+
+}
diff --git a/app/src/main/java/com/stardust/util/MapEntries.java b/app/src/main/java/com/stardust/util/MapEntries.java
new file mode 100644
index 00000000..23c618fb
--- /dev/null
+++ b/app/src/main/java/com/stardust/util/MapEntries.java
@@ -0,0 +1,37 @@
+package com.stardust.util;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Created by Stardust on 2017/1/26.
+ */
+
+public class MapEntries {
+
+ private Map mMap;
+
+ public MapEntries() {
+ this(new TreeMap<>());
+ }
+
+ public MapEntries(Map map) {
+ mMap = map;
+ }
+
+ public MapEntries entry(K key, V value) {
+ mMap.put(key, value);
+ return this;
+ }
+
+ public Map map() {
+ return mMap;
+ }
+
+ public Map putIn(Map map) {
+ map.putAll(mMap);
+ return map;
+ }
+
+
+}
diff --git a/app/src/main/java/com/stardust/util/SparseArrayEntries.java b/app/src/main/java/com/stardust/util/SparseArrayEntries.java
new file mode 100644
index 00000000..0efb3514
--- /dev/null
+++ b/app/src/main/java/com/stardust/util/SparseArrayEntries.java
@@ -0,0 +1,22 @@
+package com.stardust.util;
+
+import android.util.SparseArray;
+
+/**
+ * Created by Stardust on 2017/1/26.
+ */
+
+public class SparseArrayEntries {
+
+ private SparseArray mSparseArray = new SparseArray<>();
+
+ public SparseArrayEntries entry(int key, E value) {
+ mSparseArray.put(key, value);
+ return this;
+ }
+
+ public SparseArray sparseArray() {
+ return mSparseArray;
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/stardust/util/function/ArrayOptional.java b/app/src/main/java/com/stardust/util/function/ArrayOptional.java
new file mode 100644
index 00000000..f0d74de6
--- /dev/null
+++ b/app/src/main/java/com/stardust/util/function/ArrayOptional.java
@@ -0,0 +1,8 @@
+package com.stardust.util.function;
+
+/**
+ * Created by Stardust on 2017/1/24.
+ */
+
+public class ArrayOptional {
+}
diff --git a/app/src/main/java/com/stardust/util/function/Domino.java b/app/src/main/java/com/stardust/util/function/Domino.java
new file mode 100644
index 00000000..a19c0c16
--- /dev/null
+++ b/app/src/main/java/com/stardust/util/function/Domino.java
@@ -0,0 +1,8 @@
+package com.stardust.util.function;
+
+/**
+ * Created by Stardust on 2017/1/26.
+ */
+
+public class Domino {
+}
diff --git a/app/src/main/java/com/stardust/util/function/FunctionTool.java b/app/src/main/java/com/stardust/util/function/FunctionTool.java
new file mode 100644
index 00000000..914f3002
--- /dev/null
+++ b/app/src/main/java/com/stardust/util/function/FunctionTool.java
@@ -0,0 +1,15 @@
+package com.stardust.util.function;
+
+import android.support.annotation.RequiresApi;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+/**
+ * Created by Stardust on 2017/1/24.
+ */
+
+public class FunctionTool {
+
+
+}
diff --git a/app/src/main/java/com/stardust/util/function/ListTool.java b/app/src/main/java/com/stardust/util/function/ListTool.java
new file mode 100644
index 00000000..6e311d04
--- /dev/null
+++ b/app/src/main/java/com/stardust/util/function/ListTool.java
@@ -0,0 +1,24 @@
+package com.stardust.util.function;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Stardust on 2017/1/26.
+ */
+
+public class ListTool {
+
+ @FunctionalInterface
+ public interface ChildSupplier {
+ T getChild(int i);
+ }
+
+ public static List toList(ChildSupplier supplier, int size) {
+ ArrayList arrayList = new ArrayList(size);
+ for (int i = 0; i < size - 1; i++) {
+ arrayList.add(supplier.getChild(i));
+ }
+ return arrayList;
+ }
+}
diff --git a/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoReflect.java b/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoReflect.java
new file mode 100644
index 00000000..bc9cfb6a
--- /dev/null
+++ b/app/src/main/java/com/stardust/view/accessibility/AccessibilityNodeInfoReflect.java
@@ -0,0 +1,40 @@
+package com.stardust.view.accessibility;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.lang.reflect.Field;
+
+/**
+ * Created by Stardust on 2017/1/26.
+ */
+
+public class AccessibilityNodeInfoReflect {
+
+ private final AccessibilityNodeInfo mAccessibilityNodeInfo;
+
+ public AccessibilityNodeInfoReflect(AccessibilityNodeInfo accessibilityNodeInfo) {
+ mAccessibilityNodeInfo = accessibilityNodeInfo;
+ }
+
+ public void getChildNodeIds() {
+ try {
+ Field mChildNodeIdsField = AccessibilityNodeInfo.class.getDeclaredField("mChildNodeIds");
+ mChildNodeIdsField.setAccessible(true);
+ Object o = mChildNodeIdsField.get(mAccessibilityNodeInfo);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public long getSourceNodeId() {
+ try {
+ Field mSourceNodeIdField = AccessibilityNodeInfo.class.getDeclaredField("mSourceNodeId");
+ mSourceNodeIdField.setAccessible(true);
+ return (long) mSourceNodeIdField.get(mSourceNodeIdField);
+ } catch (IllegalAccessException | NoSuchFieldException e) {
+ e.printStackTrace();
+ }
+ return -1;
+ }
+
+}
diff --git a/app/src/main/res/anim/slide_down.xml b/app/src/main/res/anim/slide_down.xml
new file mode 100644
index 00000000..0ed5bbec
--- /dev/null
+++ b/app/src/main/res/anim/slide_down.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_up.xml b/app/src/main/res/anim/slide_up.xml
new file mode 100644
index 00000000..3bd7fba8
--- /dev/null
+++ b/app/src/main/res/anim/slide_up.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_accessibility_service.png b/app/src/main/res/drawable/ic_accessibility_service.png
new file mode 100644
index 0000000000000000000000000000000000000000..21e8d3a1131e6ca1e0d42ae68cc739f0da86b112
GIT binary patch
literal 18349
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
literal 0
HcmV?d00001
diff --git a/app/src/main/res/drawable/ic_delete_black_48dp.png b/app/src/main/res/drawable/ic_delete_black_48dp.png
new file mode 100644
index 0000000000000000000000000000000000000000..796ccd22559a9b362e6cbb0c804a443bf1b3c113
GIT binary patch
literal 148
zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DXipc%kP61P7Y=eZ7znUlT&Wh3
zX2lgXgX5!|^oI$(N1D&Q`D-6>CQj>eo}T~gdcQ3PU;8~!-+H4~OaKZjoIg)3i005}
qiMH7Fyj-Y9Kzr+B%hn@D&gi|3kyH5eQnCbS4TGnvpUXO@geCyhvNFd2
literal 0
HcmV?d00001
diff --git a/app/src/main/res/drawable/ic_delete_forever_black_48dp.png b/app/src/main/res/drawable/ic_delete_forever_black_48dp.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7468f276b79729b0aca215414a016c18f85654a
GIT binary patch
literal 227
zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DRh}-6Ar*{orydn;4iI2?zITda
zR;QVp;l@+{dT-Br-1hpB_5Y@G4=xC{JuLQ?tc$K{J+k(hMlb6#NABs%uQssiD7@L}
zGA-tYauj1CuZ~ma>14iZ9exStXS+XIU%&A>r%lU$ml8j90biG|R*fD?tHQZmgad1T
zH-6I({B6GT@9`~v#n=Azcl|GK{rh3avCvD_TE(kGEP`gLI%d{3u$KzC$8>fVA6e|m
b^O!HoW}VWqFD-09S2K9J`njxgN@xNAI80ms
literal 0
HcmV?d00001
diff --git a/app/src/main/res/drawable/ic_delete_forever_green_48dp.png b/app/src/main/res/drawable/ic_delete_forever_green_48dp.png
new file mode 100644
index 0000000000000000000000000000000000000000..710737e2867571718bd8c5265f33fd434a129f78
GIT binary patch
literal 965
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
literal 0
HcmV?d00001
diff --git a/app/src/main/res/drawable/ic_edit_green_48dp.png b/app/src/main/res/drawable/ic_edit_green_48dp.png
new file mode 100644
index 0000000000000000000000000000000000000000..8819a59b98ccd16992526e5fcc1bca40ca4d84e9
GIT binary patch
literal 767
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