From ee36b43981532b178997510f5b11d5759e863348 Mon Sep 17 00:00:00 2001 From: lihongwei Date: Wed, 23 Apr 2025 10:11:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9B=E5=BB=BA=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 15 ++ app/.gitignore | 1 + app/build.gradle.kts | 47 ++++ app/proguard-rules.pro | 21 ++ .../autoclicker/ExampleInstrumentedTest.java | 26 ++ app/src/main/AndroidManifest.xml | 52 ++++ .../auto/autoclicker/AutoClickService.java | 199 +++++++++++++++ .../auto/autoclicker/FloatingViewManager.java | 227 ++++++++++++++++++ .../auto/autoclicker/ForegroundService.java | 106 ++++++++ .../com/auto/autoclicker/MainActivity.java | 178 ++++++++++++++ .../drawable-v24/ic_launcher_foreground.xml | 30 +++ .../res/drawable/ic_launcher_background.xml | 170 +++++++++++++ app/src/main/res/drawable/notification.xml | 14 ++ app/src/main/res/drawable/shoot.xml | 9 + app/src/main/res/layout/activity_main.xml | 17 ++ app/src/main/res/layout/control_bar.xml | 16 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes app/src/main/res/values-night/themes.xml | 7 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/strings.xml | 9 + app/src/main/res/values/themes.xml | 9 + .../res/xml/accessibility_service_config.xml | 8 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ .../com/auto/autoclicker/ExampleUnitTest.java | 17 ++ build.gradle.kts | 4 + gradle.properties | 21 ++ gradle/libs.versions.toml | 22 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++++++++++++++ gradlew.bat | 89 +++++++ settings.gradle.kts | 24 ++ 44 files changed, 1578 insertions(+) create mode 100644 .gitignore create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/auto/autoclicker/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/auto/autoclicker/AutoClickService.java create mode 100644 app/src/main/java/com/auto/autoclicker/FloatingViewManager.java create mode 100644 app/src/main/java/com/auto/autoclicker/ForegroundService.java create mode 100644 app/src/main/java/com/auto/autoclicker/MainActivity.java create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/notification.xml create mode 100644 app/src/main/res/drawable/shoot.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/control_bar.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/accessibility_service_config.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/auto/autoclicker/ExampleUnitTest.java create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml 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.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..ff0d066 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace = "com.auto.autoclicker" + compileSdk = 35 + + defaultConfig { + applicationId = "com.auto.autoclicker" + minSdk = 23 + targetSdk = 35 + versionCode = 1 + versionName = "1.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures{ + viewBinding = true; + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + + implementation(libs.appcompat) + implementation(libs.material) + implementation(libs.activity) + implementation(libs.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# 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 *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/auto/autoclicker/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/auto/autoclicker/ExampleInstrumentedTest.java new file mode 100644 index 0000000..3c018ee --- /dev/null +++ b/app/src/androidTest/java/com/auto/autoclicker/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.auto.autoclicker; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.auto.autoclicker", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8a4f149 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/auto/autoclicker/AutoClickService.java b/app/src/main/java/com/auto/autoclicker/AutoClickService.java new file mode 100644 index 0000000..ca2b55b --- /dev/null +++ b/app/src/main/java/com/auto/autoclicker/AutoClickService.java @@ -0,0 +1,199 @@ +package com.auto.autoclicker; + +import android.accessibilityservice.AccessibilityService; +import android.accessibilityservice.GestureDescription; +import android.graphics.Path; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; + +/** + * 无障碍服务,执行自动点击操作 + */ +public class AutoClickService extends AccessibilityService { + private static final String TAG = "AutoClickService"; + private static volatile AutoClickService instance; + private final Handler handler = new Handler(Looper.getMainLooper()); + private boolean isClicking = false; + private int clickX = 500; + private int clickY = 500; + private long clickInterval = 1000; // 默认1秒间隔 + private int clickDuration = 200; // 默认200毫秒点击时长 + private int screenWidth, screenHeight; + + @Override + public void onCreate() { + super.onCreate(); + instance = this; + // 获取屏幕尺寸 + DisplayMetrics metrics = getResources().getDisplayMetrics(); + screenWidth = metrics.widthPixels; + screenHeight = metrics.heightPixels; + Log.d(TAG, "屏幕尺寸: " + screenWidth + "x" + screenHeight); + } + + public static AutoClickService getInstance() { + return instance; + } + + /** + * 设置点击位置 + */ + public void setClickPosition(float x, float y) { + this.clickX = (int) Math.max(0, Math.min(x, screenWidth)); + this.clickY = (int) Math.max(0, Math.min(y, screenHeight)); + Log.d(TAG, "点击位置设置为: (" + clickX + ", " + clickY + ")"); + Log.d(TAG, "设置点击位置为: (" + clickX + ", " + clickY + "), 屏幕尺寸: " + screenWidth + "x" + screenHeight); + } + + /** + * 设置点击间隔 + */ + public void setClickInterval(long interval) { + if (interval > 0) { + this.clickInterval = interval; + Log.d(TAG, "点击间隔设置为: " + interval + "ms"); + } + } + + /** + * 设置点击时长 + */ + public void setClickDuration(int duration) { + if (duration > 0) { + this.clickDuration = duration; + Log.d(TAG, "点击时长设置为: " + duration + "ms"); + } + } + + /** + * 开始自动点击 + */ + public void startClicking() { + if (!isClicking) { + isClicking = true; + performClick(); + Log.d(TAG, "开始点击: (" + clickX + ", " + clickY + ")"); + } + } + + /** + * 停止自动点击 + */ + public void stopClicking() { + if (isClicking) { + isClicking = false; + handler.removeCallbacksAndMessages(null); + Log.d(TAG, "停止点击"); + } + } + + /** + * 执行单次点击 + */ + private void performClick() { + if (!isClicking || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + Log.w(TAG, "点击被跳过(未开始或系统版本不支持)"); + return; + } + + Path path = new Path(); + path.moveTo(clickX, clickY); + path.lineTo(clickX, clickY); // 创建一个点状路径 + + GestureDescription.StrokeDescription stroke = + new GestureDescription.StrokeDescription(path, 0, 10); // 10ms 点击 + + GestureDescription gesture = + new GestureDescription.Builder().addStroke(stroke).build(); + + dispatchGesture(gesture, new GestureResultCallback() { + @Override + public void onCompleted(GestureDescription gestureDescription) { + super.onCompleted(gestureDescription); + Log.i(TAG, "点击完成: (" + clickX + ", " + clickY + ")"); + if (isClicking) { + handler.postDelayed(() -> performClick(), clickInterval); + } + } + + @Override + public void onCancelled(GestureDescription gestureDescription) { + super.onCancelled(gestureDescription); + Log.e(TAG, "点击取消(可能位置错误或界面变化): (" + clickX + ", " + clickY + ")"); + if (isClicking) { + handler.postDelayed(() -> performClick(), clickInterval + 300); // 稍微延迟重试 + } + } + }, null); + } + + public void testSingleClick(float x, float y) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + Log.e(TAG, "设备不支持手势(需要 API 24+)"); + return; + } + + try { + Path path = new Path(); + path.moveTo(x, y); + path.lineTo(x, y); // 明确指定按下和抬起 + Log.d(TAG, "测试点击: (" + x + ", " + y + ")"); + + GestureDescription.StrokeDescription stroke = new GestureDescription.StrokeDescription( + path, 0, 100 + ); + GestureDescription gesture = new GestureDescription.Builder() + .addStroke(stroke) + .build(); + + boolean dispatched = dispatchGesture(gesture, new GestureResultCallback() { + @Override + public void onCompleted(GestureDescription gestureDescription) { + Log.v(TAG, "测试点击完成: (" + x + ", " + y + ")"); + } + + @Override + public void onCancelled(GestureDescription gestureDescription) { + Log.w(TAG, "测试点击取消: (" + x + ", " + y + ")"); + } + }, null); + + Log.d(TAG, "手势分发结果: " + (dispatched ? "成功" : "失败")); + } catch (Exception e) { + Log.e(TAG, "测试点击失败", e); + } + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + // 不处理事件 + } + + @Override + public void onInterrupt() { + stopClicking(); + Log.d(TAG, "无障碍服务中断"); + } + + @Override + protected void onServiceConnected() { + super.onServiceConnected(); + Log.d(TAG, "无障碍服务已连接"); + } + + public boolean isClicking() { + return isClicking; + } + + @Override + public void onDestroy() { + super.onDestroy(); + stopClicking(); + instance = null; + Log.d(TAG, "无障碍服务已销毁"); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/auto/autoclicker/FloatingViewManager.java b/app/src/main/java/com/auto/autoclicker/FloatingViewManager.java new file mode 100644 index 0000000..75d80e8 --- /dev/null +++ b/app/src/main/java/com/auto/autoclicker/FloatingViewManager.java @@ -0,0 +1,227 @@ +package com.auto.autoclicker; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.os.Build; +import android.provider.Settings; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.Toast; + +/** + * 悬浮窗管理器,负责触摸点和控制栏的显示与交互 + */ +public class FloatingViewManager { + private static final String TAG = "FloatingViewManager"; + private final Context context; + private final WindowManager windowManager; + private View touchPointView; + private LinearLayout controlBarView; + private WindowManager.LayoutParams touchPointParams; + private WindowManager.LayoutParams controlBarParams; + private float touchPointX = 500; + private float touchPointY = 500; + private boolean isClicking = false; + private int screenWidth, screenHeight; + private long lastToggleTime = 0; + private static final long DEBOUNCE_INTERVAL = 500; + private static final int CONTROL_BAR_WIDTH = 200; // 假设控制栏宽度 + private static final int CONTROL_BAR_HEIGHT = 100; // 假设控制栏高度 + + public FloatingViewManager(Context context) { + this.context = context; + this.windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + screenWidth = metrics.widthPixels; + screenHeight = metrics.heightPixels; + Log.d(TAG, "屏幕尺寸: " + screenWidth + "x" + screenHeight); + } + + /** + * 显示悬浮窗(触摸点和控制栏) + */ + public void showFloatingViews() throws SecurityException { + if (!Settings.canDrawOverlays(context)) { + throw new SecurityException("需要悬浮窗权限"); + } + + if (touchPointView != null || controlBarView != null) { + Log.w(TAG, "悬浮窗已存在,跳过创建"); + return; + } + + initializeTouchPointView(); + initializeControlBarView(); + + windowManager.addView(touchPointView, touchPointParams); + windowManager.addView(controlBarView, controlBarParams); + Log.d(TAG, "悬浮窗已添加"); + } + + /** + * 初始化触摸点视图 + */ + private void initializeTouchPointView() { + touchPointView = new View(context); + touchPointView.setBackgroundResource(R.drawable.shoot); // 使用触摸点图标 + touchPointParams = new WindowManager.LayoutParams( + 100, 100, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : + WindowManager.LayoutParams.TYPE_PHONE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + PixelFormat.TRANSLUCENT + ); + touchPointParams.gravity = Gravity.TOP | Gravity.LEFT; + touchPointParams.x = (int) touchPointX; + touchPointParams.y = (int) touchPointY; + + touchPointView.setOnTouchListener(new View.OnTouchListener() { + private float lastX, lastY; + private float paramX, paramY; + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + lastX = event.getRawX(); + lastY = event.getRawY(); + paramX = touchPointParams.x; + paramY = touchPointParams.y; + break; + case MotionEvent.ACTION_MOVE: + float dx = event.getRawX() - lastX; + float dy = event.getRawY() - lastY; + touchPointParams.x = (int) Math.max(0, Math.min(paramX + dx, screenWidth - 100)); + touchPointParams.y = (int) Math.max(0, Math.min(paramY + dy, screenHeight - 100)); + touchPointX = touchPointParams.x; + touchPointY = touchPointParams.y; + windowManager.updateViewLayout(touchPointView, touchPointParams); + AutoClickService service = AutoClickService.getInstance(); + if (service != null) { + service.setClickPosition(touchPointX + 50, touchPointY + 50); + Log.d(TAG, "触摸点移动到: (" + touchPointX + ", " + touchPointY + ")"); + } + break; + } + return true; + } + }); + } + + /** + * 初始化控制栏视图 + */ + private void initializeControlBarView() { + controlBarView = (LinearLayout) LayoutInflater.from(context).inflate(R.layout.control_bar, null); + controlBarParams = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : + WindowManager.LayoutParams.TYPE_PHONE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + PixelFormat.TRANSLUCENT + ); + controlBarParams.gravity = Gravity.TOP | Gravity.LEFT; + controlBarParams.x = 0; + controlBarParams.y = 200; + + // 替换 controlBarView 的 onTouchListener 和 toggleButton 的 OnClickListener + controlBarView.setOnTouchListener(new View.OnTouchListener() { + private float lastX, lastY; + private float paramX, paramY; + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + lastX = event.getRawX(); + lastY = event.getRawY(); + paramX = controlBarParams.x; + paramY = controlBarParams.y; + return true; + case MotionEvent.ACTION_MOVE: + float dx = event.getRawX() - lastX; + float dy = event.getRawY() - lastY; + controlBarParams.x = (int) Math.max(0, Math.min(paramX + dx, screenWidth - CONTROL_BAR_WIDTH)); + controlBarParams.y = (int) Math.max(0, Math.min(paramY + dy, screenHeight - CONTROL_BAR_HEIGHT)); + windowManager.updateViewLayout(controlBarView, controlBarParams); + return true; + } + return false; + } + }); + + Button toggleButton = controlBarView.findViewById(R.id.toggle_button); + toggleButton.setOnClickListener(v -> { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastToggleTime < DEBOUNCE_INTERVAL) { + return; + } + lastToggleTime = currentTime; + + AutoClickService service = AutoClickService.getInstance(); + if (service == null) { + Log.e(TAG, "AutoClickService 未初始化"); + Toast.makeText(context, "请启用无障碍服务", Toast.LENGTH_SHORT).show(); + return; + } + + if (isClicking) { + service.stopClicking(); + toggleButton.setText(R.string.start_click); + touchPointView.setBackgroundResource(R.drawable.shoot); + Toast.makeText(context, "停止自动点击", Toast.LENGTH_SHORT).show(); + } else { + service.setClickPosition(touchPointX + 50, touchPointY + 50); + service.startClicking(); + toggleButton.setText(R.string.stop_click); + touchPointView.setBackgroundColor(Color.GREEN); + Toast.makeText(context, "开始自动点击", Toast.LENGTH_SHORT).show(); + } + + isClicking = !isClicking; + + // 测试单次点击(屏幕中央) + float testX = screenWidth / 2f; + float testY = screenHeight / 2f; + service.testSingleClick(testX, testY); + Toast.makeText(context, "测试点击: (" + testX + ", " + testY + ")", Toast.LENGTH_SHORT).show(); + + Log.d(TAG, "AutoClickService 实例:" + service); + Log.d(TAG, "服务是否点击中:" + service.isClicking()); + + }); + + } + + /** + * 移除悬浮窗 + */ + public void removeFloatingViews() { + try { + if (touchPointView != null) { + windowManager.removeView(touchPointView); + touchPointView = null; + } + if (controlBarView != null) { + windowManager.removeView(controlBarView); + controlBarView = null; + } + Log.d(TAG, "悬浮窗已移除"); + } catch (Exception e) { + Log.e(TAG, "移除悬浮窗失败", e); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/auto/autoclicker/ForegroundService.java b/app/src/main/java/com/auto/autoclicker/ForegroundService.java new file mode 100644 index 0000000..1d651c4 --- /dev/null +++ b/app/src/main/java/com/auto/autoclicker/ForegroundService.java @@ -0,0 +1,106 @@ +package com.auto.autoclicker; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.core.app.NotificationCompat; + +/** + * 前台服务,管理自动点击的悬浮窗和通知 + */ +public class ForegroundService extends Service { + private static final String TAG = "ForegroundService"; + private static final String CHANNEL_ID = "AutoClickerChannel"; + private static final int NOTIFICATION_ID = 1; + private FloatingViewManager floatingViewManager; + private boolean isFloatingViewShown = false; + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + try { + floatingViewManager = new FloatingViewManager(this); + } catch (Exception e) { + Log.e(TAG, "初始化 FloatingViewManager 失败", e); + stopSelf(); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // 创建通知 + Intent notificationIntent = new Intent(this, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity( + this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE + ); + + Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("自动点击运行中") + .setContentText("自动点击正在后台运行") + .setSmallIcon(R.drawable.notification) + .setContentIntent(pendingIntent) + .setOngoing(true) + .build(); + + startForeground(NOTIFICATION_ID, notification); + + // 仅在未显示悬浮窗时显示 + if (floatingViewManager != null && !isFloatingViewShown) { + try { + floatingViewManager.showFloatingViews(); + isFloatingViewShown = true; + Log.d(TAG, "悬浮窗已显示"); + } catch (SecurityException e) { + Log.e(TAG, "未授予悬浮窗权限", e); + stopSelf(); + } + } else if (floatingViewManager == null) { + Log.e(TAG, "FloatingViewManager 未初始化"); + stopSelf(); + } + + return START_STICKY; + } + + /** + * 创建通知渠道(Android 8.0+) + */ + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "自动点击服务", + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription("自动点击前台服务通知渠道"); + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (floatingViewManager != null) { + floatingViewManager.removeFloatingViews(); + isFloatingViewShown = false; + Log.d(TAG, "悬浮窗已移除"); + } + stopForeground(true); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/auto/autoclicker/MainActivity.java b/app/src/main/java/com/auto/autoclicker/MainActivity.java new file mode 100644 index 0000000..c23c05a --- /dev/null +++ b/app/src/main/java/com/auto/autoclicker/MainActivity.java @@ -0,0 +1,178 @@ +package com.auto.autoclicker; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.widget.Button; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; + +/** + * 主活动,负责权限检查和控制悬浮窗显示/隐藏 + */ +public class MainActivity extends AppCompatActivity { + private static final String TAG = "MainActivity"; + private Button startButton; + private ActivityResultLauncher permissionLauncher; + private boolean isFloatingShown = false; + private long lastClickTime = 0; + private static final long DEBOUNCE_INTERVAL = 500; // 防抖间隔(毫秒) + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + startButton = findViewById(R.id.start_Button); + startButton.setOnClickListener(v -> toggleFloatingWindow()); + + // 初始化权限请求回调 + permissionLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> checkPermissions()); + + // 同步服务状态 + syncServiceState(); + checkPermissions(); + } + + /** + * 检查必要的权限:无障碍服务、悬浮窗、电池优化 + */ + private void checkPermissions() { + boolean allPermissionsGranted = true; + + // 检查无障碍服务 + if (!isAccessibilityServiceEnabled()) { + allPermissionsGranted = false; + try { + Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + permissionLauncher.launch(intent); + Toast.makeText(this, "请启用无障碍服务", Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Toast.makeText(this, "无法打开无障碍设置", Toast.LENGTH_LONG).show(); + } + } + + // 检查悬浮窗权限 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) { + allPermissionsGranted = false; + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())); + permissionLauncher.launch(intent); + Toast.makeText(this, "请授予悬浮窗权限", Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Toast.makeText(this, "无法打开悬浮窗设置", Toast.LENGTH_LONG).show(); + } + } + + // 检查电池优化 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (!pm.isIgnoringBatteryOptimizations(getPackageName())) { + allPermissionsGranted = false; + try { + Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.parse("package:" + getPackageName())); + permissionLauncher.launch(intent); + Toast.makeText(this, "请禁用电池优化", Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Toast.makeText(this, "无法打开电池优化设置", Toast.LENGTH_LONG).show(); + } + } + } + + // 更新按钮状态 + startButton.setEnabled(allPermissionsGranted); + updateButtonText(); + } + + /** + * 检查无障碍服务是否启用 + */ + private boolean isAccessibilityServiceEnabled() { + String service = getPackageName() + "/" + AutoClickService.class.getCanonicalName(); + try { + int accessibilityEnabled = Settings.Secure.getInt( + getContentResolver(), + Settings.Secure.ACCESSIBILITY_ENABLED); + if (accessibilityEnabled == 1) { + String settingValue = Settings.Secure.getString( + getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); + return settingValue != null && settingValue.contains(service); + } + } catch (Settings.SettingNotFoundException e) { + e.printStackTrace(); + } + return false; + } + + /** + * 同步服务状态(从通知栏进入时) + */ + private void syncServiceState() { + isFloatingShown = isServiceRunning(ForegroundService.class); + updateButtonText(); + } + + /** + * 检查服务是否运行 + */ + private boolean isServiceRunning(Class serviceClass) { + ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + if (serviceClass.getName().equals(service.service.getClassName())) { + return true; + } + } + return false; + } + + /** + * 切换悬浮窗显示/隐藏 + */ + private void toggleFloatingWindow() { + // 防抖处理 + long currentTime = System.currentTimeMillis(); + if (currentTime - lastClickTime < DEBOUNCE_INTERVAL) { + return; + } + lastClickTime = currentTime; + + if (!isAccessibilityServiceEnabled() || !Settings.canDrawOverlays(this)) { + Toast.makeText(this, "请授予所有必要权限", Toast.LENGTH_LONG).show(); + checkPermissions(); + return; + } + + if (!isFloatingShown) { + Intent serviceIntent = new Intent(this, ForegroundService.class); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent); + } else { + startService(serviceIntent); + } + isFloatingShown = true; + Toast.makeText(this, "悬浮窗已显示", Toast.LENGTH_SHORT).show(); + } else { + stopService(new Intent(this, ForegroundService.class)); + isFloatingShown = false; + Toast.makeText(this, "悬浮窗已隐藏", Toast.LENGTH_SHORT).show(); + } + updateButtonText(); + } + + /** + * 更新按钮文本 + */ + private void updateButtonText() { + startButton.setText(isFloatingShown ? R.string.hide_floating_window : R.string.show_floating_window); + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/notification.xml b/app/src/main/res/drawable/notification.xml new file mode 100644 index 0000000..2ca1304 --- /dev/null +++ b/app/src/main/res/drawable/notification.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/shoot.xml b/app/src/main/res/drawable/shoot.xml new file mode 100644 index 0000000..4d72eb8 --- /dev/null +++ b/app/src/main/res/drawable/shoot.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b2e5c31 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,17 @@ + + + +