diff --git a/app/build.gradle b/app/build.gradle index dd72d79..c14ca4d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId "com.aoleyun.sn" - versionCode 256 - versionName "1.6.1026" + versionCode 257 + versionName "1.6.1027" //There are no CERT files because If the mini sdk version is 23+, the AGP will ignore the V1 scheme signature. minSdkVersion 24 diff --git a/app/src/main/java/com/aoleyun/sn/network/NetInterfaceManager.java b/app/src/main/java/com/aoleyun/sn/network/NetInterfaceManager.java index 846a70d..6128043 100644 --- a/app/src/main/java/com/aoleyun/sn/network/NetInterfaceManager.java +++ b/app/src/main/java/com/aoleyun/sn/network/NetInterfaceManager.java @@ -3240,7 +3240,8 @@ public class NetInterfaceManager { return appDateInfo.getApp_baoming(); } }).collect(Collectors.toSet()); - appNames.addAll(pkgs); + // TODO: 2026/10/27 后台 -应用上架 --应用库管理 页面上传的应用,先屏蔽 +// appNames.addAll(pkgs); } if (rankAppResponse.isSuccess()) { List appDateInfos = rankAppResponse.data; @@ -3290,7 +3291,6 @@ public class NetInterfaceManager { } else { mMMKV.encode(CommonConfig.AOLE_ALL_APP_WHITE_LIST_KEY, new HashSet<>(strings)); JgyUtils.getInstance().writeAppPackageList(strings); - JgyUtils.getInstance().deleteOtherApp(); } } @@ -3303,6 +3303,7 @@ public class NetInterfaceManager { @Override public void onComplete() { Log.e("getAppLimitList", "onComplete: "); + JgyUtils.getInstance().deleteOtherApp(); if (callback != null) callback.onComplete(); } diff --git a/app/src/main/java/com/aoleyun/sn/network/UrlAddress.java b/app/src/main/java/com/aoleyun/sn/network/UrlAddress.java index ae40b00..530c232 100644 --- a/app/src/main/java/com/aoleyun/sn/network/UrlAddress.java +++ b/app/src/main/java/com/aoleyun/sn/network/UrlAddress.java @@ -56,7 +56,7 @@ public class UrlAddress { public static final String SET_WHITE_PACKAGE_LIST = "firmware/index"; /*获取全部应用*/ public static final String GET_ALL_APP = "recommend/index"; - /*应用管控的应用*/ + /*应用库管理的应用*/ public static final String GET_ADMIN_APP = "And/app/getAdminApp"; /*公共应用*/ public static final String HTTP_TAG_COMMOAPP = "Rank/commonApp"; diff --git a/app/src/main/java/com/aoleyun/sn/service/main/MainService.java b/app/src/main/java/com/aoleyun/sn/service/main/MainService.java index f697678..c04a878 100644 --- a/app/src/main/java/com/aoleyun/sn/service/main/MainService.java +++ b/app/src/main/java/com/aoleyun/sn/service/main/MainService.java @@ -554,6 +554,12 @@ public class MainService extends BaseService implements NetworkUtils.OnNetworkSt if (TextUtils.isEmpty(allowAppList)) { Settings.System.putString(getContentResolver(), CommonConfig.SEEWO_APP_SOURCE_INSTALL_WHITELIST, String.join(",", JgyUtils.DEFAULT_APP_SOURCE_WHITE_LIST)); } + + String aole_app_forbid = Settings.System.getString(getContentResolver(), CommonConfig.AOLE_ACTION_APP_FORBID); + if (TextUtils.isEmpty(aole_app_forbid)) { + JgyUtils.getInstance().writeAppPackageList(); + } + } catch (Exception e) { Log.e(TAG, "initOnBackground: " + e.getMessage()); e.printStackTrace(); diff --git a/app/src/main/java/com/aoleyun/sn/utils/JgyUtils.java b/app/src/main/java/com/aoleyun/sn/utils/JgyUtils.java index 97970d6..911dad4 100644 --- a/app/src/main/java/com/aoleyun/sn/utils/JgyUtils.java +++ b/app/src/main/java/com/aoleyun/sn/utils/JgyUtils.java @@ -28,6 +28,7 @@ import android.net.NetworkInfo; import android.net.Uri; import android.os.BatteryManager; import android.os.Build; +import android.os.Bundle; import android.os.Debug; import android.os.Environment; import android.os.Handler; @@ -1642,7 +1643,7 @@ public class JgyUtils { String aole_app_forbid = String.join(",", pkgSet); Log.e(TAG, "writeAppPackageList: " + aole_app_forbid); try { - boolean b = Settings.System.putString(crv, CommonConfig.AOLE_ACTION_APP_FORBID, aole_app_forbid); + boolean b = androidSettingsPutString(mContext, CommonConfig.AOLE_ACTION_APP_FORBID, aole_app_forbid); Log.e("writeAppPackageList: ", "aole_app_forbid: " + b + " " + Settings.System.getString(crv, CommonConfig.AOLE_ACTION_APP_FORBID)); } catch (Exception e) { Log.e(TAG, "writeAppPackageList: Settings.System.putString failed", e); @@ -1672,12 +1673,233 @@ public class JgyUtils { String aole_app_forbid = String.join(",", pkgSet); Log.e(TAG, "writeAppPackageList: " + aole_app_forbid); try { - Settings.System.putString(crv, CommonConfig.AOLE_ACTION_APP_FORBID, aole_app_forbid); + androidSettingsPutString(mContext, CommonConfig.AOLE_ACTION_APP_FORBID, aole_app_forbid); } catch (Exception e) { Log.e(TAG, "writeAppPackageList: Settings.System.putString failed", e); } } + private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system"; + private static final String CALL_METHOD_USER_KEY = "_user"; + + private static final String SETTINGS_AUTHORITY = "settings"; + private static final Uri SETTINGS_URI = Uri.parse("content://" + SETTINGS_AUTHORITY); + private static final String SETTINGS_PROVIDER_NAME = + "com.android.providers.settings/.SettingsProvider"; + + // settings provider 的 IContentProvider 代理缓存;binder 死亡后自动重取 + private static volatile Object sSettingsProvider; + + public static boolean androidSettingsPutString(Context context, String name, String value) { + Context targetContext = context; + if (targetContext == null && sInstance != null) { + targetContext = sInstance.mContext; + } + if (targetContext == null) { + Log.e(TAG, "androidSettingsPutString: Context is null"); + return false; + } + if (TextUtils.isEmpty(name)) { + Log.e(TAG, "androidSettingsPutString: name is empty"); + return false; + } + + Bundle arg = new Bundle(); + arg.putString(Settings.NameValueTable.VALUE, value); + // _user 是 SettingsProvider 定位落库用户的必填项,与包名伪装无关;只管主用户 + arg.putInt(CALL_METHOD_USER_KEY, 0); + + // 注意:发起方进程绝不能 clearCallingIdentity——它会篡改本进程事务携带的 + // 调用身份,SettingsProvider 侧校验 calling pid 会直接拒绝("doesn't match source pid")。 + // 那个手法只适用于服务端(代理进程处理入站 binder 调用时)。 + Bundle result = callSettingsPut(targetContext, arg, name); + boolean bundleOk = isOk(result); + // 该厂商 SettingsProvider 对 PUT_system 静默返回 null(不抛异常也不回 Bundle), + // 成败以写后读回为准。 + String readBack = null; + try { + readBack = Settings.System.getString(targetContext.getContentResolver(), name); + } catch (Throwable t) { + Log.w(TAG, "androidSettingsPutString: read back failed for " + name + ": " + t); + } + boolean verified = TextUtils.equals(readBack, value); + if (!bundleOk && !verified) { + Log.w(TAG, "androidSettingsPutString: failed for " + name + + " (result=" + result + ", readBack=" + readBack + ")"); + return false; + } + Log.d(TAG, "androidSettingsPutString succeeded for " + name + + " (bundleOk=" + bundleOk + ", verified=" + verified + ")"); + return true; + } + + /** + * 直路一条:反射 acquireExistingProvider 拿 ContentProviderProxy, + * 用伪造的 AttributionSource(uid=1000, package="android") 调其 call。 + * 事务由设备框架自己生成的 Proxy 写出,事务码/布局永远和 ROM 匹配。 + * 前提:android.uid.system + platform 签名。 + */ + private static Bundle callSettingsPut(Context context, Bundle arg, String name) { + Object provider = getSettingsProvider(context); + if (provider == null) { + return null; + } + Bundle result = invokeProviderCall(provider, arg, name); + if (result == null) { + // 可能 binder 已死亡,清缓存重取再试一次 + sSettingsProvider = null; + provider = getSettingsProvider(context); + if (provider != null) { + result = invokeProviderCall(provider, arg, name); + } + } + return result; + } + + /** + * S+ 的 ContentProviderProxy 记账包名取自 AttributionSource: + * 伪造 AttributionSource(uid=1000, packageName="android") 调 + * call(AttributionSource, authority, method, arg, extras)。 + * 本 App 就是 uid 1000,enforceCallingUid 校验通过;"android" 包属于 uid 1000, + * attribution 链校验也通过。旧 4 参 call 无包名槽位(会记真实包名),不用。 + */ + private static Bundle invokeProviderCall(Object provider, Bundle arg, String name) { + Object attribution = buildAndroidAttributionSource(); + if (attribution == null) { + Log.e(TAG, "androidSettingsPutString: attribution source is null, abort"); + return null; + } + for (Method m : provider.getClass().getMethods()) { + if (!"call".equals(m.getName())) { + continue; + } + Class[] p = m.getParameterTypes(); + if (p.length == 5 && p[1] == String.class && p[4] == Bundle.class + && p[0].getName().equals("android.content.AttributionSource")) { + try { + Bundle r = (Bundle) m.invoke(provider, attribution, SETTINGS_AUTHORITY, + CALL_METHOD_PUT_SYSTEM, name, arg); + Log.d(TAG, "androidSettingsPutString: invoked call" + + java.util.Arrays.toString(p) + " -> " + + (r == null ? "null" : r.toString())); + return r; + } catch (Throwable t) { + Throwable root = t instanceof java.lang.reflect.InvocationTargetException + && t.getCause() != null ? t.getCause() : t; + Log.w(TAG, "androidSettingsPutString: provider call failed: " + root); + return null; + } + } + } + Log.w(TAG, "androidSettingsPutString: no AttributionSource call overload on " + + provider.getClass().getName()); + return null; + } + + /** + * AttributionSource.Builder(uid).setPackageName("android").build(),反射构造。 + */ + private static Object buildAndroidAttributionSource() { + try { + Class as = Class.forName("android.content.AttributionSource"); + Class builder = Class.forName("android.content.AttributionSource$Builder"); + Object b = builder.getConstructor(int.class).newInstance(Process.myUid()); + builder.getMethod("setPackageName", String.class).invoke(b, "android"); + return builder.getMethod("build").invoke(b); + } catch (Throwable t) { + Log.e(TAG, "androidSettingsPutString: build AttributionSource failed: " + t); + return null; + } + } + + private static boolean isOk(Bundle result) { + return result != null && !"error".equals(result.getString("tag")); + } + + /** + * 反射 ContentResolver.acquireProvider / acquireExistingProvider(各 ROM 签名不同, + * 扫描方法名+按参数类型填槽,String 槽位两种顺序都试),返回并缓存 IContentProvider 代理。 + * 调用者是 android.uid.system,hidden API 豁免生效。 + */ + private static Object getSettingsProvider(Context context) { + Object cached = sSettingsProvider; + if (cached != null) { + return cached; + } + synchronized (JgyUtils.class) { + if (sSettingsProvider != null) { + return sSettingsProvider; + } + ContentResolver cr = context.getContentResolver(); + java.util.List candidates = new java.util.ArrayList<>(); + for (Method m : ContentResolver.class.getDeclaredMethods()) { + String n = m.getName(); + if (("acquireProvider".equals(n) || "acquireExistingProvider".equals(n)) + && m.getReturnType().isInterface()) { + candidates.add(m); + } + } + java.util.Collections.sort(candidates, + (a, b) -> a.getParameterCount() - b.getParameterCount()); + for (Method m : candidates) { + Object provider = tryAcquire(cr, m); + if (provider != null) { + sSettingsProvider = provider; + break; + } + } + if (sSettingsProvider == null) { + Log.e(TAG, "androidSettingsPutString: settings provider unavailable"); + } + return sSettingsProvider; + } + } + + /** + * 3 参版是 (authority, name, ...),5 参版是 (config, name, userId, authority, stable), + * String 槽位顺序在不同 API 上相反,猜错会 NPE,两组组合都试。 + */ + private static Object tryAcquire(ContentResolver cr, Method m) { + Class[] types = m.getParameterTypes(); + String[][] slotCombos = { + {SETTINGS_AUTHORITY, SETTINGS_PROVIDER_NAME}, // authority 在前 + {SETTINGS_PROVIDER_NAME, SETTINGS_AUTHORITY}, // name 在前 + }; + for (String[] combo : slotCombos) { + try { + Object[] args = new Object[types.length]; + int stringSlot = 0; + for (int i = 0; i < types.length; i++) { + Class t = types[i]; + if (t == String.class) { + args[i] = stringSlot < combo.length ? combo[stringSlot] : null; + stringSlot++; + } else if (t == Uri.class) { + args[i] = SETTINGS_URI; + } else if (t == boolean.class) { + args[i] = false; // stable / tryLocal:走 binder 路径 + } else if (t == int.class) { + args[i] = 0; // userId / pid(0 会让 ActivityThread 用 myPid) + } + // IBinder、Intent 等保持 null + } + m.setAccessible(true); + Object provider = m.invoke(cr, args); + if (provider != null) { + Log.d(TAG, "androidSettingsPutString: acquired via " + m.getName() + + java.util.Arrays.toString(types) + " slots=" + combo[0] + "/" + combo[1]); + return provider; + } + } catch (Throwable t) { + Throwable root = t instanceof java.lang.reflect.InvocationTargetException + && t.getCause() != null ? t.getCause() : t; + Log.w(TAG, "androidSettingsPutString: " + m.getName() + " slots=" + combo[0] + + "/" + combo[1] + " failed: " + root); + } + } + return null; + } + @Deprecated public void checkForceDownload() { // String jsonString = cacheHelper.getAsString(UrlAddress.GET_FORCE_INSTALL_LIST);