4 Commits
Author SHA1 Message Date
LittleChest a321cc564a Update deps 2026-08-30 01:33:31 +08:00
LittleChest 61c4588c4d 1.3
* Fix launcher hook on Android 17 QPR2 Beta 4
2026-08-30 01:00:55 +08:00
LittleChest 54abef0728 1.2 2025-08-19 21:24:26 +08:00
LittleChest fd404114d8 Init 1.1 2025-08-19 20:31:48 +08:00
20 changed files with 156 additions and 270 deletions
+1 -6
View File
@@ -1,12 +1,7 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea
.DS_Store
/build
/captures
-3
View File
@@ -1,3 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
Generated
-1
View File
@@ -1 +0,0 @@
Double Tap To Wake
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+5 -5
View File
@@ -4,14 +4,14 @@ plugins {
android {
namespace = "top.littlew.acer"
compileSdk = 36
compileSdk = 37
defaultConfig {
applicationId = "top.littlew.acer"
minSdk = 33
targetSdk = 36
versionCode = 1
versionName = "1.0"
minSdk = 34
targetSdk = 37
versionCode = 4
versionName = "1.3"
}
buildTypes {
+1 -1
View File
@@ -10,7 +10,7 @@
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="Enable App Clone for all user apps." />
android:value="Enable App Clone for all or custom user apps." />
<meta-data
android:name="xposedminversion"
android:value="93" />
+117 -146
View File
@@ -2,14 +2,20 @@ package top.littlew.acer;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.UserHandle;
import android.view.Menu;
import android.view.View;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.function.Predicate;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
@@ -19,169 +25,134 @@ import de.robv.android.xposed.callbacks.XC_LoadPackage;
public class XposedInit implements IXposedHookLoadPackage {
@SuppressLint("StaticFieldLeak")
private static Context mContext;
private static final int AVAILABLE = 0;
private static final int CLONED_APPS_SUMMARY_STRING_ID = 0x7f140736;
private static Class<?> UtilsClass;
private static List<String> cachedCloneableApps;
@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) {
if (!lpparam.packageName.equals("com.android.settings")) {
return;
}
UtilsClass = XposedHelpers.findClass("com.android.settings.Utils", lpparam.classLoader);
if (lpparam.packageName.equals("com.android.settings")) {
XposedHelpers.findAndHookMethod(XposedHelpers.findClass("com.android.settings.SettingsActivity", lpparam.classLoader), "onCreate", Bundle.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
mContext = (Context) param.thisObject;
}
});
XposedHelpers.findAndHookMethod(
XposedHelpers.findClass("com.android.settings.SettingsActivity", lpparam.classLoader),
"onCreate",
Bundle.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
mContext = (Context) param.thisObject;
Class<?> clonedAppsPreferenceControllerClass = XposedHelpers.findClass("com.android.settings.applications.ClonedAppsPreferenceController", lpparam.classLoader);
XposedHelpers.findAndHookMethod(clonedAppsPreferenceControllerClass, "getAvailabilityStatus", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(0);
}
});
XposedHelpers.findAndHookMethod(clonedAppsPreferenceControllerClass, "updateSummary", int.class, int.class, new XC_MethodHook() {
@SuppressLint({"QueryPermissionsNeeded", "DiscouragedApi"})
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (mContext == null) {
return;
}
PackageManager pm = mContext.getPackageManager();
List<String> cloneableApps = getCloneableApps();
List<PackageInfo> primaryInstalledPackagesAsUser = (List<PackageInfo>) XposedHelpers.callMethod(pm, "getInstalledPackagesAsUser", 0, XposedHelpers.callStaticMethod(UserHandle.class, "myUserId"));
List<String> primaryUserApps = primaryInstalledPackagesAsUser.stream().map(x -> x.packageName).toList();
int availableAppsCount = (int) cloneableApps.stream().filter(primaryUserApps::contains).count();
int clonedUserId = (int) XposedHelpers.callStaticMethod(XposedHelpers.findClass("com.android.settings.Utils", lpparam.classLoader), "getCloneUserId", mContext);
int clonedAppsCount = 0;
if (!(clonedUserId == -1)) {
List<PackageInfo> cloneProfileInstalledPackagesAsUser = (List<PackageInfo>) XposedHelpers.callMethod(pm, "getInstalledPackagesAsUser", 0, clonedUserId);
List<String> cloneProfileApps = cloneProfileInstalledPackagesAsUser.stream().map(x -> x.packageName).toList();
clonedAppsCount = (int) cloneableApps.stream().filter(cloneProfileApps::contains).count();
}
Object mPreference = XposedHelpers.getObjectField(param.thisObject, "mPreference");
if (mPreference != null) {
XposedHelpers.callMethod(mPreference, "setSummary", String.format(mContext.getResources().getString(mContext.getResources().getIdentifier("cloned_apps_summary", "string", mContext.getPackageName())), clonedAppsCount, availableAppsCount - clonedAppsCount));
}
param.setResult(null);
}
});
XposedHelpers.findAndHookMethod(XposedHelpers.findClass("com.android.settings.applications.AppStateClonedAppsBridge", lpparam.classLoader), "updateExtraInfo", XposedHelpers.findClass("com.android.settingslib.applications.ApplicationsState$AppEntry", lpparam.classLoader), String.class, int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedHelpers.setObjectField(param.thisObject, "mAllowedApps", getCloneableApps());
}
});
XposedHelpers.findAndHookMethod(XposedHelpers.findClass("com.android.settings.applications.manageapplications.ManageApplications", lpparam.classLoader), "updateOptionsMenu", new XC_MethodHook() {
@SuppressLint("DiscouragedApi")
@Override
protected void afterHookedMethod(MethodHookParam param) {
if (XposedHelpers.getObjectField(param.thisObject, "mListType").equals(17) && (int) XposedHelpers.callStaticMethod(XposedHelpers.findClass("com.android.settings.Utils", lpparam.classLoader), "getCloneUserId", mContext) > 0) {
Menu mOptionsMenu = (Menu) XposedHelpers.getObjectField(param.thisObject, "mOptionsMenu");
if (mOptionsMenu != null) {
mOptionsMenu.findItem(mContext.getResources().getIdentifier("delete_all_app_clones", "id", mContext.getPackageName())).setVisible(true);
}
}
}
);
});
} else {
XposedHelpers.findAndHookMethod(XposedHelpers.findClass("com.android.launcher3.allapps.ActivityAllAppsContainerView$AdapterHolder", lpparam.classLoader), "setup", View.class, Predicate.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (((/* Before A17Q2B4: Integer | After A17Q2B4: Byte */ Number) XposedHelpers.getObjectField(param.thisObject, "mType")).intValue() == 0) {
Object allAppsContainerView = XposedHelpers.getObjectField(param.thisObject, "this$0");
XposedHelpers.findAndHookMethod(XposedHelpers.findClass("android.os.Flags", lpparam.classLoader), "allowPrivateProfile", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(true);
}
});
Predicate workProfileMatcher = (Predicate) XposedHelpers.callMethod(XposedHelpers.getObjectField(allAppsContainerView, "mWorkManager"), "getItemInfoMatcher");
Predicate privateProfileMatcher = (Predicate) XposedHelpers.callMethod(XposedHelpers.getObjectField(allAppsContainerView, "mPrivateProfileManager"), "getItemInfoMatcher");
Predicate WorkOrPrivate = workProfileMatcher.or(privateProfileMatcher);
Class<?> clonedAppsPreferenceControllerClass = XposedHelpers.findClass("com.android.settings.applications.ClonedAppsPreferenceController", lpparam.classLoader);
Predicate filter = WorkOrPrivate.negate();
XposedHelpers.findAndHookMethod(
clonedAppsPreferenceControllerClass,
"getAvailabilityStatus",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
param.setResult(AVAILABLE);
param.args[1] = filter;
}
});
XposedHelpers.findAndHookMethod(
clonedAppsPreferenceControllerClass,
"updatePreferenceSummary",
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
// Allowing native logic to calculate Summary.
}
});
XposedHelpers.findAndHookMethod(
clonedAppsPreferenceControllerClass,
"updateSummary",
int.class,
int.class,
new XC_MethodHook() {
@SuppressLint("QueryPermissionsNeeded")
@Override
protected void beforeHookedMethod(MethodHookParam param) {
if (mContext == null) {
return;
}
PackageManager pm = mContext.getPackageManager();
List<String> primaryUserNonSystemAppPackages = new ArrayList<>();
for (PackageInfo pkgInfo : pm.getInstalledPackages(PackageManager.GET_ACTIVITIES)) {
if ((pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0 &&
!pkgInfo.packageName.equals(lpparam.packageName)) {
primaryUserNonSystemAppPackages.add(pkgInfo.packageName);
}
}
int currentlyClonedCount = 0;
int cloneUserId = getCloneUserIDInternal();
if (cloneUserId > 0) {
@SuppressLint("PackageManagerGetInstalledPackagesAsUser")
List<PackageInfo> cloneUserPackages = (List<PackageInfo>) XposedHelpers.callMethod(pm, "getInstalledPackagesAsUser", PackageManager.GET_ACTIVITIES, cloneUserId);
for (PackageInfo clonedPkgInfo : cloneUserPackages) {
if ((clonedPkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0 &&
primaryUserNonSystemAppPackages.contains(clonedPkgInfo.packageName)) {
currentlyClonedCount++;
}
}
}
Object mPreference = XposedHelpers.getObjectField(param.thisObject, "mPreference");
if (mPreference != null) {
String newSummary = String.format(mContext.getResources().getString(CLONED_APPS_SUMMARY_STRING_ID), currentlyClonedCount, primaryUserNonSystemAppPackages.size());
XposedHelpers.callMethod(mPreference, "setSummary", newSummary);
}
param.setResult(null);
}
});
XposedHelpers.findAndHookMethod(
XposedHelpers.findClass("com.android.settings.applications.AppStateClonedAppsBridge", lpparam.classLoader),
"updateExtraInfo",
XposedHelpers.findClass("com.android.settingslib.applications.ApplicationsState$AppEntry", lpparam.classLoader),
String.class,
int.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
Object appEntry = param.args[0];
String pkg = (String) param.args[1];
ApplicationInfo appInfo = (ApplicationInfo) XposedHelpers.getObjectField(appEntry, "info");
int appEntryUserId = (int) XposedHelpers.callStaticMethod(UserHandle.class, "getUserId", appInfo.uid);
int cloneUserId = getCloneUserIDInternal();
Boolean shouldDisplay;
if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
shouldDisplay = Boolean.FALSE;
} else {
List<String> cloneProfileNonSystemAppPackages = getStrings(cloneUserId);
if (appEntryUserId == cloneUserId) {
shouldDisplay = Boolean.TRUE;
} else if (appEntryUserId == (int) XposedHelpers.callStaticMethod(UserHandle.class, "myUserId")) {
if (!cloneProfileNonSystemAppPackages.contains(pkg)) {
shouldDisplay = Boolean.TRUE;
} else {
shouldDisplay = Boolean.FALSE;
}
} else {
shouldDisplay = Boolean.FALSE;
}
}
XposedHelpers.setObjectField(appEntry, "extraInfo", shouldDisplay);
}
});
}
});
}
}
private static List<String> getStrings(int cloneUserId) {
List<String> cloneProfileNonSystemAppPackages = new ArrayList<>();
if (cloneUserId > 0 && mContext != null) {
@SuppressLint("PackageManagerGetInstalledPackagesAsUser")
List<PackageInfo> installedClonedPackages = (List<PackageInfo>) XposedHelpers.callMethod(mContext.getPackageManager(), "getInstalledPackagesAsUser", PackageManager.GET_ACTIVITIES, cloneUserId);
if (installedClonedPackages != null) {
for (PackageInfo pInfo : installedClonedPackages) {
if ((pInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
cloneProfileNonSystemAppPackages.add(pInfo.packageName);
}
@SuppressLint("QueryPermissionsNeeded")
private List<String> getCloneableApps() {
if (mContext == null) {
return new ArrayList<>();
}
if (cachedCloneableApps != null) {
return cachedCloneableApps;
}
@SuppressLint("DiscouragedApi") List<String> cloneableApps = new ArrayList<>(Arrays.asList(mContext.getResources().getStringArray(mContext.getResources().getIdentifier("cloneable_apps", "array", "android"))));
SharedPreferences sharedPrefs = mContext.getSharedPreferences("cloneable_apps", Context.MODE_PRIVATE);
List<String> newCloneableApps = new ArrayList<>(sharedPrefs.getStringSet("cloneable_apps", new HashSet<>()));
cloneableApps.addAll(newCloneableApps);
if (cloneableApps.isEmpty()) {
List<PackageInfo> primaryInstalledPackagesAsUser = (List<PackageInfo>) XposedHelpers.callMethod(mContext.getPackageManager(), "getInstalledPackagesAsUser", 0, XposedHelpers.callStaticMethod(UserHandle.class, "myUserId"));
for (PackageInfo pkgInfo : primaryInstalledPackagesAsUser) {
if ((pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
cloneableApps.add(pkgInfo.packageName);
}
}
}
return cloneProfileNonSystemAppPackages;
}
private int getCloneUserIDInternal() {
if (UtilsClass == null || mContext == null) {
return -1;
}
return (int) XposedHelpers.callStaticMethod(UtilsClass, "getCloneUserId", mContext);
cachedCloneableApps = cloneableApps;
return cachedCloneableApps;
}
}
+2
View File
@@ -2,5 +2,7 @@
<resources>
<string-array name="xposed_scope">
<item>com.android.settings</item>
<item>com.android.launcher3</item>
<item>com.google.android.apps.nexuslauncher</item>
</string-array>
</resources>
+11 -19
View File
@@ -1,21 +1,13 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.useAndroidX=false
android.nonTransitiveRClass=true
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false
+13
View File
@@ -0,0 +1,13 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
toolchainVendor=JETBRAINS
toolchainVersion=21
+1 -1
View File
@@ -1,5 +1,5 @@
[versions]
agp = "8.12.0"
agp = "9.3.2"
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
+2 -2
View File
@@ -1,6 +1,6 @@
#Thu Aug 14 21:53:58 CST 2025
#Sun Aug 30 01:04:59 CST 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+1 -1
View File
@@ -16,5 +16,5 @@ dependencyResolutionManagement {
}
}
rootProject.name = "Double Tap To Wake"
rootProject.name = "App Clone Enabler"
include(":app")