├── app
├── .gitignore
├── release
│ ├── intro.png
│ ├── qrcode.png
│ ├── app-release.apk
│ └── output-metadata.json
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ ├── ic_launcher_adaptive_back.png
│ │ │ │ └── ic_launcher_adaptive_fore.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ ├── ic_launcher_adaptive_back.png
│ │ │ │ └── ic_launcher_adaptive_fore.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ ├── ic_launcher_adaptive_back.png
│ │ │ │ └── ic_launcher_adaptive_fore.png
│ │ │ ├── values
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── colors.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── themes.xml
│ │ │ ├── xml
│ │ │ │ └── accessibility.xml
│ │ │ ├── menu
│ │ │ │ └── menu_scrolling.xml
│ │ │ ├── xml-v24
│ │ │ │ └── accessibility.xml
│ │ │ ├── values-night
│ │ │ │ └── themes.xml
│ │ │ └── layout
│ │ │ │ ├── content_scrolling.xml
│ │ │ │ └── activity_scrolling.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── top
│ │ │ └── shixinzhang
│ │ │ └── food
│ │ │ ├── service
│ │ │ ├── GrabService.java
│ │ │ └── MeituanGrabHandler.java
│ │ │ ├── ScrollingActivity.java
│ │ │ └── util
│ │ │ └── Helper.java
│ ├── test
│ │ └── java
│ │ │ └── top
│ │ │ └── shixinzhang
│ │ │ └── food
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── top
│ │ └── shixinzhang
│ │ └── food
│ │ └── ExampleInstrumentedTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── .idea
├── .gitignore
├── compiler.xml
├── vcs.xml
├── misc.xml
├── gradle.xml
└── jarRepositories.xml
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── README.md
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "food"
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/app/release/intro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/release/intro.png
--------------------------------------------------------------------------------
/app/release/qrcode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/release/qrcode.png
--------------------------------------------------------------------------------
/app/release/app-release.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/release/app-release.apk
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_back.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shixinzhang/MeituanRetailHelper/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_adaptive_fore.png
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 180dp
3 | 16dp
4 | 16dp
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Apr 07 19:04:59 CST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/release/output-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "artifactType": {
4 | "type": "APK",
5 | "kind": "Directory"
6 | },
7 | "applicationId": "top.shixinzhang.food",
8 | "variantName": "processReleaseResources",
9 | "elements": [
10 | {
11 | "type": "SINGLE",
12 | "filters": [],
13 | "versionCode": 3,
14 | "versionName": "1.3",
15 | "outputFile": "app-release.apk"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/app/src/test/java/top/shixinzhang/food/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package top.shixinzhang.food;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/accessibility.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #4CAF50
7 | #388E3C
8 | #00C853
9 | #E64A19
10 |
11 | #FF03DAC5
12 | #FF018786
13 | #FF000000
14 | #FFFFFFFF
15 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_scrolling.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/xml-v24/accessibility.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 美团买菜助手
2 |
3 | 
4 |
5 | 居家办公三周有余,买菜始终困扰着我,隔几天就要早起抢菜。
6 |
7 | 最近网上看到一个叮咚助手,被作者的创意所折服:
8 |
9 | https://github.com/Skykai521/DingDongHelper
10 |
11 | 我平时用的主要是美团买菜。
12 |
13 | 于是花些时间开发了一个买菜助手软件,可以自动完成在 美团买菜 app 上的结算到付款的整个流程,帮用户节省这些步骤。
14 |
15 | # 下载方式
16 |
17 | 1. 点击这里下载: [https://github.com/shixinzhang/MeituanRetailHelper/blob/main/app/release/app-release.apk](https://github.com/shixinzhang/MeituanRetailHelper/blob/main/app/release/app-release.apk)
18 | 2. 进入公众号后台回复:买菜助手
19 |
20 | 
21 |
22 | # 使用方式
23 |
24 | 1. 下载美团买菜,加好购物车、添加好收货人信息
25 | 2. 打开买菜助手,点击开始,到设置界面给无障碍权限
26 | 3. 回到助手,再次点击开始,会自动打开美团买菜 app、开始执行
27 |
28 | # 责任说明
29 |
30 | 本软件仅供学习交流,不保证一定成功,勿作其他用途,如作他用所承受的法律责任一概与作者无关。
31 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/top/shixinzhang/food/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package top.shixinzhang.food;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 | assertEquals("top.shixinzhang.food", appContext.getPackageName());
25 | }
26 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 买菜助手
3 | 买菜助手-开启后可帮你点击,增加买到概率
4 |
5 | "功能介绍:\n"
6 | "自动完成结算、选择时间、支付以及其中的错误重试,减少人工买菜的成本,增加成功概率\n\n"
7 |
8 | "使用步骤:\n"
9 | "1. 下载美团买菜,加好购物车、添加好收货人信息\n"
10 | "2. 打开买菜助手,点击开始,到设置界面给无障碍权限\n"
11 | "3. 回到助手,再次点击开始,会自动打开美团买菜 app、开始执行\n\n"
12 |
13 | "功能反馈请点击右上角\n\n"
14 |
15 | "责任说明:\n"
16 | "本软件仅供个人学习使用,如作他用所承受的法律责任一概与作者无关\n\n"
17 |
18 | "源码地址\n"
19 | "https://github.com/shixinzhang/MeituanRetailHelper\n"
20 |
21 |
22 |
23 |
24 | "功能介绍:\n\n"
25 |
26 | "使用步骤:\n\n"
27 |
28 | "案例说明:\n\n"
29 |
30 |
31 | 开始使用
32 | 关于作者
33 | 功能反馈
34 | 买菜助手:开启后可自动完成下单流程,失败后会自动重试,减少你买菜的成本
35 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | android.injected.testOnly=false
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_scrolling.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
18 |
19 |
23 |
24 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | }
4 |
5 | android {
6 | compileSdkVersion 31
7 | buildToolsVersion "30.0.3"
8 |
9 | defaultConfig {
10 | applicationId "top.shixinzhang.food"
11 | minSdkVersion 21
12 | targetSdkVersion 31
13 | versionCode 3
14 | versionName "1.3"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | signingConfigs {
20 | release {
21 | storeFile file('../../androidkey') //路径
22 | storePassword 'zsx250'
23 | keyAlias 'key0'
24 | keyPassword 'qq123456'
25 | }
26 | }
27 |
28 | buildTypes {
29 | release {
30 | minifyEnabled false
31 | signingConfig signingConfigs.release
32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
33 | }
34 | }
35 | compileOptions {
36 | sourceCompatibility JavaVersion.VERSION_1_8
37 | targetCompatibility JavaVersion.VERSION_1_8
38 | }
39 | }
40 |
41 | dependencies {
42 |
43 | implementation 'androidx.appcompat:appcompat:1.4.1'
44 | implementation 'com.google.android.material:material:1.5.0'
45 | testImplementation 'junit:junit:4.+'
46 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
47 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
48 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_scrolling.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
25 |
26 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
46 |
47 |
--------------------------------------------------------------------------------
/app/src/main/java/top/shixinzhang/food/service/GrabService.java:
--------------------------------------------------------------------------------
1 | package top.shixinzhang.food.service;
2 |
3 | import android.accessibilityservice.AccessibilityService;
4 | import android.text.TextUtils;
5 | import android.util.Log;
6 | import android.view.accessibility.AccessibilityEvent;
7 | import android.widget.Toast;
8 |
9 | import java.util.Arrays;
10 | import java.util.HashMap;
11 | import java.util.List;
12 | import java.util.Locale;
13 | import java.util.Map;
14 | import java.util.Set;
15 |
16 | import top.shixinzhang.food.ScrollingActivity;
17 | import top.shixinzhang.food.util.Helper;
18 |
19 | /**
20 | * @description : GrabService
21 | * @create by : zhangshixin
22 | */
23 | public class GrabService extends AccessibilityService {
24 | public final String TAG = getClass().getSimpleName();
25 | public static final String PACKAGE_MEITUAN = "com.meituan.retail.v.android";
26 |
27 | final List supportEventTypes = Arrays.asList(
28 | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED,
29 | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED,
30 | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, AccessibilityEvent.TYPE_VIEW_CLICKED
31 | );
32 |
33 | public interface IGrabHandler {
34 | void handlerEvent(AccessibilityService service, AccessibilityEvent event);
35 | }
36 |
37 | Map appHandlerMap = new HashMap<>();
38 |
39 | public GrabService() {
40 | Helper.sService = this;
41 |
42 | appHandlerMap.put(PACKAGE_MEITUAN, new MeituanGrabHandler());
43 | }
44 |
45 | @Override
46 | public void onAccessibilityEvent(AccessibilityEvent event) {
47 | if (!supportEventTypes.contains(event.getEventType())) {
48 | return;
49 | }
50 |
51 | CharSequence eventPackageName = event.getPackageName();
52 | if (eventPackageName == null) {
53 | return;
54 | }
55 |
56 |
57 | if (Helper.isDebugMode) {
58 | String eventClassName = event.getClassName().toString();
59 | String info = String.format(Locale.CHINA, "pkg: %s, claz: %s", eventPackageName.toString(), eventClassName);
60 | Toast.makeText(this, info , Toast.LENGTH_SHORT).show();
61 | }
62 |
63 | IGrabHandler grabHandler = appHandlerMap.get(eventPackageName.toString());
64 | if (grabHandler != null) {
65 | grabHandler.handlerEvent(this, event);
66 | }
67 | }
68 |
69 | @Override
70 | public void onInterrupt() {
71 |
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/top/shixinzhang/food/ScrollingActivity.java:
--------------------------------------------------------------------------------
1 | package top.shixinzhang.food;
2 |
3 | import android.content.ClipData;
4 | import android.content.ClipboardManager;
5 | import android.content.Context;
6 | import android.content.DialogInterface;
7 | import android.content.Intent;
8 | import android.net.Uri;
9 | import android.os.Bundle;
10 |
11 | import com.google.android.material.appbar.CollapsingToolbarLayout;
12 | import com.google.android.material.floatingactionbutton.FloatingActionButton;
13 | import com.google.android.material.snackbar.Snackbar;
14 |
15 | import androidx.appcompat.app.AlertDialog;
16 | import androidx.appcompat.app.AppCompatActivity;
17 | import androidx.appcompat.widget.Toolbar;
18 |
19 | import android.provider.Settings;
20 | import android.text.TextUtils;
21 | import android.util.Log;
22 | import android.view.View;
23 | import android.view.Menu;
24 | import android.view.MenuItem;
25 | import android.widget.Toast;
26 |
27 | import top.shixinzhang.food.service.GrabService;
28 | import top.shixinzhang.food.util.Helper;
29 |
30 | public class ScrollingActivity extends AppCompatActivity {
31 |
32 | public final String TAG = getClass().getSimpleName();
33 |
34 | @Override
35 | protected void onCreate(Bundle savedInstanceState) {
36 | super.onCreate(savedInstanceState);
37 | setContentView(R.layout.activity_scrolling);
38 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
39 | setSupportActionBar(toolbar);
40 | CollapsingToolbarLayout toolBarLayout = (CollapsingToolbarLayout) findViewById(R.id.toolbar_layout);
41 | toolBarLayout.setTitle("买菜助手(美团)");
42 |
43 | FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
44 | fab.setOnClickListener(startClickListener);
45 |
46 | View startButton = findViewById(R.id.btn_start);
47 | startButton.setOnClickListener(startClickListener);
48 | startButton.setOnLongClickListener(new View.OnLongClickListener() {
49 | @Override
50 | public boolean onLongClick(View v) {
51 | Toast.makeText(ScrollingActivity.this, "调试模式已打开", Toast.LENGTH_SHORT).show();
52 | Helper.isDebugMode = true;
53 | return true;
54 | }
55 | });
56 | }
57 |
58 | View.OnClickListener startClickListener = new View.OnClickListener() {
59 | @Override
60 | public void onClick(View view) {
61 |
62 | String packageName = getTargetAppPackageName();
63 | if (Helper.isAccessibilitySettingsOn(ScrollingActivity.this)) {
64 | boolean started = Helper.startApplication(ScrollingActivity.this, packageName);
65 | String msg = started ? "开始执行!" : "请手动打开美团买菜";
66 | Toast.makeText(ScrollingActivity.this, msg, Toast.LENGTH_LONG).show();
67 |
68 | // if (Helper.checkAppInstalled(ScrollingActivity.this, packageName)) {
69 | //
70 | // } else {
71 | // if (!Helper.isDebugMode) {
72 | // Snackbar.make(view, "美团买菜未安装,请先安装!", Snackbar.LENGTH_LONG)
73 | // .setAction("Action", null).show();
74 | // }
75 | // }
76 | } else {
77 | Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
78 | startActivity(intent);
79 |
80 | Toast.makeText(ScrollingActivity.this,
81 | "请先给予[买菜助手]无障碍权限,这样才能自动操作!", Toast.LENGTH_LONG).show();
82 | }
83 |
84 | }
85 | };
86 |
87 | String getTargetAppPackageName() {
88 | //todo 根据选择打开目标 app
89 | return GrabService.PACKAGE_MEITUAN;
90 | }
91 |
92 | @Override
93 | public boolean onCreateOptionsMenu(Menu menu) {
94 | // Inflate the menu; this adds items to the action bar if it is present.
95 | getMenuInflater().inflate(R.menu.menu_scrolling, menu);
96 | return true;
97 | }
98 |
99 | @Override
100 | public boolean onOptionsItemSelected(MenuItem item) {
101 | // Handle action bar item clicks here. The action bar will
102 | // automatically handle clicks on the Home/Up button, so long
103 | // as you specify a parent activity in AndroidManifest.xml.
104 | int id = item.getItemId();
105 |
106 | //noinspection SimplifiableIfStatement
107 | if (id == R.id.action_settings) {
108 | Uri uri = Uri.parse("https://mp.weixin.qq.com/s/jxB3TjTKlRyz0Ti4pnXZcw");
109 | Intent intent = new Intent(Intent.ACTION_VIEW, uri);
110 | startActivity(intent);
111 | return true;
112 | }
113 | if (id == R.id.action_report) {
114 | new AlertDialog.Builder(ScrollingActivity.this)
115 | .setTitle("功能反馈方式")
116 | .setMessage("即将复制作者公众号名称,打开微信搜索后可发消息反馈。你的每个反馈,都将帮助更多人买到菜!")
117 | .setNegativeButton("取消", new DialogInterface.OnClickListener() {
118 | @Override
119 | public void onClick(DialogInterface dialog, int which) {
120 | }
121 | })
122 | .setPositiveButton("好的", new DialogInterface.OnClickListener() {
123 | @Override
124 | public void onClick(DialogInterface dialog, int which) {
125 | contractAuthor();
126 | }
127 | }).show();
128 |
129 | }
130 | return super.onOptionsItemSelected(item);
131 | }
132 |
133 | private void contractAuthor() {
134 |
135 | ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
136 |
137 | if (cm != null) {
138 | // 创建普通字符型ClipData
139 | ClipData mClipData = ClipData.newPlainText("Label", "拭心又在思考了我的天");
140 | // 将ClipData内容放到系统剪贴板里。
141 | cm.setPrimaryClip(mClipData);
142 |
143 | Toast.makeText(ScrollingActivity.this, "内容已复制,请到微信搜索", Toast.LENGTH_SHORT).show();
144 | }
145 |
146 | Helper.openWechat(ScrollingActivity.this);
147 | }
148 |
149 | }
--------------------------------------------------------------------------------
/app/src/main/java/top/shixinzhang/food/util/Helper.java:
--------------------------------------------------------------------------------
1 | package top.shixinzhang.food.util;
2 |
3 | import android.accessibilityservice.AccessibilityService;
4 | import android.app.ActivityManager;
5 | import android.content.ActivityNotFoundException;
6 | import android.content.ComponentName;
7 | import android.content.Context;
8 | import android.content.Intent;
9 | import android.provider.Settings;
10 | import android.text.TextUtils;
11 | import android.util.Log;
12 | import android.view.accessibility.AccessibilityNodeInfo;
13 |
14 | import androidx.annotation.NonNull;
15 |
16 | import java.util.List;
17 |
18 | import top.shixinzhang.food.ScrollingActivity;
19 | import top.shixinzhang.food.service.GrabService;
20 |
21 | public class Helper {
22 | public static AccessibilityService sService;
23 | public static boolean isDebugMode;
24 |
25 | public static AccessibilityNodeInfo getRootInActiveWindow() {
26 | if (sService == null) {
27 | return null;
28 | }
29 | return sService.getRootInActiveWindow();
30 | }
31 |
32 | /**
33 | * @param node
34 | * @return
35 | */
36 | public static AccessibilityNodeInfo getClickableNode(AccessibilityNodeInfo node) {
37 | if (node.isClickable()) {
38 | return node;
39 | } else {
40 | AccessibilityNodeInfo parentNode = node;
41 | for (int i = 0; i < 5; i++) {
42 | if (null != parentNode) {
43 | parentNode = parentNode.getParent();
44 | if (null != parentNode && parentNode.isClickable()) {
45 | return parentNode;
46 | }
47 | }
48 | }
49 | }
50 |
51 | return null;
52 | }
53 |
54 | public static boolean clickNode(@NonNull AccessibilityNodeInfo root, final String viewId) {
55 | AccessibilityNodeInfo nodeInfo = findClickableNode(root, viewId);
56 | if (null != nodeInfo)
57 | return nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK);
58 | return false;
59 | }
60 |
61 | public static AccessibilityNodeInfo findClickableNode(@NonNull AccessibilityNodeInfo root, final String viewId) {
62 | AccessibilityNodeInfo node = findNode(root, viewId);
63 | if (null != node)
64 | return getClickableNode(node);
65 | return null;
66 | }
67 |
68 | public static AccessibilityNodeInfo findNode(@NonNull AccessibilityNodeInfo root, final String viewId) {
69 | if (root == null){
70 | return null;
71 | }
72 | List list = root.findAccessibilityNodeInfosByViewId(viewId);
73 |
74 | if (null == list || list.isEmpty())
75 | return null;
76 | return list.get(0);
77 | }
78 |
79 | public static AccessibilityNodeInfo getNodeByText(AccessibilityNodeInfo rootNode, String text) {
80 | List list = rootNode.findAccessibilityNodeInfosByText(text);
81 | if (null == list || list.size() == 0) return null;
82 |
83 | return list.get(list.size() - 1);
84 | }
85 |
86 | public static AccessibilityNodeInfo getClickableNode(String text) {
87 | AccessibilityNodeInfo rootNode = getRootInActiveWindow();
88 |
89 | if (null == rootNode) return null;
90 |
91 | if (TextUtils.isEmpty(text)) return null;
92 |
93 | List list = rootNode.findAccessibilityNodeInfosByText(text);
94 | if (null == list || list.size() == 0) return null;
95 |
96 | AccessibilityNodeInfo node = list.get(list.size() - 1);
97 |
98 | if (node.isClickable()) {
99 | return node;
100 | } else {
101 | AccessibilityNodeInfo parentNode = node;
102 | for (int i = 0; i < 5; i++) {
103 | if (null != parentNode) {
104 | parentNode = parentNode.getParent();
105 | if (null != parentNode && parentNode.isClickable()) {
106 | return parentNode;
107 | }
108 | }
109 | }
110 | }
111 |
112 | return null;
113 | }
114 |
115 | public static boolean actionText(String buttonText) {
116 | AccessibilityNodeInfo node = getClickableNode(buttonText);
117 | Log.d("actionText", buttonText + " getClickableNode ? " + (node != null));
118 | if (null != node) {
119 | node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
120 | return true;
121 | }
122 | return false;
123 | }
124 |
125 | public static List tryGetNodes(AccessibilityNodeInfo rootNode, String text, int interval, int maxCount) {
126 | if (rootNode == null) {
127 | return null;
128 | }
129 |
130 | for (int i = 0; i < maxCount; i++) {
131 |
132 | List nodes = rootNode.findAccessibilityNodeInfosByText(text);
133 | if (nodes != null && nodes.size() > 0) {
134 | return nodes;
135 | }
136 |
137 | try {
138 | Thread.sleep(interval);
139 | } catch (InterruptedException e) {
140 | e.printStackTrace();
141 | }
142 | }
143 | return null;
144 | }
145 |
146 | public static boolean clickNode(AccessibilityNodeInfo node) {
147 |
148 | AccessibilityNodeInfo clickableNode = getClickableNode(node);
149 | if (null != clickableNode) {
150 | clickableNode.performAction(AccessibilityNodeInfo.ACTION_CLICK);
151 | return true;
152 | }
153 |
154 | return false;
155 |
156 | }
157 |
158 | public static boolean isAccessibilitySettingsOn(Context context) {
159 | int accessibilityEnabled = 0;
160 | final String service = context.getPackageName() + "/" + GrabService.class.getCanonicalName();
161 | try {
162 | accessibilityEnabled = Settings.Secure.getInt(context.getApplicationContext().getContentResolver(),
163 | android.provider.Settings.Secure.ACCESSIBILITY_ENABLED);
164 | TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':');
165 |
166 | if (accessibilityEnabled != 1) {
167 | return false;
168 | }
169 |
170 | String settingValue = Settings.Secure.getString(context.getApplicationContext().getContentResolver(),
171 | Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
172 | if (settingValue != null) {
173 | mStringColonSplitter.setString(settingValue);
174 | while (mStringColonSplitter.hasNext()) {
175 | String accessibilityService = mStringColonSplitter.next();
176 | if (accessibilityService.equalsIgnoreCase(service)) {
177 | return true;
178 | }
179 | }
180 | }
181 | } catch (Exception e) {
182 | //ignore
183 | }
184 | return false;
185 |
186 | }
187 |
188 | public AccessibilityNodeInfo recycle(AccessibilityNodeInfo node) {
189 | if (null == node)
190 | return node;
191 |
192 | if (node.getChildCount() == 0) {
193 | if (node.getText() != null) {
194 | if (null != node.getText()) {
195 | // Log.i(TAG, "node text:" + node.getText().toString());
196 | }
197 |
198 | }
199 | } else {
200 | for (int i = 0; i < node.getChildCount(); i++) {
201 | if (node.getChild(i) != null) {
202 | recycle(node.getChild(i));
203 | }
204 | }
205 | }
206 | return node;
207 | }
208 |
209 | public static boolean startApplication(Context context, String packageName) {
210 | if (TextUtils.isEmpty(packageName)) {
211 | return false;
212 | }
213 | Intent launchIntentForPackage = context.getPackageManager().getLaunchIntentForPackage(packageName);
214 | if (launchIntentForPackage != null) {
215 | launchIntentForPackage.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
216 | context.startActivity(launchIntentForPackage);
217 | return true;
218 | }
219 | return false;
220 | }
221 |
222 | /**
223 | * 获取前台应用package name
224 | *
225 | * @param context
226 | * @return
227 | */
228 | public static String getForegroundApp(Context context) {
229 | ActivityManager am =
230 | (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
231 | List lr = am.getRunningAppProcesses();
232 | if (lr == null) {
233 | return null;
234 | }
235 |
236 | for (ActivityManager.RunningAppProcessInfo ra : lr) {
237 | if (ra.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
238 | || ra.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
239 | return ra.processName;
240 | }
241 | }
242 |
243 | return null;
244 | }
245 |
246 | public static boolean hasText(@NonNull AccessibilityNodeInfo root, final String text) {
247 | if (root == null){
248 | return false;
249 | }
250 | List list = root.findAccessibilityNodeInfosByText(text);
251 | return null != list && !list.isEmpty() && list.get(0).isVisibleToUser();
252 | }
253 |
254 |
255 | /**
256 | * @param context
257 | * @param className
258 | * @return
259 | */
260 | public static boolean isForeground(Context context, String className) {
261 | if (context == null || TextUtils.isEmpty(className)) {
262 | return false;
263 | }
264 |
265 | ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
266 | List list = am.getRunningTasks(1);
267 | if (list != null && list.size() > 0) {
268 | ComponentName cpn = list.get(0).topActivity;
269 | if (className.equals(cpn.getClassName())) {
270 | return true;
271 | }
272 | }
273 |
274 | return false;
275 | }
276 |
277 | public static String getTopActivity(Context context) {
278 | if (context == null) {
279 | return null;
280 | }
281 | String className = null;
282 | ActivityManager mActivityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
283 | List list = mActivityManager.getRunningTasks(1);
284 | if (!list.isEmpty() && list.get(0) != null && list.get(0).topActivity != null) {
285 | className = list.get(0).topActivity.getClassName();
286 | }
287 | return className;
288 | }
289 |
290 |
291 | public static boolean checkAppInstalled(Context context, String pkgName) {
292 | if (TextUtils.isEmpty(pkgName)) {
293 | return false;
294 | }
295 | try {
296 | context.getPackageManager().getPackageInfo(pkgName, 0);
297 | } catch (Exception x) {
298 | return false;
299 | }
300 | return true;
301 | }
302 |
303 | /**
304 | * 跳转到微信
305 | */
306 | public static void openWechat(Context context){
307 | try {
308 | Intent intent = new Intent(Intent.ACTION_MAIN);
309 | ComponentName cmp = new ComponentName("com.tencent.mm","com.tencent.mm.ui.LauncherUI");
310 | intent.addCategory(Intent.CATEGORY_LAUNCHER);
311 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
312 | intent.setComponent(cmp);
313 | context.startActivity(intent);
314 | } catch (ActivityNotFoundException e) {
315 | // TODO: handle exception
316 | }
317 | }
318 | }
--------------------------------------------------------------------------------
/app/src/main/java/top/shixinzhang/food/service/MeituanGrabHandler.java:
--------------------------------------------------------------------------------
1 | package top.shixinzhang.food.service;
2 |
3 | import android.accessibilityservice.AccessibilityService;
4 | import android.content.Context;
5 | import android.text.TextUtils;
6 | import android.util.Log;
7 | import android.view.accessibility.AccessibilityEvent;
8 | import android.view.accessibility.AccessibilityNodeInfo;
9 | import android.widget.Toast;
10 |
11 | import java.util.List;
12 |
13 | import top.shixinzhang.food.util.Helper;
14 |
15 | /**
16 | * @description : MeituanGrabImpl
17 | * @create by : zhangshixin
18 | */
19 | public class MeituanGrabHandler implements GrabService.IGrabHandler {
20 | public final String TAG = getClass().getSimpleName();
21 |
22 | public static final String PAGE_SPLASH = "com.meituan.retail.c.android.splash.SplashActivity";
23 | public static final String PAGE_HOME = "com.meituan.retail.c.android.newhome.newmain.NewMainActivity";
24 | public static final String PAGE_DIALOG = "com.meituan.retail.c.android.mrn.mrn.MallMrnModal";
25 | public static final String PAGE_PLACE_ORDER = "com.meituan.retail.c.android.mrn.mrn.MallMrnActivity";
26 | public static final String PAGE_PAYING = "com.meituan.android.cashier.activity.MTCashierActivity";
27 |
28 | public static final String ID_TAB_CHART = "com.meituan.retail.v.android:id/img_shopping_cart";
29 | public static final String ID_TAB_MINE = "com.meituan.retail.v.android:id/rl_main_mine";
30 |
31 | public static final String TEXT_SKIP_AD = "跳过";
32 | public static final String TEXT_CONFIRM_ADDRESS = "请确认地址";
33 | public static final String TEXT_CHART = "购物车";
34 | public static final String TEXT_ORDER_BTN = "结算";
35 | public static final String TEXT_ORDER_OPEN_BEGIN = "当前不在可下";
36 |
37 | public static final String TEXT_PLACE_ORDER = "提交订单";
38 | public static final String TEXT_BACK_HOME = "返回购物车";
39 | public static final String TEXT_DELIVERY_HOME = "送货上门";
40 | public static final String TEXT_DELIVERY_TIME = "送达时间";
41 | public static final String TEXT_DELIVERY_TIME_SELECT = "请选择送达";
42 | public static final String TEXT_DELIVERY_TIME_SELECT_DIALOG = "选择送达时间";
43 | public static final String TEXT_FINE = "我知道了";
44 | public static final String TEXT_ENSURE_ADDRESS = "确认并支付";
45 | public static final String TEXT_GIVE_UP = "放弃机会";
46 |
47 | public static final String TEXT_PAY_NOW = "立即支付";
48 | public static final String TEXT_PAY = "极速支付";
49 |
50 | public static final int COUNT_CHECK_TEXT = 4;
51 | public static final int CLICK_PAY_INTERVAL = 100;
52 |
53 | private boolean isGrabStarted = true;
54 | private GrabState lastState = GrabState.IDLE;
55 | private long lastClickPayTime;
56 |
57 | public enum GrabState {
58 | IDLE,
59 | SPLASH_PAGE,
60 | HOME_PAGE,
61 | CHART_PAGE,
62 | PAYMENT_PAGE,
63 | SELECT_DELIVERY_TIME_PAGE,
64 | }
65 |
66 | @Override
67 | public void handlerEvent(AccessibilityService service, AccessibilityEvent event) {
68 |
69 | String eventClassName = event.getClassName().toString();
70 | Log.e(TAG, "onAccessibilityEvent: " + event.getEventType() + eventClassName);
71 | Log.d(TAG, "onAccessibilityEvent: " + event);
72 |
73 | AccessibilityNodeInfo rootNode = service.getRootInActiveWindow();
74 |
75 | switch (eventClassName) {
76 | case PAGE_SPLASH: //冷启广告
77 | skipAD(rootNode, service);
78 | break;
79 | case PAGE_DIALOG: //弹窗,场景:确认地址
80 | String msg = Helper.hasText(rootNode, TEXT_CONFIRM_ADDRESS) ? TEXT_CONFIRM_ADDRESS : "请手动关闭一下弹窗!";
81 | toastWithVoice(service, msg);
82 | break;
83 | case PAGE_PLACE_ORDER:
84 | //提交订单
85 | selectTimeAndPlaceOrder(event.getSource(), service);
86 | break;
87 | case PAGE_HOME:
88 | //切换到购物车页面,点击结算
89 | checkChartAndOrder(rootNode, service);
90 | break;
91 | case PAGE_PAYING:
92 | //正在支付
93 | // TODO: 4/8/22 提示用户支付
94 | toastWithVoice(service, "快来支付!");
95 | break;
96 | default:
97 | logd("error state, lastState: " + lastState );
98 |
99 | if (lastState == GrabState.HOME_PAGE) {
100 | checkChartAndOrder(rootNode, service);
101 | } else if (lastState == GrabState.SELECT_DELIVERY_TIME_PAGE) {
102 | selectDeliveryTime(rootNode, service);
103 | } else if (lastState == GrabState.PAYMENT_PAGE) {
104 | selectTimeAndPlaceOrder(rootNode, service);
105 | } else {
106 | sleep(200);
107 | checkChartAndOrder(rootNode, service);
108 | }
109 | break;
110 | }
111 | }
112 |
113 | private void sleep(long time) {
114 | try {
115 | Thread.sleep(time);
116 | } catch (InterruptedException e) {
117 | e.printStackTrace();
118 | }
119 | }
120 |
121 | private void skipAD(AccessibilityNodeInfo rootNode, AccessibilityService service) {
122 | Log.d(TAG, "page s1 ");
123 | updateState(GrabState.SPLASH_PAGE);
124 | tryClickText(TEXT_SKIP_AD);
125 | }
126 |
127 | private void updateState(GrabState state) {
128 | this.lastState = state;
129 | }
130 |
131 | private boolean tryClickTextWithInterval(String text, long interval) {
132 | return tryClickTextWithInterval(text, interval, COUNT_CHECK_TEXT);
133 | }
134 |
135 | private boolean tryClickTextWithInterval(String text, long interval, long maxCount) {
136 |
137 | long now = System.currentTimeMillis();
138 | if (now - lastClickPayTime < CLICK_PAY_INTERVAL) {
139 | return false;
140 | }
141 | lastClickPayTime = now;
142 | return tryClickText(text, interval, interval);
143 | }
144 |
145 | private boolean tryClickText(String text) {
146 | return tryClickText(text, 200, COUNT_CHECK_TEXT);
147 | }
148 |
149 | private boolean tryClickText(String text, long interval, long maxCount) {
150 | for (int i = 0; i < maxCount; i++) {
151 | boolean clicked = Helper.actionText(text);
152 | if (clicked) {
153 | return true;
154 | }
155 | sleep(interval);
156 | }
157 |
158 | return false;
159 | }
160 |
161 | private void selectTimeAndPlaceOrder(AccessibilityNodeInfo rootNode, AccessibilityService service) {
162 | Log.d(TAG, "page s3 ");
163 | updateState(GrabState.PAYMENT_PAGE);
164 |
165 | //0.额外弹窗
166 | clickDialogIfExist(rootNode, service);
167 |
168 | //1.是否切换到送货上门
169 | boolean isDeliveryPage = Helper.hasText(rootNode, TEXT_DELIVERY_TIME);
170 |
171 | logd("selectTimeAndPlaceOrder s1, isDeliveryPage? " + isDeliveryPage);
172 |
173 | if (!isDeliveryPage) {
174 | boolean clicked = tryClickText(TEXT_DELIVERY_HOME);
175 |
176 | if (!clicked) {
177 | //没有加载到,可能是有弹窗?
178 | clickDialogIfExist(rootNode, service);
179 | return;
180 | }
181 | }
182 |
183 | //2.是否选择送达时间
184 | boolean hasDeliveryTimeSelectText = Helper.hasText(rootNode, TEXT_DELIVERY_TIME_SELECT);
185 | Log.d(TAG, "selectTimeAndPlaceOrder s2, hasDeliveryTimeSelectText? " + hasDeliveryTimeSelectText );
186 | if (hasDeliveryTimeSelectText) {
187 | //需要去选择时间
188 | updateState(GrabState.SELECT_DELIVERY_TIME_PAGE);
189 | tryClickText(TEXT_DELIVERY_TIME_SELECT);
190 | return;
191 | }
192 |
193 | //3.下单
194 | clickPay(rootNode, service);
195 | }
196 |
197 | void clickPay(AccessibilityNodeInfo rootNode, AccessibilityService service) {
198 | boolean clicked = tryClickTextWithInterval(TEXT_PAY_NOW, 100, 3);
199 | if (clicked) {
200 | return;
201 | }
202 |
203 | boolean clickedPay = tryClickTextWithInterval(TEXT_PAY, 100);
204 | loge("clicked pay? " + clickedPay);
205 | if (!clickedPay) {
206 | clickDialogIfExist(rootNode, service);
207 | }
208 | }
209 |
210 | void clickDialogIfExist(AccessibilityNodeInfo rootNode, AccessibilityService service) {
211 |
212 | if (Helper.hasText(rootNode, TEXT_FINE)) {
213 | boolean clicked = tryClickText(TEXT_FINE);
214 |
215 | if (clicked) {
216 | //点击后切换一下 tab
217 | Helper.clickNode(rootNode, ID_TAB_MINE);
218 | }
219 | }
220 |
221 |
222 | if (Helper.hasText(rootNode, TEXT_ENSURE_ADDRESS)) {
223 | tryClickText(TEXT_ENSURE_ADDRESS);
224 | }
225 |
226 | //返回购物车
227 | if (Helper.hasText(rootNode, TEXT_BACK_HOME)) {
228 | tryClickText(TEXT_BACK_HOME);
229 | updateState(GrabState.CHART_PAGE);
230 | }
231 |
232 |
233 | if (Helper.findNode(rootNode, ID_TAB_CHART) != null) {
234 | checkChartAndOrder(rootNode, service);
235 | } else {
236 | loge("find ID_TAB_CHART false ");
237 | }
238 |
239 | if (Helper.hasText(rootNode, TEXT_GIVE_UP)) {
240 | tryClickText(TEXT_GIVE_UP);
241 | }
242 | }
243 |
244 | /**
245 | * 选择送达时间
246 | * @param rootNode
247 | * @param service
248 | */
249 | void selectDeliveryTime(AccessibilityNodeInfo rootNode, AccessibilityService service) {
250 | List timeNodes = Helper.tryGetNodes(rootNode, "0-", 200, 5);
251 | int nodeSize = timeNodes == null ? 0 : timeNodes.size();
252 | loge("timeNodes: " + nodeSize);
253 |
254 | for (int i = nodeSize-1; i >= 0; i--) {
255 | boolean clicked = Helper.clickNode(timeNodes.get(i));
256 | if (clicked) {
257 | updateState(GrabState.PAYMENT_PAGE);
258 | break;
259 | }
260 | }
261 |
262 | }
263 |
264 |
265 | /**
266 | * 如果没进入购物车页面,先进入购物车,然后结算(没选择物品前,结算可能无法点击,提醒用户)
267 | * @param rootNode
268 | * @param service
269 | */
270 | private void checkChartAndOrder(AccessibilityNodeInfo rootNode, AccessibilityService service) {
271 | Log.d(TAG, "page s2 ");
272 | updateState(GrabState.HOME_PAGE);
273 |
274 | boolean isChartPage = Helper.hasText(rootNode, TEXT_ORDER_BTN);
275 |
276 | logd(" checkChartAndOrder, isChartPage? " + isChartPage);
277 | if (!isChartPage) {
278 | //切换到购物车
279 | boolean clicked = Helper.clickNode(rootNode, ID_TAB_CHART);
280 | return;
281 | }
282 |
283 | AccessibilityNodeInfo orderNode = Helper.getNodeByText(rootNode, TEXT_ORDER_BTN);
284 | if (orderNode != null && orderNode.getText() != null) {
285 | String text = orderNode.getText().toString();
286 | loge("text: " + text);
287 | if (TextUtils.equals("结算(0)", text) || TextUtils.equals("结算", text)) {
288 | //没有可结算物品
289 | toastWithVoice(service, "没有可结算物品,请先加到购物车并选择");
290 | return;
291 | }
292 | }
293 |
294 | updateState(GrabState.CHART_PAGE);
295 | //点击结算
296 | boolean placeOrdered = tryClickText(TEXT_ORDER_BTN, 100, COUNT_CHECK_TEXT);
297 | Log.e(TAG, "placeOrdered: " + placeOrdered);
298 |
299 | if (!placeOrdered) {
300 | clickDialogIfExist(rootNode, service);
301 | }
302 | }
303 |
304 | /**
305 | * todo: 加上声音提示:
306 | * @param context
307 | * @param info
308 | */
309 | void toastWithVoice(Context context, String info) {
310 |
311 | Toast.makeText(context, info, Toast.LENGTH_SHORT).show();
312 | }
313 |
314 | void toast(Context context, String info) {
315 |
316 | Toast.makeText(context, info, Toast.LENGTH_SHORT).show();
317 | }
318 |
319 | void logd(String s) {
320 | Log.d(TAG, s);
321 | }
322 |
323 | void loge(String s) {
324 | Log.e(TAG, s);
325 | }
326 | }
327 |
--------------------------------------------------------------------------------