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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- Block analytics and advertising services.
- Local notification history.
- Call audio recording.
- More frequent scheduled message backups while preserving Zalo's native backup guards.

## Requirements

Expand Down
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 = 163
versionName = "0.4.159-status-privacy+B163-20260820T1716Z"
versionCode = 166
versionName = "0.4.162-frequent-message-backup+B166-20260821T1136Z"
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 @@ -11,6 +11,8 @@ public void testPortableSettingsRoundTrip() throws Exception {
Map<String, Object> source = new LinkedHashMap<>();
source.put(Tweaks.KEY_HIDE_DISCOVERY_TAB, true);
source.put(Tweaks.KEY_NOTIFICATION_HISTORY_RETENTION, 250);
source.put(Tweaks.KEY_BACKUP_FREQUENT_PUSH, true);
source.put(Tweaks.KEY_BACKUP_PUSH_INTERVAL, 3);

String json = SettingsBackup.encodeSettings(source, 1234L, "test");
Map<String, Object> decoded = SettingsBackup.decodeSettings(json);
Expand All @@ -19,6 +21,8 @@ public void testPortableSettingsRoundTrip() throws Exception {
assertFalse(decoded.containsKey(Tweaks.KEY_CALL_RECORDING_PROBE));
assertFalse(decoded.containsKey(Tweaks.KEY_AUTO_RECORD_CALLS));
assertFalse(decoded.containsKey(Tweaks.KEY_CALL_RECORDING_NOTIFICATIONS));
assertEquals(true, decoded.get(Tweaks.KEY_BACKUP_FREQUENT_PUSH));
assertEquals(3, decoded.get(Tweaks.KEY_BACKUP_PUSH_INTERVAL));
}

public void testUnsupportedRetentionFallsBackToDefault() throws Exception {
Expand Down
23 changes: 15 additions & 8 deletions app/src/androidTest/java/com/ez/zalopatch/SettingsUiSmokeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,18 @@ public void testDependencyPagesOpenInSingleHost() {
open(activity, SectionActivity.SettingsFragment.forSection(Tweaks.SECTION_CALLS));
assertTrue(current(activity) instanceof SectionActivity.SettingsFragment);

SectionActivity.SettingsFragment backup =
SectionActivity.SettingsFragment.forSection(Tweaks.SECTION_BACKUP);
open(activity, backup);
assertNotNull(backup.findPreference(Tweaks.KEY_BACKUP_FREQUENT_PUSH));
assertNotNull(backup.findPreference(Tweaks.KEY_BACKUP_PUSH_INTERVAL));

SectionActivity.SettingsFragment developer =
SectionActivity.SettingsFragment.forSection(Tweaks.SECTION_DEVELOPER);
open(activity, developer);
assertNotNull(developer.findPreference(StatusActivity.INTERNAL_ROOT_ACCESS));
assertNotNull(developer.findPreference(
SectionActivity.SettingsFragment.DEVELOPER_RUNTIME_ENVIRONMENT));
assertNotNull(developer.findPreference("internal.developer_runtime_status"));
assertNotNull(developer.findPreference("internal.developer_compatibility_catalog"));
assertNotNull(developer.findPreference(Tweaks.KEY_CALL_RECORDING_PROBE));
Expand Down Expand Up @@ -122,7 +131,7 @@ public void testDiagnosticDescriptionUsesConnectedNonPersistentRow() {
assertFalse(row.isFocusable());
}

public void testMainRestartRemainsVisibleWithPendingChanges() {
public void testMainRestartFallsBackToAppInfoWithoutRoot() {
StatusActivity activity = getActivity();
SharedPreferences ui = SettingsChanges.preferences(activity);
Map<String, ?> originalUi = new HashMap<>(ui.getAll());
Expand All @@ -136,22 +145,20 @@ public void testMainRestartRemainsVisibleWithPendingChanges() {
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.",
assertTrue(restart.isEnabled());
assertEquals("Without root, opens Zalo app info for manual force stop.",
String.valueOf(restart.getSummary()));
assertNotNull(appInfo);
assertNotNull(root);
assertNull(root);
assertEquals(android.view.View.VISIBLE,
activity.findViewById(R.id.zp_apply_bar).getVisibility());
assertEquals("Root required; force stop Zalo to apply.",
assertEquals("1 change pending",
String.valueOf(((android.widget.TextView) activity.findViewById(
R.id.zp_apply_message)).getText()));
assertFalse(activity.findViewById(R.id.zp_apply_button).isEnabled());
assertTrue(activity.findViewById(R.id.zp_apply_button).isEnabled());
} finally {
RootAccess.restoreForTest(activity, originalRoot);
restorePreferences(ui, originalUi);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,21 @@ private static void restoreString(SharedPreferences.Editor editor, String key, S
}

private void assertHookIdentities(SymbolSchema.Active active) {
boolean backupMapped = active.minCode == 260801903L;
assertEquals(backupMapped,
!active.string("symbols.backup.interval_reader_class", "").isEmpty());
assertEquals(backupMapped,
!active.string("symbols.backup.interval_reader_method", "").isEmpty());
for (Tweaks.Item item : Tweaks.ITEMS) {
TweakHookInfo.Info info = TweakHookInfo.forKey(item.key, active);
assertNotNull("Missing hook metadata for " + item.key, info);
assertFalse("Missing hook path for " + item.key, info.path.isEmpty());
assertFalse("Placeholder hook path for " + item.key,
"No runtime hook".equals(info.path));
if (Tweaks.KEY_BACKUP_FREQUENT_PUSH.equals(item.key) && !backupMapped) {
assertTrue(info.path.contains("<Zalo preference helper>"));
continue;
}
for (String symbol : info.driftSymbols) {
assertFalse("Fallback identity displayed for " + item.key + ": " + symbol,
symbol.contains("<"));
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/assets/symbol-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,10 @@
"analytics_screen_accessor": "A",
"analytics_session_accessor": "B",
"analytics_view_accessor": "C"
},
"backup": {
"interval_reader_class": "u40.p0",
"interval_reader_method": "Y"
}
}
}
Expand Down
99 changes: 99 additions & 0 deletions app/src/main/java/com/ez/zalopatch/SectionActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@ public static final class SettingsFragment extends ZpPreferenceFragment {
private static final String ARG_SECTION = "section";
private static final String MASTER_TELEMETRY = "internal.telemetry_master";
static final String DEVELOPER_REPORT = "internal.developer_report";
static final String DEVELOPER_RUNTIME_ENVIRONMENT =
"internal.developer_runtime_environment";
private Map<String, SelfCheckData.Row> selfCheckRows = new HashMap<>();
private final Map<String, ZpSwitchPreference> switches = new HashMap<>();
private final Map<ZpRowPreference, String[]> pageStatuses = new HashMap<>();
private ZpRowPreference developerRuntimeStatus;
private ZpRowPreference developerRootAccess;
private ZpRowPreference developerRuntimeEnvironment;
private ZpRowPreference recordingsRow;
private ZpMainSwitchPreference telemetryMaster;
private boolean recordingCountLoading;
Expand Down Expand Up @@ -91,13 +95,16 @@ public void onResume() {
}
refreshRecordingCount();
refreshDeveloperRuntimeStatus();
refreshDeveloperEnvironment();
}

private void buildScreen() {
Context context = requireContext();
switches.clear();
pageStatuses.clear();
developerRuntimeStatus = null;
developerRootAccess = null;
developerRuntimeEnvironment = null;
recordingsRow = null;
telemetryMaster = null;
resourceRequirementMet = RequirementGate.isMet(
Expand Down Expand Up @@ -149,6 +156,21 @@ private void addDeveloperTools(ZpSection section, Context context) {
});
section.add(displayHookDetail);

developerRootAccess = PreferenceUi.action(context,
getString(R.string.zp_root_access_title), null);
developerRootAccess.setKey(StatusActivity.INTERNAL_ROOT_ACCESS);
developerRootAccess.setOnPreferenceClickListener(preference -> {
((StatusActivity) requireActivity()).recheckRootAccess();
return true;
});
section.add(developerRootAccess);

developerRuntimeEnvironment = PreferenceUi.info(context,
getString(R.string.zp_runtime_environment_title), null);
developerRuntimeEnvironment.setKey(DEVELOPER_RUNTIME_ENVIRONMENT);
section.add(developerRuntimeEnvironment);
refreshDeveloperEnvironment();

SelfCheckData.Counts counts = SelfCheckData.counts(SelfCheckData.load(context));
developerRuntimeStatus = PreferenceUi.nav(context,
getString(R.string.zp_runtime_status_title), runtimeSummary(counts));
Expand Down Expand Up @@ -197,6 +219,49 @@ private void refreshDeveloperRuntimeStatus() {
developerRuntimeStatus.refreshStyle();
}

void refreshDeveloperEnvironment() {
if (!isAdded()) return;
Context context = requireContext();
if (developerRootAccess != null) {
RootAccess.State state = RootAccess.cached(context);
int summary = state == RootAccess.State.GRANTED
? R.string.zp_root_access_granted
: state == RootAccess.State.DENIED
? R.string.zp_root_access_denied
: R.string.zp_root_access_absent;
developerRootAccess.value(getString(summary));
developerRootAccess.refreshStyle();
}
if (developerRuntimeEnvironment != null) {
developerRuntimeEnvironment.value(runtimeEnvironmentSummary(context));
developerRuntimeEnvironment.refreshStyle();
}
}

private String runtimeEnvironmentSummary(Context context) {
RuntimeEnvironment.Snapshot snapshot = RuntimeEnvironment.current(context);
if (!snapshot.reported) {
return getString(R.string.zp_runtime_environment_pending);
}
int framework = snapshot.framework == RuntimeEnvironment.Framework.LSPOSED
? R.string.zp_runtime_framework_lsposed
: snapshot.framework == RuntimeEnvironment.Framework.LSPATCH
? R.string.zp_runtime_framework_lspatch
: R.string.zp_runtime_framework_unknown;
return getString(R.string.zp_runtime_environment_summary,
getString(framework), getString(resourceHooksStatusRes(snapshot.resourceHooks)));
}

private int resourceHooksStatusRes(RuntimeEnvironment.ResourceHooks status) {
if (status == RuntimeEnvironment.ResourceHooks.OBSERVED) {
return R.string.zp_capability_observed;
}
if (status == RuntimeEnvironment.ResourceHooks.UNAVAILABLE) {
return R.string.zp_capability_unavailable;
}
return R.string.zp_capability_pending;
}

private String runtimeSummary(SelfCheckData.Counts counts) {
if (counts.total() == 0) return getString(R.string.zp_runtime_status_empty);
return getString(R.string.zp_runtime_status_counts,
Expand Down Expand Up @@ -228,6 +293,14 @@ private void addRowsForKey(ZpSection section, SymbolSchema.Active schema, String
Tweaks.KEY_FILTER_POPOVER_CATEGORIES);
return;
}
if (Tweaks.KEY_BACKUP_PUSH_INTERVAL.equals(key)) {
section.addDependent(backupIntPreference(context, key,
R.string.zp_backup_interval_title,
R.array.zp_backup_interval_entries,
R.array.zp_backup_interval_values),
Tweaks.KEY_BACKUP_FREQUENT_PUSH);
return;
}
Tweaks.Item item = itemFor(key);
if (item == null) {
return;
Expand Down Expand Up @@ -289,6 +362,29 @@ private Preference defaultInboxFilterPreference(Context context) {
return preference;
}

private Preference backupIntPreference(Context context, String key, int title,
int entries, int values) {
ZpListPreference preference = new ZpListPreference(context);
preference.setKey(key);
preference.setTitle(title);
preference.setEntries(entries);
preference.setEntryValues(values);
preference.setValue(String.valueOf(SettingsStore.getInt(context, key)));
preference.setSummaryProvider(androidx.preference.ListPreference
.SimpleSummaryProvider.getInstance());
preference.setOnPreferenceChangeListener((changedPreference, newValue) -> {
try {
SettingsStore.putInt(context, key,
Integer.parseInt(String.valueOf(newValue)));
SettingsChanges.markChanged(context, key);
return true;
} catch (NumberFormatException ignored) {
return false;
}
});
return preference;
}

private static Tweaks.Item itemFor(String key) {
for (Tweaks.Item item : Tweaks.ITEMS) {
if (item.key.equals(key)) {
Expand Down Expand Up @@ -524,6 +620,9 @@ private String[] relatedFeatures(String key) {
if (Tweaks.KEY_CALL_RECORDING_NOTIFICATIONS.equals(key)) {
return new String[]{"calls.auto_record.notifications"};
}
if (Tweaks.KEY_BACKUP_FREQUENT_PUSH.equals(key)) {
return new String[]{"backup.scheduled"};
}
if (Tweaks.KEY_HIDE_MEDIA_BOX.equals(key)) {
return new String[]{"inbox.media_box"};
}
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/ez/zalopatch/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ private Setting(
settings.add(intSetting(Tweaks.KEY_DEFAULT_INBOX_FILTER, Tweaks.SECTION_INBOX,
0,
java.util.Arrays.asList(0, 1, 2, 3, 4), true, true));
settings.add(intSetting(Tweaks.KEY_BACKUP_PUSH_INTERVAL, Tweaks.SECTION_BACKUP,
6, java.util.Arrays.asList(1, 3, 6, 12), true, true));
for (Tweaks.Item item : Tweaks.ITEMS) {
settings.add(boolSetting(item.key, item.section, item.defaultEnabled, item.implemented, true));
}
Expand Down
Loading