diff --git a/.github/workflows/build_apk.yml b/.github/workflows/build_apk.yml index 77c1a3c..206e768 100644 --- a/.github/workflows/build_apk.yml +++ b/.github/workflows/build_apk.yml @@ -24,19 +24,19 @@ jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: checkout codes - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: '0' # 0 indicates all history, this is needed for git revision - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: distribution: 'temurin' java-version: '17' @@ -48,7 +48,7 @@ jobs: run: ./gradlew assembleRelease --stacktrace - name: Upload APK to artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: TouchHelper path: ./app/build/outputs/apk/ @@ -65,7 +65,7 @@ jobs: echo "release_name=$(($(git rev-list HEAD --count) + 100))" >> $GITHUB_OUTPUT - name: Create Release and Upload APK - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/app/build.gradle b/app/build.gradle index 5adf81e..3bc67c9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,7 +14,7 @@ def appVersion = getDate() android { namespace "com.zfdang.touchhelper" - compileSdkVersion 35 + compileSdkVersion 36 buildFeatures { buildConfig true @@ -23,7 +23,7 @@ android { defaultConfig { applicationId "com.zfdang.touchhelper" minSdkVersion 26 - targetSdkVersion 35 + targetSdkVersion 36 versionCode gitVersion versionName appVersion @@ -81,21 +81,25 @@ android { dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.android.material:material:1.2.1' - implementation 'androidx.constraintlayout:constraintlayout:2.0.2' - implementation 'androidx.navigation:navigation-fragment:2.1.0' - implementation 'androidx.navigation:navigation-ui:2.1.0' - implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.activity:activity:1.9.3' + implementation 'androidx.fragment:fragment:1.8.5' + implementation 'com.google.android.material:material:1.12.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.0' + implementation 'androidx.navigation:navigation-fragment:2.8.5' + implementation 'androidx.navigation:navigation-ui:2.8.5' + // lifecycle-extensions is discontinued; ViewModel / LiveData ship as separate artifacts + implementation 'androidx.lifecycle:lifecycle-viewmodel:2.8.7' + implementation 'androidx.lifecycle:lifecycle-livedata:2.8.7' testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.14.1' - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' - implementation 'com.google.code.gson:gson:2.8.5' + implementation 'com.google.code.gson:gson:2.11.0' // https://developer.android.com/jetpack/androidx/releases/preference?hl=zh-cn - implementation 'androidx.preference:preference:1.1.1' + implementation 'androidx.preference:preference:1.2.1' // https://mvnrepository.com/artifact/com.belerweb/pinyin4j implementation group: 'com.belerweb', name: 'pinyin4j', version: '2.5.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e28d34d..7fd28d4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,15 +1,25 @@ - - - - - - + + + + + + + + + @@ -20,7 +30,6 @@ android:label="@string/app_name" android:roundIcon="@drawable/ic_touch_helper_icon" android:supportsRtl="true" - android:persistent="true" android:theme="@style/AppTheme"> querySystemLauncherPackages() { return pkgSystems; } + // has the user confirmed the in-app disclosure about what the accessibility service reads? + // Google Play requires this consent before the service may process screen content, so the + // service stays idle until it is recorded (also for users who enabled the service before + // the disclosure existed). + private static final String ACCESSIBILITY_DISCLOSURE_ACCEPTED = "ACCESSIBILITY_DISCLOSURE_ACCEPTED"; + private volatile boolean bDisclosureAccepted; + public boolean isDisclosureAccepted() { + return bDisclosureAccepted; + } + public void setDisclosureAccepted(boolean accepted) { + bDisclosureAccepted = accepted; + mEditor.putBoolean(ACCESSIBILITY_DISCLOSURE_ACCEPTED, accepted); + mEditor.apply(); + } + // notification on skip ads? private static final String SKIP_AD_NOTIFICATION = "SKIP_AD_NOTIFICATION"; private boolean bSkipAdNotification; diff --git a/app/src/main/java/com/zfdang/touchhelper/TouchHelperServiceImpl.java b/app/src/main/java/com/zfdang/touchhelper/TouchHelperServiceImpl.java index 4d1f32e..77cc05f 100644 --- a/app/src/main/java/com/zfdang/touchhelper/TouchHelperServiceImpl.java +++ b/app/src/main/java/com/zfdang/touchhelper/TouchHelperServiceImpl.java @@ -352,6 +352,12 @@ private void InstallReceiverAndHandler() { // 1. TYPE_WINDOW_STATE_CHANGED, 判断packageName和activityName,决定是否开始跳过检测; 然后使用三种方法去尝试跳过 // 2. TYPE_WINDOW_CONTENT_CHANGED, 使用两种方法去尝试跳过 public void onAccessibilityEvent(AccessibilityEvent event) { + if (!mSetting.isDisclosureAccepted()) { + // no screen content is read or clicked until the user has confirmed the in-app + // disclosure; tell them once per process where to do that + remindDisclosureOnce(); + return; + } if (BuildConfig.DEBUG) { Log.d(TAG, AccessibilityEvent.eventTypeToString(event.getEventType()) + " - " + event.getPackageName() + " - " + event.getClassName() + "; "); Log.d(TAG, " currentPackageName = " + currentPackageName + " currentActivityName = " + currentActivityName); @@ -501,6 +507,14 @@ public void run() { } } + private boolean disclosureReminderShown; + + private void remindDisclosureOnce() { + if (disclosureReminderShown || receiverHandler == null) return; + disclosureReminderShown = true; + receiverHandler.post(() -> Toast.makeText(service, R.string.accessibility_disclosure_reminder, Toast.LENGTH_LONG).show()); + } + public void onUnbind(Intent intent) { try { stopSkipAdProcessInner(); diff --git a/app/src/main/java/com/zfdang/touchhelper/ui/about/AboutFragment.java b/app/src/main/java/com/zfdang/touchhelper/ui/about/AboutFragment.java index 6129bc0..a1bffd6 100644 --- a/app/src/main/java/com/zfdang/touchhelper/ui/about/AboutFragment.java +++ b/app/src/main/java/com/zfdang/touchhelper/ui/about/AboutFragment.java @@ -11,9 +11,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.pm.PackageInfoCompat; import androidx.fragment.app.Fragment; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProviders; import com.zfdang.touchhelper.R; @@ -28,12 +27,12 @@ public View onCreateView(@NonNull LayoutInflater inflater, TextView tvVersion = root.findViewById(R.id.textView_version); String versionName = "unknown"; - int versionCode = 0; + long versionCode = 0; PackageManager pm = getActivity().getPackageManager(); try { PackageInfo pi = pm.getPackageInfo(getActivity().getPackageName(), 0); versionName = pi.versionName; - versionCode = pi.versionCode; + versionCode = PackageInfoCompat.getLongVersionCode(pi); // generate about_content with version from manifest } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "showInfoDialog: " + Log.getStackTraceString(e)); diff --git a/app/src/main/java/com/zfdang/touchhelper/ui/home/HomeFragment.java b/app/src/main/java/com/zfdang/touchhelper/ui/home/HomeFragment.java index 7ddfc6d..5f07590 100644 --- a/app/src/main/java/com/zfdang/touchhelper/ui/home/HomeFragment.java +++ b/app/src/main/java/com/zfdang/touchhelper/ui/home/HomeFragment.java @@ -1,9 +1,7 @@ package com.zfdang.touchhelper.ui.home; -import android.Manifest; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -11,27 +9,20 @@ import android.os.Bundle; import android.os.PowerManager; import android.provider.Settings; -import android.text.method.Touch; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.LongDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; -import androidx.core.content.res.ResourcesCompat; import androidx.fragment.app.Fragment; -import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProviders; +import androidx.lifecycle.ViewModelProvider; import com.zfdang.touchhelper.R; import com.zfdang.touchhelper.TouchHelperService; @@ -44,8 +35,7 @@ public class HomeFragment extends Fragment { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - homeViewModel = - ViewModelProviders.of(this).get(HomeViewModel.class); + homeViewModel = new ViewModelProvider(this).get(HomeViewModel.class); View root = inflater.inflate(R.layout.fragment_home, container, false); final Drawable drawableYes = ContextCompat.getDrawable(getContext(), R.drawable.ic_right); @@ -79,14 +69,7 @@ public void onChanged(Boolean aBoolean) { // set listener for buttons final ImageButton btAccessibilityPermission = root.findViewById(R.id.button_accessibility_permission); - btAccessibilityPermission.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Intent intent_abs = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); - intent_abs.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent_abs); - } - }); + btAccessibilityPermission.setOnClickListener(v -> showAccessibilityDisclosure()); final ImageButton btPowerPermission = root.findViewById(R.id.button_power_permission); btPowerPermission.setOnClickListener(new View.OnClickListener() { @@ -116,16 +99,59 @@ public void onClick(View v) { public void onResume() { checkServiceStatus(); super.onResume(); + // the service was enabled from the system settings (or before the disclosure existed) + // but the user has not confirmed the disclosure yet: it stays idle until they do + if (TouchHelperService.isServiceRunning() + && !com.zfdang.touchhelper.Settings.getInstance().isDisclosureAccepted()) { + showAccessibilityDisclosure(); + } } - public void checkServiceStatus(){ + @Override + public void onPause() { + if (disclosureDialog != null && disclosureDialog.isShowing()) { + disclosureDialog.dismiss(); + } + super.onPause(); + } + + /** + * Google Play requires a prominent in-app disclosure before an app sends the user to enable + * an accessibility service that is not an assistive tool: what the service reads, what it + * does with it, and that nothing leaves the device. + */ + private void showAccessibilityDisclosure() { + if (com.zfdang.touchhelper.Settings.getInstance().isDisclosureAccepted()) { + openAccessibilitySettings(); + return; + } + if (disclosureDialog != null && disclosureDialog.isShowing()) { + return; + } + final boolean serviceRunning = TouchHelperService.isServiceRunning(); + disclosureDialog = new AlertDialog.Builder(requireContext()) + .setTitle(R.string.accessibility_disclosure_title) + .setMessage(R.string.accessibility_disclosure_message) + .setPositiveButton(serviceRunning ? R.string.accessibility_disclosure_agree_only : R.string.accessibility_disclosure_agree, + (dialog, which) -> { + com.zfdang.touchhelper.Settings.getInstance().setDisclosureAccepted(true); + if (!serviceRunning) { + openAccessibilitySettings(); + } + }) + .setNegativeButton(R.string.accessibility_disclosure_cancel, null) + .show(); + } - // detect the app storage permission - boolean bAppPermission = - ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - MutableLiveData liveData = homeViewModel.getAppPermission(); - liveData.setValue(bAppPermission); + private AlertDialog disclosureDialog; + private void openAccessibilitySettings() { + Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + + public void checkServiceStatus(){ // detect the accessibility permission MutableLiveData accessibility = homeViewModel.getAccessibilityPermission(); accessibility.setValue(TouchHelperService.isServiceRunning()); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2a6a189..ca89df4 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,7 +4,28 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingTop="@dimen/navigation_view_padding_top"> + android:fitsSystemWindows="true"> + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a641426..02d4637 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -3,6 +3,4 @@ 16dp 16dp - 0dp - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3fa0dcc..6543d5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,4 +29,10 @@ Copyright © 2022 Zhengfa Dang 版本: %s (%d) + 开启无障碍服务前请了解 + \"开屏跳过\"通过安卓无障碍服务读取当前屏幕上的界面内容(控件文字、描述和位置),用于识别并自动点击开屏广告的\"跳过\"按钮。\n\n· 自动识别时,这些内容只在本机内存中即时比对,不保存。\n· 当您使用\"添加程序的跳过方法\"时,您所选控件的文字、描述和坐标会作为规则保存在本机的应用数据中,可在设置中查看或删除。\n· 当您使用\"复制窗口控件\"时,当前窗口的控件信息会按您的操作复制到系统剪贴板。\n\n本程序不申请网络权限,不会收集或上传任何数据。同意后即可开启无障碍服务;未同意前服务不会读取任何界面内容。 + 同意并前往设置 + 同意 + 开屏跳过:请打开应用并确认无障碍服务说明后,跳过功能才会开始工作 + 取消 \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index fac9291..9abd15b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,10 +1,17 @@ - + +