Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ android {
applicationId = "com.ez.zalopatch"
minSdk = 24
targetSdk = 34
versionCode = 160
versionName = "0.4.156-report-field-contract+B160-20260816T1756Z"
versionCode = 162
versionName = "0.4.158-non-root-capabilities+B162-20260820T1521Z"
testInstrumentationRunner = "android.test.InstrumentationTestRunner"
buildConfigField("String", "DIAGNOSTIC_INTAKE_URL", "\"$diagnosticIntakeUri\"")
buildConfigField("String", "SYMBOL_CATALOG_URL", "\"$symbolCatalogUri\"")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public void testMetadataEnvelopeUsesSharedIntakeContractWithoutPrivatePayloadFie
assertTrue(BuildConfig.DIAGNOSTIC_INTAKE_URL.endsWith("/v1/reports"));

JSONObject product = root.getJSONObject("productMetadata");
assertTrue(product.has("runtimeFramework"));
assertTrue(product.has("runtimeEnvironmentReported"));
assertTrue(product.has("resourceHooksObserved"));
assertTrue(product.has("resourceHooksStatus"));
JSONObject remap = product.getJSONObject("remapEvidence");
assertFalse(remap.getBoolean("requestedForReport"));
assertTrue(remap.has("exactBundledProfileMapped"));
Expand All @@ -34,7 +38,11 @@ public void testMetadataEnvelopeUsesSharedIntakeContractWithoutPrivatePayloadFie
assertTrue(setup.has("symbolSchemaValid"));
assertTrue(setup.has("runtimeSelfCheckPresent"));
assertTrue(setup.has("internetPermissionGranted"));
assertEquals("not_checked", setup.getString("rootAccessStatus"));
String rootStatus = setup.getString("rootAccessStatus");
assertTrue("not_checked".equals(rootStatus)
|| "granted".equals(rootStatus)
|| "denied".equals(rootStatus)
|| "error".equals(rootStatus));

JSONObject raw = root.getJSONObject("rawDiagnostics");
assertEquals("", raw.getString("diagnosticEventsAndLogs"));
Expand Down Expand Up @@ -133,4 +141,83 @@ public void testRuntimeDiscoveryReceiverFallbackPersistsEvidenceAndCompletion()
TweakStore.initialize(getContext());
}
}

public void testRuntimeEnvironmentProviderAndReceiverFallbackPersistEvidence() {
long zaloVersion = SymbolSchema.installedZaloVersionCode(getContext());
java.util.Map<String, ?> previous = RuntimeEnvironment.snapshotForTest(getContext());
RuntimeEnvironment.clearForTest(getContext());
try {
android.os.Bundle extras = new android.os.Bundle();
extras.putString("framework", "lspatch");
extras.putBoolean("resource_hooks_observed", false);
extras.putInt("module_version_code", BuildConfig.VERSION_CODE);
extras.putLong("zalo_version_code", zaloVersion);
android.os.Bundle response = getContext().getContentResolver().call(
android.net.Uri.parse("content://com.ez.zalopatch.config"),
"record_runtime_environment", null, extras);
assertNotNull(response);
assertTrue(response.getBoolean("recorded", false));
RuntimeEnvironment.Snapshot provider = RuntimeEnvironment.current(getContext());
assertTrue(provider.reported);
assertEquals(RuntimeEnvironment.Framework.LSPATCH, provider.framework);
assertFalse(provider.resourceHooksObserved);
assertEquals(RuntimeEnvironment.ResourceHooks.PENDING, provider.resourceHooks);

if (android.os.Build.VERSION.SDK_INT < 34) return;
android.content.Intent fallback = new android.content.Intent(
SelfCheckReceiver.ACTION_RECORD_RUNTIME_ENVIRONMENT)
.setComponent(new android.content.ComponentName(getContext(),
SelfCheckReceiver.class))
.putExtra("framework", "lspatch")
.putExtra("resource_hooks_status", "observed")
.putExtra("resource_hooks_observed", true)
.putExtra("module_version_code", BuildConfig.VERSION_CODE)
.putExtra("zalo_version_code", zaloVersion);
android.app.BroadcastOptions options = android.app.BroadcastOptions.makeBasic();
options.setShareIdentityEnabled(true);
getContext().sendBroadcast(fallback, null, options.toBundle());
long deadline = android.os.SystemClock.uptimeMillis() + 2_000L;
while (!RuntimeEnvironment.current(getContext()).resourceHooksObserved
&& android.os.SystemClock.uptimeMillis() < deadline) {
android.os.SystemClock.sleep(25L);
}
assertTrue(RuntimeEnvironment.current(getContext()).resourceHooksObserved);
} finally {
RuntimeEnvironment.restoreForTest(getContext(), previous);
}
}

public void testNotificationRulesReachProviderWithoutPropertyMirror() throws Exception {
android.content.SharedPreferences prefs = TweakStore.preferences(getContext());
boolean hadValue = prefs.contains(NotificationRuleStore.PREF_KEY);
String previous = prefs.getString(NotificationRuleStore.PREF_KEY, "");
NotificationRuleStore.RuleSet rules = new NotificationRuleStore.RuleSet(
java.util.Collections.singletonList("provider-only-rule"), null, null, null);
String encoded = NotificationRuleStore.encode(rules);
try {
assertTrue(prefs.edit().putString(NotificationRuleStore.PREF_KEY, encoded).commit());
android.database.Cursor cursor = getContext().getContentResolver().query(
android.net.Uri.withAppendedPath(
ConfigProvider.URI, NotificationRuleStore.PREF_KEY),
null, null, null, null);
assertNotNull(cursor);
String providerJson = null;
try {
assertTrue(cursor.moveToFirst());
providerJson = cursor.getString(cursor.getColumnIndexOrThrow("value"));
} finally {
cursor.close();
}
NotificationRuleStore.RuleSet resolved =
NotificationRuleStore.resolve(null, providerJson);
assertEquals(1, resolved.total());
assertEquals("provider-only-rule", resolved.list(
NotificationRuleStore.Type.KEYWORD_BLOCKLIST).get(0));
} finally {
android.content.SharedPreferences.Editor editor = prefs.edit();
if (hadValue) editor.putString(NotificationRuleStore.PREF_KEY, previous);
else editor.remove(NotificationRuleStore.PREF_KEY);
editor.commit();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ public final class RemoteCatalogActivationTest extends AndroidTestCase {
public void testCachedSignedProfileActivatesWithoutBundledExactProfile() throws Exception {
Context context = getContext();
ZaloArtifactIdentity identity = ZaloArtifactIdentity.capture(context, true);
SymbolCatalogContract.Entry entry = SymbolCatalogCache.load(context,
identity.versionCode, identity.baseApkSha256, identity.signerSha256);
SymbolCatalogContract.Entry entry = SymbolCatalogCache.load(context, identity.versionCode);
assertNotNull("No signed catalog entry cached for installed Zalo", entry);

JSONObject bundle = new JSONObject(readBundledSchema(context));
Expand All @@ -39,10 +38,11 @@ public void testCachedSignedProfileActivatesWithoutBundledExactProfile() throws
assertNotNull(remote);
assertTrue(remote.valid);
assertEquals("Remote catalog " + entry.sequence, remote.source);
assertEquals(identity.baseApkSha256,
remote.string("artifact.base_apk_sha256", ""));
assertEquals(identity.signerSha256,
remote.string("artifact.signer_sha256", ""));
// The entry names the container it was mapped from, which need not be the installed one:
// lookup and verification bind versionCode alone (Decision 15). Assert the mapped identity
// is carried, not that it equals this device's container.
assertEquals(64, remote.string("artifact.base_apk_sha256", "").length());
assertEquals(64, remote.string("artifact.signer_sha256", "").length());
}

private static String readBundledSchema(Context context) throws Exception {
Expand Down
51 changes: 51 additions & 0 deletions app/src/androidTest/java/com/ez/zalopatch/SettingsUiSmokeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,20 +126,67 @@ public void testMainRestartRemainsVisibleWithPendingChanges() {
StatusActivity activity = getActivity();
SharedPreferences ui = SettingsChanges.preferences(activity);
Map<String, ?> originalUi = new HashMap<>(ui.getAll());
Map<String, ?> originalRoot = RootAccess.snapshotForTest(activity);
try {
assertTrue(ui.edit().clear().commit());
RootAccess.setForTest(activity, RootAccess.State.DENIED);
SettingsChanges.markChanged(activity, "test.pending");
getInstrumentation().runOnMainSync(() -> activity.refreshApplyBar(false));
StatusActivity.DashboardFragment fragment =
new StatusActivity.DashboardFragment();
open(activity, fragment);
Preference restart = fragment.findPreference(StatusActivity.INTERNAL_RESTART);
Preference appInfo = fragment.findPreference(StatusActivity.INTERNAL_ZALO_APP_INFO);
Preference root = fragment.findPreference(StatusActivity.INTERNAL_ROOT_ACCESS);

assertNotNull(restart);
assertTrue(restart.isVisible());
assertFalse(restart.isEnabled());
assertEquals("Root required; force stop Zalo to apply.",
String.valueOf(restart.getSummary()));
assertNotNull(appInfo);
assertNotNull(root);
assertEquals(android.view.View.VISIBLE,
activity.findViewById(R.id.zp_apply_bar).getVisibility());
assertEquals("Root required; force stop Zalo to apply.",
String.valueOf(((android.widget.TextView) activity.findViewById(
R.id.zp_apply_message)).getText()));
assertFalse(activity.findViewById(R.id.zp_apply_button).isEnabled());
} finally {
RootAccess.restoreForTest(activity, originalRoot);
restorePreferences(ui, originalUi);
}
}

public void testResourceHookRequirementUsesPendingThenUnavailableRendering() {
StatusActivity activity = getActivity();
Map<String, ?> originalRuntime = RuntimeEnvironment.snapshotForTest(activity);
try {
RuntimeEnvironment.clearForTest(activity);
SectionActivity.SettingsFragment pending =
SectionActivity.SettingsFragment.forSection(Tweaks.SECTION_INBOX);
open(activity, pending);
assertTrue(pending.findPreference(Tweaks.KEY_HIDE_ZCLOUD_BANNER)
instanceof ZpSwitchPreference);

long zaloVersion = SymbolSchema.installedZaloVersionCode(activity);
assertTrue(RuntimeEnvironment.record(activity, "lspatch", "unavailable",
BuildConfig.VERSION_CODE, zaloVersion));
SectionActivity.SettingsFragment unsupported =
SectionActivity.SettingsFragment.forSection(Tweaks.SECTION_INBOX);
open(activity, unsupported);
Preference row = unsupported.findPreference(Tweaks.KEY_HIDE_ZCLOUD_BANNER);
assertNotNull(row);
assertFalse(row instanceof ZpSwitchPreference);
assertFalse(row.isSelectable());
assertNull(row.getOnPreferenceChangeListener());
assertTrue(String.valueOf(row.getSummary()).contains(
"did not expose resource hooks"));
} finally {
RuntimeEnvironment.restoreForTest(activity, originalRuntime);
}
}

public void testTelemetryUsesMainSwitchPatternAndTracksChildren() {
StatusActivity activity = getActivity();
String originalDisplayMode = UiSettings.displayMode(activity);
Expand Down Expand Up @@ -301,6 +348,10 @@ public void testVietnameseSettingsResourcesArePackaged() {
assertEquals("Ngôn ngữ", vietnamese.getString(R.string.zp_language_title));
assertEquals("Telemetry", vietnamese.getString(R.string.zp_telemetry_title));
assertEquals("Self-check", vietnamese.getString(R.string.zp_self_check_page_title));
assertEquals("Runtime environment",
vietnamese.getString(R.string.zp_runtime_environment_title));
assertEquals("Root access", vietnamese.getString(R.string.zp_root_access_title));
assertEquals("Root required.", vietnamese.getString(R.string.zp_requirement_root));
}

public void testApplicationColdStartUsesSelectedLanguage() {
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/com/ez/zalopatch/ConfigProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,19 @@ public Bundle call(String method, String arg, Bundle extras) {
result.putBoolean("recorded", recorded);
return result;
}
if ("record_runtime_environment".equals(method)) {
if (!callerAllowed() || extras == null) return null;
boolean recorded = RuntimeEnvironment.record(getContext(),
extras.getString("framework", "unknown"),
extras.getString("resource_hooks_status",
extras.getBoolean("resource_hooks_observed", false)
? "observed" : "pending"),
extras.getInt("module_version_code", -1),
extras.getLong("zalo_version_code", -1L));
Bundle result = new Bundle();
result.putBoolean("recorded", recorded);
return result;
}
if ("complete_runtime_discovery".equals(method)) {
if (!callerAllowed()) return null;
long versionCode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ final class DiagnosticCaptureCollector {
}

CapturedData collect(long startedAtWallMs) {
String rootStatus = checkRootAccess(runner);
if (!"granted".equals(rootStatus)) {
return collect(startedAtWallMs, RootAccess.probe(runner));
}

CapturedData collect(long startedAtWallMs, RootAccess.State rootState) {
String rootStatus = rootState.reportValue();
if (rootState != RootAccess.State.GRANTED) {
return new CapturedData("metadata_only_root_denied", rootStatus, "", "", "",
java.util.Collections.singletonList("root_access"),
java.util.Collections.emptyMap());
Expand Down Expand Up @@ -83,12 +87,7 @@ CapturedData collect(long startedAtWallMs) {
}

static String checkRootAccess(DiagnosticRootProcessRunner runner) {
DiagnosticRootProcessRunner.Result result = runner.run(
"id -u", DiagnosticReportContract.COMMAND_TIMEOUT_MS);
if (result.timedOut) return "error";
if (result.exitCode == 0 && "0".equals(result.output.trim())) return "granted";
if (result.exitCode < 0 && result.output.trim().isEmpty()) return "error";
return "denied";
return RootAccess.probe(runner).reportValue();
}

static String filterModuleLines(String output) {
Expand Down
Loading