diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 8096b40f8..74354df13 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -2,7 +2,7 @@ name: Android CI on: push: - branches: [ "master" ] + branches: [ "master", "feature/*" ] jobs: build: permissions: write-all @@ -22,13 +22,15 @@ jobs: - name: Write key if: github.event_name != 'pull_request' + env: + KEY_STORE: ${{ secrets.KEY_STORE }} run: | - if [ ! -z "${{ secrets.KEY_STORE }}" ]; then + if [ ! -z "$KEY_STORE" ]; then echo androidStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> gradle.properties echo androidKeyAlias='${{ secrets.ALIAS }}' >> gradle.properties echo androidKeyPassword='${{ secrets.KEY_PASSWORD }}' >> gradle.properties echo androidStoreFile='key.jks' >> gradle.properties - echo ${{ secrets.KEY_STORE }} | base64 --decode > key.jks + echo "$KEY_STORE" | base64 --decode > key.jks fi - name: Grant execute permission for gradlew diff --git a/.gitignore b/.gitignore index 1b9188730..401d5ef2b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ .cxx local.properties key.jks +key_base64.txt \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 58909f3e9..8685ac6cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import java.util.Locale +import java.util.Properties +import java.io.FileInputStream plugins { alias(libs.plugins.androidApplication) @@ -50,12 +52,23 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" signingConfigs.create("config") { + val keystorePropertiesFile = rootProject.file("local.properties") + val keystoreProperties = Properties() + if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + } + val androidStoreFile = project.findProperty("androidStoreFile") as String? + ?: keystoreProperties.getProperty("androidStoreFile") + if (!androidStoreFile.isNullOrEmpty()) { storeFile = rootProject.file(androidStoreFile) - storePassword = project.property("androidStorePassword") as String - keyAlias = project.property("androidKeyAlias") as String - keyPassword = project.property("androidKeyPassword") as String + storePassword = project.findProperty("androidStorePassword") as String? + ?: keystoreProperties.getProperty("androidStorePassword") + keyAlias = project.findProperty("androidKeyAlias") as String? + ?: keystoreProperties.getProperty("androidKeyAlias") + keyPassword = project.findProperty("androidKeyPassword") as String? + ?: keystoreProperties.getProperty("androidKeyPassword") } } @@ -166,6 +179,7 @@ dependencies { implementation(libs.arscblamer) compileOnly(libs.lombok) annotationProcessor(libs.lombok) + implementation(libs.markwon.core) } configurations.all { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e1703db50..027dff9c9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ + + + + + + + + + + + + + + + { - var dialog = new AlertDialogWpp(mActivity); - dialog.setTitle("WAE - New version available!"); - dialog.setMessage("Changelog:\n\n" + changelog); - dialog.setNegativeButton("Ignore", (dialog1, which) -> { - WppCore.setPrivString("ignored_version", hash); - dialog1.dismiss(); - }); - dialog.setPositiveButton("Update", (dialog1, which) -> { - Utils.openLink(mActivity, TELEGRAM_UPDATE_URL); - dialog1.dismiss(); - }); - dialog.show(); + showUpdateDialog(finalHash, finalChangelog, finalPublishedAt); }); + } else { + XposedBridge.log("[" + TAG + "] No update needed (isNew=" + isNewVersion + ", isIgnored=" + isIgnored + ")"); + } + } catch (java.net.SocketTimeoutException e) { + XposedBridge.log("[" + TAG + "] Update check timeout: " + e.getMessage()); + } catch (java.io.IOException e) { + XposedBridge.log("[" + TAG + "] Network error during update check: " + e.getMessage()); + } catch (Exception e) { + XposedBridge.log("[" + TAG + "] Unexpected error during update check: " + e.getMessage()); + XposedBridge.log(e); + } + } + + private void showUpdateDialog(String hash, String changelog, String publishedAt) { + XposedBridge.log("[" + TAG + "] Attempting to show update dialog"); + try { + var markwon = Markwon.create(mActivity); + var dialog = new AlertDialogWpp(mActivity); + + // Format the published date + String formattedDate = formatPublishedDate(publishedAt); + + // Build simple message with version and date + StringBuilder message = new StringBuilder(); + message.append("📦 **Version:** `").append(hash).append("`\n"); + if (!formattedDate.isEmpty()) { + message.append("📅 **Released:** ").append(formattedDate).append("\n"); + } + message.append("\n### What's New\n\n").append(changelog); + + dialog.setTitle("🎉 New Update Available!"); + dialog.setMessage(markwon.toMarkdown(message.toString())); + dialog.setNegativeButton("Ignore", (dialog1, which) -> { + WppCore.setPrivString("ignored_version", hash); + dialog1.dismiss(); + }); + dialog.setPositiveButton("Update Now", (dialog1, which) -> { + Utils.openLink(mActivity, TELEGRAM_UPDATE_URL); + dialog1.dismiss(); + }); + dialog.show(); + + XposedBridge.log("[" + TAG + "] Update dialog shown successfully"); + } catch (Exception e) { + XposedBridge.log("[" + TAG + "] Error showing update dialog: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Format ISO 8601 date to human-readable format + * @param isoDate ISO 8601 date string (e.g., "2024-02-14T12:34:56Z") + * @return Formatted date (e.g., "Feb 14, 2024" or "February 14, 2024") + */ + private String formatPublishedDate(String isoDate) { + if (isoDate == null || isoDate.isEmpty()) { + return ""; + } + + try { + // Parse ISO 8601 date + SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + Date date = isoFormat.parse(isoDate); + + if (date != null) { + // Format to readable date + SimpleDateFormat displayFormat = new SimpleDateFormat("MMM dd, yyyy", Locale.US); + return displayFormat.format(date); } - } catch (Exception ignored) { + } catch (Exception e) { + XposedBridge.log("[" + TAG + "] Error parsing date: " + e.getMessage()); } + + return ""; } } diff --git a/app/src/main/java/com/wmods/wppenhacer/activities/AboutActivity.java b/app/src/main/java/com/wmods/wppenhacer/activities/AboutActivity.java index 7d6334e33..2c669f251 100644 --- a/app/src/main/java/com/wmods/wppenhacer/activities/AboutActivity.java +++ b/app/src/main/java/com/wmods/wppenhacer/activities/AboutActivity.java @@ -27,7 +27,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { binding.btnGithub.setOnClickListener(view -> { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse("https://github.com/Dev4Mod/waenhancer")); + intent.setData(Uri.parse("https://github.com/Dev4Mod/WaEnhancer")); startActivity(intent); }); binding.btnDonate.setOnClickListener(view -> { diff --git a/app/src/main/java/com/wmods/wppenhacer/activities/CallRecordingSettingsActivity.java b/app/src/main/java/com/wmods/wppenhacer/activities/CallRecordingSettingsActivity.java new file mode 100644 index 000000000..bea9f2a60 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/activities/CallRecordingSettingsActivity.java @@ -0,0 +1,135 @@ +package com.wmods.wppenhacer.activities; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.MenuItem; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.PreferenceManager; + +import com.google.android.material.appbar.MaterialToolbar; +import com.wmods.wppenhacer.R; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.InputStreamReader; + +public class CallRecordingSettingsActivity extends AppCompatActivity { + + private static final String TAG = "WaEnhancer"; + private SharedPreferences prefs; + private RadioGroup radioGroupMode; + private RadioButton radioRoot; + private RadioButton radioNonRoot; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call_recording_settings); + + MaterialToolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setTitle(R.string.call_recording_settings); + } + + prefs = PreferenceManager.getDefaultSharedPreferences(this); + + radioGroupMode = findViewById(R.id.radio_group_mode); + radioRoot = findViewById(R.id.radio_root); + radioNonRoot = findViewById(R.id.radio_non_root); + + // Load saved preference + boolean useRoot = prefs.getBoolean("call_recording_use_root", false); + Log.d(TAG, "Loaded call_recording_use_root: " + useRoot); + + if (useRoot) { + radioRoot.setChecked(true); + } else { + radioNonRoot.setChecked(true); + } + + // Direct click listeners on radio buttons + radioRoot.setOnClickListener(v -> { + Log.d(TAG, "Root mode clicked"); + radioRoot.setChecked(true); + radioNonRoot.setChecked(false); + Toast.makeText(this, "Checking root access...", Toast.LENGTH_SHORT).show(); + checkRootAccess(); + }); + + radioNonRoot.setOnClickListener(v -> { + Log.d(TAG, "Non-root mode clicked"); + radioNonRoot.setChecked(true); + radioRoot.setChecked(false); + boolean saved = prefs.edit().putBoolean("call_recording_use_root", false).commit(); + Log.d(TAG, "Saved non-root preference: " + saved); + Toast.makeText(this, R.string.non_root_mode_enabled, Toast.LENGTH_SHORT).show(); + }); + } + + private void checkRootAccess() { + new Thread(() -> { + boolean hasRoot = false; + String rootOutput = ""; + + try { + Log.d(TAG, "Executing su command..."); + Process process = Runtime.getRuntime().exec("su"); + DataOutputStream os = new DataOutputStream(process.getOutputStream()); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + os.writeBytes("id\n"); + os.writeBytes("exit\n"); + os.flush(); + + // Read output + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + rootOutput = sb.toString(); + + int exitCode = process.waitFor(); + Log.d(TAG, "Root check exit code: " + exitCode + ", output: " + rootOutput); + + hasRoot = (exitCode == 0 && rootOutput.contains("uid=0")); + } catch (Exception e) { + Log.e(TAG, "Root check exception: " + e.getMessage()); + hasRoot = false; + } + + final boolean rootGranted = hasRoot; + final String output = rootOutput; + + runOnUiThread(() -> { + if (rootGranted) { + boolean saved = prefs.edit().putBoolean("call_recording_use_root", true).commit(); + Log.d(TAG, "Root granted, saved preference: " + saved); + Toast.makeText(this, R.string.root_access_granted, Toast.LENGTH_SHORT).show(); + } else { + boolean saved = prefs.edit().putBoolean("call_recording_use_root", false).commit(); + Log.d(TAG, "Root denied, saved preference: " + saved + ", output: " + output); + radioNonRoot.setChecked(true); + Toast.makeText(this, R.string.root_access_denied, Toast.LENGTH_LONG).show(); + } + }); + }).start(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } +} + diff --git a/app/src/main/java/com/wmods/wppenhacer/activities/DeletedMessagesActivity.java b/app/src/main/java/com/wmods/wppenhacer/activities/DeletedMessagesActivity.java new file mode 100644 index 000000000..ab4f50d49 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/activities/DeletedMessagesActivity.java @@ -0,0 +1,58 @@ +package com.wmods.wppenhacer.activities; + +import android.os.Bundle; +import android.view.MenuItem; + +import com.wmods.wppenhacer.R; +import com.wmods.wppenhacer.activities.base.BaseActivity; +import com.wmods.wppenhacer.databinding.ActivityDeletedMessagesBinding; +import com.wmods.wppenhacer.ui.fragments.DeletedMessagesFragment; + +public class DeletedMessagesActivity extends BaseActivity { + + private ActivityDeletedMessagesBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityDeletedMessagesBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + setSupportActionBar(binding.toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + if (savedInstanceState == null) { + setupViewPager(); + } + } + + private void setupViewPager() { + binding.viewPager.setAdapter(new androidx.viewpager2.adapter.FragmentStateAdapter(this) { + @androidx.annotation.NonNull + @Override + public androidx.fragment.app.Fragment createFragment(int position) { + return DeletedMessagesFragment.newInstance(position == 1); // 0 = Individual, 1 = Group + } + + @Override + public int getItemCount() { + return 2; + } + }); + + new com.google.android.material.tabs.TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> { + tab.setText(position == 0 ? "Individuals" : "Groups"); + }).attach(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/activities/MainActivity.java b/app/src/main/java/com/wmods/wppenhacer/activities/MainActivity.java index 615055adf..922e3875e 100644 --- a/app/src/main/java/com/wmods/wppenhacer/activities/MainActivity.java +++ b/app/src/main/java/com/wmods/wppenhacer/activities/MainActivity.java @@ -21,6 +21,9 @@ import com.wmods.wppenhacer.activities.base.BaseActivity; import com.wmods.wppenhacer.adapter.MainPagerAdapter; import com.wmods.wppenhacer.databinding.ActivityMainBinding; +import com.wmods.wppenhacer.ui.fragments.GeneralFragment; +import com.wmods.wppenhacer.ui.fragments.HomeFragment; +import com.wmods.wppenhacer.ui.fragments.base.BasePreferenceFragment; import com.wmods.wppenhacer.utils.FilePicker; import java.io.File; @@ -29,6 +32,9 @@ public class MainActivity extends BaseActivity { private ActivityMainBinding binding; private BatteryPermissionHelper batteryPermissionHelper = BatteryPermissionHelper.Companion.getInstance(); + private String pendingScrollToPreference = null; + private int pendingScrollToFragment = -1; + private String pendingParentKey = null; @Override protected void onCreate(Bundle savedInstanceState) { @@ -45,6 +51,11 @@ protected void onCreate(Bundle savedInstanceState) { binding.viewPager.setPageTransformer(new DepthPageTransformer()); + var prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this); + if (!prefs.getBoolean("call_recording_enable", false)) { + binding.navView.getMenu().findItem(R.id.navigation_recordings).setVisible(false); + } + binding.navView.setOnItemSelectedListener(new NavigationBarView.OnItemSelectedListener() { @SuppressLint("NonConstantResourceId") @Override @@ -70,6 +81,10 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) { binding.viewPager.setCurrentItem(4, true); yield true; } + case R.id.navigation_recordings -> { + binding.viewPager.setCurrentItem(5); + yield true; + } default -> false; }; } @@ -80,11 +95,28 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) { public void onPageSelected(int position) { super.onPageSelected(position); binding.navView.getMenu().getItem(position).setChecked(true); + + // Handle pending scroll after page change + if (pendingScrollToFragment == position && pendingScrollToPreference != null) { + final String scrollKey = pendingScrollToPreference; + final String parentKey = pendingParentKey; + pendingScrollToPreference = null; + pendingScrollToFragment = -1; + pendingParentKey = null; + + // Wait for fragment to be ready + binding.viewPager.postDelayed(() -> { + scrollToPreferenceInCurrentFragment(scrollKey, parentKey); + }, 300); + } } }); binding.viewPager.setCurrentItem(2, false); createMainDir(); FilePicker.registerFilePicker(this); + + // Handle incoming navigation from search + handleIncomingIntent(getIntent()); } private void createMainDir() { @@ -94,6 +126,100 @@ private void createMainDir() { } } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIncomingIntent(intent); + } + + private void handleIncomingIntent(Intent intent) { + if (intent == null) return; + + int fragmentPosition = intent.getIntExtra("navigate_to_fragment", -1); + String preferenceKey = intent.getStringExtra("scroll_to_preference"); + String parentKey = intent.getStringExtra("parent_preference"); + + if (fragmentPosition >= 0 && preferenceKey != null) { + // Store the scroll target + pendingScrollToPreference = preferenceKey; + pendingScrollToFragment = fragmentPosition; + pendingParentKey = parentKey; + + // Navigate to the fragment (onPageSelected will handle the scroll) + binding.viewPager.setCurrentItem(fragmentPosition, false); + + // Clear intent extras + intent.removeExtra("navigate_to_fragment"); + intent.removeExtra("scroll_to_preference"); + intent.removeExtra("parent_preference"); + } else if (fragmentPosition >= 0) { + // Just navigate without scrolling + binding.viewPager.setCurrentItem(fragmentPosition, true); + } + } + + private void scrollToPreferenceInCurrentFragment(String preferenceKey, String parentKey) { + // Get the current fragment from the ViewPager + int currentItem = binding.viewPager.getCurrentItem(); + Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + currentItem); + + if (fragment == null) return; + + // Handle different fragment types + if (fragment instanceof GeneralFragment || fragment instanceof HomeFragment) { + // These fragments have child fragments + if (parentKey != null && !parentKey.isEmpty()) { + // Navigate to sub-fragment first, then scroll + navigateToSubFragmentAndScroll(fragment, parentKey, preferenceKey); + } else { + // Direct scroll in current child fragment + scrollInChildFragment(fragment, preferenceKey); + } + } else if (fragment instanceof BasePreferenceFragment) { + // Direct preference fragments (no nesting) + ((BasePreferenceFragment) fragment).scrollToPreference(preferenceKey); + } + } + + private void navigateToSubFragmentAndScroll(Fragment parentFragment, String parentKey, String childPreferenceKey) { + // Directly instantiate the sub-fragment + Fragment subFragment = null; + + switch (parentKey) { + case "general_home": + subFragment = new GeneralFragment.HomeGeneralPreference(); + break; + case "homescreen": + subFragment = new GeneralFragment.HomeScreenGeneralPreference(); + break; + case "conversation": + subFragment = new GeneralFragment.ConversationGeneralPreference(); + break; + } + + if (subFragment != null && parentFragment.getView() != null) { + final Fragment finalSubFragment = subFragment; + // Replace the current child fragment + parentFragment.getChildFragmentManager().beginTransaction() + .replace(R.id.frag_container, subFragment) + .commitNow(); + + // Wait for fragment to be ready, then scroll + parentFragment.getView().postDelayed(() -> { + if (finalSubFragment instanceof BasePreferenceFragment) { + ((BasePreferenceFragment) finalSubFragment).scrollToPreference(childPreferenceKey); + } + }, 400); + } + } + + private void scrollInChildFragment(Fragment parentFragment, String preferenceKey) { + Fragment childFragment = parentFragment.getChildFragmentManager().findFragmentById(R.id.frag_container); + if (childFragment instanceof BasePreferenceFragment) { + ((BasePreferenceFragment) childFragment).scrollToPreference(preferenceKey); + } + } + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -115,7 +241,12 @@ public boolean onCreateOptionsMenu(Menu menu) { @SuppressLint("BatteryLife") @Override public boolean onOptionsItemSelected(@NonNull MenuItem item) { - if (item.getItemId() == R.id.menu_about) { + if (item.getItemId() == R.id.menu_search) { + var options = ActivityOptionsCompat.makeCustomAnimation( + this, R.anim.slide_in_right, R.anim.slide_out_left); + startActivity(new Intent(this, SearchActivity.class), options.toBundle()); + return true; + } else if (item.getItemId() == R.id.menu_about) { var options = ActivityOptionsCompat.makeCustomAnimation( this, R.anim.slide_in_right, R.anim.slide_out_left); startActivity(new Intent(this, AboutActivity.class), options.toBundle()); diff --git a/app/src/main/java/com/wmods/wppenhacer/activities/MessageListActivity.java b/app/src/main/java/com/wmods/wppenhacer/activities/MessageListActivity.java new file mode 100644 index 000000000..4679fafd0 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/activities/MessageListActivity.java @@ -0,0 +1,191 @@ +package com.wmods.wppenhacer.activities; + +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; + +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.wmods.wppenhacer.R; +import com.wmods.wppenhacer.activities.base.BaseActivity; +import com.wmods.wppenhacer.adapter.MessageListAdapter; +import com.wmods.wppenhacer.databinding.ActivityMessageListBinding; +import com.wmods.wppenhacer.xposed.core.WppCore; +import com.wmods.wppenhacer.xposed.core.db.DelMessageStore; +import com.wmods.wppenhacer.xposed.core.db.DeletedMessage; + +import java.util.List; + +public class MessageListActivity extends BaseActivity implements MessageListAdapter.OnRestoreClickListener { + + private ActivityMessageListBinding binding; + private MessageListAdapter adapter; + private DelMessageStore delMessageStore; + private String chatJid; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = ActivityMessageListBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + chatJid = getIntent().getStringExtra("chat_jid"); + if (chatJid == null) { + finish(); + return; + } + + setSupportActionBar(binding.toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + String title = chatJid; + if (title != null) { + title = title.replace("@s.whatsapp.net", "").replace("@g.us", ""); + if (title.contains("@")) + title = title.split("@")[0]; + } + getSupportActionBar().setTitle(title); + } + + delMessageStore = DelMessageStore.getInstance(this); + adapter = new MessageListAdapter(this); + + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + layoutManager.setStackFromEnd(true); + binding.recyclerView.setLayoutManager(layoutManager); + binding.recyclerView.setAdapter(adapter); + + loadMessages(); + } + + private void loadMessages() { + new Thread(() -> { + List messages = delMessageStore.getDeletedMessagesByChat(chatJid); + runOnUiThread(() -> { + if (messages.isEmpty()) { + binding.emptyView.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + } else { + binding.emptyView.setVisibility(View.GONE); + binding.recyclerView.setVisibility(View.VISIBLE); + adapter.setMessages(messages); + binding.recyclerView.scrollToPosition(messages.size() - 1); + } + }); + }).start(); + } + + @Override + protected void onResume() { + super.onResume(); + loadMessages(); + } + + @Override + public boolean onCreateOptionsMenu(android.view.Menu menu) { + getMenuInflater().inflate(R.menu.menu_message_list, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } else if (item.getItemId() == R.id.action_info) { + new androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("Chat Info") + .setMessage( + "This identifier (JID) or number is shown because the contact name could not be resolved at the time of deletion.\n\n" + + + "This happens if the contact is not saved in your address book or if the name wasn't available in the database when the message was processed.") + .setPositiveButton("OK", null) + .show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private androidx.appcompat.view.ActionMode actionMode; + private final androidx.appcompat.view.ActionMode.Callback actionModeCallback = new androidx.appcompat.view.ActionMode.Callback() { + @Override + public boolean onCreateActionMode(androidx.appcompat.view.ActionMode mode, android.view.Menu menu) { + mode.getMenuInflater().inflate(R.menu.menu_context_delete, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(androidx.appcompat.view.ActionMode mode, android.view.Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(androidx.appcompat.view.ActionMode mode, android.view.MenuItem item) { + if (item.getItemId() == R.id.action_delete) { + deleteSelectedMessages(); + mode.finish(); + return true; + } + return false; + } + + @Override + public void onDestroyActionMode(androidx.appcompat.view.ActionMode mode) { + adapter.clearSelection(); + actionMode = null; + } + }; + + private void deleteSelectedMessages() { + List selected = adapter.getSelectedItems(); + if (selected.isEmpty()) + return; + + new androidx.appcompat.app.AlertDialog.Builder(this) + .setTitle("Delete Messages?") + .setMessage("Are you sure you want to delete " + selected.size() + " message(s)?") + .setPositiveButton("Delete", (dialog, which) -> { + new Thread(() -> { + delMessageStore.deleteMessages(selected); + runOnUiThread(() -> { + loadMessages(); + Toast.makeText(this, "Messages deleted", Toast.LENGTH_SHORT).show(); + }); + }).start(); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + @Override + public void onRestoreClick(DeletedMessage message) { + Toast.makeText(this, "Restore coming soon!", Toast.LENGTH_SHORT).show(); + } + + @Override + public boolean onItemLongClick(DeletedMessage message) { + if (actionMode == null) { + actionMode = startSupportActionMode(actionModeCallback); + } + toggleSelection(message.getKeyId()); + return true; + } + + @Override + public void onItemClick(DeletedMessage message) { + if (actionMode != null) { + toggleSelection(message.getKeyId()); + } + } + + private void toggleSelection(String keyId) { + adapter.toggleSelection(keyId); + int count = adapter.getSelectedCount(); + if (count == 0) { + actionMode.finish(); + } else { + actionMode.setTitle(count + " selected"); + } + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/activities/SearchActivity.java b/app/src/main/java/com/wmods/wppenhacer/activities/SearchActivity.java new file mode 100644 index 000000000..db9fbd492 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/activities/SearchActivity.java @@ -0,0 +1,140 @@ +package com.wmods.wppenhacer.activities; + +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; + +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.google.android.material.textfield.TextInputEditText; +import com.wmods.wppenhacer.R; +import com.wmods.wppenhacer.activities.base.BaseActivity; +import com.wmods.wppenhacer.adapter.SearchAdapter; +import com.wmods.wppenhacer.databinding.ActivitySearchBinding; +import com.wmods.wppenhacer.model.SearchableFeature; +import com.wmods.wppenhacer.utils.FeatureCatalog; + +import java.util.ArrayList; +import java.util.List; + +/** + * Activity for searching and navigating to app features. + */ +public class SearchActivity extends BaseActivity implements SearchAdapter.OnFeatureClickListener { + + private ActivitySearchBinding binding; + private SearchAdapter adapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = ActivitySearchBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + // Setup toolbar + setSupportActionBar(binding.toolbar); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setTitle(R.string.search_features_title); + } + + // Setup RecyclerView + adapter = new SearchAdapter(this); + binding.searchResults.setLayoutManager(new LinearLayoutManager(this)); + binding.searchResults.setAdapter(adapter); + + // Setup search input + setupSearchInput(); + + // Show all features by default (grouped by category) + loadAllFeatures(); + + // Focus on search input + binding.searchInput.requestFocus(); + } + + private void setupSearchInput() { + binding.searchInput.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + performSearch(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) { + } + }); + } + + private void loadAllFeatures() { + List allFeatures = FeatureCatalog.getAllFeatures(this); + adapter.setFeatures(allFeatures); + adapter.setSearchQuery(""); + updateEmptyState(false, ""); + } + + private void performSearch(String query) { + if (query.trim().isEmpty()) { + // Show all features when search is empty + loadAllFeatures(); + return; + } + + // Search features + List results = FeatureCatalog.search(this, query); + + // Update adapter + adapter.setFeatures(results); + adapter.setSearchQuery(query); + + // Update empty state + if (results.isEmpty()) { + updateEmptyState(true, getString(R.string.search_no_results)); + } else { + updateEmptyState(false, ""); + } + } + + private void updateEmptyState(boolean show, String message) { + if (show) { + binding.emptyState.setVisibility(View.VISIBLE); + binding.searchResults.setVisibility(View.GONE); + binding.emptyStateText.setText(message); + } else { + binding.emptyState.setVisibility(View.GONE); + binding.searchResults.setVisibility(View.VISIBLE); + } + } + + @Override + public void onFeatureClick(SearchableFeature feature) { + if (feature.getFragmentType() == SearchableFeature.FragmentType.ACTIVITY) { + if ("deleted_messages_activity".equals(feature.getKey())) { + startActivity(new Intent(this, DeletedMessagesActivity.class)); + } + return; + } + + // Navigate back to MainActivity with feature information + Intent intent = new Intent(this, MainActivity.class); + intent.putExtra("navigate_to_fragment", feature.getFragmentType().getPosition()); + intent.putExtra("scroll_to_preference", feature.getKey()); + intent.putExtra("parent_preference", feature.getParentKey()); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(intent); + finish(); + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/adapter/DeletedMessagesAdapter.java b/app/src/main/java/com/wmods/wppenhacer/adapter/DeletedMessagesAdapter.java new file mode 100644 index 000000000..20a9a8241 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/adapter/DeletedMessagesAdapter.java @@ -0,0 +1,264 @@ +package com.wmods.wppenhacer.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.wmods.wppenhacer.R; +import com.wmods.wppenhacer.xposed.core.WppCore; +import com.wmods.wppenhacer.xposed.core.db.DeletedMessage; +import com.wmods.wppenhacer.xposed.utils.Utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +public class DeletedMessagesAdapter extends RecyclerView.Adapter { + + private List messages = new ArrayList<>(); + private java.util.Set selectedItems = new java.util.HashSet<>(); + private final Map iconCache = new HashMap<>(); + private final OnItemClickListener listener; + + public interface OnItemClickListener { + void onItemClick(DeletedMessage message); + + boolean onItemLongClick(DeletedMessage message); + + void onRestoreClick(DeletedMessage message); + } + + public DeletedMessagesAdapter(OnItemClickListener listener) { + this.listener = listener; + } + + public void setMessages(List messages) { + this.messages = messages; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contact_row, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + DeletedMessage message = messages.get(position); + + // Contact Name + Context context = holder.itemView.getContext(); + String displayJid = message.getChatJid(); + + // Priority 1: Use Persisted Contact Name (from DB) + String contactName = message.getContactName(); + + // Check if DB value is invalid (Generic App Name) + if (contactName != null + && (contactName.equalsIgnoreCase("WhatsApp") || contactName.equalsIgnoreCase("WhatsApp Business"))) { + contactName = null; + } + + // Priority 2: Runtime Lookup (if not in DB or was invalid) + if ((contactName == null || contactName.isEmpty()) && displayJid != null) { + contactName = com.wmods.wppenhacer.utils.ContactHelper.getContactName(context, displayJid); + } + + // Final Check: If Runtime Lookup also returned Generic App Name + if (contactName != null + && (contactName.equalsIgnoreCase("WhatsApp") || contactName.equalsIgnoreCase("WhatsApp Business"))) { + contactName = null; + } + String displayText; + if (contactName != null) { + displayText = contactName; + } else { + // Fallback to formatted JID if contact name is missing + displayText = displayJid; + if (displayText != null) { + displayText = displayText.replace("@s.whatsapp.net", "").replace("@g.us", ""); + if (displayText.contains("@")) + displayText = displayText.split("@")[0]; + } else { + displayText = "Unknown"; + } + } + holder.contactName.setText(displayText); + + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("dd/MM/yyyy hh:mm a", + java.util.Locale.getDefault()); + holder.timestamp.setText(sdf.format(new java.util.Date(message.getTimestamp()))); + + // Message Preview Logic... (unchanged) + String text = message.getTextContent(); + String senderPrefix = ""; + if (message.isFromMe()) { + senderPrefix = "You: "; + } + + // Sanitize text for media messages (Fix for weird strings/URLs) + if (message.getMediaType() > 0 && text != null) { + if (text.startsWith("http") || (text.length() > 20 && !text.contains(" "))) { + text = null; + } + } + + if (text == null || text.isEmpty()) { + int type = message.getMediaType(); + if (type != -1 && type != 0) { + switch (type) { + case 1: + text = "📷 Photo"; + break; + case 2: + text = "🔊 Audio"; + break; + case 3: + text = "🎥 Video"; + break; + case 4: + text = "👤 Contact"; + break; + case 5: + text = "📍 Location"; + break; + case 9: + text = "📄 Document"; + break; + case 13: + text = "👾 GIF"; + break; + case 20: + text = "💟 Sticker"; + break; + case 42: + text = "🔄 Status Reply"; + break; + default: + text = "📁 Media"; + break; + } + } else { + text = "🚫 Message deleted"; + } + } + holder.lastMessage.setText(senderPrefix + text); + + // Avatar (Placeholder) + // Holder.avatar.setImageDrawable(...) + + // Avatar (Now App Icon) & App Badge Logic + String pkg = message.getPackageName(); + if (pkg != null) { + // Hide the small badge + holder.appBadge.setVisibility(View.GONE); + + // Clear any tint on the avatar (xml has tint) + holder.avatar.setImageTintList(null); + + // Try to load from cache first + if (iconCache.containsKey(pkg)) { + holder.avatar.setImageDrawable(iconCache.get(pkg)); + } else { + try { + android.content.pm.PackageManager pm = holder.itemView.getContext().getPackageManager(); + android.graphics.drawable.Drawable icon = pm.getApplicationIcon(pkg); + if (icon != null) { + iconCache.put(pkg, icon); + holder.avatar.setImageDrawable(icon); + } else { + // Fallback + holder.avatar.setImageResource(R.drawable.ic_person); + } + } catch (android.content.pm.PackageManager.NameNotFoundException e) { + // App not installed or invalid package name + holder.avatar.setImageResource(R.drawable.ic_person); + } + } + } else { + // Fallback if no package name (shouldn't happen for new msgs) + holder.avatar.setImageResource(R.drawable.ic_person); + holder.appBadge.setVisibility(View.GONE); + } + + // Selection Logic + if (selectedItems.contains(message.getChatJid())) { + holder.itemView.setBackgroundColor( + holder.itemView.getContext().getResources().getColor(R.color.selected_item_color, null)); // You + // might + // need to + // define + // this + // color + } else { + android.util.TypedValue outValue = new android.util.TypedValue(); + holder.itemView.getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, + true); + holder.itemView.setBackgroundResource(outValue.resourceId); + } + + holder.itemView.setOnClickListener(v -> { + if (listener != null) + listener.onItemClick(message); + }); + + holder.itemView.setOnLongClickListener(v -> { + if (listener != null) + return listener.onItemLongClick(message); + return false; + }); + } + + public void toggleSelection(String chatJid) { + if (selectedItems.contains(chatJid)) { + selectedItems.remove(chatJid); + } else { + selectedItems.add(chatJid); + } + notifyDataSetChanged(); + } + + public void clearSelection() { + selectedItems.clear(); + notifyDataSetChanged(); + } + + public int getSelectedCount() { + return selectedItems.size(); + } + + public java.util.List getSelectedItems() { + return new ArrayList<>(selectedItems); + } + + @Override + public int getItemCount() { + return messages.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ImageView avatar; + ImageView appBadge; + TextView contactName; + TextView timestamp; // Date + TextView lastMessage; + + ViewHolder(View itemView) { + super(itemView); + avatar = itemView.findViewById(R.id.avatar); + appBadge = itemView.findViewById(R.id.app_badge); + contactName = itemView.findViewById(R.id.contact_name); + timestamp = itemView.findViewById(R.id.timestamp); + lastMessage = itemView.findViewById(R.id.last_message); + } + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/adapter/MainPagerAdapter.java b/app/src/main/java/com/wmods/wppenhacer/adapter/MainPagerAdapter.java index 851cfb10f..fca3a11b5 100644 --- a/app/src/main/java/com/wmods/wppenhacer/adapter/MainPagerAdapter.java +++ b/app/src/main/java/com/wmods/wppenhacer/adapter/MainPagerAdapter.java @@ -3,6 +3,7 @@ import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import androidx.preference.PreferenceManager; import androidx.viewpager2.adapter.FragmentStateAdapter; import com.wmods.wppenhacer.ui.fragments.CustomizationFragment; @@ -10,11 +11,16 @@ import com.wmods.wppenhacer.ui.fragments.HomeFragment; import com.wmods.wppenhacer.ui.fragments.MediaFragment; import com.wmods.wppenhacer.ui.fragments.PrivacyFragment; +import com.wmods.wppenhacer.ui.fragments.RecordingsFragment; public class MainPagerAdapter extends FragmentStateAdapter { + private final boolean isRecordingEnabled; + public MainPagerAdapter(@NonNull FragmentActivity fragmentActivity) { super(fragmentActivity); + var prefs = PreferenceManager.getDefaultSharedPreferences(fragmentActivity); + isRecordingEnabled = prefs.getBoolean("call_recording_enable", false); } @NonNull @@ -25,12 +31,13 @@ public Fragment createFragment(int position) { case 1 -> new PrivacyFragment(); case 3 -> new MediaFragment(); case 4 -> new CustomizationFragment(); + case 5 -> new RecordingsFragment(); default -> new HomeFragment(); }; } @Override public int getItemCount() { - return 5; // Number of fragments + return isRecordingEnabled ? 6 : 5; } } \ No newline at end of file diff --git a/app/src/main/java/com/wmods/wppenhacer/adapter/MessageListAdapter.java b/app/src/main/java/com/wmods/wppenhacer/adapter/MessageListAdapter.java new file mode 100644 index 000000000..a52296dd3 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/adapter/MessageListAdapter.java @@ -0,0 +1,199 @@ +package com.wmods.wppenhacer.adapter; + +import android.net.Uri; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.button.MaterialButton; +import com.wmods.wppenhacer.R; +import com.wmods.wppenhacer.xposed.core.db.DeletedMessage; +import com.wmods.wppenhacer.xposed.utils.Utils; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class MessageListAdapter extends RecyclerView.Adapter { + + private java.util.Set selectedItems = new java.util.HashSet<>(); + private List messages = new ArrayList<>(); + private final OnRestoreClickListener listener; + + public interface OnRestoreClickListener { + void onRestoreClick(DeletedMessage message); + + boolean onItemLongClick(DeletedMessage message); + + void onItemClick(DeletedMessage message); + } + + private static final int VIEW_TYPE_SENT = 1; + private static final int VIEW_TYPE_RECEIVED = 2; + + public MessageListAdapter(OnRestoreClickListener listener) { + this.listener = listener; + } + + public void setMessages(List messages) { + this.messages = messages; + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + DeletedMessage message = messages.get(position); + return message.isFromMe() ? VIEW_TYPE_SENT : VIEW_TYPE_RECEIVED; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + int layoutId = (viewType == VIEW_TYPE_SENT) ? R.layout.item_message_sent : R.layout.item_message_received; + View view = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + // ... (existing binding logic) + DeletedMessage message = messages.get(position); + + // ... (Sender Name logic - keep existing) + if (holder.senderName != null) { + boolean showName = !message.isFromMe() && message.getChatJid().contains("@g.us"); + + if (showName) { + String contactName = message.getContactName(); // Try persisted name first + if (contactName == null) { + contactName = com.wmods.wppenhacer.utils.ContactHelper.getContactName(holder.itemView.getContext(), + message.getSenderJid()); + } + + if (contactName != null) { + holder.senderName.setText(contactName); + holder.senderName.setVisibility(View.VISIBLE); + } else { + String senderJid = message.getSenderJid(); + if (senderJid != null) { + senderJid = senderJid.replace("@s.whatsapp.net", "").replace("@g.us", ""); + if (senderJid.contains("@")) + senderJid = senderJid.split("@")[0]; + holder.senderName.setText(senderJid); + holder.senderName.setVisibility(View.VISIBLE); + } else { + holder.senderName.setVisibility(View.GONE); + } + } + } else { + holder.senderName.setVisibility(View.GONE); + } + } + + // Timestamp + // Timestamp + String timeText = "Deleted:\t" + Utils.getDateTimeFromMillis(message.getTimestamp()); + + if (message.getOriginalTimestamp() > 0) { + timeText = "Original:\t" + Utils.getDateTimeFromMillis(message.getOriginalTimestamp()) + "\n" + timeText; + } + holder.timestamp.setText(timeText); + + // Message Content + String text = message.getTextContent(); + if (text != null && !text.isEmpty()) { + holder.messageContent.setText(text); + holder.messageContent.setVisibility(View.VISIBLE); + } else { + // Placeholder for media + String type = "Message"; + if (message.getMediaType() != -1) { + if (message.getMediaType() == 1) + type = "📷 Photo"; + else if (message.getMediaType() == 2) + type = "🔊 Audio"; + else if (message.getMediaType() == 3) + type = "🎥 Video"; + else + type = "📁 Media (" + message.getMediaType() + ")"; + } + + if (message.getMediaCaption() != null && !message.getMediaCaption().isEmpty()) { + type += "\n" + message.getMediaCaption(); + } + holder.messageContent.setText(type); + holder.messageContent.setVisibility(View.VISIBLE); + } + + // Restore Button + holder.btnRestore.setOnClickListener(v -> listener.onRestoreClick(message)); + + // Selection Logic + if (selectedItems.contains(message.getKeyId())) { + android.util.TypedValue typedValue = new android.util.TypedValue(); + holder.itemView.getContext().getTheme().resolveAttribute(android.R.attr.colorControlHighlight, typedValue, + true); + holder.itemView.setBackgroundColor(typedValue.data); + } else { + holder.itemView.setBackgroundColor(android.graphics.Color.TRANSPARENT); + } + + holder.itemView.setOnClickListener(v -> { + if (listener != null) + listener.onItemClick(message); + }); + + holder.itemView.setOnLongClickListener(v -> { + if (listener != null) + return listener.onItemLongClick(message); + return false; + }); + } + + public void toggleSelection(String keyId) { + if (selectedItems.contains(keyId)) { + selectedItems.remove(keyId); + } else { + selectedItems.add(keyId); + } + notifyDataSetChanged(); + } + + public void clearSelection() { + selectedItems.clear(); + notifyDataSetChanged(); + } + + public int getSelectedCount() { + return selectedItems.size(); + } + + public java.util.List getSelectedItems() { + return new ArrayList<>(selectedItems); + } + + @Override + public int getItemCount() { + return messages.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + TextView senderName; // Nullable (only in received) + TextView timestamp; + TextView messageContent; + View btnRestore; + + ViewHolder(View itemView) { + super(itemView); + senderName = itemView.findViewById(R.id.sender_name); + timestamp = itemView.findViewById(R.id.timestamp); + messageContent = itemView.findViewById(R.id.message_content); + btnRestore = itemView.findViewById(R.id.btn_restore); + } + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/adapter/RecordingsAdapter.java b/app/src/main/java/com/wmods/wppenhacer/adapter/RecordingsAdapter.java new file mode 100644 index 000000000..b84d72b44 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/adapter/RecordingsAdapter.java @@ -0,0 +1,220 @@ +package com.wmods.wppenhacer.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.card.MaterialCardView; +import com.wmods.wppenhacer.R; +import com.wmods.wppenhacer.model.Recording; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public class RecordingsAdapter extends RecyclerView.Adapter { + + private List recordings = new ArrayList<>(); + private final OnRecordingActionListener listener; + private boolean isSelectionMode = false; + private final Set selectedPositions = new HashSet<>(); + private OnSelectionChangeListener selectionChangeListener; + + public interface OnRecordingActionListener { + void onPlay(Recording recording); + void onShare(Recording recording); + void onDelete(Recording recording); + void onLongPress(Recording recording, int position); + } + + public interface OnSelectionChangeListener { + void onSelectionChanged(int count); + } + + public RecordingsAdapter(OnRecordingActionListener listener) { + this.listener = listener; + } + + public void setSelectionChangeListener(OnSelectionChangeListener listener) { + this.selectionChangeListener = listener; + } + + public void setRecordings(List recordings) { + this.recordings = recordings; + clearSelection(); + notifyDataSetChanged(); + } + + public void setSelectionMode(boolean selectionMode) { + if (this.isSelectionMode != selectionMode) { + this.isSelectionMode = selectionMode; + if (!selectionMode) { + selectedPositions.clear(); + } + notifyDataSetChanged(); + } + } + + public boolean isSelectionMode() { + return isSelectionMode; + } + + public void toggleSelection(int position) { + if (selectedPositions.contains(position)) { + selectedPositions.remove(position); + } else { + selectedPositions.add(position); + } + notifyItemChanged(position); + if (selectionChangeListener != null) { + selectionChangeListener.onSelectionChanged(selectedPositions.size()); + } + } + + public void selectAll() { + selectedPositions.clear(); + for (int i = 0; i < recordings.size(); i++) { + selectedPositions.add(i); + } + notifyDataSetChanged(); + if (selectionChangeListener != null) { + selectionChangeListener.onSelectionChanged(selectedPositions.size()); + } + } + + public void clearSelection() { + selectedPositions.clear(); + isSelectionMode = false; + notifyDataSetChanged(); + if (selectionChangeListener != null) { + selectionChangeListener.onSelectionChanged(0); + } + } + + public List getSelectedRecordings() { + List selected = new ArrayList<>(); + for (int position : selectedPositions) { + if (position < recordings.size()) { + selected.add(recordings.get(position)); + } + } + return selected; + } + + public int getSelectionCount() { + return selectedPositions.size(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_recording, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + Recording recording = recordings.get(position); + + // Contact name + holder.contactName.setText(recording.getContactName()); + + // Phone number + String phoneNumber = recording.getPhoneNumber(); + if (phoneNumber != null && !phoneNumber.equals(recording.getContactName())) { + holder.phoneNumber.setVisibility(View.VISIBLE); + holder.phoneNumber.setText(phoneNumber); + } else { + holder.phoneNumber.setVisibility(View.GONE); + } + + // Duration + holder.duration.setText(recording.getFormattedDuration()); + + // Details: size and date + SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMM yyyy, HH:mm", Locale.getDefault()); + String details = recording.getFormattedSize() + " • " + dateFormat.format(new Date(recording.getDate())); + holder.details.setText(details); + + // Selection mode UI + if (isSelectionMode) { + holder.checkbox.setVisibility(View.VISIBLE); + holder.actionsContainer.setVisibility(View.GONE); + holder.checkbox.setChecked(selectedPositions.contains(position)); + holder.card.setChecked(selectedPositions.contains(position)); + } else { + holder.checkbox.setVisibility(View.GONE); + holder.actionsContainer.setVisibility(View.VISIBLE); + holder.card.setChecked(false); + } + + // Click handling + holder.itemView.setOnClickListener(v -> { + if (isSelectionMode) { + toggleSelection(position); + } else { + listener.onPlay(recording); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (!isSelectionMode) { + listener.onLongPress(recording, position); + } + return true; + }); + + holder.checkbox.setOnClickListener(v -> toggleSelection(position)); + + // Action buttons + holder.btnPlay.setOnClickListener(v -> listener.onPlay(recording)); + holder.btnShare.setOnClickListener(v -> listener.onShare(recording)); + holder.btnDelete.setOnClickListener(v -> listener.onDelete(recording)); + } + + @Override + public int getItemCount() { + return recordings.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + MaterialCardView card; + CheckBox checkbox; + ImageView icon; + TextView contactName; + TextView phoneNumber; + TextView duration; + TextView details; + LinearLayout actionsContainer; + ImageButton btnPlay; + ImageButton btnShare; + ImageButton btnDelete; + + ViewHolder(View itemView) { + super(itemView); + card = (MaterialCardView) itemView; + checkbox = itemView.findViewById(R.id.checkbox); + icon = itemView.findViewById(R.id.icon); + contactName = itemView.findViewById(R.id.contact_name); + phoneNumber = itemView.findViewById(R.id.phone_number); + duration = itemView.findViewById(R.id.duration); + details = itemView.findViewById(R.id.details); + actionsContainer = itemView.findViewById(R.id.actions_container); + btnPlay = itemView.findViewById(R.id.btn_play); + btnShare = itemView.findViewById(R.id.btn_share); + btnDelete = itemView.findViewById(R.id.btn_delete); + } + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/adapter/SearchAdapter.java b/app/src/main/java/com/wmods/wppenhacer/adapter/SearchAdapter.java new file mode 100644 index 000000000..ab2be0080 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/adapter/SearchAdapter.java @@ -0,0 +1,194 @@ +package com.wmods.wppenhacer.adapter; + +import android.graphics.Color; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.wmods.wppenhacer.R; +import com.wmods.wppenhacer.model.SearchableFeature; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Adapter for displaying search results in a RecyclerView with section headers. + */ +public class SearchAdapter extends RecyclerView.Adapter { + + private static final int VIEW_TYPE_HEADER = 0; + private static final int VIEW_TYPE_ITEM = 1; + + private final List items; // Can be SearchableFeature or String (section header) + private String searchQuery = ""; + private final OnFeatureClickListener listener; + + public interface OnFeatureClickListener { + void onFeatureClick(SearchableFeature feature); + } + + public SearchAdapter(OnFeatureClickListener listener) { + this.items = new ArrayList<>(); + this.listener = listener; + } + + public void setFeatures(List newFeatures) { + items.clear(); + + // Group features by category + Map> groupedFeatures = new LinkedHashMap<>(); + for (SearchableFeature feature : newFeatures) { + groupedFeatures.computeIfAbsent(feature.getCategory(), k -> new ArrayList<>()).add(feature); + } + + // Add items with section headers + for (Map.Entry> entry : groupedFeatures.entrySet()) { + items.add(entry.getKey().getDisplayName()); // Add section header + items.addAll(entry.getValue()); // Add features in that section + } + + notifyDataSetChanged(); + } + + public void setSearchQuery(String query) { + this.searchQuery = query != null ? query : ""; + } + + @Override + public int getItemViewType(int position) { + return items.get(position) instanceof String ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == VIEW_TYPE_HEADER) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_search_section_header, parent, false); + return new SectionHeaderViewHolder(view); + } else { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_search_result, parent, false); + return new SearchResultViewHolder(view); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if (holder instanceof SectionHeaderViewHolder) { + ((SectionHeaderViewHolder) holder).bind((String) items.get(position)); + } else if (holder instanceof SearchResultViewHolder) { + ((SearchResultViewHolder) holder).bind((SearchableFeature) items.get(position), searchQuery, listener); + } + } + + @Override + public int getItemCount() { + return items.size(); + } + + static class SectionHeaderViewHolder extends RecyclerView.ViewHolder { + private final TextView sectionTitle; + + public SectionHeaderViewHolder(@NonNull View itemView) { + super(itemView); + sectionTitle = itemView.findViewById(R.id.sectionTitle); + } + + public void bind(String title) { + sectionTitle.setText(title); + } + } + + static class SearchResultViewHolder extends RecyclerView.ViewHolder { + private final TextView titleTextView; + private final TextView summaryTextView; + private final TextView categoryBadge; + + public SearchResultViewHolder(@NonNull View itemView) { + super(itemView); + titleTextView = itemView.findViewById(R.id.featureTitle); + summaryTextView = itemView.findViewById(R.id.featureSummary); + categoryBadge = itemView.findViewById(R.id.categoryBadge); + } + + public void bind(SearchableFeature feature, String query, OnFeatureClickListener listener) { + // Set title with highlighting + titleTextView.setText(highlightText(feature.getTitle(), query)); + + // Set summary with highlighting + if (feature.getSummary() != null && !feature.getSummary().isEmpty()) { + summaryTextView.setText(highlightText(feature.getSummary(), query)); + summaryTextView.setVisibility(View.VISIBLE); + } else { + summaryTextView.setVisibility(View.GONE); + } + + // Set category badge + categoryBadge.setText(feature.getCategory().getDisplayName().toUpperCase(Locale.ROOT)); + categoryBadge.setBackgroundColor(getCategoryColor(feature.getCategory())); + + // Set click listener + itemView.setOnClickListener(v -> { + if (listener != null) { + listener.onFeatureClick(feature); + } + }); + } + + private CharSequence highlightText(String text, String query) { + if (text == null || query == null || query.isEmpty()) { + return text; + } + + SpannableString spannable = new SpannableString(text); + String lowerText = text.toLowerCase(); + String lowerQuery = query.toLowerCase(); + + int start = lowerText.indexOf(lowerQuery); + if (start >= 0) { + int end = start + query.length(); + spannable.setSpan( + new BackgroundColorSpan(Color.parseColor("#4DFFD700")), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + + return spannable; + } + + private int getCategoryColor(SearchableFeature.Category category) { + switch (category) { + case GENERAL: + case GENERAL_HOME: + case GENERAL_HOMESCREEN: + case GENERAL_CONVERSATION: + return Color.parseColor("#4CAF50"); // Green + case PRIVACY: + return Color.parseColor("#2196F3"); // Blue + case MEDIA: + return Color.parseColor("#FF9800"); // Orange + case CUSTOMIZATION: + return Color.parseColor("#9C27B0"); // Purple + case RECORDINGS: + return Color.parseColor("#F44336"); // Red + case HOME_ACTIONS: + return Color.parseColor("#607D8B"); // Blue Grey + default: + return Color.parseColor("#757575"); // Grey + } + } + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/model/Recording.java b/app/src/main/java/com/wmods/wppenhacer/model/Recording.java new file mode 100644 index 000000000..046516558 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/model/Recording.java @@ -0,0 +1,202 @@ +package com.wmods.wppenhacer.model; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; + +import java.io.File; +import java.io.RandomAccessFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Model class representing a call recording with metadata. + */ +public class Recording { + + private final File file; + private String phoneNumber; + private String contactName; + private long duration; // in milliseconds + private final long date; + private final long size; + + // Pattern to extract phone number from filename: Call_+1234567890_20261226_164651.wav + private static final Pattern PHONE_PATTERN = Pattern.compile("Call_([+\\d]+)_\\d{8}_\\d{6}\\.wav"); + + public Recording(File file, Context context) { + this.file = file; + this.date = file.lastModified(); + this.size = file.length(); + + // Extract phone number from filename + extractPhoneNumber(); + + // Resolve contact name + if (context != null && phoneNumber != null) { + resolveContactName(context); + } + + // Parse duration from WAV header + parseDuration(); + } + + private void extractPhoneNumber() { + String filename = file.getName(); + Matcher matcher = PHONE_PATTERN.matcher(filename); + if (matcher.matches()) { + phoneNumber = matcher.group(1); + } else { + // Fallback: try to find any phone number pattern + Pattern fallbackPattern = Pattern.compile("([+]?\\d{10,15})"); + Matcher fallbackMatcher = fallbackPattern.matcher(filename); + if (fallbackMatcher.find()) { + phoneNumber = fallbackMatcher.group(1); + } + } + + // Default contact name to phone number + contactName = phoneNumber != null ? phoneNumber : "Unknown"; + } + + private void resolveContactName(Context context) { + if (phoneNumber == null || phoneNumber.isEmpty()) return; + + try { + ContentResolver resolver = context.getContentResolver(); + Uri uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)); + + try (Cursor cursor = resolver.query(uri, + new String[]{ContactsContract.PhoneLookup.DISPLAY_NAME}, + null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + String name = cursor.getString(0); + if (name != null && !name.isEmpty()) { + contactName = name; + } + } + } + } catch (Exception e) { + // Keep phone number as name if lookup fails + } + } + + private void parseDuration() { + if (!file.exists() || file.length() < 44) { + duration = 0; + return; + } + + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + // Read WAV header + byte[] header = new byte[44]; + raf.read(header); + + // Verify RIFF header + if (header[0] != 'R' || header[1] != 'I' || header[2] != 'F' || header[3] != 'F') { + duration = estimateDuration(); + return; + } + + // Get sample rate (bytes 24-27, little endian) + int sampleRate = (header[24] & 0xFF) | + ((header[25] & 0xFF) << 8) | + ((header[26] & 0xFF) << 16) | + ((header[27] & 0xFF) << 24); + + // Get byte rate (bytes 28-31, little endian) + int byteRate = (header[28] & 0xFF) | + ((header[29] & 0xFF) << 8) | + ((header[30] & 0xFF) << 16) | + ((header[31] & 0xFF) << 24); + + // Get data size (bytes 40-43, little endian) + long dataSize = (header[40] & 0xFF) | + ((header[41] & 0xFF) << 8) | + ((header[42] & 0xFF) << 16) | + ((long)(header[43] & 0xFF) << 24); + + if (byteRate > 0) { + duration = (dataSize * 1000L) / byteRate; + } else if (sampleRate > 0) { + // Assume 16-bit mono + duration = (dataSize * 1000L) / (sampleRate * 2); + } + + } catch (Exception e) { + duration = estimateDuration(); + } + } + + private long estimateDuration() { + // Estimate based on file size (assume 48kHz, 16-bit, mono = 96000 bytes/sec) + return (file.length() - 44) * 1000L / 96000; + } + + // Getters + + public File getFile() { + return file; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public String getContactName() { + return contactName; + } + + public long getDuration() { + return duration; + } + + public long getDate() { + return date; + } + + public long getSize() { + return size; + } + + public String getFormattedDuration() { + long seconds = duration / 1000; + long minutes = seconds / 60; + seconds = seconds % 60; + + if (minutes >= 60) { + long hours = minutes / 60; + minutes = minutes % 60; + return String.format("%d:%02d:%02d", hours, minutes, seconds); + } + return String.format("%d:%02d", minutes, seconds); + } + + public String getFormattedSize() { + if (size < 1024) return size + " B"; + if (size < 1024 * 1024) return String.format("%.1f KB", size / 1024.0); + return String.format("%.1f MB", size / (1024.0 * 1024.0)); + } + + /** + * Returns a grouping key for this recording (phone number or "Unknown") + */ + public String getGroupKey() { + return phoneNumber != null ? phoneNumber : "unknown"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Recording recording = (Recording) o; + return file.equals(recording.file); + } + + @Override + public int hashCode() { + return file.hashCode(); + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/model/SearchableFeature.java b/app/src/main/java/com/wmods/wppenhacer/model/SearchableFeature.java new file mode 100644 index 000000000..55aabb1ce --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/model/SearchableFeature.java @@ -0,0 +1,154 @@ +package com.wmods.wppenhacer.model; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * Data model representing a searchable feature in the WaEnhancer app. + * Each feature corresponds to a preference item that can be found via search. + */ +public class SearchableFeature { + + public enum Category { + GENERAL("General"), + GENERAL_HOME("General"), + GENERAL_HOMESCREEN("General"), + GENERAL_CONVERSATION("General"), + PRIVACY("Privacy"), + MEDIA("Media"), + CUSTOMIZATION("Customization"), + RECORDINGS("Recordings"), + HOME_ACTIONS("Home"); + + private final String displayName; + + Category(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + } + + public enum FragmentType { + GENERAL(0), + PRIVACY(1), + HOME(2), + MEDIA(3), + CUSTOMIZATION(4), + RECORDINGS(5), + ACTIVITY(99); + + private final int position; + + FragmentType(int position) { + this.position = position; + } + + public int getPosition() { + return position; + } + } + + private final String key; + private final String title; + private final String summary; + private final Category category; + private final FragmentType fragmentType; + private final String parentKey; // For nested preferences + private final List searchTags; + + public SearchableFeature(String key, String title, String summary, + Category category, FragmentType fragmentType) { + this(key, title, summary, category, fragmentType, null, new ArrayList<>()); + } + + public SearchableFeature(String key, String title, String summary, + Category category, FragmentType fragmentType, + String parentKey, List searchTags) { + this.key = key; + this.title = title; + this.summary = summary; + this.category = category; + this.fragmentType = fragmentType; + this.parentKey = parentKey; + this.searchTags = searchTags != null ? searchTags : new ArrayList<>(); + } + + public String getKey() { + return key; + } + + public String getTitle() { + return title; + } + + public String getSummary() { + return summary; + } + + public Category getCategory() { + return category; + } + + public FragmentType getFragmentType() { + return fragmentType; + } + + public String getParentKey() { + return parentKey; + } + + public List getSearchTags() { + return searchTags; + } + + /** + * Check if this feature matches the search query. + * Performs case-insensitive matching against title, summary, and tags. + */ + public boolean matches(String query) { + if (query == null || query.trim().isEmpty()) { + return false; + } + + String lowerQuery = query.toLowerCase().trim(); + + // Check title + if (title != null && title.toLowerCase().contains(lowerQuery)) { + return true; + } + + // Check summary + if (summary != null && summary.toLowerCase().contains(lowerQuery)) { + return true; + } + + // Check tags + for (String tag : searchTags) { + if (tag.toLowerCase().contains(lowerQuery)) { + return true; + } + } + + // Check category + if (category.getDisplayName().toLowerCase().contains(lowerQuery)) { + return true; + } + + return false; + } + + @NonNull + @Override + public String toString() { + return "SearchableFeature{" + + "key='" + key + '\'' + + ", title='" + title + '\'' + + ", category=" + category + + '}'; + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/provider/DeletedMessagesProvider.java b/app/src/main/java/com/wmods/wppenhacer/provider/DeletedMessagesProvider.java new file mode 100644 index 000000000..02417c6b3 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/provider/DeletedMessagesProvider.java @@ -0,0 +1,84 @@ +package com.wmods.wppenhacer.provider; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.wmods.wppenhacer.xposed.core.db.DelMessageStore; + +public class DeletedMessagesProvider extends ContentProvider { + + public static final String AUTHORITY = "com.wmods.wppenhacer.provider"; + public static final String PATH_DELETED_MESSAGES = "deleted_messages"; + public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_DELETED_MESSAGES); + + private static final int DELETED_MESSAGES = 1; + private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + static { + uriMatcher.addURI(AUTHORITY, PATH_DELETED_MESSAGES, DELETED_MESSAGES); + } + + private DelMessageStore dbHelper; + + @Override + public boolean onCreate() { + dbHelper = DelMessageStore.getInstance(getContext()); + return true; + } + + @Nullable + @Override + public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { + // Not needed for now, but good practice to implement basic query if UI needs it later + // or just return null if we only use it for insertion from Xposed + return null; + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + return null; + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + if (uriMatcher.match(uri) == DELETED_MESSAGES && values != null) { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + + // --- NEW: Propagate Contact Name to all messages in this chat --- + String chatJid = values.getAsString("chat_jid"); + String contactName = values.getAsString("contact_name"); + + if (chatJid != null && contactName != null && !contactName.isEmpty()) { + ContentValues updateValues = new ContentValues(); + updateValues.put("contact_name", contactName); + db.update(DelMessageStore.TABLE_DELETED_FOR_ME, updateValues, "chat_jid = ?", new String[]{chatJid}); + } + // ---------------------------------------------------------------- + + long id = db.insertWithOnConflict(DelMessageStore.TABLE_DELETED_FOR_ME, null, values, SQLiteDatabase.CONFLICT_REPLACE); + if (id > 0) { + return Uri.withAppendedPath(CONTENT_URI, String.valueOf(id)); + } + } + return null; + } + + @Override + public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } + + @Override + public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/ui/dialogs/AudioPlayerDialog.java b/app/src/main/java/com/wmods/wppenhacer/ui/dialogs/AudioPlayerDialog.java new file mode 100644 index 000000000..39c18de94 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/ui/dialogs/AudioPlayerDialog.java @@ -0,0 +1,170 @@ +package com.wmods.wppenhacer.ui.dialogs; + +import android.app.Dialog; +import android.content.Context; +import android.media.MediaPlayer; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageButton; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.wmods.wppenhacer.R; + +import java.io.File; +import java.io.IOException; + +/** + * A custom dialog for playing audio files in-app + */ +public class AudioPlayerDialog extends Dialog { + + private MediaPlayer mediaPlayer; + private Handler handler; + private Runnable updateRunnable; + + private SeekBar seekBar; + private ImageButton btnPlayPause; + private TextView tvCurrentTime; + private TextView tvTotalTime; + private TextView tvTitle; + + private boolean isPlaying = false; + + public AudioPlayerDialog(Context context, File audioFile) { + super(context, com.google.android.material.R.style.Theme_Material3_DayNight_Dialog); + + View view = LayoutInflater.from(context).inflate(R.layout.dialog_audio_player, null); + setContentView(view); + + // Set dialog window to have proper width + if (getWindow() != null) { + getWindow().setLayout( + (int)(context.getResources().getDisplayMetrics().widthPixels * 0.9), + android.view.ViewGroup.LayoutParams.WRAP_CONTENT + ); + getWindow().setBackgroundDrawableResource(android.R.color.transparent); + } + + // Initialize views + seekBar = view.findViewById(R.id.seekBar); + btnPlayPause = view.findViewById(R.id.btn_play_pause); + tvCurrentTime = view.findViewById(R.id.tv_current_time); + tvTotalTime = view.findViewById(R.id.tv_total_time); + tvTitle = view.findViewById(R.id.tv_title); + ImageButton btnClose = view.findViewById(R.id.btn_close); + + // Set title + tvTitle.setText(audioFile.getName()); + + // Initialize MediaPlayer + handler = new Handler(Looper.getMainLooper()); + + try { + mediaPlayer = new MediaPlayer(); + mediaPlayer.setDataSource(audioFile.getAbsolutePath()); + mediaPlayer.prepare(); + + int duration = mediaPlayer.getDuration(); + seekBar.setMax(duration); + tvTotalTime.setText(formatTime(duration)); + tvCurrentTime.setText(formatTime(0)); + + mediaPlayer.setOnCompletionListener(mp -> { + isPlaying = false; + btnPlayPause.setImageResource(R.drawable.ic_play); + seekBar.setProgress(0); + tvCurrentTime.setText(formatTime(0)); + mediaPlayer.seekTo(0); + }); + + } catch (IOException e) { + e.printStackTrace(); + dismiss(); + return; + } + + // Set up click listeners + btnPlayPause.setOnClickListener(v -> togglePlayPause()); + btnClose.setOnClickListener(v -> dismiss()); + + // Set up seekbar listener + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser && mediaPlayer != null) { + mediaPlayer.seekTo(progress); + tvCurrentTime.setText(formatTime(progress)); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) {} + + @Override + public void onStopTrackingTouch(SeekBar seekBar) {} + }); + + // Update runnable + updateRunnable = new Runnable() { + @Override + public void run() { + if (mediaPlayer != null && isPlaying) { + int currentPosition = mediaPlayer.getCurrentPosition(); + seekBar.setProgress(currentPosition); + tvCurrentTime.setText(formatTime(currentPosition)); + handler.postDelayed(this, 100); + } + } + }; + + // Start playing automatically + togglePlayPause(); + + // Handle dialog dismiss + setOnDismissListener(dialog -> releasePlayer()); + } + + private void togglePlayPause() { + if (mediaPlayer == null) return; + + if (isPlaying) { + mediaPlayer.pause(); + btnPlayPause.setImageResource(R.drawable.ic_play); + handler.removeCallbacks(updateRunnable); + } else { + mediaPlayer.start(); + btnPlayPause.setImageResource(R.drawable.ic_pause); + handler.post(updateRunnable); + } + isPlaying = !isPlaying; + } + + private void releasePlayer() { + if (handler != null) { + handler.removeCallbacks(updateRunnable); + } + if (mediaPlayer != null) { + if (mediaPlayer.isPlaying()) { + mediaPlayer.stop(); + } + mediaPlayer.release(); + mediaPlayer = null; + } + } + + private String formatTime(int millis) { + int seconds = millis / 1000; + int minutes = seconds / 60; + seconds = seconds % 60; + + if (minutes >= 60) { + int hours = minutes / 60; + minutes = minutes % 60; + return String.format("%d:%02d:%02d", hours, minutes, seconds); + } + return String.format("%d:%02d", minutes, seconds); + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/CustomizationFragment.java b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/CustomizationFragment.java index 29979a7e1..4583d19ca 100644 --- a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/CustomizationFragment.java +++ b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/CustomizationFragment.java @@ -2,6 +2,7 @@ import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.wmods.wppenhacer.R; @@ -19,5 +20,20 @@ public void onResume() { super.onResume(); setDisplayHomeAsUpEnabled(false); } + + @Override + public void onViewCreated(@NonNull android.view.View view, @Nullable android.os.Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // Handle scroll to preference from search + if (getActivity() != null && getActivity().getIntent() != null) { + String scrollToKey = getActivity().getIntent().getStringExtra("scroll_to_preference"); + if (scrollToKey != null) { + scrollToPreference(scrollToKey); + // Clear the intent extra + getActivity().getIntent().removeExtra("scroll_to_preference"); + } + } + } } diff --git a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/DeletedMessagesFragment.java b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/DeletedMessagesFragment.java new file mode 100644 index 000000000..0f3ede7f9 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/DeletedMessagesFragment.java @@ -0,0 +1,244 @@ +package com.wmods.wppenhacer.ui.fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.wmods.wppenhacer.R; +import com.wmods.wppenhacer.adapter.DeletedMessagesAdapter; +import com.wmods.wppenhacer.xposed.core.db.DelMessageStore; +import com.wmods.wppenhacer.xposed.core.db.DeletedMessage; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DeletedMessagesFragment extends Fragment implements DeletedMessagesAdapter.OnItemClickListener { + + private RecyclerView recyclerView; + private View emptyView; + private DeletedMessagesAdapter adapter; + private DelMessageStore delMessageStore; + + private boolean isGroup; + + public static DeletedMessagesFragment newInstance(boolean isGroup) { + DeletedMessagesFragment fragment = new DeletedMessagesFragment(); + Bundle args = new Bundle(); + args.putBoolean("is_group", isGroup); + fragment.setArguments(args); + return fragment; + } + + private int currentFilter = R.id.filter_all; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + isGroup = getArguments().getBoolean("is_group"); + } + setHasOptionsMenu(true); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_deleted_messages, container, false); + } + + @Override + public void onCreateOptionsMenu(@NonNull android.view.Menu menu, @NonNull android.view.MenuInflater inflater) { + inflater.inflate(R.menu.menu_deleted_messages, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(@NonNull android.view.MenuItem item) { + if (item.getItemId() == R.id.filter_all || item.getItemId() == R.id.filter_whatsapp + || item.getItemId() == R.id.filter_whatsapp_business) { + currentFilter = item.getItemId(); + item.setChecked(true); + loadMessages(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + recyclerView = view.findViewById(R.id.recyclerView); + emptyView = view.findViewById(R.id.empty_view); + + delMessageStore = DelMessageStore.getInstance(requireContext()); + adapter = new DeletedMessagesAdapter(this); + + recyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + recyclerView.setAdapter(adapter); + + // Check for updates to names if permission is already granted, but don't ask + if (requireContext().checkSelfPermission( + android.Manifest.permission.READ_CONTACTS) == android.content.pm.PackageManager.PERMISSION_GRANTED) { + adapter.notifyDataSetChanged(); + } + + loadMessages(); + } + + @Override + public void onResume() { + super.onResume(); + if (requireContext().checkSelfPermission( + android.Manifest.permission.READ_CONTACTS) == android.content.pm.PackageManager.PERMISSION_GRANTED) { + if (adapter != null) + adapter.notifyDataSetChanged(); + } + loadMessages(); + } + + private void loadMessages() { + new Thread(() -> { + List allMessages = delMessageStore.getDeletedMessages(isGroup); // Fetch ALL first, then + // filter + Map latestMessagesMap = new HashMap<>(); + + for (DeletedMessage msg : allMessages) { + // Apps Filter Logic + boolean matchesFilter = true; + if (currentFilter == R.id.filter_whatsapp) { + matchesFilter = "com.whatsapp".equals(msg.getPackageName()); + } else if (currentFilter == R.id.filter_whatsapp_business) { + matchesFilter = "com.whatsapp.w4b".equals(msg.getPackageName()); + } + + if (!matchesFilter) + continue; + + if (!latestMessagesMap.containsKey(msg.getChatJid())) { + latestMessagesMap.put(msg.getChatJid(), msg); + } else { + if (msg.getTimestamp() > latestMessagesMap.get(msg.getChatJid()).getTimestamp()) { + latestMessagesMap.put(msg.getChatJid(), msg); + } + } + } + + List uniqueChats = new ArrayList<>(latestMessagesMap.values()); + uniqueChats.sort((m1, m2) -> Long.compare(m2.getTimestamp(), m1.getTimestamp())); + + requireActivity().runOnUiThread(() -> { + if (uniqueChats.isEmpty()) { + emptyView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.GONE); + } else { + emptyView.setVisibility(View.GONE); + recyclerView.setVisibility(View.VISIBLE); + adapter.setMessages(uniqueChats); + } + }); + }).start(); + } + + private androidx.appcompat.view.ActionMode actionMode; + private final androidx.appcompat.view.ActionMode.Callback actionModeCallback = new androidx.appcompat.view.ActionMode.Callback() { + @Override + public boolean onCreateActionMode(androidx.appcompat.view.ActionMode mode, android.view.Menu menu) { + mode.getMenuInflater().inflate(R.menu.menu_context_delete, menu); // Need to create this menu + return true; + } + + @Override + public boolean onPrepareActionMode(androidx.appcompat.view.ActionMode mode, android.view.Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(androidx.appcompat.view.ActionMode mode, android.view.MenuItem item) { + if (item.getItemId() == R.id.action_delete) { + deleteSelectedChats(); + mode.finish(); + return true; + } + return false; + } + + @Override + public void onDestroyActionMode(androidx.appcompat.view.ActionMode mode) { + adapter.clearSelection(); + actionMode = null; + } + }; + + private void deleteSelectedChats() { + List selected = adapter.getSelectedItems(); + if (selected.isEmpty()) + return; + + new androidx.appcompat.app.AlertDialog.Builder(requireContext()) + .setTitle("Delete Chats?") + .setMessage("Are you sure you want to delete " + selected.size() + " chat(s)? This cannot be undone.") + .setPositiveButton("Delete", (dialog, which) -> { + new Thread(() -> { + for (String jid : selected) { + delMessageStore.deleteMessagesByChat(jid); + } + requireActivity().runOnUiThread(() -> { + loadMessages(); + android.widget.Toast + .makeText(requireContext(), "Chats deleted", android.widget.Toast.LENGTH_SHORT) + .show(); + }); + }).start(); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + @Override + public void onItemClick(DeletedMessage message) { + if (actionMode != null) { + toggleSelection(message.getChatJid()); + } else { + android.content.Intent intent = new android.content.Intent(requireContext(), + com.wmods.wppenhacer.activities.MessageListActivity.class); + intent.putExtra("chat_jid", message.getChatJid()); + startActivity(intent); + } + } + + @Override + public boolean onItemLongClick(DeletedMessage message) { + if (actionMode == null) { + actionMode = ((androidx.appcompat.app.AppCompatActivity) requireActivity()) + .startSupportActionMode(actionModeCallback); + } + toggleSelection(message.getChatJid()); + return true; + } + + private void toggleSelection(String chatJid) { + adapter.toggleSelection(chatJid); + int count = adapter.getSelectedCount(); + if (count == 0) { + actionMode.finish(); + } else { + actionMode.setTitle(count + " selected"); + } + } + + @Override + public void onRestoreClick(DeletedMessage message) { + // Not used + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/GeneralFragment.java b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/GeneralFragment.java index 7e29f0f48..3590cf8e8 100644 --- a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/GeneralFragment.java +++ b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/GeneralFragment.java @@ -21,6 +21,22 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c if (savedInstanceState == null) { getChildFragmentManager().beginTransaction().add(R.id.frag_container, new GeneralPreferenceFragment()).commitNow(); } + + // Handle scroll to preference from search + if (getActivity() != null && getActivity().getIntent() != null) { + String scrollToKey = getActivity().getIntent().getStringExtra("scroll_to_preference"); + if (scrollToKey != null) { + getView().postDelayed(() -> { + BasePreferenceFragment activeFragment = (BasePreferenceFragment) getChildFragmentManager().findFragmentById(R.id.frag_container); + if (activeFragment != null) { + activeFragment.scrollToPreference(scrollToKey); + } + }, 300); + // Clear the intent extra + getActivity().getIntent().removeExtra("scroll_to_preference"); + } + } + return root; } diff --git a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/MediaFragment.java b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/MediaFragment.java index ffde3e6af..fa901d6f0 100644 --- a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/MediaFragment.java +++ b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/MediaFragment.java @@ -1,10 +1,14 @@ package com.wmods.wppenhacer.ui.fragments; +import android.content.Intent; import android.os.Bundle; +import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.wmods.wppenhacer.R; +import com.wmods.wppenhacer.activities.CallRecordingSettingsActivity; import com.wmods.wppenhacer.ui.fragments.base.BasePreferenceFragment; public class MediaFragment extends BasePreferenceFragment { @@ -20,10 +24,24 @@ public void onResume() { setDisplayHomeAsUpEnabled(false); } - @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { super.onCreatePreferences(savedInstanceState, rootKey); setPreferencesFromResource(R.xml.fragment_media, rootKey); + + // Call Recording Settings preference + var callRecordingSettings = findPreference("call_recording_settings"); + if (callRecordingSettings != null) { + callRecordingSettings.setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(requireContext(), CallRecordingSettingsActivity.class); + startActivity(intent); + return true; + }); + } + + var videoCallScreenRec = findPreference("video_call_screen_rec"); + if (videoCallScreenRec != null) { + videoCallScreenRec.setEnabled(false); + } } } diff --git a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/PrivacyFragment.java b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/PrivacyFragment.java index 174d68498..7f0d00cc8 100644 --- a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/PrivacyFragment.java +++ b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/PrivacyFragment.java @@ -5,7 +5,9 @@ import android.app.Activity; import android.content.Intent; import android.os.Bundle; +import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.wmods.wppenhacer.R; @@ -20,6 +22,11 @@ public class PrivacyFragment extends BasePreferenceFragment { public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { super.onCreatePreferences(savedInstanceState, rootKey); setPreferencesFromResource(R.xml.fragment_privacy, rootKey); + + findPreference("open_deleted_messages").setOnPreferenceClickListener(preference -> { + startActivity(new Intent(requireContext(), com.wmods.wppenhacer.activities.DeletedMessagesActivity.class)); + return true; + }); } @Override @@ -45,5 +52,20 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { } } } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + // Handle scroll to preference from search + if (getActivity() != null && getActivity().getIntent() != null) { + String scrollToKey = getActivity().getIntent().getStringExtra("scroll_to_preference"); + if (scrollToKey != null) { + scrollToPreference(scrollToKey); + // Clear the intent extra + getActivity().getIntent().removeExtra("scroll_to_preference"); + } + } + } } diff --git a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/RecordingsFragment.java b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/RecordingsFragment.java new file mode 100644 index 000000000..791e2d4e8 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/RecordingsFragment.java @@ -0,0 +1,297 @@ +package com.wmods.wppenhacer.ui.fragments; + +import android.app.AlertDialog; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.PopupMenu; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.FileProvider; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.google.android.material.chip.Chip; +import com.wmods.wppenhacer.R; +import com.wmods.wppenhacer.adapter.RecordingsAdapter; +import com.wmods.wppenhacer.databinding.FragmentRecordingsBinding; +import com.wmods.wppenhacer.model.Recording; +import com.wmods.wppenhacer.ui.dialogs.AudioPlayerDialog; + +import java.io.File; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RecordingsFragment extends Fragment implements RecordingsAdapter.OnRecordingActionListener { + + private FragmentRecordingsBinding binding; + private RecordingsAdapter adapter; + private List allRecordings = new ArrayList<>(); + private List baseDirs = new ArrayList<>(); + private boolean isGroupByContact = false; + private int currentSortType = 1; // 1=date, 2=name, 3=duration, 4=contact + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + binding = FragmentRecordingsBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + adapter = new RecordingsAdapter(this); + binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + binding.recyclerView.setAdapter(adapter); + + // Set up selection change listener + adapter.setSelectionChangeListener(count -> { + if (count > 0) { + binding.selectionBar.setVisibility(View.VISIBLE); + binding.tvSelectionCount.setText(getString(R.string.selected_count, count)); + } else { + binding.selectionBar.setVisibility(View.GONE); + } + }); + + // Initialize base directories + initializeBaseDirs(); + + // View mode toggle + binding.chipList.setOnClickListener(v -> { + isGroupByContact = false; + loadRecordings(); + }); + + binding.chipGroupByContact.setOnClickListener(v -> { + isGroupByContact = true; + loadRecordings(); + }); + + // Selection bar buttons + binding.btnCloseSelection.setOnClickListener(v -> adapter.clearSelection()); + binding.btnSelectAll.setOnClickListener(v -> adapter.selectAll()); + binding.btnShareSelected.setOnClickListener(v -> shareSelectedRecordings()); + binding.btnDeleteSelected.setOnClickListener(v -> deleteSelectedRecordings()); + + // Sort FAB + binding.fabSort.setOnClickListener(v -> showSortMenu()); + + loadRecordings(); + } + + private void initializeBaseDirs() { + var prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + String path = prefs.getString("call_recording_path", null); + + baseDirs.clear(); + + // 1. Root folder if MANAGE_EXTERNAL_STORAGE + if (Environment.isExternalStorageManager()) { + baseDirs.add(new File(Environment.getExternalStorageDirectory(), "WA Call Recordings")); + } + + // 2. Settings path + if (path != null && !path.isEmpty()) { + baseDirs.add(new File(path, "WA Call Recordings")); + } + + // 3. WhatsApp app external files + baseDirs.add(new File("/sdcard/Android/data/com.whatsapp/files/Recordings")); + baseDirs.add(new File("/sdcard/Android/data/com.whatsapp.w4b/files/Recordings")); + + // 4. Legacy fallback + baseDirs.add(new File(Environment.getExternalStorageDirectory(), "Music/WaEnhancer/Recordings")); + } + + private void loadRecordings() { + allRecordings.clear(); + + for (File baseDir : baseDirs) { + if (baseDir.exists() && baseDir.isDirectory()) { + traverseDirectory(baseDir); + } + } + + if (allRecordings.isEmpty()) { + binding.emptyView.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + } else { + binding.emptyView.setVisibility(View.GONE); + binding.recyclerView.setVisibility(View.VISIBLE); + + // Apply sorting + applySort(); + + if (isGroupByContact) { + // For group by contact, we'll navigate to ContactRecordingsActivity when a contact is clicked + // For now, just show sorted list (full group UI needs ContactRecordingsActivity) + adapter.setRecordings(allRecordings); + } else { + adapter.setRecordings(allRecordings); + } + } + } + + private void traverseDirectory(File dir) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + traverseDirectory(file); + } else { + String name = file.getName().toLowerCase(); + if (name.endsWith(".wav") || name.endsWith(".mp3") || name.endsWith(".aac") || name.endsWith(".m4a")) { + allRecordings.add(new Recording(file, requireContext())); + } + } + } + } + } + + private void applySort() { + switch (currentSortType) { + case 1 -> allRecordings.sort((r1, r2) -> Long.compare(r2.getDate(), r1.getDate())); // Date desc + case 2 -> allRecordings.sort(Comparator.comparing(Recording::getContactName)); // Name + case 3 -> allRecordings.sort((r1, r2) -> Long.compare(r2.getDuration(), r1.getDuration())); // Duration desc + case 4 -> allRecordings.sort(Comparator.comparing(Recording::getContactName) + .thenComparing((r1, r2) -> Long.compare(r2.getDate(), r1.getDate()))); // Contact then date + } + } + + private void showSortMenu() { + PopupMenu popup = new PopupMenu(requireContext(), binding.fabSort); + popup.getMenu().add(0, 1, 0, R.string.sort_date); + popup.getMenu().add(0, 2, 0, R.string.sort_name); + popup.getMenu().add(0, 3, 0, R.string.sort_duration); + popup.getMenu().add(0, 4, 0, R.string.sort_contact); + + popup.setOnMenuItemClickListener(item -> { + currentSortType = item.getItemId(); + applySort(); + adapter.setRecordings(allRecordings); + return true; + }); + popup.show(); + } + + // RecordingsAdapter.OnRecordingActionListener implementation + + @Override + public void onPlay(Recording recording) { + // Use in-app audio player + AudioPlayerDialog dialog = new AudioPlayerDialog(requireContext(), recording.getFile()); + dialog.show(); + } + + @Override + public void onShare(Recording recording) { + shareRecording(recording.getFile()); + } + + @Override + public void onDelete(Recording recording) { + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.delete_confirmation) + .setMessage(recording.getFile().getName()) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + if (recording.getFile().delete()) { + loadRecordings(); + } else { + Toast.makeText(requireContext(), "Failed to delete", Toast.LENGTH_SHORT).show(); + } + }) + .setNegativeButton(android.R.string.no, null) + .show(); + } + + @Override + public void onLongPress(Recording recording, int position) { + // Enter selection mode + adapter.setSelectionMode(true); + adapter.toggleSelection(position); + } + + private void shareRecording(File file) { + try { + Uri uri = FileProvider.getUriForFile(requireContext(), + requireContext().getPackageName() + ".fileprovider", file); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("audio/*"); + intent.putExtra(Intent.EXTRA_STREAM, uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(Intent.createChooser(intent, getString(R.string.share_recording))); + } catch (Exception e) { + Toast.makeText(requireContext(), "Error sharing: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + + private void shareSelectedRecordings() { + List selected = adapter.getSelectedRecordings(); + if (selected.isEmpty()) return; + + if (selected.size() == 1) { + shareRecording(selected.get(0).getFile()); + adapter.clearSelection(); + return; + } + + ArrayList uris = new ArrayList<>(); + for (Recording rec : selected) { + try { + Uri uri = FileProvider.getUriForFile(requireContext(), + requireContext().getPackageName() + ".fileprovider", rec.getFile()); + uris.add(uri); + } catch (Exception ignored) {} + } + + if (!uris.isEmpty()) { + Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); + intent.setType("audio/*"); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(Intent.createChooser(intent, getString(R.string.share_recordings))); + } + adapter.clearSelection(); + } + + private void deleteSelectedRecordings() { + List selected = adapter.getSelectedRecordings(); + if (selected.isEmpty()) return; + + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.delete_confirmation) + .setMessage(getString(R.string.delete_multiple_confirmation, selected.size())) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + int deleted = 0; + for (Recording rec : selected) { + if (rec.getFile().delete()) { + deleted++; + } + } + Toast.makeText(requireContext(), "Deleted " + deleted + " recordings", Toast.LENGTH_SHORT).show(); + adapter.clearSelection(); + loadRecordings(); + }) + .setNegativeButton(android.R.string.no, null) + .show(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/base/BasePreferenceFragment.java b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/base/BasePreferenceFragment.java index 775966f70..4ec3674c2 100644 --- a/app/src/main/java/com/wmods/wppenhacer/ui/fragments/base/BasePreferenceFragment.java +++ b/app/src/main/java/com/wmods/wppenhacer/ui/fragments/base/BasePreferenceFragment.java @@ -28,7 +28,8 @@ import rikka.material.preference.MaterialSwitchPreference; -public abstract class BasePreferenceFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { +public abstract class BasePreferenceFragment extends PreferenceFragmentCompat + implements SharedPreferences.OnSharedPreferenceChangeListener { protected SharedPreferences mPrefs; @Override @@ -49,7 +50,8 @@ public void handleOnBackPressed() { @NonNull @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { chanceStates(null); monitorPreference(); return super.onCreateView(inflater, container, savedInstanceState); @@ -93,7 +95,10 @@ private void monitorPreference() { private boolean checkStoragePermission(Object newValue) { if (newValue instanceof Boolean && (Boolean) newValue) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) || (Build.VERSION.SDK_INT < Build.VERSION_CODES.R && ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)) { + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) + || (Build.VERSION.SDK_INT < Build.VERSION_CODES.R + && ContextCompat.checkSelfPermission(requireContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)) { App.showRequestStoragePermission(requireActivity()); return false; } @@ -159,14 +164,12 @@ private void chanceStates(String key) { setPreferenceState("showonlinetext", !freezelastseen); setPreferenceState("dotonline", !freezelastseen); - var separategroups = mPrefs.getBoolean("separategroups", false); setPreferenceState("filtergroups", !separategroups); var filtergroups = mPrefs.getBoolean("filtergroups", false); setPreferenceState("separategroups", !filtergroups); - var callBlockContacts = findPreference("call_block_contacts"); var callWhiteContacts = findPreference("call_white_contacts"); if (callBlockContacts != null && callWhiteContacts != null) { @@ -190,10 +193,182 @@ private void chanceStates(String key) { } public void setDisplayHomeAsUpEnabled(boolean enabled) { - if (getActivity() == null) return; + if (getActivity() == null) + return; var actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(enabled); } } + + /** + * Scroll to a specific preference by key. + * This is called when navigating from search results. + */ + public void scrollToPreference(String preferenceKey) { + if (preferenceKey == null) + return; + + // Small delay to ensure preference screen is fully loaded + getView().postDelayed(() -> { + var preference = findPreference(preferenceKey); + if (preference != null) { + scrollToPreference(preference); + + // Highlight the preference for visibility + highlightPreference(preference); + } + }, 100); + } + + /** + * Highlight a preference with a temporary background color. + */ + private void highlightPreference(androidx.preference.Preference preference) { + // Wait longer to ensure RecyclerView has laid out the views after scrolling + getView().postDelayed(() -> { + androidx.recyclerview.widget.RecyclerView recyclerView = getListView(); + if (recyclerView == null || preference == null || preference.getKey() == null) + return; + + // Find the preference view by iterating through visible items + String targetKey = preference.getKey(); + boolean found = false; + + for (int i = 0; i < recyclerView.getChildCount(); i++) { + android.view.View child = recyclerView.getChildAt(i); + androidx.recyclerview.widget.RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(child); + + if (holder instanceof androidx.preference.PreferenceViewHolder) { + androidx.preference.PreferenceViewHolder prefHolder = (androidx.preference.PreferenceViewHolder) holder; + + // Try to match by adapter position + int position = prefHolder.getBindingAdapterPosition(); + if (position != androidx.recyclerview.widget.RecyclerView.NO_POSITION) { + try { + // Get all preferences recursively + androidx.preference.Preference pref = findPreferenceAtPosition(getPreferenceScreen(), + position); + if (pref != null && pref.getKey() != null && pref.getKey().equals(targetKey)) { + animateHighlight(prefHolder.itemView); + found = true; + break; + } + } catch (Exception e) { + // Continue searching + } + } + } + } + + // If not found, try a second time after a longer delay + if (!found) { + getView().postDelayed(() -> tryHighlightAgain(targetKey), 500); + } + }, 500); + } + + private void tryHighlightAgain(String targetKey) { + androidx.recyclerview.widget.RecyclerView recyclerView = getListView(); + if (recyclerView == null) + return; + + for (int i = 0; i < recyclerView.getChildCount(); i++) { + android.view.View child = recyclerView.getChildAt(i); + + // Simple approach: check all text views in the item for matching preference + if (child instanceof android.view.ViewGroup) { + android.view.ViewGroup group = (android.view.ViewGroup) child; + // Get the preference at this position and check key + androidx.recyclerview.widget.RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(child); + if (holder instanceof androidx.preference.PreferenceViewHolder) { + int position = holder.getBindingAdapterPosition(); + if (position != androidx.recyclerview.widget.RecyclerView.NO_POSITION) { + androidx.preference.Preference pref = findPreferenceAtPosition(getPreferenceScreen(), position); + if (pref != null && pref.getKey() != null && pref.getKey().equals(targetKey)) { + animateHighlight(child); + break; + } + } + } + } + } + } + + private androidx.preference.Preference findPreferenceAtPosition(androidx.preference.PreferenceGroup group, + int targetPosition) { + if (group == null) + return null; + + int currentPosition = 0; + for (int i = 0; i < group.getPreferenceCount(); i++) { + androidx.preference.Preference pref = group.getPreference(i); + if (pref == null) + continue; + + if (currentPosition == targetPosition) { + return pref; + } + currentPosition++; + + // Recursively check groups + if (pref instanceof androidx.preference.PreferenceGroup) { + androidx.preference.PreferenceGroup subGroup = (androidx.preference.PreferenceGroup) pref; + int subCount = countPreferences(subGroup); + if (targetPosition < currentPosition + subCount) { + return findPreferenceAtPosition(subGroup, targetPosition - currentPosition); + } + currentPosition += subCount; + } + } + return null; + } + + private int countPreferences(androidx.preference.PreferenceGroup group) { + int count = 0; + for (int i = 0; i < group.getPreferenceCount(); i++) { + androidx.preference.Preference pref = group.getPreference(i); + if (pref instanceof androidx.preference.PreferenceGroup) { + count += countPreferences((androidx.preference.PreferenceGroup) pref); + } else { + count++; + } + } + return count; + } + + /** + * Animate a highlight effect on the view. + */ + private void animateHighlight(android.view.View view) { + if (view == null || getContext() == null) + return; + + // Get primary color using android attribute + android.util.TypedValue typedValue = new android.util.TypedValue(); + view.getContext().getTheme().resolveAttribute(android.R.attr.colorPrimary, typedValue, true); + int primaryColor = typedValue.data; + + // Make it 20% opacity (dim) + int highlightColor = android.graphics.Color.argb( + 51, // ~20% of 255 + android.graphics.Color.red(primaryColor), + android.graphics.Color.green(primaryColor), + android.graphics.Color.blue(primaryColor)); + + // Save original background + android.graphics.drawable.Drawable originalBackground = view.getBackground(); + + // Set highlight background + view.setBackgroundColor(highlightColor); + + // Fade out after 1.5 seconds + view.postDelayed(() -> { + if (originalBackground != null) { + view.setBackground(originalBackground); + } else { + view.setBackgroundColor(android.graphics.Color.TRANSPARENT); + } + }, 1500); + } } diff --git a/app/src/main/java/com/wmods/wppenhacer/utils/ContactHelper.java b/app/src/main/java/com/wmods/wppenhacer/utils/ContactHelper.java new file mode 100644 index 000000000..ceb1dc9c1 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/utils/ContactHelper.java @@ -0,0 +1,33 @@ +package com.wmods.wppenhacer.utils; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; + +public class ContactHelper { + + public static String getContactName(Context context, String jid) { + if (jid == null) return null; + if (context.checkSelfPermission(android.Manifest.permission.READ_CONTACTS) != android.content.pm.PackageManager.PERMISSION_GRANTED) { + return null; + } + + String phoneNumber = jid.replace("@s.whatsapp.net", "").replace("@g.us", ""); + if (phoneNumber.contains("@")) phoneNumber = phoneNumber.split("@")[0]; + + try { + Uri uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber)); + String[] projection = new String[]{ContactsContract.PhoneLookup.DISPLAY_NAME}; + + try (Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getString(0); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/utils/FeatureCatalog.java b/app/src/main/java/com/wmods/wppenhacer/utils/FeatureCatalog.java new file mode 100644 index 000000000..a7d09b6bb --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/utils/FeatureCatalog.java @@ -0,0 +1,1015 @@ +package com.wmods.wppenhacer.utils; + +import android.content.Context; + +import com.wmods.wppenhacer.R; +import com.wmods.wppenhacer.model.SearchableFeature; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Central catalog of all searchable features in the WaEnhancer app. + * This class builds and maintains a complete index of all features from + * preference XMLs. + */ +public class FeatureCatalog { + + private static List features; + + /** + * Initialize and return the complete feature catalog. + */ + public static List getAllFeatures(Context context) { + if (features == null) { + features = buildFeatureCatalog(context); + } + return features; + } + + /** + * Search features by query string. + * Returns all features that match the search query. + */ + public static List search(Context context, String query) { + if (query == null || query.trim().isEmpty()) { + return new ArrayList<>(); + } + + return getAllFeatures(context).stream() + .filter(feature -> feature.matches(query)) + .collect(Collectors.toList()); + } + + /** + * Build the complete feature catalog from all preference XMLs. + */ + private static List buildFeatureCatalog(Context context) { + List catalog = new ArrayList<>(); + + // GENERAL FRAGMENT - General sub-preferences + catalog.add(new SearchableFeature("thememode", + context.getString(R.string.theme_mode), + context.getString(R.string.theme_mode_sum), + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("dark", "light", "theme"))); + + catalog.add(new SearchableFeature("update_check", + context.getString(R.string.update_check), + context.getString(R.string.update_check_sum), + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("update", "check", "automatic"))); + + catalog.add(new SearchableFeature("disable_expiration", + context.getString(R.string.disable_whatsapp_expiration), + context.getString(R.string.disable_whatsapp_expiration_sum), + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("expiration", "version"))); + + catalog.add(new SearchableFeature("force_restore_backup_feature", + context.getString(R.string.force_restore_backup), + context.getString(R.string.force_restore_backup_summary), + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("backup", "restore", "force"))); + + catalog.add(new SearchableFeature("disable_ads", + context.getString(R.string.disable_ads), + context.getString(R.string.disable_ads_sum), + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("ads", "advertising", "block"))); + + catalog.add(new SearchableFeature("lite_mode", + context.getString(R.string.lite_mode), + context.getString(R.string.lite_mode_sum), + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("lite", "performance", "battery"))); + + catalog.add(new SearchableFeature("force_english", + context.getString(R.string.force_english), + null, + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("english", "language"))); + + catalog.add(new SearchableFeature("enablelogs", + context.getString(R.string.verbose_logs), + null, + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("logs", "debug", "verbose"))); + + catalog.add(new SearchableFeature("bypass_version_check", + context.getString(R.string.disable_version_check), + context.getString(R.string.disable_version_check_sum), + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("version", "check", "bypass"))); + + catalog.add(new SearchableFeature("bootloader_spoofer", + context.getString(R.string.bootloader_spoofer), + context.getString(R.string.bootloader_spoofer_sum), + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("bootloader", "spoofer", "ban"))); + + catalog.add(new SearchableFeature("ampm", + context.getString(R.string.ampm), + null, + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("time", "12", "hour", "format"))); + + catalog.add(new SearchableFeature("segundos", + context.getString(R.string.segundosnahora), + context.getString(R.string.segundosnahora_sum), + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("seconds", "timestamp", "time"))); + + catalog.add(new SearchableFeature("secondstotime", + context.getString(R.string.textonahora), + context.getString(R.string.textonahora_sum), + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("text", "timestamp", "custom"))); + + catalog.add(new SearchableFeature("tasker", + context.getString(R.string.enable_tasker_automation), + context.getString(R.string.enable_tasker_automation_sum), + SearchableFeature.Category.GENERAL_HOME, + SearchableFeature.FragmentType.GENERAL, + "general_home", + Arrays.asList("tasker", "automation", "intent"))); + + // GENERAL FRAGMENT - Homescreen sub-preferences + catalog.add(new SearchableFeature("buttonaction", + context.getString(R.string.show_menu_buttons_as_icons), + context.getString(R.string.show_menu_buttons_as_icons_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("menu", "icons", "buttons"))); + + catalog.add(new SearchableFeature("shownamehome", + context.getString(R.string.showname), + context.getString(R.string.showname_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("name", "profile", "title"))); + + catalog.add(new SearchableFeature("showbiohome", + context.getString(R.string.showbio), + context.getString(R.string.showbio_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("bio", "status", "toolbar"))); + + catalog.add(new SearchableFeature("show_dndmode", + context.getString(R.string.show_dnd_button), + context.getString(R.string.show_dnd_button_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("dnd", "do not disturb", "button"))); + + catalog.add(new SearchableFeature("newchat", + context.getString(R.string.enable_new_chat_button), + context.getString(R.string.enable_new_chat_button_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("new", "chat", "button"))); + + catalog.add(new SearchableFeature("restartbutton", + context.getString(R.string.enable_restart_button), + context.getString(R.string.enable_restart_button_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("restart", "reboot", "button"))); + + catalog.add(new SearchableFeature("open_wae", + context.getString(R.string.enable_wa_enhancer_button), + context.getString(R.string.enable_wa_enhancer_button_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("wa enhancer", "open", "button"))); + + catalog.add(new SearchableFeature("separategroups", + context.getString(R.string.separate_groups), + context.getString(R.string.separate_groups_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("separate", "groups", "filter"))); + + catalog.add(new SearchableFeature("filtergroups", + context.getString(R.string.new_ui_group_filter), + context.getString(R.string.new_ui_group_filter_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("filter", "groups", "ui"))); + + catalog.add(new SearchableFeature("dotonline", + context.getString(R.string.show_online_dot_in_conversation_list), + context.getString(R.string.show_online_dot_in_conversation_list_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("online", "dot", "green"))); + + catalog.add(new SearchableFeature("showonlinetext", + context.getString(R.string.show_online_last_seen_in_conversation_list), + context.getString(R.string.show_online_last_seen_in_conversation_list_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("online", "last seen", "text"))); + + catalog.add(new SearchableFeature("filterseen", + context.getString(R.string.enable_filter_chats), + context.getString(R.string.enable_filter_chats_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("filter", "chats", "unseen"))); + + catalog.add(new SearchableFeature("metaai", + context.getString(R.string.disable_metaai), + context.getString(R.string.disable_metaai_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("meta", "ai", "disable"))); + + catalog.add(new SearchableFeature("chatfilter", + context.getString(R.string.novofiltro), + context.getString(R.string.novofiltro_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("search", "filter", "icon", "bar"))); + + catalog.add(new SearchableFeature("disable_profile_status", + context.getString(R.string.disable_status_in_the_profile_photo), + context.getString(R.string.disable_status_in_the_profile_photo_sum), + SearchableFeature.Category.GENERAL_HOMESCREEN, + SearchableFeature.FragmentType.GENERAL, + "homescreen", + Arrays.asList("status", "profile", "photo", "circle"))); + + // GENERAL FRAGMENT - Conversation sub-preferences + catalog.add(new SearchableFeature("showonline", + context.getString(R.string.show_toast_on_contact_online), + context.getString(R.string.show_toast_on_contact_online_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("toast", "online", "notification"))); + + catalog.add(new SearchableFeature("toastdeleted", + context.getString(R.string.toast_on_delete), + context.getString(R.string.toast_on_delete_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("toast", "deleted", "notification"))); + + catalog.add(new SearchableFeature("toast_viewed_message", + context.getString(R.string.toast_on_viewed_message), + context.getString(R.string.toast_on_viewed_message_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("toast", "viewed", "read", "notification"))); + + catalog.add(new SearchableFeature("antirevoke", + context.getString(R.string.antirevoke), + context.getString(R.string.antirevoke_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("anti", "revoke", "delete", "deleted"))); + + catalog.add(new SearchableFeature("antirevokestatus", + context.getString(R.string.antirevokestatus), + context.getString(R.string.antirevokestatus_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("anti", "delete", "status"))); + + catalog.add(new SearchableFeature("antidisappearing", + context.getString(R.string.antidisappearing), + context.getString(R.string.antidisappearing_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("anti", "disappearing", "temporary", "messages"))); + + catalog.add(new SearchableFeature("broadcast_tag", + context.getString(R.string.show_chat_broadcast_icon), + context.getString(R.string.show_chat_broadcast_icon_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("broadcast", "icon", "tag"))); + + catalog.add(new SearchableFeature("pinnedlimit", + context.getString(R.string.disable_pinned_limit), + context.getString(R.string.disable_pinned_limit_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("pinned", "limit", "chats"))); + + catalog.add(new SearchableFeature("removeforwardlimit", + context.getString(R.string.removeforwardlimit), + context.getString(R.string.removeforwardlimit_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("forward", "limit", "remove"))); + + catalog.add(new SearchableFeature("hidetag", + context.getString(R.string.hidetag), + context.getString(R.string.hidetag_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("forwarded", "tag", "hide"))); + + catalog.add(new SearchableFeature("revokeallmessages", + context.getString(R.string.delete_for_everyone_all_messages), + context.getString(R.string.delete_for_everyone_all_messages_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("delete", "everyone", "limit", "revoke"))); + + catalog.add(new SearchableFeature("removeseemore", + context.getString(R.string.remove_see_more_button), + context.getString(R.string.remove_see_more_button_), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("see more", "button", "remove"))); + + catalog.add(new SearchableFeature("antieditmessages", + context.getString(R.string.show_edited_message_history), + context.getString(R.string.show_edited_message_history_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("edited", "history", "message"))); + + catalog.add(new SearchableFeature("alertsticker", + context.getString(R.string.enable_confirmation_to_send_sticker), + context.getString(R.string.enable_confirmation_to_send_sticker_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("sticker", "confirmation", "alert"))); + + catalog.add(new SearchableFeature("calltype", + context.getString(R.string.selection_of_call_type), + context.getString(R.string.selection_of_call_type_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("call", "type", "selection", "phone"))); + + catalog.add(new SearchableFeature("disable_defemojis", + context.getString(R.string.disable_default_emojis), + context.getString(R.string.disable_default_emojis_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("emoji", "default", "disable"))); + + catalog.add(new SearchableFeature("stamp_copied_message", + context.getString(R.string.stamp_copied_messages), + context.getString(R.string.stamp_copied_messages_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("copy", "stamp", "copied", "messages"))); + + catalog.add(new SearchableFeature("doubletap2like", + context.getString(R.string.double_click_to_react), + context.getString(R.string.double_click_to_like_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("double", "tap", "click", "react", "like"))); + + catalog.add(new SearchableFeature("doubletap2like_emoji", + context.getString(R.string.custom_reaction), + context.getString(R.string.custom_reaction_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("reaction", "emoji", "custom"))); + + catalog.add(new SearchableFeature("google_translate", + context.getString(R.string.google_translate), + context.getString(R.string.google_translate_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("translate", "google", "language"))); + + catalog.add(new SearchableFeature("deleted_messages_activity", + context.getString(R.string.deleted_messages_title), + context.getString(R.string.deleted_messages_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.ACTIVITY, + null, + Arrays.asList("deleted", "messages", "restore", "history", "log"))); + + catalog.add(new SearchableFeature("verify_blocked_contact", + context.getString(R.string.show_contact_blocked), + context.getString(R.string.show_contact_blocked_sum), + SearchableFeature.Category.GENERAL_CONVERSATION, + SearchableFeature.FragmentType.GENERAL, + "conversation", + Arrays.asList("blocked", "contact", "verify"))); + + // GENERAL FRAGMENT - Status + catalog.add(new SearchableFeature("autonext_status", + context.getString(R.string.disable_auto_status), + context.getString(R.string.disable_auto_status_sum), + SearchableFeature.Category.GENERAL, + SearchableFeature.FragmentType.GENERAL, + null, + Arrays.asList("auto", "status", "skip"))); + + catalog.add(new SearchableFeature("copystatus", + context.getString(R.string.enable_copy_status), + context.getString(R.string.enable_copy_status_sum), + SearchableFeature.Category.GENERAL, + SearchableFeature.FragmentType.GENERAL, + null, + Arrays.asList("copy", "status", "caption"))); + + catalog.add(new SearchableFeature("toast_viewed_status", + context.getString(R.string.toast_on_viewed_status), + context.getString(R.string.toast_on_viewed_status_sum), + SearchableFeature.Category.GENERAL, + SearchableFeature.FragmentType.GENERAL, + null, + Arrays.asList("toast", "viewed", "status", "notification"))); + + // PRIVACY FRAGMENT + catalog.add(new SearchableFeature("typearchive", + context.getString(R.string.hide_archived_chat), + context.getString(R.string.hide_archived_chat_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("archive", "hide", "hidden"))); + + catalog.add(new SearchableFeature("show_freezeLastSeen", + context.getString(R.string.show_freezeLastSeen_button), + context.getString(R.string.show_freezeLastSeen_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("freeze", "last seen", "button"))); + + catalog.add(new SearchableFeature("ghostmode", + context.getString(R.string.ghost_mode_title), + context.getString(R.string.ghost_mode_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("ghost", "mode", "invisible"))); + + catalog.add(new SearchableFeature("always_online", + context.getString(R.string.always_online), + context.getString(R.string.always_online_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("always", "online", "status"))); + + catalog.add(new SearchableFeature("lockedchats_enhancer", + context.getString(R.string.lockedchats_enhancer), + context.getString(R.string.lockedchats_enhancer_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("locked", "chats", "enhanced"))); + + catalog.add(new SearchableFeature("custom_privacy_type", + context.getString(R.string.custom_privacy_per_contact), + context.getString(R.string.custom_privacy_per_contact_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("custom", "privacy", "contact"))); + + catalog.add(new SearchableFeature("freezelastseen", + context.getString(R.string.freezelastseen), + context.getString(R.string.freezelastseen_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("freeze", "last seen"))); + + catalog.add(new SearchableFeature("hideread", + context.getString(R.string.hideread), + context.getString(R.string.hideread_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("hide", "read", "blue", "ticks"))); + + catalog.add(new SearchableFeature("hide_seen_view", + context.getString(R.string.view_seen_tick), + context.getString(R.string.view_seen_tick_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("view", "seen", "tick"))); + + catalog.add(new SearchableFeature("blueonreply", + context.getString(R.string.blueonreply), + context.getString(R.string.blueonreply_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("blue", "tick", "reply"))); + + catalog.add(new SearchableFeature("hideread_group", + context.getString(R.string.hideread_group), + context.getString(R.string.hideread_group_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("hide", "read", "group", "ticks"))); + + catalog.add(new SearchableFeature("hidereceipt", + context.getString(R.string.hidereceipt), + context.getString(R.string.hidereceipt_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("hide", "delivered", "receipt"))); + + catalog.add(new SearchableFeature("ghostmode_t", + context.getString(R.string.ghostmode), + context.getString(R.string.ghostmode_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("ghost", "typing", "hide"))); + + catalog.add(new SearchableFeature("ghostmode_r", + context.getString(R.string.ghostmode_r), + context.getString(R.string.ghostmode_sum_r), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("ghost", "recording", "audio"))); + + catalog.add(new SearchableFeature("hideonceseen", + context.getString(R.string.hide_once_view_seen), + context.getString(R.string.hide_once_view_seen_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("view", "once", "seen", "hide"))); + + catalog.add(new SearchableFeature("hideaudioseen", + context.getString(R.string.hide_audio_seen), + context.getString(R.string.hide_audio_seen_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("audio", "seen", "hide"))); + + catalog.add(new SearchableFeature("viewonce", + context.getString(R.string.viewonce), + context.getString(R.string.viewonce_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("view", "once", "unlimited"))); + + catalog.add(new SearchableFeature("seentick", + context.getString(R.string.show_button_to_send_blue_tick), + context.getString(R.string.show_button_to_send_blue_tick_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("blue", "tick", "button", "mark", "read"))); + + catalog.add(new SearchableFeature("hidestatusview", + context.getString(R.string.hidestatusview), + context.getString(R.string.hidestatusview_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("status", "view", "hide"))); + + catalog.add(new SearchableFeature("call_info", + context.getString(R.string.additional_call_information), + context.getString(R.string.additional_call_information_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("call", "information", "additional"))); + + catalog.add(new SearchableFeature("call_privacy", + context.getString(R.string.call_blocker), + context.getString(R.string.call_blocker_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("call", "blocker", "block"))); + + catalog.add(new SearchableFeature("call_type", + context.getString(R.string.call_blocking_type), + context.getString(R.string.call_blocking_type_sum), + SearchableFeature.Category.PRIVACY, + SearchableFeature.FragmentType.PRIVACY, + null, + Arrays.asList("call", "blocking", "type"))); + + // Continue in next part... + addMediaFeatures(context, catalog); + addCustomizationFeatures(context, catalog); + addHomeActions(context, catalog); + + return catalog; + } + + private static void addMediaFeatures(Context context, List catalog) { + catalog.add(new SearchableFeature("imagequality", + context.getString(R.string.imagequality), + context.getString(R.string.imagequality_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("image", "quality", "hd"))); + + catalog.add(new SearchableFeature("download_local", + context.getString(R.string.local_download), + null, + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("download", "local", "folder"))); + + catalog.add(new SearchableFeature("downloadstatus", + context.getString(R.string.statusdowload), + context.getString(R.string.statusdowload_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("download", "status", "share"))); + + catalog.add(new SearchableFeature("downloadviewonce", + context.getString(R.string.downloadviewonce), + context.getString(R.string.downloadviewonce_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("download", "view", "once"))); + + catalog.add(new SearchableFeature("video_limit_size", + context.getString(R.string.increase_video_size_limit), + null, + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("video", "size", "limit", "mb"))); + + catalog.add(new SearchableFeature("videoquality", + context.getString(R.string.videoquality), + context.getString(R.string.videoquality_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("video", "quality", "hd"))); + + catalog.add(new SearchableFeature("video_real_resolution", + context.getString(R.string.send_video_in_real_resolution), + context.getString(R.string.send_video_in_real_resolution_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("video", "resolution", "real"))); + + catalog.add(new SearchableFeature("video_maxfps", + context.getString(R.string.send_video_in_60fps), + context.getString(R.string.send_video_in_60fps_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("video", "60fps", "fps"))); + + catalog.add(new SearchableFeature("call_recording_enable", + context.getString(R.string.call_recording_enable), + context.getString(R.string.call_recording_enable_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("call", "recording", "record"))); + + catalog.add(new SearchableFeature("call_recording_path", + context.getString(R.string.call_recording_path), + null, + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("recording", "path", "folder"))); + + catalog.add(new SearchableFeature("call_recording_toast", + context.getString(R.string.call_recording_toast_title), + context.getString(R.string.call_recording_toast_summary), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("recording", "toast", "notification", "show", "hide"))); + + catalog.add(new SearchableFeature("disable_sensor_proximity", + context.getString(R.string.disable_the_proximity_sensor), + context.getString(R.string.disable_the_proximity_sensor_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("proximity", "sensor", "screen"))); + + catalog.add(new SearchableFeature("proximity_audios", + context.getString(R.string.disable_audio_sensor), + context.getString(R.string.disable_audio_sensor_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("audio", "proximity", "sensor"))); + + catalog.add(new SearchableFeature("audio_type", + context.getString(R.string.send_audio_as_voice_audio_note), + context.getString(R.string.send_audio_as_voice_audio_note_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("audio", "voice", "note"))); + + catalog.add(new SearchableFeature("voicenote_speed", + context.getString(R.string.voice_note_speed), + null, + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("voice", "note", "speed"))); + + catalog.add(new SearchableFeature("audio_transcription", + context.getString(R.string.audio_transcription), + context.getString(R.string.audio_transcription_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("audio", "transcription", "text"))); + + catalog.add(new SearchableFeature("transcription_provider", + context.getString(R.string.transcription_provider), + context.getString(R.string.transcription_provider_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("transcription", "provider", "ai"))); + + catalog.add(new SearchableFeature("media_preview", + context.getString(R.string.enable_media_preview), + context.getString(R.string.enable_media_preview_sum), + SearchableFeature.Category.MEDIA, + SearchableFeature.FragmentType.MEDIA, + null, + Arrays.asList("media", "preview", "temporary"))); + } + + private static void addCustomizationFeatures(Context context, List catalog) { + catalog.add(new SearchableFeature("changecolor", + context.getString(R.string.colors_customization), + context.getString(R.string.colors_customization_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("colors", "customization", "theme"))); + + catalog.add(new SearchableFeature("primary_color", + context.getString(R.string.primary_color), + null, + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("primary", "color"))); + + catalog.add(new SearchableFeature("background_color", + context.getString(R.string.background_color), + null, + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("background", "color"))); + + catalog.add(new SearchableFeature("text_color", + context.getString(R.string.text_color), + null, + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("text", "color"))); + + catalog.add(new SearchableFeature("wallpaper", + context.getString(R.string.wallpaper_in_home_screen), + context.getString(R.string.wallpaper_in_home_screen_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("wallpaper", "background", "image"))); + + catalog.add(new SearchableFeature("hidetabs", + context.getString(R.string.hide_tabs_on_home), + context.getString(R.string.hide_tabs_on_home_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("hide", "tabs", "home"))); + + catalog.add(new SearchableFeature("custom_filters", + context.getString(R.string.custom_appearance), + context.getString(R.string.custom_filters_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("custom", "appearance", "filters", "css"))); + + catalog.add(new SearchableFeature("animation_list", + context.getString(R.string.list_animations_home_screen), + context.getString(R.string.list_animations_home_screen_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("animation", "list", "home"))); + + catalog.add(new SearchableFeature("admin_grp", + context.getString(R.string.show_admin_group_icon), + context.getString(R.string.show_admin_group_icon_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("admin", "group", "icon"))); + + catalog.add(new SearchableFeature("floatingmenu", + context.getString(R.string.new_context_menu_ui), + context.getString(R.string.new_context_menu_ui_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("floating", "menu", "context", "ios"))); + + catalog.add(new SearchableFeature("animation_emojis", + context.getString(R.string.animation_emojis), + context.getString(R.string.animation_emojis_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("animation", "emojis", "large"))); + + catalog.add(new SearchableFeature("bubble_color", + context.getString(R.string.change_bubble_colors), + context.getString(R.string.change_blubble_color_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("bubble", "color", "chat"))); + + catalog.add(new SearchableFeature("menuwicon", + context.getString(R.string.menuwicon), + context.getString(R.string.menuwicon_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("menu", "icons"))); + + catalog.add(new SearchableFeature("novaconfig", + context.getString(R.string.novaconfig), + context.getString(R.string.novaconfig_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("settings", "style", "profile"))); + + catalog.add(new SearchableFeature("igstatus", + context.getString(R.string.igstatus_on_home_screen), + context.getString(R.string.igstatus_on_home_screen_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("instagram", "status", "ig"))); + + catalog.add(new SearchableFeature("channels", + context.getString(R.string.disable_channels), + context.getString(R.string.disable_channels_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("channels", "disable", "hide"))); + + catalog.add(new SearchableFeature("removechannel_rec", + context.getString(R.string.remove_channel_recomendations), + context.getString(R.string.remove_channel_recomendations_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("channel", "recommendations", "remove"))); + + catalog.add(new SearchableFeature("status_style", + context.getString(R.string.style_of_stories_status), + context.getString(R.string.style_of_stories_status_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("status", "style", "stories"))); + + catalog.add(new SearchableFeature("oldstatus", + context.getString(R.string.old_statuses), + context.getString(R.string.old_statuses_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("old", "status", "vertical"))); + + catalog.add(new SearchableFeature("statuscomposer", + context.getString(R.string.custom_colors_for_text_status), + context.getString(R.string.custom_colors_for_text_status_sum), + SearchableFeature.Category.CUSTOMIZATION, + SearchableFeature.FragmentType.CUSTOMIZATION, + null, + Arrays.asList("status", "composer", "colors", "text"))); + } + + private static void addHomeActions(Context context, List catalog) { + // Home Fragment Actions + catalog.add(new SearchableFeature("export_config", + context.getString(R.string.export_settings), + context.getString(R.string.backup_settings), + SearchableFeature.Category.HOME_ACTIONS, + SearchableFeature.FragmentType.HOME, + null, + Arrays.asList("export", "backup", "settings", "config"))); + + catalog.add(new SearchableFeature("import_config", + context.getString(R.string.import_settings), + context.getString(R.string.backup_settings), + SearchableFeature.Category.HOME_ACTIONS, + SearchableFeature.FragmentType.HOME, + null, + Arrays.asList("import", "restore", "settings", "config"))); + + catalog.add(new SearchableFeature("reset_config", + context.getString(R.string.reset_settings), + null, + SearchableFeature.Category.HOME_ACTIONS, + SearchableFeature.FragmentType.HOME, + null, + Arrays.asList("reset", "settings", "clear"))); + + catalog.add(new SearchableFeature("reboot_wpp", + context.getString(R.string.restart_whatsapp), + null, + SearchableFeature.Category.HOME_ACTIONS, + SearchableFeature.FragmentType.HOME, + null, + Arrays.asList("restart", "reboot", "whatsapp", "refresh"))); + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/views/HorizontalListView.java b/app/src/main/java/com/wmods/wppenhacer/views/HorizontalListView.java index 8c45e914a..55c7b9a30 100644 --- a/app/src/main/java/com/wmods/wppenhacer/views/HorizontalListView.java +++ b/app/src/main/java/com/wmods/wppenhacer/views/HorizontalListView.java @@ -35,7 +35,6 @@ public class HorizontalListView extends AdapterView { private OnItemLongClickListener mOnItemLongClicked; private boolean mDataChanged = false; - public HorizontalListView(Context context, AttributeSet attrs) { super(context, attrs); initView(); @@ -46,7 +45,6 @@ public HorizontalListView(Context context) { initView(); } - private synchronized void initView() { mLeftViewIndex = -1; mRightViewIndex = 0; @@ -122,7 +120,7 @@ private synchronized void reset() { @Override public void setSelection(int position) { - //TODO: implement + // TODO: implement } private void addAndMeasureChild(final View child, int viewPos) { @@ -145,13 +143,13 @@ public boolean onDown(@NonNull MotionEvent e) { @Override public boolean onFling(MotionEvent e1, @NonNull MotionEvent e2, float velocityX, - float velocityY) { + float velocityY) { return HorizontalListView.this.onFling(e1, e2, velocityX, velocityY); } @Override public boolean onScroll(MotionEvent e1, @NonNull MotionEvent e2, - float distanceX, float distanceY) { + float distanceX, float distanceY) { getParent().requestDisallowInterceptTouchEvent(true); @@ -175,14 +173,17 @@ public boolean onSingleTapConfirmed(@NonNull MotionEvent e) { viewRect.set(left, top, right, bottom); if (viewRect.contains((int) e.getX(), (int) e.getY())) { if (mOnItemClicked != null) { - mOnItemClicked.onItemClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i)); + mOnItemClicked.onItemClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, + mAdapter.getItemId(mLeftViewIndex + 1 + i)); } if (mOnItemSelected != null) { - mOnItemSelected.onItemSelected(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i)); + mOnItemSelected.onItemSelected(HorizontalListView.this, child, mLeftViewIndex + 1 + i, + mAdapter.getItemId(mLeftViewIndex + 1 + i)); } int x = (int) e.getX() - left; int y = (int) e.getY() - top; - MotionEvent motionEvent = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, x, y, 0); + MotionEvent motionEvent = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), + MotionEvent.ACTION_DOWN, x, y, 0); child.dispatchTouchEvent(motionEvent); motionEvent.recycle(); child.performClick(); @@ -206,7 +207,8 @@ public void onLongPress(@NonNull MotionEvent e) { viewRect.set(left, top, right, bottom); if (viewRect.contains((int) e.getX(), (int) e.getY())) { if (mOnItemLongClicked != null) { - mOnItemLongClicked.onItemLongClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i)); + mOnItemLongClicked.onItemLongClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, + mAdapter.getItemId(mLeftViewIndex + 1 + i)); } break; } @@ -231,7 +233,6 @@ private void fillList(final int dx) { } fillListLeft(edge, dx); - } private void fillListRight(int rightEdge, final int dx) { @@ -320,7 +321,7 @@ private void requestParentIntercept(boolean allowIntercept) { } protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, - float velocityY) { + float velocityY) { synchronized (HorizontalListView.this) { mScroller.fling(mNextX, 0, (int) -velocityX, 0, 0, mMaxX, 0, 0); } diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/core/FeatureLoader.java b/app/src/main/java/com/wmods/wppenhacer/xposed/core/FeatureLoader.java index a134654f5..a01280b40 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/core/FeatureLoader.java +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/core/FeatureLoader.java @@ -43,6 +43,7 @@ import com.wmods.wppenhacer.xposed.features.general.ChatLimit; import com.wmods.wppenhacer.xposed.features.general.DeleteStatus; import com.wmods.wppenhacer.xposed.features.general.LiteMode; +import com.wmods.wppenhacer.xposed.features.general.RecoverDeleteForMe; import com.wmods.wppenhacer.xposed.features.general.NewChat; import com.wmods.wppenhacer.xposed.features.general.Others; import com.wmods.wppenhacer.xposed.features.general.PinnedLimit; @@ -53,12 +54,14 @@ import com.wmods.wppenhacer.xposed.features.listeners.ContactItemListener; import com.wmods.wppenhacer.xposed.features.listeners.ConversationItemListener; import com.wmods.wppenhacer.xposed.features.listeners.MenuStatusListener; +import com.wmods.wppenhacer.xposed.features.media.CallRecording; import com.wmods.wppenhacer.xposed.features.media.DownloadProfile; import com.wmods.wppenhacer.xposed.features.media.DownloadViewOnce; import com.wmods.wppenhacer.xposed.features.media.MediaPreview; import com.wmods.wppenhacer.xposed.features.media.MediaQuality; import com.wmods.wppenhacer.xposed.features.media.StatusDownload; import com.wmods.wppenhacer.xposed.features.others.ActivityController; +import com.wmods.wppenhacer.xposed.features.others.BackupRestore; import com.wmods.wppenhacer.xposed.features.others.AudioTranscript; import com.wmods.wppenhacer.xposed.features.others.Channels; import com.wmods.wppenhacer.xposed.features.others.ChatFilters; @@ -70,6 +73,7 @@ import com.wmods.wppenhacer.xposed.features.others.Stickers; import com.wmods.wppenhacer.xposed.features.others.TextStatusComposer; import com.wmods.wppenhacer.xposed.features.others.ToastViewer; +import com.wmods.wppenhacer.xposed.features.others.Spy; import com.wmods.wppenhacer.xposed.features.privacy.AntiWa; import com.wmods.wppenhacer.xposed.features.privacy.CallPrivacy; import com.wmods.wppenhacer.xposed.features.privacy.CustomPrivacy; @@ -124,82 +128,100 @@ public static void start(@NonNull ClassLoader loader, @NonNull XSharedPreference Feature.DEBUG = pref.getBoolean("enablelogs", true); Utils.xprefs = pref; - XposedHelpers.findAndHookMethod(Instrumentation.class, "callApplicationOnCreate", Application.class, new XC_MethodHook() { - @SuppressWarnings("deprecation") - protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - mApp = (Application) param.args[0]; + XposedHelpers.findAndHookMethod(Instrumentation.class, "callApplicationOnCreate", Application.class, + new XC_MethodHook() { + @SuppressWarnings("deprecation") + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + mApp = (Application) param.args[0]; - // Inject Booloader Spoofer - if (pref.getBoolean("bootloader_spoofer", false)) { - HookBL.hook(loader, pref); - XposedBridge.log("Bootloader Spoofer is Injected"); - } + // Inject Booloader Spoofer + if (pref.getBoolean("bootloader_spoofer", false)) { + HookBL.hook(loader, pref); + XposedBridge.log("Bootloader Spoofer is Injected"); + } - PackageManager packageManager = mApp.getPackageManager(); - pref.registerOnSharedPreferenceChangeListener((sharedPreferences, s) -> pref.reload()); - PackageInfo packageInfo = packageManager.getPackageInfo(mApp.getPackageName(), 0); - XposedBridge.log(packageInfo.versionName); - currentVersion = packageInfo.versionName; - supportedVersions = Arrays.asList(mApp.getResources().getStringArray(Objects.equals(mApp.getPackageName(), FeatureLoader.PACKAGE_WPP) ? ResId.array.supported_versions_wpp : ResId.array.supported_versions_business)); - mApp.registerActivityLifecycleCallbacks(new WaCallback()); - registerReceivers(); - try { - var timemillis = System.currentTimeMillis(); - UnobfuscatorCache.init(mApp); - SharedPreferencesWrapper.hookInit(mApp.getClassLoader()); - ReflectionUtils.initCache(mApp); - boolean isSupported = supportedVersions.stream().anyMatch(s -> packageInfo.versionName.startsWith(s.replace(".xx", ""))); - if (!isSupported) { - disableExpirationVersion(mApp.getClassLoader()); - if (!pref.getBoolean("bypass_version_check", false)) { - String sb = "Unsupported version: " + - packageInfo.versionName + - "\n" + - "Only the function of ignoring the expiration of the WhatsApp version has been applied!"; - throw new Exception(sb); + PackageManager packageManager = mApp.getPackageManager(); + pref.registerOnSharedPreferenceChangeListener((sharedPreferences, s) -> pref.reload()); + PackageInfo packageInfo = packageManager.getPackageInfo(mApp.getPackageName(), 0); + XposedBridge.log(packageInfo.versionName); + currentVersion = packageInfo.versionName; + supportedVersions = Arrays.asList(mApp.getResources() + .getStringArray(Objects.equals(mApp.getPackageName(), FeatureLoader.PACKAGE_WPP) + ? ResId.array.supported_versions_wpp + : ResId.array.supported_versions_business)); + mApp.registerActivityLifecycleCallbacks(new WaCallback()); + registerReceivers(); + try { + var timemillis = System.currentTimeMillis(); + UnobfuscatorCache.init(mApp); + SharedPreferencesWrapper.hookInit(mApp.getClassLoader()); + ReflectionUtils.initCache(mApp); + boolean isSupported = supportedVersions.stream() + .anyMatch(s -> packageInfo.versionName.startsWith(s.replace(".xx", ""))); + if (!isSupported) { + disableExpirationVersion(mApp.getClassLoader()); + if (!pref.getBoolean("bypass_version_check", false)) { + String sb = "Unsupported version: " + + packageInfo.versionName + + "\n" + + "Only the function of ignoring the expiration of the WhatsApp version has been applied!"; + throw new Exception(sb); + } + } + initComponents(loader, pref); + plugins(loader, pref, packageInfo.versionName); + sendEnabledBroadcast(mApp); + // XposedHelpers.setStaticIntField(XposedHelpers.findClass("com.whatsapp.infra.logging.Log", + // loader), "level", 5); + var timemillis2 = System.currentTimeMillis() - timemillis; + XposedBridge.log("Loaded Hooks in " + timemillis2 + "ms"); + } catch (Throwable e) { + XposedBridge.log(e); + var error = new ErrorItem(); + error.setPluginName("MainFeatures[Critical]"); + error.setWhatsAppVersion(packageInfo.versionName); + error.setModuleVersion(BuildConfig.VERSION_NAME); + error.setMessage(e.getMessage()); + error.setError(Arrays.toString(Arrays.stream(e.getStackTrace()) + .filter(s -> !s.getClassName().startsWith("android") + && !s.getClassName().startsWith("com.android")) + .map(StackTraceElement::toString).toArray())); + list.add(error); } } - initComponents(loader, pref); - plugins(loader, pref, packageInfo.versionName); - sendEnabledBroadcast(mApp); -// XposedHelpers.setStaticIntField(XposedHelpers.findClass("com.whatsapp.infra.logging.Log", loader), "level", 5); - var timemillis2 = System.currentTimeMillis() - timemillis; - XposedBridge.log("Loaded Hooks in " + timemillis2 + "ms"); - } catch (Throwable e) { - XposedBridge.log(e); - var error = new ErrorItem(); - error.setPluginName("MainFeatures[Critical]"); - error.setWhatsAppVersion(packageInfo.versionName); - error.setModuleVersion(BuildConfig.VERSION_NAME); - error.setMessage(e.getMessage()); - error.setError(Arrays.toString(Arrays.stream(e.getStackTrace()).filter(s -> !s.getClassName().startsWith("android") && !s.getClassName().startsWith("com.android")).map(StackTraceElement::toString).toArray())); - list.add(error); - } - } - }); + }); - XposedHelpers.findAndHookMethod(WppCore.getHomeActivityClass(loader), "onCreate", Bundle.class, new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) throws Throwable { - super.afterHookedMethod(param); - if (!list.isEmpty()) { - var activity = (Activity) param.thisObject; - var msg = String.join("\n", list.stream().map(item -> item.getPluginName() + " - " + item.getMessage()).toArray(String[]::new)); + XposedHelpers.findAndHookMethod(WppCore.getHomeActivityClass(loader), "onCreate", Bundle.class, + new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + super.afterHookedMethod(param); + if (!list.isEmpty()) { + var activity = (Activity) param.thisObject; + var msg = String.join("\n", + list.stream().map(item -> item.getPluginName() + " - " + item.getMessage()) + .toArray(String[]::new)); - new AlertDialogWpp(activity) - .setTitle(activity.getString(ResId.string.error_detected)) - .setMessage(activity.getString(ResId.string.version_error) + msg + "\n\nCurrent Version: " + currentVersion + "\nSupported Versions:\n" + String.join("\n", supportedVersions)) - .setPositiveButton(activity.getString(ResId.string.copy_to_clipboard), (dialog, which) -> { - var clipboard = (ClipboardManager) mApp.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("text", String.join("\n", list.stream().map(ErrorItem::toString).toArray(String[]::new))); - clipboard.setPrimaryClip(clip); - Toast.makeText(mApp, ResId.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); - dialog.dismiss(); - }) - .show(); - } - } - }); + new AlertDialogWpp(activity) + .setTitle(activity.getString(ResId.string.error_detected)) + .setMessage(activity.getString(ResId.string.version_error) + msg + + "\n\nCurrent Version: " + currentVersion + "\nSupported Versions:\n" + + String.join("\n", supportedVersions)) + .setPositiveButton(activity.getString(ResId.string.copy_to_clipboard), + (dialog, which) -> { + var clipboard = (ClipboardManager) mApp + .getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("text", String.join("\n", + list.stream().map(ErrorItem::toString).toArray(String[]::new))); + clipboard.setPrimaryClip(clip); + Toast.makeText(mApp, ResId.string.copied_to_clipboard, + Toast.LENGTH_SHORT).show(); + dialog.dismiss(); + }) + .show(); + } + } + }); } public static void disableExpirationVersion(ClassLoader classLoader) throws Exception { @@ -229,23 +251,27 @@ private static void initComponents(ClassLoader loader, XSharedPreferences pref) } // Check for WAE Update - //noinspection ConstantValue + // noinspection ConstantValue if (App.isOriginalPackage() && pref.getBoolean("update_check", true)) { - if (activity.getClass().getSimpleName().equals("HomeActivity") && state == WppCore.ActivityChangeState.ChangeType.CREATED) { - CompletableFuture.runAsync(new UpdateChecker(activity)); + if (activity.getClass().getSimpleName().equals("HomeActivity") + && state == WppCore.ActivityChangeState.ChangeType.RESUMED) { + // Delay to ensure smooth activity transition + XposedBridge.log("[WAE] Scheduling update check in 2 seconds..."); + activity.getWindow().getDecorView().postDelayed(() -> { + XposedBridge.log("[WAE] Launching UpdateChecker now"); + CompletableFuture.runAsync(new UpdateChecker(activity)); + }, 2000); // 2 second delay } } }); } - private static void checkUpdate(@NonNull Activity activity) { if (WppCore.getPrivBoolean("need_restart", false)) { WppCore.setPrivBoolean("need_restart", false); try { - new AlertDialogWpp(activity). - setMessage(activity.getString(ResId.string.restart_wpp)). - setPositiveButton(activity.getString(ResId.string.yes), (dialog, which) -> { + new AlertDialogWpp(activity).setMessage(activity.getString(ResId.string.restart_wpp)) + .setPositiveButton(activity.getString(ResId.string.yes), (dialog, which) -> { if (!Utils.doRestart(activity)) Toast.makeText(activity, "Unable to rebooting activity", Toast.LENGTH_SHORT).show(); }) @@ -263,13 +289,15 @@ private static void registerReceivers() { public void onReceive(Context context, Intent intent) { if (context.getPackageName().equals(intent.getStringExtra("PKG"))) { var appName = context.getPackageManager().getApplicationLabel(context.getApplicationInfo()); - Toast.makeText(context, context.getString(ResId.string.rebooting) + " " + appName + "...", Toast.LENGTH_SHORT).show(); + Toast.makeText(context, context.getString(ResId.string.rebooting) + " " + appName + "...", + Toast.LENGTH_SHORT).show(); if (!Utils.doRestart(context)) Toast.makeText(context, "Unable to rebooting " + appName, Toast.LENGTH_SHORT).show(); } } }; - ContextCompat.registerReceiver(mApp, restartReceiver, new IntentFilter(BuildConfig.APPLICATION_ID + ".WHATSAPP.RESTART"), ContextCompat.RECEIVER_EXPORTED); + ContextCompat.registerReceiver(mApp, restartReceiver, + new IntentFilter(BuildConfig.APPLICATION_ID + ".WHATSAPP.RESTART"), ContextCompat.RECEIVER_EXPORTED); /// Wpp receiver BroadcastReceiver wppReceiver = new BroadcastReceiver() { @@ -278,7 +306,8 @@ public void onReceive(Context context, Intent intent) { sendEnabledBroadcast(context); } }; - ContextCompat.registerReceiver(mApp, wppReceiver, new IntentFilter(BuildConfig.APPLICATION_ID + ".CHECK_WPP"), ContextCompat.RECEIVER_EXPORTED); + ContextCompat.registerReceiver(mApp, wppReceiver, new IntentFilter(BuildConfig.APPLICATION_ID + ".CHECK_WPP"), + ContextCompat.RECEIVER_EXPORTED); // Dialog receiver restart BroadcastReceiver restartManualReceiver = new BroadcastReceiver() { @@ -287,13 +316,15 @@ public void onReceive(Context context, Intent intent) { WppCore.setPrivBoolean("need_restart", true); } }; - ContextCompat.registerReceiver(mApp, restartManualReceiver, new IntentFilter(BuildConfig.APPLICATION_ID + ".MANUAL_RESTART"), ContextCompat.RECEIVER_EXPORTED); + ContextCompat.registerReceiver(mApp, restartManualReceiver, + new IntentFilter(BuildConfig.APPLICATION_ID + ".MANUAL_RESTART"), ContextCompat.RECEIVER_EXPORTED); } private static void sendEnabledBroadcast(Context context) { try { Intent wppIntent = new Intent(BuildConfig.APPLICATION_ID + ".RECEIVER_WPP"); - wppIntent.putExtra("VERSION", context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName); + wppIntent.putExtra("VERSION", + context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName); wppIntent.putExtra("PKG", context.getPackageName()); wppIntent.setPackage(BuildConfig.APPLICATION_ID); context.sendBroadcast(wppIntent); @@ -301,9 +332,10 @@ private static void sendEnabledBroadcast(Context context) { } } - private static void plugins(@NonNull ClassLoader loader, @NonNull XSharedPreferences pref, @NonNull String versionWpp) throws Exception { + private static void plugins(@NonNull ClassLoader loader, @NonNull XSharedPreferences pref, + @NonNull String versionWpp) throws Exception { - var classes = new Class[]{ + var classes = new Class[] { DebugFeature.class, ContactItemListener.class, ConversationItemListener.class, @@ -359,7 +391,11 @@ private static void plugins(@NonNull ClassLoader loader, @NonNull XSharedPrefere AudioTranscript.class, GoogleTranslate.class, ContactBlockedVerify.class, - LockedChatsEnhancer.class + LockedChatsEnhancer.class, + CallRecording.class, + BackupRestore.class, + Spy.class, + RecoverDeleteForMe.class }; XposedBridge.log("Loading Plugins"); var executorService = Executors.newWorkStealingPool(Math.min(Runtime.getRuntime().availableProcessors(), 4)); @@ -378,7 +414,9 @@ private static void plugins(@NonNull ClassLoader loader, @NonNull XSharedPrefere error.setWhatsAppVersion(versionWpp); error.setModuleVersion(BuildConfig.VERSION_NAME); error.setMessage(e.getMessage()); - error.setError(Arrays.toString(Arrays.stream(e.getStackTrace()).filter(s -> !s.getClassName().startsWith("android") && !s.getClassName().startsWith("com.android")).map(StackTraceElement::toString).toArray())); + error.setError(Arrays.toString(Arrays.stream(e.getStackTrace()).filter( + s -> !s.getClassName().startsWith("android") && !s.getClassName().startsWith("com.android")) + .map(StackTraceElement::toString).toArray())); list.add(error); } var timemillis2 = System.currentTimeMillis() - timemillis; diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/core/WppCore.java b/app/src/main/java/com/wmods/wppenhacer/xposed/core/WppCore.java index 4f87d2ffa..a24670383 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/core/WppCore.java +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/core/WppCore.java @@ -68,21 +68,26 @@ public class WppCore { private static Class actionUser; private static Method cachedMessageStoreKey; - public static void Initialize(ClassLoader loader, XSharedPreferences pref) throws Exception { privPrefs = Utils.getApplication().getSharedPreferences("WaGlobal", Context.MODE_PRIVATE); // init UserJID - var mSendReadClass = Unobfuscator.findFirstClassUsingName(loader, StringMatchType.EndsWith, "SendReadReceiptJob"); - var subClass = ReflectionUtils.findConstructorUsingFilter(mSendReadClass, (constructor) -> constructor.getParameterCount() == 8).getParameterTypes()[0]; - mGenJidClass = ReflectionUtils.findFieldUsingFilter(subClass, (field) -> Modifier.isStatic(field.getModifiers())).getType(); - mGenJidMethod = ReflectionUtils.findMethodUsingFilter(mGenJidClass, (method) -> method.getParameterCount() == 1 && !Modifier.isStatic(method.getModifiers())); + var mSendReadClass = Unobfuscator.findFirstClassUsingName(loader, StringMatchType.EndsWith, + "SendReadReceiptJob"); + var subClass = ReflectionUtils + .findConstructorUsingFilter(mSendReadClass, (constructor) -> constructor.getParameterCount() == 8) + .getParameterTypes()[0]; + mGenJidClass = ReflectionUtils + .findFieldUsingFilter(subClass, (field) -> Modifier.isStatic(field.getModifiers())).getType(); + mGenJidMethod = ReflectionUtils.findMethodUsingFilter(mGenJidClass, + (method) -> method.getParameterCount() == 1 && !Modifier.isStatic(method.getModifiers())); // Bottom Dialog bottomDialog = Unobfuscator.loadDialogViewClass(loader); convChatField = Unobfuscator.loadAntiRevokeConvChatField(loader); chatJidField = Unobfuscator.loadAntiRevokeChatJidField(loader); - // Settings notifications activity (required for ActivityController.EXPORTED_ACTIVITY) + // Settings notifications activity (required for + // ActivityController.EXPORTED_ACTIVITY) mSettingsNotificationsClass = getSettingsNotificationsActivityClass(loader); // StartUpPrefs @@ -133,10 +138,12 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable { } public static Object getPhoneJidFromUserJid(Object lid) { - if (lid == null) return null; + if (lid == null) + return null; try { var rawString = (String) XposedHelpers.callMethod(lid, "getRawString"); - if (rawString == null || !rawString.contains("@lid")) return lid; + if (rawString == null || !rawString.contains("@lid")) + return lid; rawString = rawString.replaceFirst("\\.[\\d:]+@", "@"); var newUser = WppCore.createUserJid(rawString); var result = ReflectionUtils.callMethod(convertLidToJid, mWaJidMapRepository, newUser); @@ -148,10 +155,12 @@ public static Object getPhoneJidFromUserJid(Object lid) { } public static Object getUserJidFromPhoneJid(Object userJid) { - if (userJid == null) return null; + if (userJid == null) + return null; try { var rawString = (String) XposedHelpers.callMethod(userJid, "getRawString"); - if (rawString == null || rawString.contains("@lid")) return userJid; + if (rawString == null || rawString.contains("@lid")) + return userJid; rawString = rawString.replaceFirst("\\.[\\d:]+@", "@"); var newUser = WppCore.createUserJid(rawString); var result = ReflectionUtils.callMethod(convertJidToLid, mWaJidMapRepository, newUser); @@ -164,7 +173,8 @@ public static Object getUserJidFromPhoneJid(Object userJid) { public static void initBridge(Context context) throws Exception { var prefsCacheHooks = UnobfuscatorCache.getInstance().sPrefsCacheHooks; - int preferredOrder = prefsCacheHooks.getInt("preferredOrder", 1); // 0 for ProviderClient first, 1 for BridgeClient first + int preferredOrder = prefsCacheHooks.getInt("preferredOrder", 1); // 0 for ProviderClient first, 1 for + // BridgeClient first boolean connected = false; if (preferredOrder == 0) { @@ -191,14 +201,14 @@ public static void initBridge(Context context) throws Exception { prefsCacheHooks.edit().putInt("preferredOrder", preferredOrder).apply(); } - private static boolean tryConnectBridge(BaseClient baseClient) throws Exception { try { XposedBridge.log("Trying to connect to " + baseClient.getClass().getSimpleName()); client = baseClient; CompletableFuture canLoadFuture = baseClient.connect(); Boolean canLoad = canLoadFuture.get(); - if (!canLoad) throw new Exception(); + if (!canLoad) + throw new Exception(); } catch (Exception e) { return false; } @@ -207,7 +217,9 @@ private static boolean tryConnectBridge(BaseClient baseClient) throws Exception public static void sendMessage(String number, String message) { try { - var senderMethod = ReflectionUtils.findMethodUsingFilterIfExists(actionUser, (method) -> List.class.isAssignableFrom(method.getReturnType()) && ReflectionUtils.findIndexOfType(method.getParameterTypes(), String.class) != -1); + var senderMethod = ReflectionUtils.findMethodUsingFilterIfExists(actionUser, + (method) -> List.class.isAssignableFrom(method.getReturnType()) + && ReflectionUtils.findIndexOfType(method.getParameterTypes(), String.class) != -1); if (senderMethod != null) { var userJid = createUserJid(number + "@s.whatsapp.net"); if (userJid == null) { @@ -234,7 +246,9 @@ public static void sendMessage(String number, String message) { public static void sendReaction(String s, Object objMessage) { try { - var senderMethod = ReflectionUtils.findMethodUsingFilter(actionUser, (method) -> method.getParameterCount() == 3 && Arrays.equals(method.getParameterTypes(), new Class[]{FMessageWpp.TYPE, String.class, boolean.class})); + var senderMethod = ReflectionUtils.findMethodUsingFilter(actionUser, + (method) -> method.getParameterCount() == 3 && Arrays.equals(method.getParameterTypes(), + new Class[] { FMessageWpp.TYPE, String.class, boolean.class })); senderMethod.invoke(getActionUser(), objMessage, s, !TextUtils.isEmpty(s)); } catch (Exception e) { Utils.showToast("Error in sending reaction:" + e.getMessage(), Toast.LENGTH_SHORT); @@ -253,9 +267,9 @@ public static Object getActionUser() { return mActionUser; } - public static void loadWADatabase() { - if (mWaDatabase != null) return; + if (mWaDatabase != null) + return; var dataDir = Utils.getApplication().getFilesDir().getParentFile(); var database = new File(dataDir, "databases/wa.db"); if (database.exists()) { @@ -263,7 +277,6 @@ public static void loadWADatabase() { } } - public static Activity getCurrentActivity() { return mCurrentActivity; } @@ -346,7 +359,7 @@ public synchronized static Class getDataUsageActivityClass(@NonNull ClassLoader } public synchronized static Class getTextStatusComposerFragmentClass(@NonNull ClassLoader loader) throws Exception { - var classes = new String[]{ + var classes = new String[] { "com.whatsapp.status.composer.TextStatusComposerFragment", "com.whatsapp.statuscomposer.composer.TextStatusComposerFragment" }; @@ -359,7 +372,7 @@ public synchronized static Class getTextStatusComposerFragmentClass(@NonNull Cla } public synchronized static Class getVoipManagerClass(@NonNull ClassLoader loader) throws Exception { - var classes = new String[]{ + var classes = new String[] { "com.whatsapp.voipcalling.Voip", "com.whatsapp.calling.voipcalling.Voip" }; @@ -372,7 +385,7 @@ public synchronized static Class getVoipManagerClass(@NonNull ClassLoader loader } public synchronized static Class getVoipCallInfoClass(@NonNull ClassLoader loader) throws Exception { - var classes = new String[]{ + var classes = new String[] { "com.whatsapp.voipcalling.CallInfo", "com.whatsapp.calling.infra.voipcalling.CallInfo" }; @@ -384,22 +397,23 @@ public synchronized static Class getVoipCallInfoClass(@NonNull ClassLoader loade throw new Exception("VoipCallInfoClass not found"); } -// public static Activity getActivityBySimpleName(String name) { -// for (var activity : activities) { -// if (activity.getClass().getSimpleName().equals(name)) { -// return activity; -// } -// } -// return null; -// } - + // public static Activity getActivityBySimpleName(String name) { + // for (var activity : activities) { + // if (activity.getClass().getSimpleName().equals(name)) { + // return activity; + // } + // } + // return null; + // } public static int getDefaultTheme() { if (mStartUpConfig != null) { - var result = ReflectionUtils.findMethodUsingFilterIfExists(mStartUpConfig.getClass(), (method) -> method.getParameterCount() == 0 && method.getReturnType() == int.class); + var result = ReflectionUtils.findMethodUsingFilterIfExists(mStartUpConfig.getClass(), + (method) -> method.getParameterCount() == 0 && method.getReturnType() == int.class); if (result != null) { var value = ReflectionUtils.callMethod(result, mStartUpConfig); - if (value != null) return (int) value; + if (value != null) + return (int) value; } } var startup_prefs = Utils.getApplication().getSharedPreferences("startup_prefs", Context.MODE_PRIVATE); @@ -409,16 +423,19 @@ public static int getDefaultTheme() { @NonNull public static String getContactName(FMessageWpp.UserJid userJid) { loadWADatabase(); - if (mWaDatabase == null || userJid.isNull()) return "Whatsapp Contact"; + if (mWaDatabase == null || userJid.isNull()) + return "Whatsapp Contact"; String name = getSContactName(userJid, false); - if (!TextUtils.isEmpty(name)) return name; + if (!TextUtils.isEmpty(name)) + return name; return getWppContactName(userJid); } @NonNull public static String getSContactName(FMessageWpp.UserJid userJid, boolean saveOnly) { loadWADatabase(); - if (mWaDatabase == null || userJid == null) return ""; + if (mWaDatabase == null || userJid == null) + return ""; String selection; if (saveOnly) { selection = "jid = ? AND raw_contact_id > 0"; @@ -427,7 +444,8 @@ public static String getSContactName(FMessageWpp.UserJid userJid, boolean saveOn } String name = null; var rawJid = userJid.getPhoneRawString(); - var cursor = mWaDatabase.query("wa_contacts", new String[]{"display_name"}, selection, new String[]{rawJid}, null, null, null); + var cursor = mWaDatabase.query("wa_contacts", new String[] { "display_name" }, selection, + new String[] { rawJid }, null, null, null); if (cursor.moveToFirst()) { name = cursor.getString(0); cursor.close(); @@ -438,10 +456,12 @@ public static String getSContactName(FMessageWpp.UserJid userJid, boolean saveOn @NonNull public static String getWppContactName(FMessageWpp.UserJid userJid) { loadWADatabase(); - if (mWaDatabase == null || userJid.isNull()) return ""; + if (mWaDatabase == null || userJid.isNull()) + return ""; String name = null; var rawJid = userJid.getPhoneRawString(); - var cursor2 = mWaDatabase.query("wa_vnames", new String[]{"verified_name"}, "jid = ?", new String[]{rawJid}, null, null, null); + var cursor2 = mWaDatabase.query("wa_vnames", new String[] { "verified_name" }, "jid = ?", + new String[] { rawJid }, null, null, null); if (cursor2.moveToFirst()) { name = cursor2.getString(0); cursor2.close(); @@ -450,7 +470,8 @@ public static String getWppContactName(FMessageWpp.UserJid userJid) { } public static Object getFMessageFromKey(Object messageKey) { - if (messageKey == null) return null; + if (messageKey == null) + return null; try { if (mCachedMessageStore == null) { XposedBridge.log("CachedMessageStore is null"); @@ -463,10 +484,10 @@ public static Object getFMessageFromKey(Object messageKey) { } } - @Nullable public static Object createUserJid(@Nullable String rawjid) { - if (rawjid == null) return null; + if (rawjid == null) + return null; var genInstance = XposedHelpers.newInstance(mGenJidClass); try { return mGenJidMethod.invoke(genInstance, rawjid); @@ -480,7 +501,8 @@ public static Object createUserJid(@Nullable String rawjid) { public static FMessageWpp.UserJid getCurrentUserJid() { try { var conversation = getCurrentConversation(); - if (conversation == null) return null; + if (conversation == null) + return null; Object chatField; if (conversation.getClass().getSimpleName().equals("HomeActivity")) { // tablet mode found @@ -501,10 +523,12 @@ public static FMessageWpp.UserJid getCurrentUserJid() { public static String stripJID(String str) { try { - if (str == null) return null; + if (str == null) + return null; if (str.contains(".") && str.contains("@") && str.indexOf(".") < str.indexOf("@")) { return str.substring(0, str.indexOf(".")); - } else if (str.contains("@g.us") || str.contains("@s.whatsapp.net") || str.contains("@broadcast") || str.contains("@lid")) { + } else if (str.contains("@g.us") || str.contains("@s.whatsapp.net") || str.contains("@broadcast") + || str.contains("@lid")) { return str.substring(0, str.indexOf("@")); } return str; @@ -516,9 +540,11 @@ public static String stripJID(String str) { @Nullable public static Drawable getContactPhotoDrawable(String jid) { - if (jid == null) return null; + if (jid == null) + return null; var file = getContactPhotoFile(jid); - if (file == null) return null; + if (file == null) + return null; return Drawable.createFromPath(file.getAbsolutePath()); } @@ -527,7 +553,8 @@ public static File getContactPhotoFile(String jid) { File file = new File(datafolder + "/cache/" + "Profile Pictures" + "/" + stripJID(jid) + ".jpg"); if (!file.exists()) file = new File(datafolder + "files" + "/" + "Avatars" + "/" + jid + ".j"); - if (file.exists()) return file; + if (file.exists()) + return file; return null; } @@ -536,16 +563,16 @@ public static String getMyName() { return startup_prefs.getString("push_name", "WhatsApp"); } -// public static String getMyNumber() { -// var mainPrefs = getMainPrefs(); -// return mainPrefs.getString("registration_jid", ""); -// } + // public static String getMyNumber() { + // var mainPrefs = getMainPrefs(); + // return mainPrefs.getString("registration_jid", ""); + // } public static SharedPreferences getMainPrefs() { - return Utils.getApplication().getSharedPreferences(Utils.getApplication().getPackageName() + "_preferences_light", Context.MODE_PRIVATE); + return Utils.getApplication().getSharedPreferences( + Utils.getApplication().getPackageName() + "_preferences_light", Context.MODE_PRIVATE); } - public static String getMyBio() { var mainPrefs = getMainPrefs(); return mainPrefs.getString("my_current_status", ""); @@ -554,7 +581,8 @@ public static String getMyBio() { public static Drawable getMyPhoto() { String datafolder = Utils.getApplication().getCacheDir().getParent() + "/"; File file = new File(datafolder + "files" + "/" + "me.jpg"); - if (file.exists()) return Drawable.createFromPath(file.getAbsolutePath()); + if (file.exists()) + return Drawable.createFromPath(file.getAbsolutePath()); return null; } @@ -564,15 +592,56 @@ public static BottomDialogWpp createBottomDialog(Context context) { @Nullable public static Activity getCurrentConversation() { - if (mCurrentActivity == null) return null; - Class conversation = XposedHelpers.findClass("com.whatsapp.Conversation", mCurrentActivity.getClassLoader()); - if (conversation.isInstance(mCurrentActivity)) return mCurrentActivity; - - // for tablet UI, they're using HomeActivity instead of Conversation - // TODO: Add more checks for ConversationFragment - Class home = getHomeActivityClass(mCurrentActivity.getClassLoader()); - if (mCurrentActivity.getResources().getConfiguration().smallestScreenWidthDp >= 600 && home.isInstance(mCurrentActivity)) - return mCurrentActivity; + if (mCurrentActivity == null) + return null; + try { + Class conversation = XposedHelpers.findClass("com.whatsapp.Conversation", + mCurrentActivity.getClassLoader()); + if (conversation.isInstance(mCurrentActivity)) + return mCurrentActivity; + + // for tablet UI, they're using HomeActivity instead of Conversation + Class home = getHomeActivityClass(mCurrentActivity.getClassLoader()); + if (mCurrentActivity.getResources().getConfiguration().smallestScreenWidthDp >= 600 + && home.isInstance(mCurrentActivity)) + return mCurrentActivity; + } catch (Exception ignored) { + } + return null; + } + + @Nullable + public static String getCurrentChatTitle() { + try { + Activity conversation = getCurrentConversation(); + if (conversation == null) + return null; + + // Strategy 1: Safely get title via reflection to avoid UI thread issues + try { + Field f = Activity.class.getDeclaredField("mTitle"); + f.setAccessible(true); + Object titleObj = f.get(conversation); + if (titleObj instanceof CharSequence) { + String title = titleObj.toString(); + if (!TextUtils.isEmpty(title) && !title.equalsIgnoreCase("WhatsApp")) { + return title; + } + } + } catch (Throwable ignored) { + } + + // Strategy 2: Fallback to Activity.getTitle() (might be safer than + // findViewById) + CharSequence activityTitle = conversation.getTitle(); + if (activityTitle != null && activityTitle.length() > 0 + && !activityTitle.toString().equalsIgnoreCase("WhatsApp")) { + return activityTitle.toString(); + } + + } catch (Throwable t) { + // Extremely defensive + } return null; } @@ -591,7 +660,8 @@ public static String getPrivString(String key, String defaultValue) { public static JSONObject getPrivJSON(String key, JSONObject defaultValue) { var jsonStr = privPrefs.getString(key, null); - if (jsonStr == null) return defaultValue; + if (jsonStr == null) + return defaultValue; try { return new JSONObject(jsonStr); } catch (Exception e) { @@ -610,7 +680,6 @@ public static void removePrivKey(String s) { privPrefs.edit().remove(s).commit(); } - @SuppressLint("ApplySharedPref") public static void setPrivBoolean(String key, boolean value) { privPrefs.edit().putBoolean(key, value).commit(); @@ -625,11 +694,13 @@ public static void addListenerActivity(ActivityChangeState listener) { } public static WaeIIFace getClientBridge() throws Exception { - if (client == null || client.getService() == null || !client.getService().asBinder().isBinderAlive() || !client.getService().asBinder().pingBinder()) { + if (client == null || client.getService() == null || !client.getService().asBinder().isBinderAlive() + || !client.getService().asBinder().pingBinder()) { WppCore.getCurrentActivity().runOnUiThread(() -> { var dialog = new AlertDialogWpp(WppCore.getCurrentActivity()); dialog.setTitle("Bridge Error"); - dialog.setMessage("The Connection with WaEnhancer was lost, it is necessary to reconnect with WaEnhancer in order to reestablish the connection."); + dialog.setMessage( + "The Connection with WaEnhancer was lost, it is necessary to reconnect with WaEnhancer in order to reestablish the connection."); dialog.setPositiveButton("reconnect", (dialog1, which) -> { client.tryReconnect(); dialog.dismiss(); @@ -642,7 +713,6 @@ public static WaeIIFace getClientBridge() throws Exception { return client.getService(); } - public interface ActivityChangeState { void onChange(Activity activity, ChangeType type); @@ -652,5 +722,4 @@ enum ChangeType { } } - } diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/AlertDialogWpp.java b/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/AlertDialogWpp.java index 04fd20bf9..28729c873 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/AlertDialogWpp.java +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/AlertDialogWpp.java @@ -91,7 +91,7 @@ public AlertDialogWpp setTitle(int title) { return this; } - public AlertDialogWpp setMessage(String message) { + public AlertDialogWpp setMessage(CharSequence message) { if (isSystemDialog()) { mAlertDialog.setMessage(message); return this; diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/FMessageWpp.java b/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/FMessageWpp.java index d32e24b63..cbdc227ff 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/FMessageWpp.java +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/FMessageWpp.java @@ -196,6 +196,7 @@ public File getMediaFile() { */ public int getMediaType() { try { + if (mediaTypeField == null) return -1; return mediaTypeField.getInt(fmessage); } catch (Exception e) { XposedBridge.log(e); diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/WaContactWpp.java b/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/WaContactWpp.java index dedfca02e..25322682d 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/WaContactWpp.java +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/core/components/WaContactWpp.java @@ -31,18 +31,21 @@ public record WaContactWpp(Object mInstance) { private static Object mInstanceGetWaContact; private static Object mInstanceGetProfilePhoto; - public WaContactWpp(Object mInstance) { - if (TYPE == null) throw new RuntimeException("WaContactWpp not initialized"); - if (mInstance == null) throw new RuntimeException("object is null"); - if (!TYPE.isInstance(mInstance)) throw new RuntimeException("object is not a WaContactWpp"); + if (TYPE == null) + throw new RuntimeException("WaContactWpp not initialized"); + if (mInstance == null) + throw new RuntimeException("object is null"); + if (!TYPE.isInstance(mInstance)) + throw new RuntimeException("object is not a WaContactWpp"); this.mInstance = TYPE.cast(mInstance); } public static void initialize(ClassLoader classLoader) { try { TYPE = Unobfuscator.loadWaContactClass(classLoader); - var classPhoneUserJid = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, "jid.PhoneUserJid"); + var classPhoneUserJid = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, + "jid.PhoneUserJid"); var classJid = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, "jid.Jid"); var phoneUserJid = ReflectionUtils.getFieldByExtendType(TYPE, classPhoneUserJid); @@ -57,16 +60,40 @@ public static void initialize(ClassLoader classLoader) { fieldGetWaName = Unobfuscator.loadWaContactGetWaNameField(classLoader); getWaContactMethod = Unobfuscator.loadGetWaContactMethod(classLoader); - XposedBridge.hookAllConstructors(getWaContactMethod.getDeclaringClass(), new XC_MethodHook() { - @Override - protected void afterHookedMethod(MethodHookParam param) throws Throwable { - mInstanceGetWaContact = param.thisObject; + Class contactManagerClass = getWaContactMethod.getDeclaringClass(); + + // Try to find existing instance via static method (getInstance pattern) + for (Method m : contactManagerClass.getDeclaredMethods()) { + if (java.lang.reflect.Modifier.isStatic(m.getModifiers()) + && m.getReturnType() == contactManagerClass + && m.getParameterCount() == 0) { + try { + Object instance = m.invoke(null); + if (instance != null) { + mInstanceGetWaContact = instance; + XposedBridge.log("WAE: WaContactWpp: Captured instance via static method: " + m.getName()); + break; + } + } catch (Exception ignored) { + } } - }); + } + + if (mInstanceGetWaContact == null) { + XposedBridge.hookAllConstructors(contactManagerClass, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + mInstanceGetWaContact = param.thisObject; + XposedBridge.log("WAE: WaContactWpp: Captured instance via constructor"); + } + }); + } else { + XposedBridge.log("WAE: WaContactWpp: Instance already captured, skipping constructor hook"); + } + getProfilePhoto = Unobfuscator.loadGetProfilePhotoMethod(classLoader); getProfilePhotoHighQuality = Unobfuscator.loadGetProfilePhotoHighQMethod(classLoader); - XposedBridge.hookAllConstructors(getProfilePhoto.getDeclaringClass(), new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { @@ -83,7 +110,6 @@ public Object getObject() { return mInstance; } - public FMessageWpp.UserJid getUserJid() { try { if (fieldContactData != null) { @@ -117,8 +143,27 @@ public String getWaName() { } public static WaContactWpp getWaContactFromJid(FMessageWpp.UserJid userJid) { + if (mInstanceGetWaContact == null) { + XposedBridge.log("WAE: WaContactWpp: mInstanceGetWaContact is NULL. ContactManager not initialized?"); + return null; + } try { - return new WaContactWpp(getWaContactMethod.invoke(mInstanceGetWaContact, userJid.userJid)); + Object contact = null; + if (userJid.userJid != null) { + contact = getWaContactMethod.invoke(mInstanceGetWaContact, userJid.userJid); + } + + // Fallback to phoneJid if userJid lookup failed or userJid was null + if (contact == null && userJid.phoneJid != null) { + XposedBridge.log("WAE: WaContactWpp: userJid lookup failed, trying phoneJid"); + contact = getWaContactMethod.invoke(mInstanceGetWaContact, userJid.phoneJid); + } + + if (contact != null) { + return new WaContactWpp(contact); + } else { + XposedBridge.log("WAE: WaContactWpp: Contact lookup returned null for " + userJid); + } } catch (Exception e) { XposedBridge.log(e); } @@ -128,17 +173,18 @@ public static WaContactWpp getWaContactFromJid(FMessageWpp.UserJid userJid) { public File getProfilePhoto() { try { File file = (File) getProfilePhotoHighQuality.invoke(mInstanceGetProfilePhoto, mInstance); - if (file != null && file.exists()) return file; + if (file != null && file.exists()) + return file; } catch (Exception e) { XposedBridge.log(e); } try { return (File) getProfilePhoto.invoke(mInstanceGetProfilePhoto, mInstance); - } catch ( - Exception e) { + } catch (Exception e) { XposedBridge.log(e); } return null; + } } diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/core/db/DelMessageStore.java b/app/src/main/java/com/wmods/wppenhacer/xposed/core/db/DelMessageStore.java index 044d600fb..f6987404a 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/core/db/DelMessageStore.java +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/core/db/DelMessageStore.java @@ -13,8 +13,11 @@ public class DelMessageStore extends SQLiteOpenHelper { private static DelMessageStore mInstance; + private static final int DATABASE_VERSION = 10; + public static final String TABLE_DELETED_FOR_ME = "deleted_for_me"; + private DelMessageStore(@NonNull Context context) { - super(context, "delmessages.db", null, 5); + super(context, "delmessages.db", null, DATABASE_VERSION); } public static DelMessageStore getInstance(Context ctx) { @@ -33,6 +36,66 @@ public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVers sqLiteDatabase.execSQL("ALTER TABLE delmessages ADD COLUMN timestamp INTEGER DEFAULT 0;"); } } + if (oldVersion < 6) { + createDeletedForMeTable(sqLiteDatabase); + } + if (oldVersion < 7) { + if (!checkColumnExists(sqLiteDatabase, TABLE_DELETED_FOR_ME, "is_from_me")) { + try { + sqLiteDatabase.execSQL( + "ALTER TABLE " + TABLE_DELETED_FOR_ME + " ADD COLUMN is_from_me INTEGER DEFAULT 0;"); + } catch (Exception e) { + // Ignore if fails + } + } + } + if (oldVersion < 8) { + if (!checkColumnExists(sqLiteDatabase, TABLE_DELETED_FOR_ME, "contact_name")) { + try { + sqLiteDatabase.execSQL("ALTER TABLE " + TABLE_DELETED_FOR_ME + " ADD COLUMN contact_name TEXT;"); + } catch (Exception e) { + // Ignore if fails + } + } + } + if (oldVersion < 9) { + if (!checkColumnExists(sqLiteDatabase, TABLE_DELETED_FOR_ME, "package_name")) { + try { + sqLiteDatabase.execSQL("ALTER TABLE " + TABLE_DELETED_FOR_ME + + " ADD COLUMN package_name TEXT DEFAULT 'com.whatsapp';"); + } catch (Exception e) { + // Ignore if fails + } + } + } + if (oldVersion < 10) { + if (!checkColumnExists(sqLiteDatabase, TABLE_DELETED_FOR_ME, "original_timestamp")) { + try { + sqLiteDatabase.execSQL("ALTER TABLE " + TABLE_DELETED_FOR_ME + + " ADD COLUMN original_timestamp INTEGER DEFAULT 0;"); + } catch (Exception e) { + // Ignore if fails + } + } + } + } + + private void createDeletedForMeTable(SQLiteDatabase db) { + db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_DELETED_FOR_ME + " (" + + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "key_id TEXT, " + + "chat_jid TEXT, " + + "sender_jid TEXT, " + + "timestamp INTEGER, " + + "original_timestamp INTEGER DEFAULT 0, " + + "media_type INTEGER, " + + "text_content TEXT, " + + "media_path TEXT, " + + "media_caption TEXT, " + + "is_from_me INTEGER DEFAULT 0, " + + "contact_name TEXT, " + + "package_name TEXT, " + + "UNIQUE(key_id, chat_jid))"); } public void insertMessage(String jid, String msgid, long timestamp) { @@ -41,15 +104,163 @@ public void insertMessage(String jid, String msgid, long timestamp) { values.put("jid", jid); values.put("msgid", msgid); values.put("timestamp", timestamp); - dbWrite.insert("delmessages", null, values); + dbWrite.insertWithOnConflict("delmessages", null, values, SQLiteDatabase.CONFLICT_IGNORE); + } + } + + public void insertDeletedMessage(DeletedMessage message) { + try (SQLiteDatabase dbWrite = this.getWritableDatabase()) { + ContentValues values = new ContentValues(); + values.put("key_id", message.getKeyId()); + values.put("chat_jid", message.getChatJid()); + values.put("sender_jid", message.getSenderJid()); + values.put("timestamp", message.getTimestamp()); + values.put("original_timestamp", message.getOriginalTimestamp()); + values.put("media_type", message.getMediaType()); + values.put("text_content", message.getTextContent()); + values.put("media_path", message.getMediaPath()); + values.put("media_caption", message.getMediaCaption()); + values.put("is_from_me", message.isFromMe() ? 1 : 0); + values.put("contact_name", message.getContactName()); + values.put("package_name", message.getPackageName()); + dbWrite.insertWithOnConflict(TABLE_DELETED_FOR_ME, null, values, SQLiteDatabase.CONFLICT_REPLACE); + } + } + + public java.util.ArrayList getDeletedMessagesByChat(String chatJid) { + java.util.ArrayList messages = new java.util.ArrayList<>(); + SQLiteDatabase dbReader = this.getReadableDatabase(); + try (Cursor cursor = dbReader.query(TABLE_DELETED_FOR_ME, null, "chat_jid=?", new String[] { chatJid }, null, + null, "timestamp ASC")) { + if (cursor.moveToFirst()) { + do { + long originalTs = 0; + if (cursor.getColumnIndex("original_timestamp") != -1) { + originalTs = cursor.getLong(cursor.getColumnIndexOrThrow("original_timestamp")); + } + + messages.add(new DeletedMessage( + cursor.getLong(cursor.getColumnIndexOrThrow("_id")), + cursor.getString(cursor.getColumnIndexOrThrow("key_id")), + cursor.getString(cursor.getColumnIndexOrThrow("chat_jid")), + cursor.getString(cursor.getColumnIndexOrThrow("sender_jid")), + cursor.getLong(cursor.getColumnIndexOrThrow("timestamp")), + originalTs, + cursor.getInt(cursor.getColumnIndexOrThrow("media_type")), + cursor.getString(cursor.getColumnIndexOrThrow("text_content")), + cursor.getString(cursor.getColumnIndexOrThrow("media_path")), + cursor.getString(cursor.getColumnIndexOrThrow("media_caption")), + cursor.getInt(cursor.getColumnIndexOrThrow("is_from_me")) == 1, + cursor.getString(cursor.getColumnIndexOrThrow("contact_name")), + cursor.getString(cursor.getColumnIndexOrThrow("package_name")))); + } while (cursor.moveToNext()); + } + } + return messages; + } + + public java.util.ArrayList getAllDeletedMessages() { + return getDeletedMessages(false); + } + + public java.util.ArrayList getDeletedMessages(boolean isGroup) { + java.util.ArrayList messages = new java.util.ArrayList<>(); + SQLiteDatabase dbReader = this.getReadableDatabase(); + String selection = isGroup ? "chat_jid LIKE '%@g.us'" : "chat_jid NOT LIKE '%@g.us'"; + + try (Cursor cursor = dbReader.query(TABLE_DELETED_FOR_ME, null, selection, null, null, null, + "timestamp DESC")) { + if (cursor.moveToFirst()) { + do { + long originalTs = 0; + if (cursor.getColumnIndex("original_timestamp") != -1) { + originalTs = cursor.getLong(cursor.getColumnIndexOrThrow("original_timestamp")); + } + messages.add(new DeletedMessage( + cursor.getLong(cursor.getColumnIndexOrThrow("_id")), + cursor.getString(cursor.getColumnIndexOrThrow("key_id")), + cursor.getString(cursor.getColumnIndexOrThrow("chat_jid")), + cursor.getString(cursor.getColumnIndexOrThrow("sender_jid")), + cursor.getLong(cursor.getColumnIndexOrThrow("timestamp")), + originalTs, + cursor.getInt(cursor.getColumnIndexOrThrow("media_type")), + cursor.getString(cursor.getColumnIndexOrThrow("text_content")), + cursor.getString(cursor.getColumnIndexOrThrow("media_path")), + cursor.getString(cursor.getColumnIndexOrThrow("media_caption")), + cursor.getInt(cursor.getColumnIndexOrThrow("is_from_me")) == 1, + cursor.getString(cursor.getColumnIndexOrThrow("contact_name")), + cursor.getString(cursor.getColumnIndexOrThrow("package_name")))); + } while (cursor.moveToNext()); + } + } + return messages; + } + + public java.util.ArrayList getAllDeletedMessagesInternal() { + java.util.ArrayList messages = new java.util.ArrayList<>(); + SQLiteDatabase dbReader = this.getReadableDatabase(); + try (dbReader; + Cursor cursor = dbReader.query(TABLE_DELETED_FOR_ME, null, null, null, null, null, "timestamp DESC")) { + if (cursor.moveToFirst()) { + do { + long originalTs = 0; + if (cursor.getColumnIndex("original_timestamp") != -1) { + originalTs = cursor.getLong(cursor.getColumnIndexOrThrow("original_timestamp")); + } + messages.add(new DeletedMessage( + cursor.getLong(cursor.getColumnIndexOrThrow("_id")), + cursor.getString(cursor.getColumnIndexOrThrow("key_id")), + cursor.getString(cursor.getColumnIndexOrThrow("chat_jid")), + cursor.getString(cursor.getColumnIndexOrThrow("sender_jid")), + cursor.getLong(cursor.getColumnIndexOrThrow("timestamp")), + originalTs, + cursor.getInt(cursor.getColumnIndexOrThrow("media_type")), + cursor.getString(cursor.getColumnIndexOrThrow("text_content")), + cursor.getString(cursor.getColumnIndexOrThrow("media_path")), + cursor.getString(cursor.getColumnIndexOrThrow("media_caption")), + cursor.getInt(cursor.getColumnIndexOrThrow("is_from_me")) == 1, + cursor.getString(cursor.getColumnIndexOrThrow("contact_name")), + cursor.getString(cursor.getColumnIndexOrThrow("package_name")))); + } while (cursor.moveToNext()); + } + } + return messages; + } + + public void deleteMessage(String keyId) { + try (SQLiteDatabase dbWrite = this.getWritableDatabase()) { + dbWrite.delete(TABLE_DELETED_FOR_ME, "key_id=?", new String[] { keyId }); + } + } + + public void deleteMessages(java.util.List keyIds) { + if (keyIds == null || keyIds.isEmpty()) + return; + try (SQLiteDatabase dbWrite = this.getWritableDatabase()) { + StringBuilder args = new StringBuilder(); + for (int i = 0; i < keyIds.size(); i++) { + args.append("?,"); + } + if (args.length() > 0) + args.setLength(args.length() - 1); // remove last comma + dbWrite.delete(TABLE_DELETED_FOR_ME, "key_id IN (" + args.toString() + ")", keyIds.toArray(new String[0])); + } + } + + public void deleteMessagesByChat(String chatJid) { + try (SQLiteDatabase dbWrite = this.getWritableDatabase()) { + dbWrite.delete(TABLE_DELETED_FOR_ME, "chat_jid=?", new String[] { chatJid }); } } public HashSet getMessagesByJid(String jid) { HashSet messages = new HashSet<>(); - if (jid == null) return messages; + if (jid == null) + return messages; SQLiteDatabase dbReader = this.getReadableDatabase(); - try (dbReader; Cursor query = dbReader.query("delmessages", new String[]{"_id", "jid", "msgid"}, "jid=?", new String[]{jid}, null, null, null)) { + try (dbReader; + Cursor query = dbReader.query("delmessages", new String[] { "_id", "jid", "msgid" }, "jid=?", + new String[] { jid }, null, null, null)) { if (query.moveToFirst()) { do { messages.add(query.getString(query.getColumnIndexOrThrow("msgid"))); @@ -61,12 +272,16 @@ public HashSet getMessagesByJid(String jid) { @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { - sqLiteDatabase.execSQL("CREATE TABLE IF NOT EXISTS delmessages (_id INTEGER PRIMARY KEY AUTOINCREMENT, jid TEXT, msgid TEXT, timestamp INTEGER DEFAULT 0, UNIQUE(jid, msgid))"); + sqLiteDatabase.execSQL( + "CREATE TABLE IF NOT EXISTS delmessages (_id INTEGER PRIMARY KEY AUTOINCREMENT, jid TEXT, msgid TEXT, timestamp INTEGER DEFAULT 0, UNIQUE(jid, msgid))"); + createDeletedForMeTable(sqLiteDatabase); } public long getTimestampByMessageId(String msgid) { SQLiteDatabase dbReader = this.getReadableDatabase(); - try (dbReader; Cursor query = dbReader.query("delmessages", new String[]{"timestamp"}, "msgid=?", new String[]{msgid}, null, null, null)) { + try (dbReader; + Cursor query = dbReader.query("delmessages", new String[] { "timestamp" }, "msgid=?", + new String[] { msgid }, null, null, null)) { if (query.moveToFirst()) { return query.getLong(query.getColumnIndexOrThrow("timestamp")); } diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/core/db/DeletedMessage.java b/app/src/main/java/com/wmods/wppenhacer/xposed/core/db/DeletedMessage.java new file mode 100644 index 000000000..dae5684d0 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/core/db/DeletedMessage.java @@ -0,0 +1,177 @@ +package com.wmods.wppenhacer.xposed.core.db; + +import androidx.annotation.NonNull; + +public class DeletedMessage { + private long id; + private String keyId; + private String chatJid; + private String senderJid; + private long timestamp; + private long originalTimestamp; + private int mediaType; + private String textContent; + private String mediaPath; + private String mediaCaption; + private boolean isFromMe; + private String contactName; + private String packageName; + + public DeletedMessage(long id, String keyId, String chatJid, String senderJid, long timestamp, int mediaType, + String textContent, String mediaPath, String mediaCaption, boolean isFromMe) { + this(id, keyId, chatJid, senderJid, timestamp, 0, mediaType, textContent, mediaPath, mediaCaption, isFromMe, + null, + null); + } + + public DeletedMessage(long id, String keyId, String chatJid, String senderJid, long timestamp, int mediaType, + String textContent, String mediaPath, String mediaCaption, boolean isFromMe, String contactName) { + this(id, keyId, chatJid, senderJid, timestamp, 0, mediaType, textContent, mediaPath, mediaCaption, isFromMe, + contactName, null); + } + + public DeletedMessage(long id, String keyId, String chatJid, String senderJid, long timestamp, + long originalTimestamp, int mediaType, + String textContent, String mediaPath, String mediaCaption, boolean isFromMe, String contactName, + String packageName) { + this.id = id; + this.keyId = keyId; + this.chatJid = chatJid; + this.senderJid = senderJid; + this.timestamp = timestamp; + this.originalTimestamp = originalTimestamp; + this.mediaType = mediaType; + this.textContent = textContent; + this.mediaPath = mediaPath; + this.mediaCaption = mediaCaption; + this.isFromMe = isFromMe; + this.contactName = contactName; + this.packageName = packageName; + } + + public DeletedMessage() { + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getKeyId() { + return keyId; + } + + public void setKeyId(String keyId) { + this.keyId = keyId; + } + + public String getChatJid() { + return chatJid; + } + + public void setChatJid(String chatJid) { + this.chatJid = chatJid; + } + + public String getSenderJid() { + return senderJid; + } + + public void setSenderJid(String senderJid) { + this.senderJid = senderJid; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public long getOriginalTimestamp() { + return originalTimestamp; + } + + public void setOriginalTimestamp(long originalTimestamp) { + this.originalTimestamp = originalTimestamp; + } + + public int getMediaType() { + return mediaType; + } + + public void setMediaType(int mediaType) { + this.mediaType = mediaType; + } + + public String getTextContent() { + return textContent; + } + + public void setTextContent(String textContent) { + this.textContent = textContent; + } + + public String getMediaPath() { + return mediaPath; + } + + public void setMediaPath(String mediaPath) { + this.mediaPath = mediaPath; + } + + public String getMediaCaption() { + return mediaCaption; + } + + public void setMediaCaption(String mediaCaption) { + this.mediaCaption = mediaCaption; + } + + public boolean isFromMe() { + return isFromMe; + } + + public void setFromMe(boolean fromMe) { + isFromMe = fromMe; + } + + public String getContactName() { + return contactName; + } + + public void setContactName(String contactName) { + this.contactName = contactName; + } + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + @NonNull + @Override + public String toString() { + return "DeletedMessage{" + + "id=" + id + + ", keyId='" + keyId + '\'' + + ", chatJid='" + chatJid + '\'' + + ", senderJid='" + senderJid + '\'' + + ", timestamp=" + timestamp + + ", mediaType=" + mediaType + + ", textContent='" + textContent + '\'' + + ", mediaPath='" + mediaPath + '\'' + + ", mediaCaption='" + mediaCaption + '\'' + + ", isFromMe=" + isFromMe + + ", contactName='" + contactName + '\'' + + ", packageName='" + packageName + '\'' + + '}'; + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/core/devkit/Unobfuscator.java b/app/src/main/java/com/wmods/wppenhacer/xposed/core/devkit/Unobfuscator.java index ea9ebdb7d..ee989dcef 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/core/devkit/Unobfuscator.java +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/core/devkit/Unobfuscator.java @@ -81,78 +81,95 @@ public static boolean initWithPath(String path) { } // TODO: Functions to find classes and methods - public synchronized static Method findFirstMethodUsingStrings(ClassLoader classLoader, StringMatchType type, String... strings) throws Exception { + public synchronized static Method findFirstMethodUsingStrings(ClassLoader classLoader, StringMatchType type, + String... strings) throws Exception { MethodMatcher matcher = new MethodMatcher(); for (String string : strings) { matcher.addUsingString(string, type); } MethodDataList result = dexkit.findMethod(FindMethod.create().matcher(matcher)); - if (result.isEmpty()) return null; + if (result.isEmpty()) + return null; for (MethodData methodData : result) { - if (methodData.isMethod()) return methodData.getMethodInstance(classLoader); + if (methodData.isMethod()) + return methodData.getMethodInstance(classLoader); } return null; } - public synchronized static Method findFirstMethodUsingStringsFilter(ClassLoader classLoader, String packageFilter, StringMatchType type, String... strings) throws Exception { + public synchronized static Method findFirstMethodUsingStringsFilter(ClassLoader classLoader, String packageFilter, + StringMatchType type, String... strings) throws Exception { MethodMatcher matcher = new MethodMatcher(); for (String string : strings) { matcher.addUsingString(string, type); } MethodDataList result = dexkit.findMethod(FindMethod.create().searchPackages(packageFilter).matcher(matcher)); - if (result.isEmpty()) return null; + if (result.isEmpty()) + return null; for (MethodData methodData : result) { - if (methodData.isMethod()) return methodData.getMethodInstance(classLoader); + if (methodData.isMethod()) + return methodData.getMethodInstance(classLoader); } throw new NoSuchMethodException(); } - public synchronized static Method[] findAllMethodUsingStrings(ClassLoader classLoader, StringMatchType type, String... strings) { + public synchronized static Method[] findAllMethodUsingStrings(ClassLoader classLoader, StringMatchType type, + String... strings) { MethodMatcher matcher = new MethodMatcher(); for (String string : strings) { matcher.addUsingString(string, type); } MethodDataList result = dexkit.findMethod(FindMethod.create().matcher(matcher)); - if (result.isEmpty()) return new Method[0]; - return result.stream().filter(MethodData::isMethod).map(methodData -> convertRealMethod(methodData, classLoader)).filter(Objects::nonNull).toArray(Method[]::new); + if (result.isEmpty()) + return new Method[0]; + return result.stream().filter(MethodData::isMethod) + .map(methodData -> convertRealMethod(methodData, classLoader)).filter(Objects::nonNull) + .toArray(Method[]::new); } - public synchronized static Class findFirstClassUsingStrings(ClassLoader classLoader, StringMatchType type, String... strings) throws Exception { + public synchronized static Class findFirstClassUsingStrings(ClassLoader classLoader, StringMatchType type, + String... strings) throws Exception { var matcher = new ClassMatcher(); for (String string : strings) { matcher.addUsingString(string, type); } var result = dexkit.findClass(FindClass.create().matcher(matcher)); - if (result.isEmpty()) return null; + if (result.isEmpty()) + return null; return result.get(0).getInstance(classLoader); } - - public synchronized static Class[] findAllClassUsingStrings(ClassLoader classLoader, StringMatchType type, String... strings) throws Exception { + public synchronized static Class[] findAllClassUsingStrings(ClassLoader classLoader, StringMatchType type, + String... strings) throws Exception { var matcher = new ClassMatcher(); for (String string : strings) { matcher.addUsingString(string, type); } var result = dexkit.findClass(FindClass.create().matcher(matcher)); - if (result.isEmpty()) return null; - return result.stream().map(classData -> convertRealClass(classData, classLoader)).filter(Objects::nonNull).toArray(Class[]::new); + if (result.isEmpty()) + return null; + return result.stream().map(classData -> convertRealClass(classData, classLoader)).filter(Objects::nonNull) + .toArray(Class[]::new); } - - public synchronized static Class findFirstClassUsingStringsFilter(ClassLoader classLoader, String packageFilter, StringMatchType type, String... strings) throws Exception { + public synchronized static Class findFirstClassUsingStringsFilter(ClassLoader classLoader, String packageFilter, + StringMatchType type, String... strings) throws Exception { var matcher = new ClassMatcher(); for (String string : strings) { matcher.addUsingString(string, type); } var result = dexkit.findClass(FindClass.create().searchPackages(packageFilter).matcher(matcher)); - if (result.isEmpty()) return null; + if (result.isEmpty()) + return null; return result.get(0).getInstance(classLoader); } - public synchronized static Class findFirstClassUsingName(ClassLoader classLoader, StringMatchType type, String name) throws Exception { + public synchronized static Class findFirstClassUsingName(ClassLoader classLoader, StringMatchType type, + String name) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, name, () -> { - var result = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().className(name, type))).firstOrNull(); + var result = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().className(name, type))) + .firstOrNull(); if (result == null) throw new ClassNotFoundException("Class not found: " + name); return result.getInstance(classLoader); @@ -160,8 +177,10 @@ public synchronized static Class findFirstClassUsingName(ClassLoader classLoa } public synchronized static String getMethodDescriptor(Method method) { - if (method == null) return null; - return method.getDeclaringClass().getName() + "->" + method.getName() + "(" + Arrays.stream(method.getParameterTypes()).map(Class::getName).collect(Collectors.joining(",")) + ")"; + if (method == null) + return null; + return method.getDeclaringClass().getName() + "->" + method.getName() + "(" + + Arrays.stream(method.getParameterTypes()).map(Class::getName).collect(Collectors.joining(",")) + ")"; } public synchronized static String getFieldDescriptor(Field field) { @@ -188,14 +207,19 @@ public synchronized static Class convertRealClass(ClassData classData, ClassL // TODO: Classes and Methods for FreezeSeen public synchronized static Method loadFreezeSeenMethod(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "presencestatemanager/setAvailable/new-state"))); + return UnobfuscatorCache.getInstance().getMethod(classLoader, + () -> UnobfuscatorCache.getInstance().getMethod(classLoader, + () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, + "presencestatemanager/setAvailable/new-state"))); } // TODO: Classes and Methods for GhostMode public synchronized static Method loadGhostModeMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - Method method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "HandleMeComposing/sendComposing"); - if (method == null) throw new Exception("GhostMode method not found"); + Method method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, + "HandleMeComposing/sendComposing"); + if (method == null) + throw new Exception("GhostMode method not found"); if (method.getParameterTypes().length > 2 && method.getParameterTypes()[2] == int.class) return method; throw new Exception("GhostMode method not found parameter type"); @@ -206,15 +230,15 @@ public synchronized static Method loadGhostModeMethod(ClassLoader classLoader) t public synchronized static Method loadReceiptMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var classDeviceJid = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, "jid.DeviceJid"); - var classPhoneUserJid = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, "jid.PhoneUserJid"); + var classDeviceJid = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, + "jid.DeviceJid"); + var classPhoneUserJid = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, + "jid.PhoneUserJid"); var methods = dexkit.findMethod( FindMethod.create() .matcher(MethodMatcher.create() .addUsingString("receipt") - .paramCount(5, 8) - ) - ); + .paramCount(5, 8))); for (var method : methods) { var params = method.getParamTypeNames(); @@ -229,11 +253,15 @@ public synchronized static Method loadReceiptMethod(ClassLoader classLoader) thr public synchronized static Method loadReceiptOutsideChat(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { var method = loadReceiptMethod(classLoader); - if (method == null) throw new Exception("Receipt method not found"); + if (method == null) + throw new Exception("Receipt method not found"); var classData = dexkit.getClassData(method.getDeclaringClass()); - if (classData == null) throw new Exception("Receipt method not found"); - var methodResult = classData.findMethod(new FindMethod().matcher(new MethodMatcher().addUsingString("sender"))); - if (methodResult.isEmpty()) throw new Exception("Receipt method not found"); + if (classData == null) + throw new Exception("Receipt method not found"); + var methodResult = classData + .findMethod(new FindMethod().matcher(new MethodMatcher().addUsingString("sender"))); + if (methodResult.isEmpty()) + throw new Exception("Receipt method not found"); return methodResult.get(0).getMethodInstance(classLoader); }); } @@ -241,8 +269,11 @@ public synchronized static Method loadReceiptOutsideChat(ClassLoader classLoader public synchronized static Method loadReceiptInChat(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { var method = loadReceiptMethod(classLoader); - var methodDataList = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("callCreatorJid").addUsingString("reject").addInvoke(DexSignUtil.getMethodDescriptor(method)))); - if (methodDataList.isEmpty()) throw new Exception("Receipt method not found"); + var methodDataList = dexkit + .findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("callCreatorJid") + .addUsingString("reject").addInvoke(DexSignUtil.getMethodDescriptor(method)))); + if (methodDataList.isEmpty()) + throw new Exception("Receipt method not found"); return methodDataList.get(0).getMethodInstance(classLoader); }); } @@ -252,14 +283,16 @@ public synchronized static Method loadReceiptInChat(ClassLoader classLoader) thr public synchronized static Method loadForwardTagMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { Class messageInfoClass = loadFMessageClass(classLoader); - var methodList = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("chatInfo/incrementUnseenImportantMessageCount"))); - if (methodList.isEmpty()) throw new Exception("ForwardTag method support not found"); + var methodList = dexkit.findMethod(FindMethod.create() + .matcher(MethodMatcher.create().addUsingString("chatInfo/incrementUnseenImportantMessageCount"))); + if (methodList.isEmpty()) + throw new Exception("ForwardTag method support not found"); var invokes = methodList.get(0).getInvokes(); for (var invoke : invokes) { var method = invoke.getMethodInstance(classLoader); if (method.getParameterCount() == 1 && (method.getParameterTypes()[0] == int.class - || method.getParameterTypes()[0] == long.class) + || method.getParameterTypes()[0] == long.class) && method.getDeclaringClass() == messageInfoClass && method.getReturnType() == void.class) { return method; @@ -272,19 +305,25 @@ public synchronized static Method loadForwardTagMethod(ClassLoader classLoader) public synchronized static Field loadBroadcastTagField(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getField(classLoader, () -> { var fmessage = loadFMessageClass(classLoader); - var clazzData = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().addUsingString("UPDATE_MESSAGE_MAIN_BROADCAST_SCAN_SQL"))); - if (clazzData.isEmpty()) throw new Exception("BroadcastTag class not found"); - var methodData = dexkit.findMethod(FindMethod.create().searchInClass(clazzData).matcher(MethodMatcher.create().usingStrings("participant_hash", "view_mode", "broadcast"))); + var clazzData = dexkit.findClass(FindClass.create() + .matcher(ClassMatcher.create().addUsingString("UPDATE_MESSAGE_MAIN_BROADCAST_SCAN_SQL"))); + if (clazzData.isEmpty()) + throw new Exception("BroadcastTag class not found"); + var methodData = dexkit.findMethod(FindMethod.create().searchInClass(clazzData) + .matcher(MethodMatcher.create().usingStrings("participant_hash", "view_mode", "broadcast"))); // 2.25.18.xx, they splitted method and moved to the fmessage if (methodData.isEmpty()) { - methodData = dexkit.findMethod(FindMethod.create().searchInClass(clazzData).matcher(MethodMatcher.create().usingStrings("received_timestamp", "view_mode", "message"))); + methodData = dexkit.findMethod(FindMethod.create().searchInClass(clazzData) + .matcher(MethodMatcher.create().usingStrings("received_timestamp", "view_mode", "message"))); if (!methodData.isEmpty()) { var calledMethods = methodData.get(0).getInvokes(); for (var cmethod : calledMethods) { - if (Modifier.isStatic(cmethod.getModifiers()) && cmethod.getParamCount() == 2 && fmessage.getName().equals(cmethod.getDeclaredClass().getName())) { + if (Modifier.isStatic(cmethod.getModifiers()) && cmethod.getParamCount() == 2 + && fmessage.getName().equals(cmethod.getDeclaredClass().getName())) { var pTypes = cmethod.getParamTypes(); - if (pTypes.get(0).getName().equals(ContentValues.class.getName()) && pTypes.get(1).getName().equals(fmessage.getName())) { + if (pTypes.get(0).getName().equals(ContentValues.class.getName()) + && pTypes.get(1).getName().equals(fmessage.getName())) { methodData.clear(); methodData.add(cmethod); break; @@ -294,13 +333,13 @@ public synchronized static Field loadBroadcastTagField(ClassLoader classLoader) } } - if (methodData.isEmpty()) throw new Exception("BroadcastTag method support not found"); + if (methodData.isEmpty()) + throw new Exception("BroadcastTag method support not found"); var usingFields = methodData.get(0).getUsingFields(); for (var ufield : usingFields) { var field = ufield.getField(); if (field.getDeclaredClass().getName().equals(fmessage.getName()) && - field.getType().getName().equals(boolean.class.getName()) - ) { + field.getType().getName().equals(boolean.class.getName())) { return field.getFieldInstance(classLoader); } } @@ -310,39 +349,45 @@ public synchronized static Field loadBroadcastTagField(ClassLoader classLoader) public synchronized static Class loadForwardClassMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { - for (var s : new String[]{ + for (var s : new String[] { "UserActions/userActionForwardMessage", "UserActionsMessageForwarding/userActionForwardMessage" }) { var cls = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, s); - if (cls != null) return cls; + if (cls != null) + return cls; } throw new ClassNotFoundException("ForwardClass method not found"); }); } - // TODO: Classes and Methods for HideView public synchronized static Method loadHideViewSendReadJob(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var classData = dexkit.getClassData(findFirstClassUsingName(classLoader, StringMatchType.EndsWith, "SendReadReceiptJob")); - var methodResult = classData.findMethod(new FindMethod().matcher(new MethodMatcher().addUsingString("receipt", StringMatchType.Equals))); + var classData = dexkit + .getClassData(findFirstClassUsingName(classLoader, StringMatchType.EndsWith, "SendReadReceiptJob")); + var methodResult = classData.findMethod( + new FindMethod().matcher(new MethodMatcher().addUsingString("receipt", StringMatchType.Equals))); if (methodResult.isEmpty()) { - methodResult = classData.getSuperClass().findMethod(new FindMethod().matcher(new MethodMatcher().addUsingString("receipt", StringMatchType.Equals))); + methodResult = classData.getSuperClass().findMethod(new FindMethod() + .matcher(new MethodMatcher().addUsingString("receipt", StringMatchType.Equals))); } - if (methodResult.isEmpty()) throw new Exception("HideViewSendReadJob method not found"); + if (methodResult.isEmpty()) + throw new Exception("HideViewSendReadJob method not found"); return methodResult.get(0).getMethodInstance(classLoader); }); } public synchronized static Method loadHideViewInChatMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var strings = new String[]{ - "ReadReceipts/sendReceiptForIncomingMessage", "ReadReceipts/sendDeliveryReadReceipt", "ReadReceipts/acknowledgeMessageIfNeeded", "ReadReceipts/sendDeliveryReceiptIfNotRetry" + var strings = new String[] { + "ReadReceipts/sendReceiptForIncomingMessage", "ReadReceipts/sendDeliveryReadReceipt", + "ReadReceipts/acknowledgeMessageIfNeeded", "ReadReceipts/sendDeliveryReceiptIfNotRetry" }; for (var s : strings) { var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, s); - if (method != null) return method; + if (method != null) + return method; } throw new Exception("HideViewInChat method not found"); }); @@ -350,8 +395,10 @@ public synchronized static Method loadHideViewInChatMethod(ClassLoader classLoad public synchronized static Class loadFMessageClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { - var messageClass = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "FMessage/getSenderUserJid/key.id"); - if (messageClass == null) throw new Exception("Message class not found"); + var messageClass = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, + "FMessage/getSenderUserJid/key.id"); + if (messageClass == null) + throw new Exception("Message class not found"); return messageClass; }); } @@ -360,28 +407,38 @@ public synchronized static Class loadFMessageClass(ClassLoader classLoader) t public synchronized static Method loadTabListMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var classData = dexkit.findClass(FindClass.create().searchPackages("X.").matcher(ClassMatcher.create().addUsingString("mainContainer"))); - if (classData.isEmpty()) throw new Exception("mainContainer class not found"); + var classData = dexkit.findClass(FindClass.create().searchPackages("X.") + .matcher(ClassMatcher.create().addUsingString("mainContainer"))); + if (classData.isEmpty()) + throw new Exception("mainContainer class not found"); var classMain = classData.get(0).getInstance(classLoader); - Method method = Arrays.stream(classMain.getDeclaredMethods()).parallel().filter(m -> m.getName().equals("onCreate")).findFirst().orElse(null); - if (method == null) throw new Exception("onCreate method not found"); + Method method = Arrays.stream(classMain.getDeclaredMethods()).parallel() + .filter(m -> m.getName().equals("onCreate")).findFirst().orElse(null); + if (method == null) + throw new Exception("onCreate method not found"); return method; }); } public synchronized static Method loadGetTabMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - Method result = findFirstMethodUsingStringsFilter(classLoader, "X.", StringMatchType.Contains, "No HomeFragment mapping for community tab id:"); - if (result == null) throw new Exception("GetTab method not found"); + Method result = findFirstMethodUsingStringsFilter(classLoader, "X.", StringMatchType.Contains, + "No HomeFragment mapping for community tab id:"); + if (result == null) + throw new Exception("GetTab method not found"); return result; }); } public synchronized static Method loadTabFragmentMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - Class clsFrag = XposedHelpers.findClass("com.whatsapp.conversationslist.ConversationsFragment", classLoader); - Method result = Arrays.stream(clsFrag.getDeclaredMethods()).parallel().filter(m -> m.getParameterTypes().length == 0 && m.getReturnType().equals(List.class)).findFirst().orElse(null); - if (result == null) throw new Exception("TabFragment method not found"); + Class clsFrag = XposedHelpers.findClass("com.whatsapp.conversationslist.ConversationsFragment", + classLoader); + Method result = Arrays.stream(clsFrag.getDeclaredMethods()).parallel() + .filter(m -> m.getParameterTypes().length == 0 && m.getReturnType().equals(List.class)).findFirst() + .orElse(null); + if (result == null) + throw new Exception("TabFragment method not found"); return result; }); } @@ -389,9 +446,12 @@ public synchronized static Method loadTabFragmentMethod(ClassLoader classLoader) public synchronized static Method loadTabNameMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { int id = UnobfuscatorCache.getInstance().getOfuscateIDString("updates"); - if (id < 1) throw new Exception("TabName ID not found"); - MethodDataList result = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().returnType(String.class).usingNumbers(id))); - if (result.isEmpty()) throw new Exception("TabName method not found"); + if (id < 1) + throw new Exception("TabName ID not found"); + MethodDataList result = dexkit.findMethod( + FindMethod.create().matcher(MethodMatcher.create().returnType(String.class).usingNumbers(id))); + if (result.isEmpty()) + throw new Exception("TabName method not found"); return result.get(0).getMethodInstance(classLoader); }); } @@ -399,34 +459,40 @@ public synchronized static Method loadTabNameMethod(ClassLoader classLoader) thr public synchronized static Method loadFabMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { ClassData classData = dexkit.getClassData("com.whatsapp.conversationslist.ConversationsFragment"); - var result = classData.findMethod(FindMethod.create().matcher(MethodMatcher.create().paramCount(0).usingNumbers(200).returnType(int.class))); - if (result.isEmpty()) throw new Exception("Fab method not found"); + var result = classData.findMethod(FindMethod.create() + .matcher(MethodMatcher.create().paramCount(0).usingNumbers(200).returnType(int.class))); + if (result.isEmpty()) + throw new Exception("Fab method not found"); return result.get(0).getMethodInstance(classLoader); }); } public synchronized static Method loadIconTabMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - Method result = findFirstMethodUsingStringsFilter(classLoader, "X.", StringMatchType.Contains, "homeFabManager"); - if (result == null) throw new Exception("IconTab method not found"); + Method result = findFirstMethodUsingStringsFilter(classLoader, "X.", StringMatchType.Contains, + "homeFabManager"); + if (result == null) + throw new Exception("IconTab method not found"); return result; }); } - public synchronized static Method loadTabCountMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - Method result = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "required free space should be > 0"); - if (result == null) throw new Exception("TabCount method not found"); + Method result = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, + "required free space should be > 0"); + if (result == null) + throw new Exception("TabCount method not found"); return result; }); } - public synchronized static Method loadEnableCountTabMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var result = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "Tried to set badge for invalid"); - if (result == null) throw new Exception("EnableCountTab method not found"); + var result = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, + "Tried to set badge for invalid"); + if (result == null) + throw new Exception("EnableCountTab method not found"); return result; }); } @@ -435,8 +501,10 @@ public synchronized static Constructor loadEnableCountTabConstructor1(ClassLoade return UnobfuscatorCache.getInstance().getConstructor(classLoader, () -> { var countMethod = loadEnableCountTabMethod(classLoader); var indiceClass = countMethod.getParameterTypes()[1]; - var result = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().superClass(indiceClass.getName()).addMethod(MethodMatcher.create().paramCount(1)))); - if (result.isEmpty()) throw new Exception("EnableCountTab method not found"); + var result = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create() + .superClass(indiceClass.getName()).addMethod(MethodMatcher.create().paramCount(1)))); + if (result.isEmpty()) + throw new Exception("EnableCountTab method not found"); return result.get(0).getInstance(classLoader).getConstructors()[0]; }); } @@ -445,8 +513,11 @@ public synchronized static Constructor loadEnableCountTabConstructor2(ClassLoade return UnobfuscatorCache.getInstance().getConstructor(classLoader, () -> { var countTabConstructor1 = loadEnableCountTabConstructor1(classLoader); var indiceClass = countTabConstructor1.getParameterTypes()[0]; - var result = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().superClass(indiceClass.getName()).addMethod(MethodMatcher.create().paramCount(1).addParamType(int.class)))); - if (result.isEmpty()) throw new Exception("EnableCountTab method not found"); + var result = dexkit + .findClass(FindClass.create().matcher(ClassMatcher.create().superClass(indiceClass.getName()) + .addMethod(MethodMatcher.create().paramCount(1).addParamType(int.class)))); + if (result.isEmpty()) + throw new Exception("EnableCountTab method not found"); return result.get(0).getInstance(classLoader).getConstructors()[0]; }); } @@ -455,8 +526,10 @@ public synchronized static Constructor loadEnableCountTabConstructor3(ClassLoade return UnobfuscatorCache.getInstance().getConstructor(classLoader, () -> { var countTabConstructor1 = loadEnableCountTabConstructor1(classLoader); var indiceClass = countTabConstructor1.getParameterTypes()[0]; - var result = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().superClass(indiceClass.getName()).addMethod(MethodMatcher.create().paramCount(0)))); - if (result.isEmpty()) throw new Exception("EnableCountTab method not found"); + var result = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create() + .superClass(indiceClass.getName()).addMethod(MethodMatcher.create().paramCount(0)))); + if (result.isEmpty()) + throw new Exception("EnableCountTab method not found"); return result.get(0).getInstance(classLoader).getConstructors()[0]; }); } @@ -465,11 +538,14 @@ public synchronized static Constructor loadEnableCountTabConstructor3(ClassLoade public synchronized static Method loadTimeToSecondsMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { Class cls = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "aBhHKm"); - if (cls == null) throw new Exception("TimeToSeconds class not found"); + if (cls == null) + throw new Exception("TimeToSeconds class not found"); var clsData = dexkit.getClassData(cls); var method = XposedHelpers.findMethodBestMatch(Calendar.class, "setTimeInMillis", long.class); - var result = clsData.findMethod(new FindMethod().matcher(new MethodMatcher().addInvoke(DexSignUtil.getMethodDescriptor(method)).returnType(String.class).paramCount(2))); - if (result.isEmpty()) throw new Exception("TimeToSeconds method not found"); + var result = clsData.findMethod(new FindMethod().matcher(new MethodMatcher() + .addInvoke(DexSignUtil.getMethodDescriptor(method)).returnType(String.class).paramCount(2))); + if (result.isEmpty()) + throw new Exception("TimeToSeconds method not found"); return result.get(0).getMethodInstance(classLoader); }); } @@ -479,7 +555,8 @@ public synchronized static Method loadTimeToSecondsMethod(ClassLoader classLoade public synchronized static Method loadDndModeMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Equals, "MessageHandler/start"); - if (method == null) throw new Exception("DndMode method not found"); + if (method == null) + throw new Exception("DndMode method not found"); return method; }); } @@ -488,12 +565,14 @@ public synchronized static Method loadDndModeMethod(ClassLoader classLoader) thr public synchronized static Method loadMediaQualityVideoMethod2(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "getCorrectedResolution"); - if (method == null) throw new Exception("MediaQualityVideo method not found"); + if (method == null) + throw new Exception("MediaQualityVideo method not found"); return method; }); } - public synchronized static HashMap loadMediaQualityVideoFields(ClassLoader classLoader) throws Exception { + public synchronized static HashMap loadMediaQualityVideoFields(ClassLoader classLoader) + throws Exception { return UnobfuscatorCache.getInstance().getMapField(classLoader, () -> { var method = loadMediaQualityVideoMethod2(classLoader); var methodString = method.getReturnType().getDeclaredMethod("toString"); @@ -504,7 +583,8 @@ public synchronized static HashMap loadMediaQualityVideoFields(Cl var idxStrings = 0; var idxFields = 0; while (idxStrings < usingStrings.size()) { - if (idxFields == usingFields.size()) break; + if (idxFields == usingFields.size()) + break; if (usingStrings.get(idxStrings).equals("outputAspectRatio")) { idxStrings++; continue; @@ -518,7 +598,8 @@ public synchronized static HashMap loadMediaQualityVideoFields(Cl }); } - public synchronized static HashMap loadMediaQualityOriginalVideoFields(ClassLoader classLoader) throws Exception { + public synchronized static HashMap loadMediaQualityOriginalVideoFields(ClassLoader classLoader) + throws Exception { return UnobfuscatorCache.getInstance().getMapField(classLoader, () -> { var method = loadMediaQualityVideoMethod2(classLoader); Method methodString; @@ -532,7 +613,8 @@ public synchronized static HashMap loadMediaQualityOriginalVideoF var usingStrings = Objects.requireNonNull(methodData).getUsingStrings(); var result = new HashMap(); for (int i = 0; i < usingStrings.size(); i++) { - if (i == usingFields.size()) break; + if (i == usingFields.size()) + break; var field = usingFields.get(i).getField().getFieldInstance(classLoader); result.put(usingStrings.get(i), field); } @@ -543,13 +625,14 @@ public synchronized static HashMap loadMediaQualityOriginalVideoF public synchronized static Class loadProcessVideoQualityClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "ProcessVideoQuality("); - if (clazz == null) throw new Exception("ProcessVideoQuality method not found"); + if (clazz == null) + throw new Exception("ProcessVideoQuality method not found"); return clazz; }); } - - public synchronized static HashMap loadProcessVideoQualityFields(ClassLoader classLoader) throws Exception { + public synchronized static HashMap loadProcessVideoQualityFields(ClassLoader classLoader) + throws Exception { return UnobfuscatorCache.getInstance().getMapField(classLoader, () -> { var clazz = loadProcessVideoQualityClass(classLoader); Method methodString; @@ -564,13 +647,17 @@ public synchronized static HashMap loadProcessVideoQualityFields( var result = new HashMap(); var idxFields = 0; for (int i = 0; i < usingStrings.size(); i++) { - if (idxFields == usingFields.size()) break; + if (idxFields == usingFields.size()) + break; var raw = usingStrings.get(i); - if (raw == null) continue; + if (raw == null) + continue; var string = raw.strip(); - if (string.isEmpty()) continue; + if (string.isEmpty()) + continue; int eq = string.lastIndexOf('='); - if (eq < 0) continue; + if (eq < 0) + continue; int start = 0; for (int j = eq - 1; j >= 0; j--) { char c = string.charAt(j); @@ -579,9 +666,11 @@ public synchronized static HashMap loadProcessVideoQualityFields( break; } } - if (start >= eq) continue; + if (start >= eq) + continue; var name = string.substring(start, eq); - if (name.isEmpty()) continue; + if (name.isEmpty()) + continue; var field = usingFields.get(idxFields).getField().getFieldInstance(classLoader); result.put(name, field); idxFields++; @@ -592,11 +681,11 @@ public synchronized static HashMap loadProcessVideoQualityFields( // TODO: Classes and methods to ShareLimit - public synchronized static Method loadShareLimitMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "send_max_video_duration"); - if (method == null) throw new Exception("ShareLimit method not found"); + if (method == null) + throw new Exception("ShareLimit method not found"); return method; }); } @@ -608,7 +697,8 @@ public synchronized static Field loadShareMapItemField(ClassLoader classLoader) var usingFields = Objects.requireNonNull(methodData).getUsingFields(); for (var ufield : usingFields) { var field = ufield.getField().getFieldInstance(classLoader); - if (field.getType() == Map.class) return field; + if (field.getType() == Map.class) + return field; } throw new Exception("ShareItem field not found"); }); @@ -618,18 +708,21 @@ public synchronized static Field loadShareMapItemField(ClassLoader classLoader) public synchronized static Method loadStatusActivePage(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "playbackFragment/setPageActive"); - if (method == null) throw new Exception("StatusActivePage method not found"); + var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, + "playbackFragment/setPageActive"); + if (method == null) + throw new Exception("StatusActivePage method not found"); return method; }); } - public synchronized static Class loadMenuManagerClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { - var methods = findAllMethodUsingStrings(classLoader, StringMatchType.Contains, "MenuPopupHelper cannot be used without an anchor"); + var methods = findAllMethodUsingStrings(classLoader, StringMatchType.Contains, + "MenuPopupHelper cannot be used without an anchor"); for (var method : methods) { - if (method.getReturnType() == void.class) return method.getDeclaringClass(); + if (method.getReturnType() == void.class) + return method.getDeclaringClass(); } throw new Exception("MenuManager class not found"); }); @@ -639,7 +732,8 @@ public synchronized static Method loadMenuStatusMethod(ClassLoader loader) throw return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var id = Utils.getID("menuitem_conversations_message_contact", "id"); var methods = dexkit.findMethod(new FindMethod().matcher(new MethodMatcher().addUsingNumber(id))); - if (methods.isEmpty()) throw new Exception("MenuStatus method not found"); + if (methods.isEmpty()) + throw new Exception("MenuStatus method not found"); return methods.get(0).getMethodInstance(loader); }); } @@ -648,24 +742,30 @@ public synchronized static Method loadMenuStatusMethod(ClassLoader loader) throw public synchronized static Method[] loadViewOnceMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethods(classLoader, () -> { - var method = dexkit.findMethod(new FindMethod().matcher(new MethodMatcher().addUsingString("INSERT_VIEW_ONCE_SQL", StringMatchType.Contains))); - if (method.isEmpty()) throw new Exception("ViewOnce method not found"); + var method = dexkit.findMethod(new FindMethod() + .matcher(new MethodMatcher().addUsingString("INSERT_VIEW_ONCE_SQL", StringMatchType.Contains))); + if (method.isEmpty()) + throw new Exception("ViewOnce method not found"); var methodData = method.get(0); var listMethods = methodData.getInvokes(); var list = new ArrayList(); for (MethodData m : listMethods) { var mInstance = m.getMethodInstance(classLoader); - if (mInstance.getDeclaringClass().isInterface() && mInstance.getDeclaringClass().getMethods().length == 2) { - ClassDataList listClasses = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().addInterface(mInstance.getDeclaringClass().getName()))); + if (mInstance.getDeclaringClass().isInterface() + && mInstance.getDeclaringClass().getMethods().length == 2) { + ClassDataList listClasses = dexkit.findClass(FindClass.create() + .matcher(ClassMatcher.create().addInterface(mInstance.getDeclaringClass().getName()))); for (ClassData c : listClasses) { Class clazz = c.getInstance(classLoader); for (Method m2 : clazz.getDeclaredMethods()) { - if (m2.getParameterCount() != 1 || m2.getParameterTypes()[0] != int.class || m2.getReturnType() != void.class) + if (m2.getParameterCount() != 1 || m2.getParameterTypes()[0] != int.class + || m2.getReturnType() != void.class) continue; list.add(m2); } } - if (list.isEmpty()) throw new Exception("ViewOnce method not found"); + if (list.isEmpty()) + throw new Exception("ViewOnce method not found"); return list.toArray(new Method[0]); } } @@ -673,7 +773,6 @@ public synchronized static Method[] loadViewOnceMethod(ClassLoader classLoader) }); } - /** * @noinspection SimplifyOptionalCallChains */ @@ -683,20 +782,20 @@ public synchronized static Method loadViewOnceDownloadMenuMethod(ClassLoader cla var method = Arrays.stream(clazz.getDeclaredMethods()).filter(m -> m.getParameterCount() == 2 && Objects.equals(m.getParameterTypes()[0], Menu.class) && Objects.equals(m.getParameterTypes()[1], MenuInflater.class) && - m.getDeclaringClass() == clazz - ).findFirst(); - if (!method.isPresent()) throw new Exception("ViewOnceDownloadMenu method not found"); + m.getDeclaringClass() == clazz).findFirst(); + if (!method.isPresent()) + throw new Exception("ViewOnceDownloadMenu method not found"); return method.get(); }); } - // TODO: Methods and Classes for Change Colors public synchronized static Class loadExpandableWidgetClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, "expandableWidgetHelper"); - if (clazz == null) throw new Exception("ExpandableWidgetHelper class not found"); + if (clazz == null) + throw new Exception("ExpandableWidgetHelper class not found"); return clazz; }); } @@ -704,7 +803,8 @@ public synchronized static Class loadExpandableWidgetClass(ClassLoader loader public synchronized static Class loadMaterialShapeDrawableClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, "Compatibility shadow requested"); - if (clazz == null) throw new Exception("MaterialShapeDrawable class not found"); + if (clazz == null) + throw new Exception("MaterialShapeDrawable class not found"); return clazz; }); } @@ -712,7 +812,8 @@ public synchronized static Class loadMaterialShapeDrawableClass(ClassLoader l public synchronized static Method loadPropsBooleanMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "Unknown BooleanField"); - if (method == null) throw new Exception("Props method not found"); + if (method == null) + throw new Exception("Props method not found"); return method; }); } @@ -720,7 +821,8 @@ public synchronized static Method loadPropsBooleanMethod(ClassLoader loader) thr public synchronized static Method loadPropsIntegerMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "Unknown IntField"); - if (method == null) throw new Exception("Props method not found"); + if (method == null) + throw new Exception("Props method not found"); return method; }); } @@ -728,19 +830,20 @@ public synchronized static Method loadPropsIntegerMethod(ClassLoader loader) thr public synchronized static Method loadPropsJsonMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "Unknown JsonField"); - if (method == null) throw new Exception("Props method not found"); + if (method == null) + throw new Exception("Props method not found"); return method; }); } - private static ClassData loadAntiRevokeImplClass() throws Exception { - var classes = dexkit.findClass(new FindClass().matcher(new ClassMatcher().addUsingString("smb_eu_tos_update_url"))); - if (classes.isEmpty()) throw new Exception("AntiRevokeImpl class not found"); + var classes = dexkit + .findClass(new FindClass().matcher(new ClassMatcher().addUsingString("smb_eu_tos_update_url"))); + if (classes.isEmpty()) + throw new Exception("AntiRevokeImpl class not found"); return classes.get(0); } - public synchronized static Method loadHomeConversationFragmentMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var homeClass = WppCore.getHomeActivityClass(loader); @@ -750,37 +853,44 @@ public synchronized static Method loadHomeConversationFragmentMethod(ClassLoader Collections.singletonList( dexkit.getClassData(homeClass))) .matcher(MethodMatcher.create().returnType(convFragment))).singleOrNull(); - if (method == null) throw new Exception("HomeConversationFragmentMethod not found"); + if (method == null) + throw new Exception("HomeConversationFragmentMethod not found"); return method.getMethodInstance(loader); }); } public synchronized static Field loadAntiRevokeConvFragmentField(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getField(loader, () -> { - Class chatClass = findFirstClassUsingStrings(loader, StringMatchType.Contains, "conversation/createconversation"); + Class chatClass = findFirstClassUsingStrings(loader, StringMatchType.Contains, + "conversation/createconversation"); Class conversation = XposedHelpers.findClass("com.whatsapp.ConversationFragment", loader); Field field = ReflectionUtils.getFieldByType(conversation, chatClass); - if (field == null) throw new Exception("AntiRevokeConvChat field not found"); + if (field == null) + throw new Exception("AntiRevokeConvChat field not found"); return field; }); } public synchronized static Field loadAntiRevokeConvChatField(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getField(loader, () -> { - Class chatClass = findFirstClassUsingStrings(loader, StringMatchType.Contains, "conversation/createconversation"); + Class chatClass = findFirstClassUsingStrings(loader, StringMatchType.Contains, + "conversation/createconversation"); Class conversation = XposedHelpers.findClass("com.whatsapp.Conversation", loader); Field field = ReflectionUtils.getFieldByType(conversation, chatClass); - if (field == null) throw new Exception("AntiRevokeConvChat field not found"); + if (field == null) + throw new Exception("AntiRevokeConvChat field not found"); return field; }); } public synchronized static Field loadAntiRevokeChatJidField(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getField(loader, () -> { - Class chatClass = findFirstClassUsingStrings(loader, StringMatchType.Contains, "conversation/createconversation"); + Class chatClass = findFirstClassUsingStrings(loader, StringMatchType.Contains, + "conversation/createconversation"); Class jidClass = Unobfuscator.findFirstClassUsingName(loader, StringMatchType.EndsWith, "jid.Jid"); Field field = ReflectionUtils.getFieldByExtendType(chatClass, jidClass); - if (field == null) throw new Exception("AntiRevokeChatJid field not found"); + if (field == null) + throw new Exception("AntiRevokeChatJid field not found"); return field; }); } @@ -788,20 +898,79 @@ public synchronized static Field loadAntiRevokeChatJidField(ClassLoader loader) public synchronized static Method loadAntiRevokeMessageMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { Method method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "msgstore/edit/revoke"); - if (method == null) throw new Exception("AntiRevokeMessage method not found"); + if (method == null) + throw new Exception("AntiRevokeMessage method not found"); return method; }); } + public synchronized static Class loadSettingsGoogleDriveActivity(ClassLoader loader) throws Exception { + return UnobfuscatorCache.getInstance().getClass(loader, () -> { + var classes = findAllClassUsingStrings(loader, StringMatchType.Contains, "SettingsGoogleDrive"); + if (classes == null) + throw new Exception("SettingsGoogleDriveActivity not found (No classes with string)"); + + StringBuilder candidates = new StringBuilder(); + for (var cls : classes) { + String name = cls.getName(); + if (name.contains("com.whatsapp.deeplink")) + continue; + + candidates.append(name).append(", "); + + // Check for onCreate method (Standard for Activity/Fragment) + try { + // Check declared methods for "onCreate" + for (Method m : cls.getDeclaredMethods()) { + if (m.getName().equals("onCreate")) { + return cls; + } + } + } catch (Throwable t) { + // Ignore reflection errors + } + } + throw new Exception("SettingsGoogleDriveActivity not found. Candidates checked: " + candidates.toString()); + + }); + } + + public synchronized static Class loadRestoreBackupActivity(ClassLoader loader) throws Exception { + return UnobfuscatorCache.getInstance().getClass(loader, () -> { + var strings = new String[] { "RestoreFromBackupActivity", "gdrive/restore/activity", + "gdrive_restore_title" }; + for (String s : strings) { + var classes = findAllClassUsingStrings(loader, StringMatchType.Contains, s); + if (classes != null) { + for (var cls : classes) { + try { + for (Method m : cls.getDeclaredMethods()) { + if (m.getName().equals("onCreate")) { + return cls; + } + } + } catch (Throwable t) { + } + } + } + + } + throw new Exception("RestoreBackupActivity not found"); + }); + } + public synchronized static Field loadMessageKeyField(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getField(loader, () -> { - var classList = dexkit.findClass(new FindClass().matcher(new ClassMatcher().fieldCount(3).addMethod(new MethodMatcher().addUsingString("Key").name("toString")))); - if (classList.isEmpty()) throw new Exception("MessageKey class not found"); + var classList = dexkit.findClass(new FindClass().matcher(new ClassMatcher().fieldCount(3) + .addMethod(new MethodMatcher().addUsingString("Key").name("toString")))); + if (classList.isEmpty()) + throw new Exception("MessageKey class not found"); for (ClassData classData : classList) { Class keyMessageClass = classData.getInstance(loader); var classMessage = loadFMessageClass(loader); var fields = ReflectionUtils.getFieldsByExtendType(classMessage, keyMessageClass); - if (fields.isEmpty()) continue; + if (fields.isEmpty()) + continue; return fields.get(fields.size() - 1); } throw new Exception("MessageKey field not found"); @@ -810,24 +979,38 @@ public synchronized static Field loadMessageKeyField(ClassLoader loader) throws public synchronized static Class loadConversationRowClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { - var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, "ConversationRow/setupUserNameInGroupView/"); - if (clazz != null) return clazz; + var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, + "ConversationRow/setupUserNameInGroupView/"); + if (clazz != null) + return clazz; var conversation_header = Utils.getID("conversation_row_participant_header_view_stub", "id"); var nameId = Utils.getID("name_in_group", "id"); - var classData = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().addMethod(MethodMatcher.create().addUsingNumber(conversation_header).addUsingNumber(nameId)))).singleOrNull(); - if (classData == null) throw new Exception("ConversationRow class not found"); + var classData = dexkit + .findClass(FindClass.create() + .matcher(ClassMatcher.create().addMethod( + MethodMatcher.create().addUsingNumber(conversation_header).addUsingNumber(nameId)))) + .singleOrNull(); + if (classData == null) + throw new Exception("ConversationRow class not found"); return classData.getInstance(loader); }); } public synchronized static Method loadUnknownStatusPlaybackMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var statusPlaybackClass = XposedHelpers.findClass("com.whatsapp.status.playback.fragment.StatusPlaybackContactFragment", loader); - var refreshCurrentPage = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("playbackFragment/refreshCurrentPageSubTitle message is empty"))).get(0); + var statusPlaybackClass = XposedHelpers + .findClass("com.whatsapp.status.playback.fragment.StatusPlaybackContactFragment", loader); + var refreshCurrentPage = dexkit + .findMethod(FindMethod.create() + .matcher(MethodMatcher.create() + .addUsingString("playbackFragment/refreshCurrentPageSubTitle message is empty"))) + .get(0); var invokes = refreshCurrentPage.getInvokes(); for (var invoke : invokes) { var method = invoke.getMethodInstance(loader); - if (Modifier.isStatic(method.getModifiers()) && method.getParameterCount() > 1 && List.of(method.getParameterTypes()).contains(statusPlaybackClass) && method.getDeclaringClass() == statusPlaybackClass) { + if (Modifier.isStatic(method.getModifiers()) && method.getParameterCount() > 1 + && List.of(method.getParameterTypes()).contains(statusPlaybackClass) + && method.getDeclaringClass() == statusPlaybackClass) { return method; } } @@ -841,20 +1024,18 @@ public synchronized static Class loadStatusPlaybackViewClass(ClassLoader loader) var clazz = dexkit.findClass( FindClass.create().matcher( ClassMatcher.create().addMethod( - MethodMatcher.create().usingNumbers(ids) - ) - ) - ); - if (clazz.isEmpty()) throw new Exception("Not Found StatusPlaybackViewClass"); + MethodMatcher.create().usingNumbers(ids)))); + if (clazz.isEmpty()) + throw new Exception("Not Found StatusPlaybackViewClass"); return clazz.get(0).getInstance(loader); }); } - public synchronized static Method loadBlueOnReplayMessageJobMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var result = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "SendE2EMessageJob/onRun"); - if (result == null) throw new Exception("BlueOnReplayMessageJob method not found"); + if (result == null) + throw new Exception("BlueOnReplayMessageJob method not found"); return result; }); } @@ -863,36 +1044,46 @@ public synchronized static Method loadBlueOnReplayWaJobManagerMethod(ClassLoader return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var result = findFirstClassUsingStrings(loader, StringMatchType.Contains, "WaJobManager/start"); var job = XposedHelpers.findClass("org.whispersystems.jobqueue.Job", loader); - if (result == null) throw new Exception("BlueOnReplayWaJobManager method not found"); - var method = Arrays.stream(result.getMethods()).filter(m -> m.getParameterCount() == 1 && m.getParameterTypes()[0] == job).findFirst().orElse(null); - if (method == null) throw new Exception("BlueOnReplayWaJobManager method not found"); + if (result == null) + throw new Exception("BlueOnReplayWaJobManager method not found"); + var method = Arrays.stream(result.getMethods()) + .filter(m -> m.getParameterCount() == 1 && m.getParameterTypes()[0] == job).findFirst() + .orElse(null); + if (method == null) + throw new Exception("BlueOnReplayWaJobManager method not found"); return method; }); } public synchronized static Class loadArchiveChatClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { - var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, "archive/set-content-indicator-to-empty"); + var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, + "archive/set-content-indicator-to-empty"); + if (clazz == null) + clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, + "archive/Unsupported mode in ArchivePreviewView:"); if (clazz == null) - clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, "archive/Unsupported mode in ArchivePreviewView:"); - if (clazz == null) throw new Exception("ArchiveHideView method not found"); + throw new Exception("ArchiveHideView method not found"); return clazz; }); } - public synchronized static Method loadAntiRevokeOnCallReceivedMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "voip/callStateChangedOnUIThread"); - if (method == null) throw new Exception("OnCallReceiver method not found"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "voip/callStateChangedOnUIThread"); + if (method == null) + throw new Exception("OnCallReceiver method not found"); return method; }); } public synchronized static Method loadOnChangeStatus(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - Method method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "ConversationViewFiller/setParentGroupProfilePhoto"); - if (method == null) throw new Exception("OnChangeStatus method not found"); + Method method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "ConversationViewFiller/setParentGroupProfilePhoto"); + if (method == null) + throw new Exception("OnChangeStatus method not found"); // for 19.xx, the current implementation returns wrong method if (method.getParameterCount() < 6) { @@ -926,10 +1117,12 @@ public synchronized static Class loadViewHolder(ClassLoader loader) throws Ex Utils.getID("conversations_row_header_stub", "id"), Utils.getID("pin_indicator", "id"), Utils.getID("mute_indicator", "id"), - Utils.getID("contact_photo", "id") - ); - var methods = dexkit.findMethod(FindMethod.create().matcher(methodMatcher)).stream().filter(methodData -> methodData.getParamTypes().get(0).getName().equals(Context.class.getName())).collect(Collectors.toList()); - if (methods.isEmpty()) throw new ClassNotFoundException("View Holder not found!"); + Utils.getID("contact_photo", "id")); + var methods = dexkit.findMethod(FindMethod.create().matcher(methodMatcher)).stream() + .filter(methodData -> methodData.getParamTypes().get(0).getName().equals(Context.class.getName())) + .collect(Collectors.toList()); + if (methods.isEmpty()) + throw new ClassNotFoundException("View Holder not found!"); return methods.get(0).getMethodInstance(loader).getDeclaringClass(); }); } @@ -944,65 +1137,82 @@ public synchronized static Field loadViewHolderField1(ClassLoader loader) throws public synchronized static Method loadStatusUserMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var id = UnobfuscatorCache.getInstance().getOfuscateIDString("lastseensun%s"); - if (id < 1) throw new Exception("GetStatusUser ID not found"); - var result = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingNumber(id).returnType(String.class))); - if (result.isEmpty()) throw new Exception("GetStatusUser method not found"); + if (id < 1) + throw new Exception("GetStatusUser ID not found"); + var result = dexkit.findMethod( + FindMethod.create().matcher(MethodMatcher.create().addUsingNumber(id).returnType(String.class))); + if (result.isEmpty()) + throw new Exception("GetStatusUser method not found"); return result.get(result.size() - 1).getMethodInstance(loader); }); } public synchronized static Method loadSendPresenceMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var methodData = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("app/send-presence-subscription jid="))); - if (methodData.isEmpty()) throw new Exception("SendPresence method not found"); + var methodData = dexkit.findMethod(FindMethod.create() + .matcher(MethodMatcher.create().addUsingString("app/send-presence-subscription jid="))); + if (methodData.isEmpty()) + throw new Exception("SendPresence method not found"); var methodCallers = methodData.get(0).getCallers(); if (methodCallers.isEmpty()) { var method = methodData.get(0); var superMethodInterfaces = method.getDeclaredClass().getInterfaces(); if (superMethodInterfaces.isEmpty()) throw new Exception("SendPresence method interface list empty"); - var superMethod = superMethodInterfaces.get(0).findMethod(FindMethod.create().matcher(MethodMatcher.create().name(method.getName()))).firstOrNull(); + var superMethod = superMethodInterfaces.get(0) + .findMethod(FindMethod.create().matcher(MethodMatcher.create().name(method.getName()))) + .firstOrNull(); if (superMethod == null) throw new Exception("SendPresence method interface method not found"); methodCallers = superMethod.getCallers(); } var newMethod = methodCallers.firstOrNull(method1 -> method1.getParamCount() == 4); - if (newMethod == null) throw new Exception("SendPresence method not found 2"); + if (newMethod == null) + throw new Exception("SendPresence method not found 2"); return newMethod.getMethodInstance(loader); }); } - public synchronized static Method loadPinnedHashSetMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "getPinnedJids/QUERY_CHAT_SETTINGS"); - if (method == null) throw new Exception("PinnedHashSet method not found"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "getPinnedJids/QUERY_CHAT_SETTINGS"); + if (method == null) + throw new Exception("PinnedHashSet method not found"); return method; }); } public synchronized static Method loadGetFiltersMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var clazzFilters = findFirstClassUsingStrings(loader, StringMatchType.Contains, "conversations/filter/performFiltering"); - if (clazzFilters == null) throw new RuntimeException("Filters class not found"); - return Arrays.stream(clazzFilters.getDeclaredMethods()).parallel().filter(m -> m.getName().equals("publishResults")).findFirst().orElse(null); + var clazzFilters = findFirstClassUsingStrings(loader, StringMatchType.Contains, + "conversations/filter/performFiltering"); + if (clazzFilters == null) + throw new RuntimeException("Filters class not found"); + return Arrays.stream(clazzFilters.getDeclaredMethods()).parallel() + .filter(m -> m.getName().equals("publishResults")).findFirst().orElse(null); }); } public synchronized static Method loadPinnedInChatMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = dexkit.findMethod(new FindMethod().matcher(new MethodMatcher().addUsingNumber(3732).returnType(int.class))); - if (method.isEmpty()) throw new RuntimeException("PinnedInChat method not found"); + var method = dexkit.findMethod( + new FindMethod().matcher(new MethodMatcher().addUsingNumber(3732).returnType(int.class))); + if (method.isEmpty()) + throw new RuntimeException("PinnedInChat method not found"); return method.get(0).getMethodInstance(loader); }); } - public synchronized static Method loadBlueOnReplayCreateMenuConversationMethod(ClassLoader loader) throws Exception { + public synchronized static Method loadBlueOnReplayCreateMenuConversationMethod(ClassLoader loader) + throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var conversationClass = XposedHelpers.findClass("com.whatsapp.Conversation", loader); if (conversationClass == null) throw new RuntimeException("BlueOnReplayCreateMenuConversation class not found"); - var method = Arrays.stream(conversationClass.getDeclaredMethods()).filter(m -> m.getParameterCount() == 1 && m.getParameterTypes()[0].equals(Menu.class)).findFirst().orElse(null); + var method = Arrays.stream(conversationClass.getDeclaredMethods()) + .filter(m -> m.getParameterCount() == 1 && m.getParameterTypes()[0].equals(Menu.class)).findFirst() + .orElse(null); if (method == null) throw new RuntimeException("BlueOnReplayCreateMenuConversation method not found"); return method; @@ -1011,7 +1221,8 @@ public synchronized static Method loadBlueOnReplayCreateMenuConversationMethod(C public synchronized static Method loadBlueOnReplayViewButtonMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "PLAYBACK_PAGE_ITEM_ON_CREATE_VIEW_END"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "PLAYBACK_PAGE_ITEM_ON_CREATE_VIEW_END"); if (method == null) throw new RuntimeException("BlueOnReplayViewButton method not found"); return method; @@ -1035,7 +1246,8 @@ public synchronized static Field loadBlueOnReplayViewButtonOutSideField(ClassLoa public synchronized static Method loadBlueOnReplayStatusViewMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "StatusPlaybackPage/onViewCreated"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "StatusPlaybackPage/onViewCreated"); if (method == null) throw new RuntimeException("BlueOnReplayViewButton method not found"); return method; @@ -1044,17 +1256,22 @@ public synchronized static Method loadBlueOnReplayStatusViewMethod(ClassLoader l public synchronized static Method loadChatLimitDeleteMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, "app/time server update processed"); - if (clazz == null) throw new RuntimeException("ChatLimitDelete class not found"); - var method = Arrays.stream(clazz.getDeclaredMethods()).filter(m -> m.getReturnType().equals(long.class) && Modifier.isStatic(m.getModifiers())).findFirst().orElse(null); + var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, + "app/time server update processed"); + if (clazz == null) + throw new RuntimeException("ChatLimitDelete class not found"); + var method = Arrays.stream(clazz.getDeclaredMethods()) + .filter(m -> m.getReturnType().equals(long.class) && Modifier.isStatic(m.getModifiers())) + .findFirst().orElse(null); if (method == null) { - var methodList = Objects.requireNonNull(dexkit.getClassData(clazz)).findMethod(new FindMethod().matcher(new MethodMatcher().opCodes(new OpCodesMatcher().opNames( - List.of("invoke-static", - "move-result-wide", "iget-wide", "const-wide/16", "cmp-long", - "if-eqz", "iget-wide", "add-long/2addr", "return-wide", - "iget-wide", "cmp-long", "if-eqz", "iget-wide", - "goto", "invoke-static", "move-result-wide", "iget-wide", - "sub-long/2addr", "return-wide"))))); + var methodList = Objects.requireNonNull(dexkit.getClassData(clazz)) + .findMethod(new FindMethod().matcher(new MethodMatcher().opCodes(new OpCodesMatcher().opNames( + List.of("invoke-static", + "move-result-wide", "iget-wide", "const-wide/16", "cmp-long", + "if-eqz", "iget-wide", "add-long/2addr", "return-wide", + "iget-wide", "cmp-long", "if-eqz", "iget-wide", + "goto", "invoke-static", "move-result-wide", "iget-wide", + "sub-long/2addr", "return-wide"))))); if (methodList.isEmpty()) throw new RuntimeException("ChatLimitDelete method not found"); method = methodList.get(0).getMethodInstance(loader); @@ -1065,8 +1282,10 @@ public synchronized static Method loadChatLimitDeleteMethod(ClassLoader loader) public synchronized static Method loadChatLimitDelete2Method(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "pref_revoke_admin_nux", "dialog/delete no messages"); - if (method == null) throw new RuntimeException("ChatLimitDelete2 method not found"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "pref_revoke_admin_nux", + "dialog/delete no messages"); + if (method == null) + throw new RuntimeException("ChatLimitDelete2 method not found"); return method; }); } @@ -1074,40 +1293,58 @@ public synchronized static Method loadChatLimitDelete2Method(ClassLoader loader) public synchronized static Method loadNewMessageMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var clazzMessageName = loadFMessageClass(loader).getName(); - var listMethods = dexkit.findMethod(FindMethod.create().searchPackages("com.whatsapp").matcher(MethodMatcher.create().addUsingString("extra_payment_note", StringMatchType.Equals))); - if (listMethods.isEmpty()) throw new Exception("NewMessage method not found"); + var listMethods = dexkit.findMethod(FindMethod.create().searchPackages("com.whatsapp") + .matcher(MethodMatcher.create().addUsingString("extra_payment_note", StringMatchType.Equals))); + if (listMethods.isEmpty()) + throw new Exception("NewMessage method not found"); var invokes = listMethods.get(0).getInvokes(); - var method = invokes.parallelStream().filter(invoke -> clazzMessageName.equals(invoke.getDeclaredClass().getName()) && invoke.getReturnType() != null && invoke.getReturnType().getName().equals("java.lang.String")).findFirst().orElse(null); - if (method == null) throw new RuntimeException("NewMessage method not found"); + var method = invokes.parallelStream() + .filter(invoke -> clazzMessageName.equals(invoke.getDeclaredClass().getName()) + && invoke.getReturnType() != null + && invoke.getReturnType().getName().equals("java.lang.String")) + .findFirst().orElse(null); + if (method == null) + throw new RuntimeException("NewMessage method not found"); return method.getMethodInstance(loader); }); } public synchronized static Method loadOriginalMessageKey(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "FMessageUtil/getOriginalMessageKeyIfEdited"); - if (method == null) throw new RuntimeException("MessageEdit method not found"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "FMessageUtil/getOriginalMessageKeyIfEdited"); + if (method == null) + throw new RuntimeException("MessageEdit method not found"); return method; }); } public synchronized static Method loadNewMessageWithMediaMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var methodList = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("INSERT_TABLE_MESSAGE_QUOTED", StringMatchType.Equals))); - if (methodList.isEmpty()) throw new Exception("NewMessageWithMedia method not found"); + var methodList = dexkit.findMethod(FindMethod.create().matcher( + MethodMatcher.create().addUsingString("INSERT_TABLE_MESSAGE_QUOTED", StringMatchType.Equals))); + if (methodList.isEmpty()) + throw new Exception("NewMessageWithMedia method not found"); var methodData = methodList.get(0); var invokes = methodData.getInvokes(); var clazzMessageName = loadFMessageClass(loader).getName(); - var method = invokes.parallelStream().filter(invoke -> clazzMessageName.equals(invoke.getDeclaredClass().getName()) && invoke.getReturnType() != null && invoke.getReturnType().getName().equals("java.lang.String")).findFirst().orElse(null); - if (method == null) throw new RuntimeException("NewMessageWithMedia method not found"); + var method = invokes.parallelStream() + .filter(invoke -> clazzMessageName.equals(invoke.getDeclaredClass().getName()) + && invoke.getReturnType() != null + && invoke.getReturnType().getName().equals("java.lang.String")) + .findFirst().orElse(null); + if (method == null) + throw new RuntimeException("NewMessageWithMedia method not found"); return method.getMethodInstance(loader); }); } public synchronized static Method loadMessageEditMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "MessageEditInfoStore/insertEditInfo/missing"); - if (method == null) throw new RuntimeException("MessageEdit method not found"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "MessageEditInfoStore/insertEditInfo/missing"); + if (method == null) + throw new RuntimeException("MessageEdit method not found"); return method; }); } @@ -1118,12 +1355,12 @@ public synchronized static Method loadCallerMessageEditMethod(ClassLoader loader var FMessage = loadFMessageClass(loader); var invokes = methodData1.getInvokes(); for (var methodData : invokes) { - if (methodData.isConstructor()) continue; + if (methodData.isConstructor()) + continue; var method = methodData.getMethodInstance(loader); if (Modifier.isStatic(method.getModifiers()) && method.getParameterCount() == 1 && method.getParameterTypes()[0].equals(FMessage) && - !method.getReturnType().isPrimitive() - ) { + !method.getReturnType().isPrimitive()) { return methodData.getMethodInstance(loader); } } @@ -1131,23 +1368,28 @@ public synchronized static Method loadCallerMessageEditMethod(ClassLoader loader }); } - public synchronized static Method loadGetEditMessageMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "MessageEditInfoStore/insertEditInfo/missing"); - if (method == null) throw new RuntimeException("GetEditMessage method not found"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "MessageEditInfoStore/insertEditInfo/missing"); + if (method == null) + throw new RuntimeException("GetEditMessage method not found"); var methodData = dexkit.getMethodData(DexSignUtil.getMethodDescriptor(method)); - if (methodData == null) throw new RuntimeException("GetEditMessage method not found"); + if (methodData == null) + throw new RuntimeException("GetEditMessage method not found"); var invokes = methodData.getInvokes(); for (var invoke : invokes) { // pre 21.xx method - if (invoke.getParamTypes().isEmpty() && Objects.equals(invoke.getDeclaredClass(), methodData.getParamTypes().get(0))) { + if (invoke.getParamTypes().isEmpty() + && Objects.equals(invoke.getDeclaredClass(), methodData.getParamTypes().get(0))) { return invoke.getMethodInstance(loader); } // 21.xx+ method (static) // 25.xx+ added additional type check - if (Modifier.isStatic(invoke.getMethodInstance(loader).getModifiers()) && Objects.equals(invoke.getParamTypes().get(0), methodData.getParamTypes().get(0)) && !Objects.equals(invoke.getParamTypes().get(0), invoke.getDeclaredClass())) { + if (Modifier.isStatic(invoke.getMethodInstance(loader).getModifiers()) + && Objects.equals(invoke.getParamTypes().get(0), methodData.getParamTypes().get(0)) + && !Objects.equals(invoke.getParamTypes().get(0), invoke.getDeclaredClass())) { return invoke.getMethodInstance(loader); } } @@ -1155,20 +1397,26 @@ public synchronized static Method loadGetEditMessageMethod(ClassLoader loader) t }); } + /** + * @noinspection DataFlowIssue + */ /** * @noinspection DataFlowIssue */ public synchronized static Field loadSetEditMessageField(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getField(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "CoreMessageStore/updateCheckoutMessageWithTransactionInfo"); + var classData = dexkit.getClassData(loadCoreMessageStore(loader)); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "CoreMessageStore/updateCheckoutMessageWithTransactionInfo"); if (method == null) - method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "UPDATE_MESSAGE_ADD_ON_FLAGS_MAIN_SQL"); - var classData = dexkit.getClassData(loadFMessageClass(loader)); + method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "UPDATE_MESSAGE_ADD_ON_FLAGS_MAIN_SQL"); var methodData = dexkit.getMethodData(DexSignUtil.getMethodDescriptor(method)); var usingFields = methodData.getUsingFields(); for (var f : usingFields) { var field = f.getField(); - if (field.getDeclaredClass().equals(classData) && field.getType().getName().equals(long.class.getName())) { + if (field.getDeclaredClass().equals(classData) + && field.getType().getName().equals(long.class.getName())) { return field.getFieldInstance(loader); } } @@ -1176,6 +1424,18 @@ public synchronized static Field loadSetEditMessageField(ClassLoader loader) thr }); } + public synchronized static Class loadCoreMessageStore(ClassLoader loader) throws Exception { + return UnobfuscatorCache.getInstance().getClass(loader, () -> { + var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, + "CoreMessageStore/updateCheckoutMessageWithTransactionInfo"); + if (clazz == null) + clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, + "UPDATE_MESSAGE_ADD_ON_FLAGS_MAIN_SQL"); + if (clazz == null) + throw new Exception("CoreMessageStore class not found"); + return clazz; + }); + } /** * @noinspection DataFlowIssue @@ -1183,37 +1443,44 @@ public synchronized static Field loadSetEditMessageField(ClassLoader loader) thr public synchronized static Class loadDialogViewClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { var id = Utils.getID("touch_outside", "id"); - var result = dexkit.findMethod(new FindMethod().matcher(new MethodMatcher().addUsingNumber(id).returnType(FrameLayout.class))); - if (result.isEmpty()) throw new RuntimeException("DialogView class not found"); - return result.get(0).getDeclaredClass().getInstance(loader); + var results = dexkit.findMethod( + new FindMethod().matcher(new MethodMatcher().addUsingNumber(id).returnType(FrameLayout.class))); + if (results.isEmpty()) + throw new Exception("DialogView class not found"); + return results.get(0).getDeclaredClass().getInstance(loader); }); } public synchronized static Constructor loadRecreateFragmentConstructor(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getConstructor(loader, () -> { - var data = dexkit.findMethod(FindMethod.create().searchPackages("X.").matcher(MethodMatcher.create().addUsingString("Instantiated fragment"))); - if (data.isEmpty()) throw new RuntimeException("RecreateFragment method not found"); + var data = dexkit.findMethod(FindMethod.create().searchPackages("X.") + .matcher(MethodMatcher.create().addUsingString("Instantiated fragment"))); + if (data.isEmpty()) + throw new RuntimeException("RecreateFragment method not found"); if (!data.single().isConstructor()) throw new RuntimeException("RecreateFragment method not found"); return data.single().getConstructorInstance(loader); }); } - public synchronized static Method loadOnTabItemAddMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var result = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "Maximum number of items supported by"); - if (result == null) throw new RuntimeException("OnTabItemAdd method not found"); + var result = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "Maximum number of items supported by"); + if (result == null) + throw new RuntimeException("OnTabItemAdd method not found"); return result; }); } - public synchronized static Method loadGetViewConversationMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var clazz = XposedHelpers.findClass("com.whatsapp.conversationslist.ConversationsFragment", loader); - var method = Arrays.stream(clazz.getDeclaredMethods()).filter(m -> m.getParameterCount() == 3 && m.getReturnType().equals(View.class) && m.getParameterTypes()[1].equals(LayoutInflater.class)).findFirst().orElse(null); - if (method == null) throw new RuntimeException("GetViewConversation method not found"); + var method = Arrays.stream(clazz.getDeclaredMethods()).filter(m -> m.getParameterCount() == 3 + && m.getReturnType().equals(View.class) && m.getParameterTypes()[1].equals(LayoutInflater.class)) + .findFirst().orElse(null); + if (method == null) + throw new RuntimeException("GetViewConversation method not found"); return method; }); } @@ -1224,14 +1491,13 @@ public synchronized static Method loadGetViewConversationMethod(ClassLoader load public synchronized static Method loadOnMenuItemSelected(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var aClass = XposedHelpers.findClass("androidx.viewpager.widget.ViewPager", loader); - var result = Arrays.stream(aClass.getDeclaredMethods()). - filter(m -> m.getParameterCount() == 4 && - m.getParameterTypes()[0].equals(int.class) && - m.getParameterTypes()[1].equals(int.class) && - m.getParameterTypes()[2].equals(boolean.class) && - m.getParameterTypes()[3].equals(boolean.class) - ).collect(Collectors.toList()); - if (result.isEmpty()) throw new RuntimeException("OnMenuItemSelected method not found"); + var result = Arrays.stream(aClass.getDeclaredMethods()).filter(m -> m.getParameterCount() == 4 && + m.getParameterTypes()[0].equals(int.class) && + m.getParameterTypes()[1].equals(int.class) && + m.getParameterTypes()[2].equals(boolean.class) && + m.getParameterTypes()[3].equals(boolean.class)).collect(Collectors.toList()); + if (result.isEmpty()) + throw new RuntimeException("OnMenuItemSelected method not found"); return result.get(1); }); } @@ -1240,10 +1506,13 @@ public synchronized static Method loadOnUpdateStatusChanged(ClassLoader loader) return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var clazz = getClassByName("UpdatesViewModel", loader); var clazzData = dexkit.getClassData(clazz); - var methodSeduleche = XposedHelpers.findMethodBestMatch(Timer.class, "schedule", TimerTask.class, long.class, long.class); - var result = dexkit.findMethod(new FindMethod().searchInClass(List.of(clazzData)).matcher(new MethodMatcher().addInvoke(DexSignUtil.getMethodDescriptor(methodSeduleche)))); + var methodSeduleche = XposedHelpers.findMethodBestMatch(Timer.class, "schedule", TimerTask.class, + long.class, long.class); + var result = dexkit.findMethod(new FindMethod().searchInClass(List.of(clazzData)) + .matcher(new MethodMatcher().addInvoke(DexSignUtil.getMethodDescriptor(methodSeduleche)))); if (result.isEmpty()) - result = dexkit.findMethod(new FindMethod().searchInClass(List.of(clazzData)).matcher(new MethodMatcher().addUsingString("UpdatesViewModel/Scheduled updates list refresh"))); + result = dexkit.findMethod(new FindMethod().searchInClass(List.of(clazzData)).matcher( + new MethodMatcher().addUsingString("UpdatesViewModel/Scheduled updates list refresh"))); if (result.isEmpty()) throw new RuntimeException("OnUpdateStatusChanged method not found"); return result.get(0).getMethodInstance(loader); @@ -1258,8 +1527,10 @@ public synchronized static Field loadGetInvokeField(ClassLoader loader) throws E var method = loadOnUpdateStatusChanged(loader); var methodData = dexkit.getMethodData(DexSignUtil.getMethodDescriptor(method)); var fields = methodData.getUsingFields(); - var field = fields.stream().map(UsingFieldData::getField).filter(f -> f.getDeclaredClass().equals(methodData.getDeclaredClass())).findFirst().orElse(null); - if (field == null) throw new RuntimeException("GetInvokeField method not found"); + var field = fields.stream().map(UsingFieldData::getField) + .filter(f -> f.getDeclaredClass().equals(methodData.getDeclaredClass())).findFirst().orElse(null); + if (field == null) + throw new RuntimeException("GetInvokeField method not found"); return field.getFieldInstance(loader); }); } @@ -1267,7 +1538,8 @@ public synchronized static Field loadGetInvokeField(ClassLoader loader) throws E public synchronized static Class loadStatusInfoClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, "ContactStatusDataItem"); - if (clazz == null) throw new RuntimeException("StatusInfo class not found"); + if (clazz == null) + throw new RuntimeException("StatusInfo class not found"); return clazz; }); } @@ -1275,7 +1547,8 @@ public synchronized static Class loadStatusInfoClass(ClassLoader loader) thro public synchronized static Class loadStatusListUpdatesClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, "StatusListUpdates"); - if (clazz == null) throw new RuntimeException("StatusListUpdates class not found"); + if (clazz == null) + throw new RuntimeException("StatusListUpdates class not found"); return clazz; }); } @@ -1283,7 +1556,8 @@ public synchronized static Class loadStatusListUpdatesClass(ClassLoader loader) public synchronized static Class loadTabFrameClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, "android:menu:presenters"); - if (clazz == null) throw new RuntimeException("TabFrame class not found"); + if (clazz == null) + throw new RuntimeException("TabFrame class not found"); return clazz; }); } @@ -1291,15 +1565,18 @@ public synchronized static Class loadTabFrameClass(ClassLoader loader) throws Ex public synchronized static Class loadRemoveChannelRecClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, "hasNewsletterSubscriptions"); - if (clazz == null) throw new RuntimeException("RemoveChannelRec class not found"); + if (clazz == null) + throw new RuntimeException("RemoveChannelRec class not found"); return clazz; }); } public synchronized static Class loadFilterAdaperClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { - var clazzList = dexkit.findClass(new FindClass().matcher(new ClassMatcher().addMethod(new MethodMatcher().addUsingString("CONTACTS_FILTER").paramCount(1).addParamType(int.class)))); - if (clazzList.isEmpty()) throw new RuntimeException("FilterAdapter class not found"); + var clazzList = dexkit.findClass(new FindClass().matcher(new ClassMatcher().addMethod( + new MethodMatcher().addUsingString("CONTACTS_FILTER").paramCount(1).addParamType(int.class)))); + if (clazzList.isEmpty()) + throw new RuntimeException("FilterAdapter class not found"); return clazzList.get(0).getInstance(loader); }); } @@ -1318,11 +1595,14 @@ public synchronized static Constructor loadSeeMoreConstructor(ClassLoader loader })); var clazzData = dexkit.findClass(FindClass.create().searchIn(arrayList).matcher(ClassMatcher.create() - .addMethod(MethodMatcher.create().addUsingNumber(16384).addUsingNumber(512).addUsingNumber(64).addUsingNumber(16)) - )).singleOrNull(); - if (clazzData == null) throw new RuntimeException("SeeMore constructor 1 not found"); + .addMethod(MethodMatcher.create().addUsingNumber(16384).addUsingNumber(512).addUsingNumber(64) + .addUsingNumber(16)))) + .singleOrNull(); + if (clazzData == null) + throw new RuntimeException("SeeMore constructor 1 not found"); for (var method : clazzData.getMethods()) { - if (method.getParamCount() > 1 && method.isConstructor() && method.getParamTypes().stream().allMatch(c -> c.getName().equals(int.class.getName()))) { + if (method.getParamCount() > 1 && method.isConstructor() + && method.getParamTypes().stream().allMatch(c -> c.getName().equals(int.class.getName()))) { return method.getConstructorInstance(loader); } } @@ -1332,8 +1612,10 @@ public synchronized static Constructor loadSeeMoreConstructor(ClassLoader loader public synchronized static Method[] loadSendStickerMethods(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethods(loader, () -> { - var methods = findAllMethodUsingStrings(loader, StringMatchType.Contains, "StickerGridViewItem.StickerLocal"); - if (methods == null) throw new RuntimeException("SendSticker method not found"); + var methods = findAllMethodUsingStrings(loader, StringMatchType.Contains, + "StickerGridViewItem.StickerLocal"); + if (methods == null) + throw new RuntimeException("SendSticker method not found"); return methods; }); @@ -1341,17 +1623,21 @@ public synchronized static Method[] loadSendStickerMethods(ClassLoader loader) t public synchronized static Method loadMaterialAlertDialog(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var callConfirmationFragment = XposedHelpers.findClass("com.whatsapp.calling.fragment.CallConfirmationFragment", loader); - var method = ReflectionUtils.findMethodUsingFilter(callConfirmationFragment, m -> m.getParameterCount() == 1 && m.getParameterTypes()[0].equals(android.os.Bundle.class)); + var callConfirmationFragment = XposedHelpers + .findClass("com.whatsapp.calling.fragment.CallConfirmationFragment", loader); + var method = ReflectionUtils.findMethodUsingFilter(callConfirmationFragment, + m -> m.getParameterCount() == 1 && m.getParameterTypes()[0].equals(android.os.Bundle.class)); var methodData = dexkit.getMethodData(method); var invokes = methodData.getInvokes(); for (var invoke : invokes) { - if (invoke.isMethod() && Modifier.isStatic(invoke.getModifiers()) && invoke.getParamCount() == 1 && invoke.getParamTypes().get(0).getName().equals(Context.class.getName())) { + if (invoke.isMethod() && Modifier.isStatic(invoke.getModifiers()) && invoke.getParamCount() == 1 + && invoke.getParamTypes().get(0).getName().equals(Context.class.getName())) { return invoke.getMethodInstance(loader); } // for 22.xx, MaterialAlertDialog method is not static - if (invoke.isMethod() && invoke.getParamCount() == 1 && invoke.getParamTypes().get(0).getName().equals(Context.class.getName())) { + if (invoke.isMethod() && invoke.getParamCount() == 1 + && invoke.getParamTypes().get(0).getName().equals(Context.class.getName())) { return invoke.getMethodInstance(loader); } } @@ -1361,7 +1647,9 @@ public synchronized static Method loadMaterialAlertDialog(ClassLoader loader) th public synchronized static Method loadGetIntPreferences(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var methodList = dexkit.findMethod(new FindMethod().matcher(new MethodMatcher().paramCount(2).addParamType(SharedPreferences.class).addParamType(String.class).modifiers(Modifier.STATIC | Modifier.PUBLIC).returnType(int.class))); + var methodList = dexkit.findMethod(new FindMethod().matcher( + new MethodMatcher().paramCount(2).addParamType(SharedPreferences.class).addParamType(String.class) + .modifiers(Modifier.STATIC | Modifier.PUBLIC).returnType(int.class))); if (methodList.isEmpty()) throw new RuntimeException("CallConfirmationLimit method not found"); return methodList.get(0).getMethodInstance(loader); @@ -1370,15 +1658,18 @@ public synchronized static Method loadGetIntPreferences(ClassLoader loader) thro public synchronized static Method loadAudioProximitySensorMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "messageaudioplayer/onearproximity"); - if (method == null) throw new RuntimeException("ProximitySensor method not found"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "messageaudioplayer/onearproximity"); + if (method == null) + throw new RuntimeException("ProximitySensor method not found"); return method; }); } public synchronized static Method loadGroupAdminMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().name("setupUsernameInGroupViewContainer"))); + var method = dexkit.findMethod( + FindMethod.create().matcher(MethodMatcher.create().name("setupUsernameInGroupViewContainer"))); if (method.isEmpty()) throw new RuntimeException("GroupAdmin method not found"); return method.get(0).getMethodInstance(loader); @@ -1387,8 +1678,10 @@ public synchronized static Method loadGroupAdminMethod(ClassLoader loader) throw public synchronized static Method loadJidFactory(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "lid_me", "status_me", "s.whatsapp.net"); - if (method == null) throw new RuntimeException("JidFactory method not found"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "lid_me", "status_me", + "s.whatsapp.net"); + if (method == null) + throw new RuntimeException("JidFactory method not found"); return method; }); } @@ -1396,11 +1689,18 @@ public synchronized static Method loadJidFactory(ClassLoader loader) throws Exce public synchronized static Method loadGroupCheckAdminMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var classData = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().addUsingString("saveGroupParticipants/INSERT_GROUP_PARTICIPANT_USER"))).singleOrNull(); + var classData = dexkit + .findClass(FindClass.create() + .matcher(ClassMatcher.create() + .addUsingString("saveGroupParticipants/INSERT_GROUP_PARTICIPANT_USER"))) + .singleOrNull(); var GroupChatClass = findFirstClassUsingName(loader, StringMatchType.EndsWith, "GroupChatInfoActivity"); - var onCreateMenu = ReflectionUtils.findMethodUsingFilter(GroupChatClass, method -> method.getName().equals("onCreateContextMenu")); + var onCreateMenu = ReflectionUtils.findMethodUsingFilter(GroupChatClass, + method -> method.getName().equals("onCreateContextMenu")); var onCreateMenuData = dexkit.getMethodData(onCreateMenu); - var invokes = onCreateMenuData.getInvokes().stream().filter(m -> Objects.equals(m.getDeclaredClassName(), classData.getName())).collect(Collectors.toList()); + var invokes = onCreateMenuData.getInvokes().stream() + .filter(m -> Objects.equals(m.getDeclaredClassName(), classData.getName())) + .collect(Collectors.toList()); for (var invoke : invokes) { var invokeMethod = invoke.getMethodInstance(loader); if (invokeMethod.getParameterCount() != 2 || invokeMethod.getReturnType() != boolean.class) @@ -1417,7 +1717,8 @@ public synchronized static Method loadGroupCheckAdminMethod(ClassLoader loader) public synchronized static Constructor loadStartPrefsConfig(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getConstructor(loader, () -> { - var results = dexkit.findMethod(new FindMethod().matcher(new MethodMatcher().addUsingString("startup_migrated_version"))); + var results = dexkit.findMethod( + new FindMethod().matcher(new MethodMatcher().addUsingString("startup_migrated_version"))); if (results.isEmpty()) throw new RuntimeException("StartPrefsConfig constructor not found"); return results.get(0).getConstructorInstance(loader); @@ -1426,18 +1727,25 @@ public synchronized static Constructor loadStartPrefsConfig(ClassLoader loader) public synchronized static Method loadCheckOnlineMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "MessageHandler/handleConnectionThreadReady connectionready"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "MessageHandler/handleConnectionThreadReady connectionready"); if (method == null) - method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "app/xmpp/recv/handle_available"); - if (method == null) throw new RuntimeException("CheckOnline method not found"); + method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "app/xmpp/recv/handle_available"); + if (method == null) + throw new RuntimeException("CheckOnline method not found"); return method; }); } public synchronized static Method loadEphemeralInsertdb(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("expire_timestamp").addUsingString("ephemeral_initiated_by_me").addUsingString("ephemeral_trigger").returnType(ContentValues.class))); - if (method.isEmpty()) throw new RuntimeException("FieldExpireTime method not found"); + var method = dexkit.findMethod(FindMethod.create() + .matcher(MethodMatcher.create().addUsingString("expire_timestamp") + .addUsingString("ephemeral_initiated_by_me").addUsingString("ephemeral_trigger") + .returnType(ContentValues.class))); + if (method.isEmpty()) + throw new RuntimeException("FieldExpireTime method not found"); var methodData = method.get(0); return methodData.getMethodInstance(loader); }); @@ -1446,7 +1754,8 @@ public synchronized static Method loadEphemeralInsertdb(ClassLoader loader) thro public synchronized static Method loadDefEmojiClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "emojis.oba"); - if (method == null) throw new RuntimeException("DefEmoji class not found"); + if (method == null) + throw new RuntimeException("DefEmoji class not found"); return method; }); } @@ -1454,7 +1763,8 @@ public synchronized static Method loadDefEmojiClass(ClassLoader loader) throws E public synchronized static Class loadVideoViewContainerClass(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { var clazz = findFirstClassUsingStrings(loader, StringMatchType.Contains, "frame_visibility_serial_worker"); - if (clazz == null) throw new RuntimeException("VideoViewContainer class not found"); + if (clazz == null) + throw new RuntimeException("VideoViewContainer class not found"); return clazz; }); } @@ -1466,9 +1776,7 @@ public synchronized static Class loadImageVewContainerClass(ClassLoader loader) .addMethod( MethodMatcher.create() .addUsingNumber(Utils.getID("hd_invisible_touch", "id")) - .addUsingNumber(Utils.getID("control_btn", "id")) - )) - ); + .addUsingNumber(Utils.getID("control_btn", "id"))))); if (clazzList.isEmpty()) throw new RuntimeException("ImageViewContainer class not found"); for (var clazzData : clazzList) { @@ -1480,21 +1788,25 @@ public synchronized static Class loadImageVewContainerClass(ClassLoader loader) }); } - public synchronized static Method getFilterInitMethod(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { var filterAdaperClass = Unobfuscator.loadFilterAdaperClass(loader); var constructor = filterAdaperClass.getConstructors()[0]; - var methods = dexkit.findMethod(new FindMethod().matcher(new MethodMatcher().addInvoke(DexSignUtil.getMethodDescriptor(constructor)))); - if (methods.isEmpty()) throw new RuntimeException("FilterInit method not found"); + var methods = dexkit.findMethod(new FindMethod() + .matcher(new MethodMatcher().addInvoke(DexSignUtil.getMethodDescriptor(constructor)))); + if (methods.isEmpty()) + throw new RuntimeException("FilterInit method not found"); var cFrag = XposedHelpers.findClass("com.whatsapp.conversationslist.ConversationsFragment", loader); - var method = methods.stream().filter(m -> Arrays.asList(1, 2).contains(m.getParamCount()) && m.getParamTypes().get(0).getName().equals(cFrag.getName())).findFirst().orElse(null); - if (method == null) throw new RuntimeException("FilterInit method not found 2"); + var method = methods.stream().filter(m -> Arrays.asList(1, 2).contains(m.getParamCount()) + && m.getParamTypes().get(0).getName().equals(cFrag.getName())).findFirst().orElse(null); + if (method == null) + throw new RuntimeException("FilterInit method not found 2"); // for 20.xx, it returned with 2 parameter count if (method.getParamCount() == 2) { var callers = method.getCallers(); - method = callers.stream().filter(methodData -> methodData.isMethod() && methodData.getDeclaredClassName().equals(cFrag.getName())).findAny().orElse(null); + method = callers.stream().filter(methodData -> methodData.isMethod() + && methodData.getDeclaredClassName().equals(cFrag.getName())).findAny().orElse(null); if (method == null) throw new RuntimeException("FilterInit method not found 3"); } @@ -1505,8 +1817,10 @@ public synchronized static Method getFilterInitMethod(ClassLoader loader) throws public synchronized static Class getFilterView(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getClass(loader, () -> { var filter_id = Utils.getID("conversations_swipe_to_reveal_filters_stub", "id"); - var results = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().addMethod(MethodMatcher.create().addUsingNumber(filter_id)))); - if (results.isEmpty()) throw new RuntimeException("FilterView class not found"); + var results = dexkit.findClass(FindClass.create() + .matcher(ClassMatcher.create().addMethod(MethodMatcher.create().addUsingNumber(filter_id)))); + if (results.isEmpty()) + throw new RuntimeException("FilterView class not found"); return results.get(0).getInstance(loader); }); } @@ -1518,7 +1832,9 @@ public synchronized static Class loadActionUser(ClassLoader loader) throws Excep throw new RuntimeException("SingleSelectedMessage class not found"); var fields = classData.getFields().stream().map(FieldData::getType).collect(Collectors.toList()); var fmessage = loadFMessageClass(loader); - var classResult = dexkit.findClass(FindClass.create().searchIn(fields).matcher(ClassMatcher.create().addMethod(MethodMatcher.create().paramCount(3).paramTypes(fmessage, String.class, boolean.class)))); + var classResult = dexkit + .findClass(FindClass.create().searchIn(fields).matcher(ClassMatcher.create().addMethod( + MethodMatcher.create().paramCount(3).paramTypes(fmessage, String.class, boolean.class)))); if (classResult.isEmpty()) throw new RuntimeException("ActionUser class not found"); return classResult.get(0).getInstance(loader); @@ -1527,23 +1843,30 @@ public synchronized static Class loadActionUser(ClassLoader loader) throws Excep public synchronized static Method loadOnPlaybackFinished(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "playbackPage/onPlaybackContentFinished"); - if (method == null) throw new RuntimeException("OnPlaybackFinished method not found"); + var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, + "playbackPage/onPlaybackContentFinished"); + if (method == null) + throw new RuntimeException("OnPlaybackFinished method not found"); return method; }); } public synchronized static Method loadNextStatusRunMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var methodList = dexkit.findMethod(new FindMethod().matcher(new MethodMatcher().addUsingString("playMiddleTone").name("run"))); - if (methodList.isEmpty()) throw new RuntimeException("RunNextStatus method not found"); + var methodList = dexkit.findMethod( + new FindMethod().matcher(new MethodMatcher().addUsingString("playMiddleTone").name("run"))); + if (methodList.isEmpty()) + throw new RuntimeException("RunNextStatus method not found"); return methodList.get(0).getMethodInstance(classLoader); }); } public synchronized static Method loadOnInsertReceipt(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var method = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("INSERT_RECEIPT_USER").paramCount(1))).singleOrNull(); + var method = dexkit + .findMethod(FindMethod.create() + .matcher(MethodMatcher.create().addUsingString("INSERT_RECEIPT_USER").paramCount(1))) + .singleOrNull(); if (method == null) throw new RuntimeException("OnInsertReceipt method not found"); return method.getMethodInstance(classLoader); @@ -1553,14 +1876,17 @@ public synchronized static Method loadOnInsertReceipt(ClassLoader classLoader) t public synchronized static Method loadSendAudioTypeMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var classMsgReplyAct = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, "MessageReplyActivity"); + var classMsgReplyAct = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, + "MessageReplyActivity"); if (classMsgReplyAct == null) throw new ClassNotFoundException("Class MessageReplyActivity not found"); - var method = classMsgReplyAct.getMethod("onActivityResult", int.class, int.class, android.content.Intent.class); + var method = classMsgReplyAct.getMethod("onActivityResult", int.class, int.class, + android.content.Intent.class); var methodData = Objects.requireNonNull(dexkit.getMethodData(method)); var invokes = methodData.getInvokes(); for (var invoke : invokes) { - if (!invoke.isMethod()) continue; + if (!invoke.isMethod()) + continue; var m1 = invoke.getMethodInstance(classLoader); var params = Arrays.asList(m1.getParameterTypes()); if (params.contains(List.class) && params.contains(int.class) && params.contains(Uri.class)) { @@ -1573,18 +1899,33 @@ public synchronized static Method loadSendAudioTypeMethod(ClassLoader classLoade public synchronized static Field loadOriginFMessageField(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getField(classLoader, () -> { - var result = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("audio/ogg; codecs=opu").returnType(boolean.class))); - var FMessageClass = loadFMessageClass(classLoader); - if (result.isEmpty()) { - throw new RuntimeException("OriginFMessageField not found"); - } - for (var clazz : result) { - var fields = clazz.getUsingFields(); - for (var field : fields) { - var f = field.getField().getFieldInstance(classLoader); - if (FMessageClass.isAssignableFrom(f.getDeclaringClass())) { - return f; + String[] commonStrings = new String[] { + "audio/ogg; codecs=opus", + "audio/ogg", + "audio/amr", + "audio/mp4", + "audio/aac" + }; + + var clazz = loadFMessageClass(classLoader); + + for (String str : commonStrings) { + try { + var result = dexkit.findMethod(new FindMethod() + .matcher(new MethodMatcher().addUsingString(str, StringMatchType.Contains))); + if (result.isEmpty()) + continue; + + for (var m : result) { + var fields = m.getUsingFields(); + for (var field : fields) { + var f = field.getField().getFieldInstance(classLoader); + if (f.getDeclaringClass().equals(clazz)) { + return f; + } + } } + } catch (Exception ignored) { } } throw new RuntimeException("OriginFMessageField field not found"); @@ -1593,12 +1934,14 @@ public synchronized static Field loadOriginFMessageField(ClassLoader classLoader public synchronized static Method loadForwardAudioTypeMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var results = findAllMethodUsingStrings(classLoader, StringMatchType.Contains, "FMessageFactory/newFMessageForForward/thumbnail"); + var results = findAllMethodUsingStrings(classLoader, StringMatchType.Contains, + "FMessageFactory/newFMessageForForward/thumbnail"); if (results == null || results.length < 1) throw new RuntimeException("ForwardAudioType method not found"); Method result; if (results.length > 1) { - result = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "forwardable", "FMessageFactory/newFMessageForForward/thumbnail"); + result = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "forwardable", + "FMessageFactory/newFMessageForForward/thumbnail"); } else { // 2.24.18.xx method is changed result = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "Non-forwardable message("); @@ -1609,8 +1952,10 @@ public synchronized static Method loadForwardAudioTypeMethod(ClassLoader classLo public synchronized static Class loadFragmentLoader(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { - var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "not associated with a fragment manager."); - if (clazz == null) throw new RuntimeException("FragmentLoader class not found"); + var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, + "not associated with a fragment manager."); + if (clazz == null) + throw new RuntimeException("FragmentLoader class not found"); return clazz; }); } @@ -1620,49 +1965,64 @@ public synchronized static Method loadShowDialogStatusMethod(ClassLoader classLo var clazz = loadFragmentLoader(classLoader); var frag = classLoader.loadClass("androidx.fragment.app.DialogFragment"); var result = dexkit.findMethod(FindMethod.create().matcher( - MethodMatcher.create().paramCount(2).addParamType(frag).addParamType(clazz) - .returnType(void.class).modifiers(Modifier.PUBLIC | Modifier.STATIC) - .opNames(List.of("iget-boolean", "if-nez"), OpCodeMatchType.Contains) - ) - ); - if (result.isEmpty()) throw new RuntimeException("showDialogStatus not found"); + MethodMatcher.create().paramCount(2).addParamType(frag).addParamType(clazz) + .returnType(void.class).modifiers(Modifier.PUBLIC | Modifier.STATIC) + .opNames(List.of("iget-boolean", "if-nez"), OpCodeMatchType.Contains))); + if (result.isEmpty()) + throw new RuntimeException("showDialogStatus not found"); return result.get(0).getMethodInstance(classLoader); }); } public synchronized static Method loadPlaybackSpeed(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "heroaudioplayer/setPlaybackSpeed"); - if (method == null) throw new RuntimeException("PlaybackSpeed method not found"); + var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, + "heroaudioplayer/setPlaybackSpeed"); + if (method == null) + throw new RuntimeException("PlaybackSpeed method not found"); return method; }); } -// public synchronized static Method loadArchiveCheckLockedChatsMethod(ClassLoader classLoader) throws Exception { -// var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "conversationsfragment/verticalswipetorevealbehavior"); -// if (method == null) throw new RuntimeException("ArchiveCheckLockedChats method not found"); -// return method; -// } -// -// public synchronized static Method loadArchiveCheckLockedChatsMethod2(ClassLoader classLoader) throws Exception { -// var methods = findAllMethodUsingStrings(classLoader, StringMatchType.Contains, "registration_device_id"); -// if (methods.length == 0) -// throw new RuntimeException("ArchiveCheckLockedChats method not found"); -// return Arrays.stream(methods).filter(m -> m.getReturnType().equals(boolean.class) && m.getParameterTypes().length == 0).findFirst().orElse(null); -// } -// -// public synchronized static Class loadArchiveLockedChatClass(ClassLoader classLoader) throws Exception { -// return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { -// var clazzList = dexkit.findClass(new FindClass().matcher(new ClassMatcher().addMethod(new MethodMatcher().name("setLockedRowVisibility")).addMethod(new MethodMatcher().name("setEnableStateForChatLock")))); -// if (clazzList.isEmpty()) -// throw new RuntimeException("ArchiveLockedChatFrame class not found"); -// return clazzList.get(0).getInstance(classLoader); -// }); -// } + // public synchronized static Method + // loadArchiveCheckLockedChatsMethod(ClassLoader classLoader) throws Exception { + // var method = findFirstMethodUsingStrings(classLoader, + // StringMatchType.Contains, + // "conversationsfragment/verticalswipetorevealbehavior"); + // if (method == null) throw new RuntimeException("ArchiveCheckLockedChats + // method not found"); + // return method; + // } + // + // public synchronized static Method + // loadArchiveCheckLockedChatsMethod2(ClassLoader classLoader) throws Exception + // { + // var methods = findAllMethodUsingStrings(classLoader, + // StringMatchType.Contains, "registration_device_id"); + // if (methods.length == 0) + // throw new RuntimeException("ArchiveCheckLockedChats method not found"); + // return Arrays.stream(methods).filter(m -> + // m.getReturnType().equals(boolean.class) && m.getParameterTypes().length == + // 0).findFirst().orElse(null); + // } + // + // public synchronized static Class loadArchiveLockedChatClass(ClassLoader + // classLoader) throws Exception { + // return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { + // var clazzList = dexkit.findClass(new FindClass().matcher(new + // ClassMatcher().addMethod(new + // MethodMatcher().name("setLockedRowVisibility")).addMethod(new + // MethodMatcher().name("setEnableStateForChatLock")))); + // if (clazzList.isEmpty()) + // throw new RuntimeException("ArchiveLockedChatFrame class not found"); + // return clazzList.get(0).getInstance(classLoader); + // }); + // } public synchronized static Method loadListUpdateItems(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var method = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("Running diff util, updates list size", StringMatchType.Contains))); + var method = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create() + .addUsingString("Running diff util, updates list size", StringMatchType.Contains))); if (method.isEmpty()) throw new RuntimeException("ListUpdateItems method not found"); return method.get(0).getMethodInstance(classLoader); @@ -1672,7 +2032,8 @@ public synchronized static Method loadListUpdateItems(ClassLoader classLoader) t public synchronized static Class loadHeaderChannelItemClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "statusTilesEnabled"); - if (clazz == null) throw new RuntimeException("HeaderChannelItem class not found"); + if (clazz == null) + throw new RuntimeException("HeaderChannelItem class not found"); return clazz; }); } @@ -1680,16 +2041,17 @@ public synchronized static Class loadHeaderChannelItemClass(ClassLoader classLoa public synchronized static Class loadListChannelItemClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "isMuteIndicatorEnabled"); - if (clazz == null) throw new RuntimeException("NewsletterDataItem class not found"); + if (clazz == null) + throw new RuntimeException("NewsletterDataItem class not found"); return clazz; }); } - public synchronized static Method[] loadTextStatusData(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethods(classLoader, () -> { Class textData; - var textDataList = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().addUsingString("TextData;"))); + var textDataList = dexkit + .findClass(FindClass.create().matcher(ClassMatcher.create().addUsingString("TextData;"))); if (textDataList.isEmpty()) { textData = findFirstClassUsingName(classLoader, StringMatchType.EndsWith, "TextData"); } else { @@ -1697,38 +2059,42 @@ public synchronized static Method[] loadTextStatusData(ClassLoader classLoader) } var methods = dexkit.findMethod( FindMethod.create().matcher( - MethodMatcher.create().addParamType(textData) - ) - ); + MethodMatcher.create().addParamType(textData))); if (methods.isEmpty()) throw new RuntimeException("loadTextStatusData method not found"); - return methods.stream().filter(MethodData::isMethod).map(methodData -> convertRealMethod(methodData, classLoader)).toArray(Method[]::new); + return methods.stream().filter(MethodData::isMethod) + .map(methodData -> convertRealMethod(methodData, classLoader)).toArray(Method[]::new); }); } public synchronized static Class loadExpirationClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { - var methods = findAllMethodUsingStrings(classLoader, StringMatchType.Contains, "software_forced_expiration"); - var expirationMethod = Arrays.stream(methods).filter(methodData -> methodData.getReturnType().equals(Date.class)).findFirst().orElse(null); - if (expirationMethod == null) throw new RuntimeException("Expiration class not found"); + var methods = findAllMethodUsingStrings(classLoader, StringMatchType.Contains, + "software_forced_expiration"); + var expirationMethod = Arrays.stream(methods) + .filter(methodData -> methodData.getReturnType().equals(Date.class)).findFirst().orElse(null); + if (expirationMethod == null) + throw new RuntimeException("Expiration class not found"); return expirationMethod.getDeclaringClass(); }); } - public synchronized static Class loadAbsViewHolder(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "not recyclable"); - if (clazz == null) throw new RuntimeException("AbsViewHolder class not found"); + if (clazz == null) + throw new RuntimeException("AbsViewHolder class not found"); return clazz; }); } public synchronized static Method loadFragmentViewMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "this was called before onCreateView()"); - if (method == null) throw new RuntimeException("FragmentView method not found"); + var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, + "this was called before onCreateView()"); + if (method == null) + throw new RuntimeException("FragmentView method not found"); return method; }); } @@ -1736,7 +2102,8 @@ public synchronized static Method loadFragmentViewMethod(ClassLoader classLoader public synchronized static Method loadCopiedMessageMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "conversation/copymessage"); - if (method == null) throw new RuntimeException("CopiedMessage method not found"); + if (method == null) + throw new RuntimeException("CopiedMessage method not found"); return method; }); } @@ -1744,7 +2111,8 @@ public synchronized static Method loadCopiedMessageMethod(ClassLoader classLoade public synchronized static Class loadSenderPlayedClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "sendmethods/sendClearDirty"); - if (clazz == null) throw new RuntimeException("SenderPlayed class not found"); + if (clazz == null) + throw new RuntimeException("SenderPlayed class not found"); return clazz; }); } @@ -1760,9 +2128,9 @@ public synchronized static Method loadSenderPlayedMethod(ClassLoader classLoader interfacesList.addAll(Arrays.asList(interfaces)); Method methodResult = null; - main_loop: - for (var method : clazz.getMethods()) { - if (method.getParameterCount() != 1) continue; + main_loop: for (var method : clazz.getMethods()) { + if (method.getParameterCount() != 1) + continue; var parameterType = method.getParameterTypes()[0]; for (var interfaceClass : interfacesList) { if (interfaceClass.isAssignableFrom(parameterType)) { @@ -1775,7 +2143,8 @@ public synchronized static Method loadSenderPlayedMethod(ClassLoader classLoader // 2.25.19.xx, they refactored the SenderPlayed class var fmessageClass = Unobfuscator.loadFMessageClass(classLoader); if (methodResult == null) { - var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "mediaHash and fileType not both present for upload URL generation"); + var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, + "mediaHash and fileType not both present for upload URL generation"); if (method != null) { var cMethods = dexkit.getMethodData(method).getInvokes(); Collections.reverse(cMethods); @@ -1791,7 +2160,8 @@ public synchronized static Method loadSenderPlayedMethod(ClassLoader classLoader } } - if (methodResult == null) throw new RuntimeException("SenderPlayed method not found 2"); + if (methodResult == null) + throw new RuntimeException("SenderPlayed method not found 2"); return methodResult; }); } @@ -1799,7 +2169,8 @@ public synchronized static Method loadSenderPlayedMethod(ClassLoader classLoader public synchronized static Method loadSenderPlayedBusiness(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { var loadSenderPlayed = loadSenderPlayedClass(classLoader); - var foundMethod = ReflectionUtils.findMethodUsingFilter(loadSenderPlayed, method -> method.getParameterCount() > 0 && method.getParameterTypes()[0] == Set.class); + var foundMethod = ReflectionUtils.findMethodUsingFilter(loadSenderPlayed, + method -> method.getParameterCount() > 0 && method.getParameterTypes()[0] == Set.class); if (foundMethod == null) throw new RuntimeException("SenderPlayedBusiness method not found"); return foundMethod; @@ -1808,8 +2179,10 @@ public synchronized static Method loadSenderPlayedBusiness(ClassLoader classLoad public synchronized static Field loadMediaTypeField(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getField(classLoader, () -> { - var methodData = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("conversation/refresh"))); - if (methodData.isEmpty()) throw new RuntimeException("MediaType: aux method not found"); + var methodData = dexkit.findMethod( + FindMethod.create().matcher(MethodMatcher.create().addUsingString("conversation/refresh"))); + if (methodData.isEmpty()) + throw new RuntimeException("MediaType: aux method not found"); var fclass = dexkit.getClassData(loadFMessageClass(classLoader)); var usingFields = methodData.get(0).getUsingFields(); for (var f : usingFields) { @@ -1825,19 +2198,26 @@ public synchronized static Field loadMediaTypeField(ClassLoader classLoader) thr public synchronized static Method loadBubbleDrawableMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var methodData = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("Unreachable code: direction=").returnType(Drawable.class))); - if (methodData.isEmpty()) throw new Exception("BubbleDrawable method not found"); + var methodData = dexkit.findMethod(FindMethod.create().matcher( + MethodMatcher.create().addUsingString("Unreachable code: direction=").returnType(Drawable.class))); + if (methodData.isEmpty()) + throw new Exception("BubbleDrawable method not found"); return methodData.get(0).getMethodInstance(classLoader); }); } public synchronized static Method loadBallonDateDrawable(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var methodData = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("Unreachable code: direction=").returnType(Rect.class))); - if (methodData.isEmpty()) throw new Exception("LoadDateWrapper method not found"); + var methodData = dexkit.findMethod(FindMethod.create().matcher( + MethodMatcher.create().addUsingString("Unreachable code: direction=").returnType(Rect.class))); + if (methodData.isEmpty()) + throw new Exception("LoadDateWrapper method not found"); var clazz = methodData.get(0).getMethodInstance(classLoader).getDeclaringClass(); - var method = ReflectionUtils.findMethodUsingFilterIfExists(clazz, m -> List.of(1, 2).contains(m.getParameterCount()) && m.getParameterTypes()[0].equals(int.class) && m.getReturnType().equals(Drawable.class)); - if (method == null) throw new RuntimeException("DateWrapper method not found"); + var method = ReflectionUtils.findMethodUsingFilterIfExists(clazz, + m -> List.of(1, 2).contains(m.getParameterCount()) && m.getParameterTypes()[0].equals(int.class) + && m.getReturnType().equals(Drawable.class)); + if (method == null) + throw new RuntimeException("DateWrapper method not found"); return method; }); } @@ -1845,8 +2225,10 @@ public synchronized static Method loadBallonDateDrawable(ClassLoader classLoader public synchronized static Method loadBallonBorderDrawable(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { var clazz = loadBallonDateDrawable(classLoader).getDeclaringClass(); - var method = ReflectionUtils.findMethodUsingFilterIfExists(clazz, m -> m.getParameterCount() == 3 && m.getReturnType().equals(Drawable.class)); - if (method == null) throw new RuntimeException("Ballon Border method not found"); + var method = ReflectionUtils.findMethodUsingFilterIfExists(clazz, + m -> m.getParameterCount() == 3 && m.getReturnType().equals(Drawable.class)); + if (method == null) + throw new RuntimeException("Ballon Border method not found"); return method; }); } @@ -1854,15 +2236,18 @@ public synchronized static Method loadBallonBorderDrawable(ClassLoader classLoad public static synchronized Method[] loadRootDetector(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethods(classLoader, () -> { var methods = findAllMethodUsingStrings(classLoader, StringMatchType.Contains, "/system/bin/su"); - if (methods.length == 0) throw new RuntimeException("RootDetector method not found"); + if (methods.length == 0) + throw new RuntimeException("RootDetector method not found"); return methods; }); } public static synchronized Method loadCheckEmulator(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "Android SDK built for x86"); - if (method == null) throw new RuntimeException("CheckEmulator method not found"); + var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, + "Android SDK built for x86"); + if (method == null) + throw new RuntimeException("CheckEmulator method not found"); return method; }); } @@ -1870,30 +2255,36 @@ public static synchronized Method loadCheckEmulator(ClassLoader classLoader) thr public static synchronized Method loadCheckCustomRom(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { var method = findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "cyanogen"); - if (method == null) throw new RuntimeException("CheckCustomRom method not found"); + if (method == null) + throw new RuntimeException("CheckCustomRom method not found"); return method; }); } public static synchronized Method loadTranscribeMethod(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "transcribe: starting transcription")); + return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, + StringMatchType.Contains, "transcribe: starting transcription")); } public static synchronized Method loadCheckSupportLanguage(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "Unsupported language")); + return UnobfuscatorCache.getInstance().getMethod(classLoader, + () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "Unsupported language")); } public static synchronized Class loadTranscriptSegment(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getClass(classLoader, () -> findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "TranscriptionSegment(")); + return UnobfuscatorCache.getInstance().getClass(classLoader, + () -> findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "TranscriptionSegment(")); } public static synchronized Method loadStateChangeMethod(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "presencestatemanager/startTransitionToUnavailable/new-state")); + return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, + StringMatchType.Contains, "presencestatemanager/startTransitionToUnavailable/new-state")); } public static synchronized Method loadCachedMessageStoreKey(ClassLoader loader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(loader, () -> { - var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, "CachedMessageStore/getAvailableMessage/key"); + var method = findFirstMethodUsingStrings(loader, StringMatchType.Contains, + "CachedMessageStore/getAvailableMessage/key"); if (method == null) throw new RuntimeException("CachedMessageStore class not found"); return method; @@ -1917,7 +2308,8 @@ public static synchronized Class loadAbstractMediaMessageClass(ClassLoader loade public static Class loadFragmentClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "mFragmentId=#"); - if (clazz == null) throw new RuntimeException("Fragment class not found"); + if (clazz == null) + throw new RuntimeException("Fragment class not found"); return clazz; }); } @@ -1925,15 +2317,11 @@ public static Class loadFragmentClass(ClassLoader classLoader) throws Excepti public static Method loadMediaQualitySelectionMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { var methodData = dexkit.findMethod(FindMethod.create().matcher( - MethodMatcher.create().addUsingString("enable_media_quality_tool"). - returnType(boolean.class) - )); + MethodMatcher.create().addUsingString("enable_media_quality_tool").returnType(boolean.class))); if (methodData.isEmpty()) { methodData = dexkit.findMethod(FindMethod.create().matcher( - MethodMatcher.create().addUsingString("show_media_quality_toggle"). - returnType(boolean.class) - )); + MethodMatcher.create().addUsingString("show_media_quality_toggle").returnType(boolean.class))); } if (methodData.isEmpty()) @@ -1960,29 +2348,30 @@ public static Field loadFmessageTimestampField(ClassLoader classLoader) throws E public static Class loadStatusDistributionClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { - var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.Equals, "Only set a valid status distribution mode"); - if (clazz == null) throw new RuntimeException("StatusDistribution not found!"); + var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.Equals, + "Only set a valid status distribution mode"); + if (clazz == null) + throw new RuntimeException("StatusDistribution not found!"); return clazz; }); } - public static Class loadFilterItemClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { var methodList = dexkit.findMethod(FindMethod.create().matcher( MethodMatcher.create().addUsingNumber(Utils.getID("invisible_height_placeholder", "id")) - .addUsingNumber(Utils.getID("container_view", "id")) - )); + .addUsingNumber(Utils.getID("container_view", "id")))); if (!methodList.isEmpty()) return methodList.get(0).getClassInstance(classLoader); for (var s : List.of("ConversationsFilter/selectFilter", "has_seen_detected_outcomes_nux")) { var applyClazz = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, s); - if (applyClazz == null) continue; + if (applyClazz == null) + continue; methodList = dexkit.findMethod(FindMethod.create().matcher( - MethodMatcher.create().paramTypes(View.class, applyClazz) - )); - if (!methodList.isEmpty()) return methodList.get(0).getClassInstance(classLoader); + MethodMatcher.create().paramTypes(View.class, applyClazz))); + if (!methodList.isEmpty()) + return methodList.get(0).getClassInstance(classLoader); } throw new RuntimeException("FilterItemClass Not Found"); }); @@ -1991,9 +2380,12 @@ public static Class loadFilterItemClass(ClassLoader classLoader) throws Excep public static Class[] loadProximitySensorListenerClasses(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClasses(classLoader, () -> { var classDataList = dexkit.findClass( - FindClass.create().matcher(ClassMatcher.create().addInterface(SensorEventListener.class.getName()))); - if (classDataList.isEmpty()) throw new Exception("Class SensorEventListener not found"); - return classDataList.stream().map(classData -> convertRealClass(classData, classLoader)).filter(Objects::nonNull).toArray(Class[]::new); + FindClass.create() + .matcher(ClassMatcher.create().addInterface(SensorEventListener.class.getName()))); + if (classDataList.isEmpty()) + throw new Exception("Class SensorEventListener not found"); + return classDataList.stream().map(classData -> convertRealClass(classData, classLoader)) + .filter(Objects::nonNull).toArray(Class[]::new); }); } @@ -2005,8 +2397,7 @@ public static Class loadRefreshStatusClass(ClassLoader classLoader) throws Ex MethodMatcher.create().returnType(String.class) .addInvoke(DexSignUtil.getMethodDescriptor(keyset)) .addUsingString(",", StringMatchType.Equals) - .addUsingString("", StringMatchType.Equals) - ) + .addUsingString("", StringMatchType.Equals)) .addMethod(MethodMatcher.create().addUsingNumber(0x3684)); List results = dexkit.findClass(FindClass.create().matcher(matcher)); @@ -2017,13 +2408,15 @@ public static Class loadRefreshStatusClass(ClassLoader classLoader) throws Ex } public static Method loadTcTokenMethod(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "GET_RECEIVED_TOKEN_AND_TIMESTAMP_BY_JID")); + return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, + StringMatchType.Contains, "GET_RECEIVED_TOKEN_AND_TIMESTAMP_BY_JID")); } public static Class getClassByName(String className, ClassLoader classLoader) throws ClassNotFoundException { if (cacheClasses.containsKey(className)) return cacheClasses.get(className); - var classDataList = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().className(className, StringMatchType.EndsWith))); + var classDataList = dexkit.findClass( + FindClass.create().matcher(ClassMatcher.create().className(className, StringMatchType.EndsWith))); if (classDataList.isEmpty()) throw new RuntimeException("Class " + className + " not found!"); var clazz = classDataList.get(0).getInstance(classLoader); @@ -2034,7 +2427,8 @@ public static Class getClassByName(String className, ClassLoader classLoader) public static Class loadVoipManager(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { var voipClass = WppCore.getVoipManagerClass(classLoader); - var superClasses = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().superClass(voipClass.getName()))); + var superClasses = dexkit + .findClass(FindClass.create().matcher(ClassMatcher.create().superClass(voipClass.getName()))); if (superClasses.isEmpty()) throw new ClassNotFoundException("VoipManager Class not found"); for (var supclass : superClasses) { @@ -2046,20 +2440,27 @@ public static Class loadVoipManager(ClassLoader classLoader) throws Exception { } public static Class loadWaContactClass(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getClass(classLoader, () -> findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "problematic contact:")); + return UnobfuscatorCache.getInstance().getClass(classLoader, + () -> findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "problematic contact:")); } - public static Method loadViewAddSearchBarMethod(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "HeaderFooterRecyclerViewAdapter/addHeaderViewItemIfNeeded/duplicate-item")); + return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, + StringMatchType.Contains, "HeaderFooterRecyclerViewAdapter/addHeaderViewItemIfNeeded/duplicate-item")); } public static Method loadMenuSearchMethod(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingNumber(8013).paramCount(0).returnType(boolean.class))).single().getMethodInstance(classLoader)); + return UnobfuscatorCache.getInstance() + .getMethod(classLoader, + () -> dexkit + .findMethod( + FindMethod.create() + .matcher(MethodMatcher.create().addUsingNumber(8013).paramCount(0) + .returnType(boolean.class))) + .single().getMethodInstance(classLoader)); } - public static Method loadAddOptionSearchBarMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { var classData = Objects.requireNonNull(dexkit.getClassData(WppCore.getHomeActivityClass(classLoader))); @@ -2067,8 +2468,7 @@ public static Method loadAddOptionSearchBarMethod(ClassLoader classLoader) throw .matcher(MethodMatcher.create().addUsingNumber(Utils.getID("menuitem_search", "id")) .addUsingNumber(200) .paramCount(1) - .addParamType(Menu.class) - )); + .addParamType(Menu.class))); if (methodData.isEmpty()) throw new NoSuchMethodError("MenuSearch not found in HomeActivity"); @@ -2077,25 +2477,30 @@ public static Method loadAddOptionSearchBarMethod(ClassLoader classLoader) throw } public static Method loadAddMenuAndroidX(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "Maximum number of items supported by")); + return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, + StringMatchType.Contains, "Maximum number of items supported by")); } public static Method loadConvertLidToJid(ClassLoader loader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(loader, () -> findFirstMethodUsingStrings(loader, StringMatchType.Contains, "WaJidMapRepository/getPhoneJidByAccountUserJid")); + return UnobfuscatorCache.getInstance().getMethod(loader, () -> findFirstMethodUsingStrings(loader, + StringMatchType.Contains, "WaJidMapRepository/getPhoneJidByAccountUserJid")); } public static Method loadConvertJidToLid(ClassLoader loader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(loader, () -> findFirstMethodUsingStrings(loader, StringMatchType.Contains, "WaJidMapRepository/getAccountUserJidByPhoneJid")); + return UnobfuscatorCache.getInstance().getMethod(loader, () -> findFirstMethodUsingStrings(loader, + StringMatchType.Contains, "WaJidMapRepository/getAccountUserJidByPhoneJid")); } public static Class loadWaContactData(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getClass(classLoader, () -> findFirstClassUsingStrings(classLoader, StringMatchType.EndsWith, "WaContactData")); + return UnobfuscatorCache.getInstance().getClass(classLoader, + () -> findFirstClassUsingStrings(classLoader, StringMatchType.EndsWith, "WaContactData")); } public static Class loadMeManagerClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.StartsWith, "memanager/"); - if (clazz == null) throw new RuntimeException("MeManager class not found"); + if (clazz == null) + throw new RuntimeException("MeManager class not found"); return clazz; }); } @@ -2103,20 +2508,19 @@ public static Class loadMeManagerClass(ClassLoader classLoader) throws Except public synchronized static Class loadVerifyKeyClass(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { var classList = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().addMethod( - MethodMatcher.create().addUsingNumber(2966).paramCount(1).addParamType(int.class) - ) - )).singleOrNull(); + MethodMatcher.create().addUsingNumber(2966).paramCount(1).addParamType(int.class)))).singleOrNull(); if (classList == null) throw new ClassNotFoundException("VerifyKey class not found"); return classList.getInstance(classLoader); }); } - public synchronized static Method loadMySearchBarMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - Method method = findFirstMethodUsingStrings(classLoader, StringMatchType.EndsWith, "search_bar_render_start"); - if (method == null) throw new NoSuchMethodException("MySearchBar method not found"); + Method method = findFirstMethodUsingStrings(classLoader, StringMatchType.EndsWith, + "search_bar_render_start"); + if (method == null) + throw new NoSuchMethodException("MySearchBar method not found"); return method; }); } @@ -2126,8 +2530,10 @@ public synchronized static Method loadAdVerifyMethod(ClassLoader classLoader) th var clazz = findFirstClassUsingStrings(classLoader, StringMatchType.Contains, "WamoAccountSettingManager"); if (clazz == null) throw new ClassNotFoundException("WamoAccountSettingManager Not Found"); - var method = ReflectionUtils.findMethodUsingFilter(clazz, method1 -> method1.getParameterCount() == 0 && method1.getReturnType() == boolean.class); - if (method == null) throw new NoSuchMethodException("loadAdVerify Not Found"); + var method = ReflectionUtils.findMethodUsingFilter(clazz, + method1 -> method1.getParameterCount() == 0 && method1.getReturnType() == boolean.class); + if (method == null) + throw new NoSuchMethodException("loadAdVerify Not Found"); return method; }); } @@ -2135,15 +2541,23 @@ public synchronized static Method loadAdVerifyMethod(ClassLoader classLoader) th public synchronized static Class loadChatFilterView(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClass(classLoader, () -> { int value = Utils.getID("conversations_inbox_filters_stub", "id"); - var clazz = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().addMethod(MethodMatcher.create().addUsingNumber(value)))).singleOrNull(); - if (clazz == null) return null; + var clazz = dexkit + .findClass(FindClass.create() + .matcher(ClassMatcher.create().addMethod(MethodMatcher.create().addUsingNumber(value)))) + .singleOrNull(); + if (clazz == null) + return null; return clazz.getInstance(classLoader); }); } public static Method loadNotificationMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var invokedMethod = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("LastMessageStore/getLastMessagesForNotificationAfterReply"))).singleOrNull(); + var invokedMethod = dexkit + .findMethod(FindMethod.create() + .matcher(MethodMatcher.create() + .addUsingString("LastMessageStore/getLastMessagesForNotificationAfterReply"))) + .singleOrNull(); if (invokedMethod == null) throw new RuntimeException("Notification invoked method not found"); return invokedMethod.getMethodInstance(classLoader); @@ -2152,15 +2566,21 @@ public static Method loadNotificationMethod(ClassLoader classLoader) throws Exce public static Method loadLockedChatsMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var classData = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().addUsingString("conversationsmgr/replacecontact"))).singleOrNull(); + var classData = dexkit + .findClass(FindClass.create() + .matcher(ClassMatcher.create().addUsingString("conversationsmgr/replacecontact"))) + .singleOrNull(); if (classData == null) throw new RuntimeException("ConversationsManager class not found"); var invokedMethod = dexkit.getMethodData(loadNotificationMethod(classLoader)); var invokes = invokedMethod.getInvokes(); for (var invoke : invokes) { - if (!invoke.isMethod()) continue; - if (!invoke.getClassName().equals(classData.getName())) continue; - if (!invoke.getReturnType().getName().equals(ArrayList.class.getName())) continue; + if (!invoke.isMethod()) + continue; + if (!invoke.getClassName().equals(classData.getName())) + continue; + if (!invoke.getReturnType().getName().equals(ArrayList.class.getName())) + continue; return invoke.getMethodInstance(classLoader); } throw new RuntimeException("LockedChats method not found"); @@ -2168,42 +2588,49 @@ public static Method loadLockedChatsMethod(ClassLoader classLoader) throws Excep } public static Method loadGetProfilePhotoMethod(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(classLoader, ()-> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains,"Avatars",".j")); + return UnobfuscatorCache.getInstance().getMethod(classLoader, + () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "Avatars", ".j")); } - public static Method loadGetProfilePhotoHighQMethod(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "me.jpg", "Profile Pictures")); + return UnobfuscatorCache.getInstance().getMethod(classLoader, + () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "me.jpg", "Profile Pictures")); } - - public static Class loadChatCacheClass(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getClass(classLoader, () -> findFirstClassUsingStrings(classLoader, StringMatchType.StartsWith, "Chatscache/")); + return UnobfuscatorCache.getInstance().getClass(classLoader, + () -> findFirstClassUsingStrings(classLoader, StringMatchType.StartsWith, "Chatscache/")); } public static Method loadLoadedContactsMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var methods = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingNumber(8726).paramCount(1).addParamType(Object.class))); - if (methods.isEmpty()) return null; + var methods = dexkit.findMethod(FindMethod.create() + .matcher(MethodMatcher.create().addUsingNumber(8726).paramCount(1).addParamType(Object.class))); + if (methods.isEmpty()) + return null; return methods.get(0).getMethodInstance(classLoader); }); } public static Method loadVideoTranscoderStartMethod(ClassLoader classLoader) throws Exception { - return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "VideoTranscoder/transcodeVideoNew/")); + return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> findFirstMethodUsingStrings(classLoader, + StringMatchType.Contains, "VideoTranscoder/transcodeVideoNew/")); } public static Field loadWaContactGetWaNameField(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getField(classLoader, () -> { - var method = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("ContactManagerDatabase/updateContactWAName"))).singleOrNull(); + var method = dexkit + .findMethod(FindMethod.create().matcher( + MethodMatcher.create().addUsingString("ContactManagerDatabase/updateContactWAName"))) + .singleOrNull(); if (method == null) throw new NoSuchMethodException("WaContactGetWaName field not found"); var waContact = loadWaContactClass(classLoader).getName(); var usingFields = method.getUsingFields(); for (var usingField : usingFields) { var field = usingField.getField(); - if (field.getClassName().equals(waContact) && field.getType().getName().equals(String.class.getName())) { + if (field.getClassName().equals(waContact) + && field.getType().getName().equals(String.class.getName())) { return field.getFieldInstance(classLoader); } } @@ -2213,12 +2640,15 @@ public static Field loadWaContactGetWaNameField(ClassLoader classLoader) throws public static Method loadWaContactDisplayNameMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - var methods = dexkit.findMethod(FindMethod.create().matcher(MethodMatcher.create().addUsingString("ContactManagerDatabase/updateGroupInfo"))); - if (methods.isEmpty()) throw new NoSuchMethodException("WaContactDiplayName not found"); + var methods = dexkit.findMethod(FindMethod.create() + .matcher(MethodMatcher.create().addUsingString("ContactManagerDatabase/updateGroupInfo"))); + if (methods.isEmpty()) + throw new NoSuchMethodException("WaContactDiplayName not found"); var invokes = methods.get(0).getInvokes(); var waContactClass = loadWaContactClass(classLoader); for (var invoke : invokes) { - if (!invoke.getClassName().equals(waContactClass.getName())) continue; + if (!invoke.getClassName().equals(waContactClass.getName())) + continue; if (invoke.getReturnTypeName().equals(String.class.getName())) return invoke.getMethodInstance(classLoader); } @@ -2228,15 +2658,19 @@ public static Method loadWaContactDisplayNameMethod(ClassLoader classLoader) thr public static Method loadGetWaContactMethod(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getMethod(classLoader, () -> { - return findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, "ContactManager/getContactFromCacheOrDbByJid"); + return findFirstMethodUsingStrings(classLoader, StringMatchType.Contains, + "ContactManager/getContactFromCacheOrDbByJid"); }); } public static Class[] loadSharedPreferencesClasses(ClassLoader classLoader) throws Exception { return UnobfuscatorCache.getInstance().getClasses(classLoader, () -> { - var classesData = dexkit.findClass(FindClass.create().matcher(ClassMatcher.create().addInterface(SharedPreferences.class.getName()))); - if (classesData.isEmpty()) return null; - return classesData.stream().map(classData -> convertRealClass(classData, classLoader)).toArray(Class[]::new); + var classesData = dexkit.findClass( + FindClass.create().matcher(ClassMatcher.create().addInterface(SharedPreferences.class.getName()))); + if (classesData.isEmpty()) + return null; + return classesData.stream().map(classData -> convertRealClass(classData, classLoader)) + .toArray(Class[]::new); }); } } diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/features/customization/CustomThemeV2.java b/app/src/main/java/com/wmods/wppenhacer/xposed/features/customization/CustomThemeV2.java index cbe834c17..7a9524258 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/features/customization/CustomThemeV2.java +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/features/customization/CustomThemeV2.java @@ -51,7 +51,7 @@ public class CustomThemeV2 extends Feature { private HashMap navAlpha; private HashMap toolbarAlpha; private Properties properties; -// private ViewGroup mContent; + // private ViewGroup mContent; public CustomThemeV2(@NonNull ClassLoader classLoader, @NonNull XSharedPreferences preferences) { super(classLoader, preferences); @@ -76,7 +76,6 @@ private static void processColors(String color, HashMap mapColor return; } - for (var c : mapColors.keySet()) { String value = mapColors.get(c); @@ -115,32 +114,37 @@ public void doHook() throws Throwable { properties = Utils.getProperties(prefs, "custom_css", "custom_filters"); hookTheme(); hookWallpaper(); - XposedBridge.hookAllMethods(XposedHelpers.findClass("android.app.ActivityThread", classLoader), "handleRelaunchActivity", new XC_MethodHook() { - @Override - protected void beforeHookedMethod(MethodHookParam param) throws Throwable { - loadAndApplyColors(); - loadAndApplyColorsWallpaper(); - } - }); + XposedBridge.hookAllMethods(XposedHelpers.findClass("android.app.ActivityThread", classLoader), + "handleRelaunchActivity", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + loadAndApplyColors(); + loadAndApplyColorsWallpaper(); + } + }); } private void loadAndApplyColorsWallpaper() { - if (prefs.getBoolean("lite_mode", false)) return; + if (prefs.getBoolean("lite_mode", false)) + return; var customWallpaper = prefs.getBoolean("wallpaper", false); if (customWallpaper || properties.containsKey("wallpaper")) { wallAlpha = new HashMap<>(IColors.colors); - var wallpaperAlpha = customWallpaper ? prefs.getInt("wallpaper_alpha", 30) : Utils.tryParseInt(properties.getProperty("wallpaper_alpha"), 30); + var wallpaperAlpha = customWallpaper ? prefs.getInt("wallpaper_alpha", 30) + : Utils.tryParseInt(properties.getProperty("wallpaper_alpha"), 30); replaceTransparency(wallAlpha, (100 - wallpaperAlpha) / 100.0f); navAlpha = new HashMap<>(IColors.colors); - var wallpaperAlphaNav = customWallpaper ? prefs.getInt("wallpaper_alpha_navigation", 30) : Utils.tryParseInt(properties.getProperty("wallpaper_alpha_navigation"), 30); + var wallpaperAlphaNav = customWallpaper ? prefs.getInt("wallpaper_alpha_navigation", 30) + : Utils.tryParseInt(properties.getProperty("wallpaper_alpha_navigation"), 30); replaceTransparency(navAlpha, (100 - wallpaperAlphaNav) / 100.0f); toolbarAlpha = new HashMap<>(IColors.colors); - var wallpaperToolbarAlpha = customWallpaper ? prefs.getInt("wallpaper_alpha_toolbar", 30) : Utils.tryParseInt(properties.getProperty("wallpaper_alpha_toolbar"), 30); + var wallpaperToolbarAlpha = customWallpaper ? prefs.getInt("wallpaper_alpha_toolbar", 30) + : Utils.tryParseInt(properties.getProperty("wallpaper_alpha_toolbar"), 30); replaceTransparency(toolbarAlpha, (100 - wallpaperToolbarAlpha) / 100.0f); } } @@ -166,28 +170,33 @@ private void hookWallpaper() throws Exception { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { var activity = (Activity) param.thisObject; - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission(activity, + Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED + || ContextCompat.checkSelfPermission(activity, + Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { injectWallpaper(activity.findViewById(Utils.getID("root_view", "id"))); } } }); -// var revertWallAlpha = revertColors(wallAlpha); - -// WppCore.addListenerActivity((activity, type) -> { -// var isHome = homeActivityClass.isInstance(activity); -// if (WppCore.ActivityChangeState.ChangeType.RESUMED == type && isHome) { -// mContent = activity.findViewById(android.R.id.content); -// if (mContent != null) { -// replaceColors(mContent, wallAlpha); -// } -// } else if (WppCore.ActivityChangeState.ChangeType.CREATED == type && !isHome && -// !activity.getClass().getSimpleName().equals("QuickContactActivity") && !DesignUtils.isNightMode()) { -// if (mContent != null) { -// replaceColors(mContent, revertWallAlpha); -// } -// } -// }); + // var revertWallAlpha = revertColors(wallAlpha); + + // WppCore.addListenerActivity((activity, type) -> { + // var isHome = homeActivityClass.isInstance(activity); + // if (WppCore.ActivityChangeState.ChangeType.RESUMED == type && isHome) { + // mContent = activity.findViewById(android.R.id.content); + // if (mContent != null) { + // replaceColors(mContent, wallAlpha); + // } + // } else if (WppCore.ActivityChangeState.ChangeType.CREATED == type && !isHome + // && + // !activity.getClass().getSimpleName().equals("QuickContactActivity") && + // !DesignUtils.isNightMode()) { + // if (mContent != null) { + // replaceColors(mContent, revertWallAlpha); + // } + // } + // }); var hookFragmentView = Unobfuscator.loadFragmentViewMethod(classLoader); @@ -195,7 +204,8 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable { new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { - if (checkNotHomeActivity()) return; + if (checkNotHomeActivity()) + return; var viewGroup = (ViewGroup) param.getResult(); replaceColors(viewGroup, wallAlpha); } @@ -205,18 +215,18 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable { XposedHelpers.findAndHookMethod(FrameLayout.class, "onMeasure", int.class, int.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { - if (!loadTabFrameClass.isInstance(param.thisObject)) return; + if (!loadTabFrameClass.isInstance(param.thisObject)) + return; var viewGroup = (ViewGroup) param.thisObject; - if (checkNotHomeActivity()) return; + if (checkNotHomeActivity()) + return; var background = viewGroup.getBackground(); replaceColor(background, navAlpha); } }); - } - public void hookTheme() throws Throwable { loadAndApplyColors(); @@ -226,8 +236,10 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable { var typedValue = (TypedValue) param.args[2]; if (typedValue.type >= TypedValue.TYPE_FIRST_INT && typedValue.type <= TypedValue.TYPE_LAST_INT) { - if (typedValue.data == 0) return; - if (checkNotApplyColor(typedValue.data)) return; + if (typedValue.data == 0) + return; + if (checkNotApplyColor(typedValue.data)) + return; typedValue.data = IColors.getFromIntColor(typedValue.data, IColors.colors); } } @@ -269,15 +281,15 @@ protected void afterHookedMethod(MethodHookParam param) throws Throwable { } }); -// Method activeButtonNav = Unobfuscator.loadActiveButtonNav(classLoader); -// -// XposedBridge.hookMethod(activeButtonNav, new XC_MethodHook() { -// @Override -// protected void beforeHookedMethod(MethodHookParam param) throws Throwable { -// var drawable = (Drawable) param.args[0]; -// DrawableColors.replaceColor(drawable, alphacolors); -// } -// }); + // Method activeButtonNav = Unobfuscator.loadActiveButtonNav(classLoader); + // + // XposedBridge.hookMethod(activeButtonNav, new XC_MethodHook() { + // @Override + // protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + // var drawable = (Drawable) param.args[0]; + // DrawableColors.replaceColor(drawable, alphacolors); + // } + // }); } public void loadAndApplyColors() { @@ -289,16 +301,24 @@ public void loadAndApplyColors() { var backgroundColorInt = prefs.getInt("background_color", 0); var changeColorEnabled = prefs.getBoolean("changecolor", false); var changeColorMode = prefs.getString("changecolor_mode", "manual"); - var useMonetColors = changeColorEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && Objects.equals(changeColorMode, "monet"); + var useMonetColors = changeColorEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + && Objects.equals(changeColorMode, "monet"); if (useMonetColors) { - var primaryMonetColor = resolveMonetColor(DesignUtils.isNightMode() ? "system_accent1_300" : "system_accent1_600"); - var textMonetColor = resolveMonetColor(DesignUtils.isNightMode() ? "system_neutral1_100" : "system_neutral1_900"); - var backgroundMonetColor = resolveMonetColor(DesignUtils.isNightMode() ? "system_neutral1_900" : "system_neutral1_10"); + var primaryMonetColor = resolveMonetColor( + DesignUtils.isNightMode() ? "system_accent1_300" : "system_accent1_600"); + var textMonetColor = resolveMonetColor( + DesignUtils.isNightMode() ? "system_neutral1_100" : "system_neutral1_900"); + var backgroundMonetColor = resolveMonetColor( + DesignUtils.isNightMode() ? "system_neutral1_900" : "system_neutral1_10"); + + if (primaryMonetColor != 0) + primaryColorInt = primaryMonetColor; + if (textMonetColor != 0) + textColorInt = textMonetColor; + if (backgroundMonetColor != 0) + backgroundColorInt = backgroundMonetColor; - if (primaryMonetColor != 0) primaryColorInt = primaryMonetColor; - if (textMonetColor != 0) textColorInt = textMonetColor; - if (backgroundMonetColor != 0) backgroundColorInt = backgroundMonetColor; } var primaryColor = DesignUtils.checkSystemColor(properties.getProperty("primary_color", "0")); @@ -385,7 +405,8 @@ private void replaceTransparency(HashMap wallpaperColors, float hexAlpha = hexAlpha.length() == 1 ? "0" + hexAlpha : hexAlpha; for (var c : backgroundColors.keySet()) { var oldColor = wallpaperColors.getOrDefault(c, backgroundColors.get(c)); - if (oldColor == null || oldColor.length() < 9) continue; + if (oldColor == null || oldColor.length() < 9) + continue; var newColor = "#" + hexAlpha + oldColor.substring(3); wallpaperColors.put(c, newColor); wallpaperColors.put(oldColor, newColor); @@ -413,7 +434,8 @@ private static int getOriginalColor(String sColor) { var resultColor = -1; for (var c : colors) { var vColor = IColors.colors.getOrDefault(c, ""); - if (vColor.length() < 9) continue; + if (vColor.length() < 9) + continue; if (sColor.equals(vColor)) { resultColor = IColors.parseColor(c); break; @@ -424,7 +446,9 @@ private static int getOriginalColor(String sColor) { private boolean checkNotApplyColor(int color) { var activity = WppCore.getCurrentActivity(); - if (activity != null && activity.getClass().getSimpleName().equals("Conversation") && ReflectionUtils.isCalledFromStrings("getValue") && !ReflectionUtils.isCalledFromStrings("android.view")) { + if (activity != null && activity.getClass().getSimpleName().equals("Conversation") + && ReflectionUtils.isCalledFromStrings("getValue") + && !ReflectionUtils.isCalledFromStrings("android.view")) { return color != 0xff12181c; } return false; @@ -436,10 +460,8 @@ public String getPluginName() { return "Custom Theme V2"; } - public static class IntBgColorHook extends XC_MethodHook { - @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { var color = (int) param.args[0]; @@ -456,5 +478,4 @@ protected void beforeHookedMethod(MethodHookParam param) throws Throwable { } } - } diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/features/general/Others.java b/app/src/main/java/com/wmods/wppenhacer/xposed/features/general/Others.java index 30eddaf33..fc1702a46 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/features/general/Others.java +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/features/general/Others.java @@ -221,7 +221,11 @@ public void doHook() throws Exception { } if (audio_type > 0) { - sendAudioType(audio_type); + try { + sendAudioType(audio_type); + } catch (Exception e) { + logDebug(e); + } } customPlayBackSpeed(); diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/features/general/RecoverDeleteForMe.java b/app/src/main/java/com/wmods/wppenhacer/xposed/features/general/RecoverDeleteForMe.java new file mode 100644 index 000000000..a1eff94b1 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/features/general/RecoverDeleteForMe.java @@ -0,0 +1,479 @@ +package com.wmods.wppenhacer.xposed.features.general; + +import android.content.Context; + +import com.wmods.wppenhacer.xposed.core.Feature; +import com.wmods.wppenhacer.xposed.core.components.FMessageWpp; +import com.wmods.wppenhacer.xposed.core.components.WaContactWpp; +import com.wmods.wppenhacer.xposed.core.db.DelMessageStore; +import com.wmods.wppenhacer.xposed.core.db.DeletedMessage; +import com.wmods.wppenhacer.xposed.core.devkit.Unobfuscator; +import com.wmods.wppenhacer.xposed.utils.Utils; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collection; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XSharedPreferences; +import de.robv.android.xposed.XposedBridge; + +public class RecoverDeleteForMe extends Feature { + + public RecoverDeleteForMe(ClassLoader loader, XSharedPreferences preferences) { + super(loader, preferences); + } + + @Override + public void doHook() throws Exception { + try { + Class cms = Unobfuscator.loadCoreMessageStore(classLoader); + + // Dynamic method search based on signature: (Any, Collection, int) + Method targetMethod = null; + for (Method m : cms.getDeclaredMethods()) { + Class[] p = m.getParameterTypes(); + if (p.length == 3 && Collection.class.isAssignableFrom(p[1]) && p[2] == int.class) { + targetMethod = m; + XposedBridge.log("WAE: Found potential DeleteForMe method: " + m.getName()); + break; // Assuming only one method matches this signature in CoreMessageStore + } + } + + if (targetMethod == null) { + XposedBridge.log("WAE: RecoverDeleteForMe: A06 not found"); + return; + } + + XposedBridge.hookMethod(targetMethod, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) { + try { + XposedBridge.log("WAE: A06 Fired!"); + Collection msgs = (Collection) param.args[1]; + if (msgs == null) { + XposedBridge.log("WAE: msgs is null"); + return; + } + if (msgs.isEmpty()) { + XposedBridge.log("WAE: msgs is empty"); + return; + } + Context ctx = Utils.getApplication(); + if (ctx == null) { + XposedBridge.log("WAE: Context is null"); + return; + } + // DelMessageStore store = DelMessageStore.getInstance(ctx); // No longer needed + // for insertion + for (Object msg : msgs) { + try { + saveOne(ctx, msg); + } catch (Throwable t) { + XposedBridge.log("WAE: RecoverDeleteForMe saveOne: " + t.getMessage()); + } + } + } catch (Throwable t) { + XposedBridge.log("WAE: RecoverDeleteForMe hook: " + t.getMessage()); + } + } + }); + XposedBridge.log("WAE: RecoverDeleteForMe hooked A06 OK"); + + } catch (Exception e) { + XposedBridge.log("WAE: RecoverDeleteForMe init: " + e.getMessage()); + } + } + + private void saveOne(Context context, Object msg) throws Exception { + if (msg == null) + return; + Class msgClass = msg.getClass(); + + // 1. Find Key Field + Object key = null; + if (FMessageWpp.Key.TYPE != null) { + key = getFirstNonNullFieldByType(msg, FMessageWpp.Key.TYPE); + } + if (key == null) { + Field keyField = findField(msgClass, "key"); + if (keyField != null) + key = keyField.get(msg); + } + if (key == null) + return; + + // 2. Extract Message ID (Key ID) + String keyId = getStr(key, "id"); + if (keyId == null) + keyId = getStr(key, "A01"); + if (keyId == null) + return; + + // 3. Extract RemoteJid / ChatJid + String chatJid = null; + Object jidObj = getObj(key, "remoteJid"); + if (jidObj == null) + jidObj = getObj(key, "chatJid"); + if (jidObj == null) + jidObj = getObj(key, "A00"); + if (jidObj != null) + chatJid = jidObj.toString(); + if (chatJid != null && (chatJid.equalsIgnoreCase("false") || chatJid.equalsIgnoreCase("true"))) { + chatJid = null; + } + + // 4. Extract isFromMe + boolean fromMe = false; + Field fmField = findField(key.getClass(), "fromMe"); + if (fmField == null) + fmField = findField(key.getClass(), "isFromMe"); + if (fmField == null) + fmField = findField(key.getClass(), "A02"); + if (fmField != null) { + Object v = fmField.get(key); + if (v instanceof Boolean) + fromMe = (Boolean) v; + } + + // 5. Media Type (Moved up to prioritize detection) + int mediaType = -1; + Field mtf = findField(msgClass, "mediaType"); + if (mtf == null) + mtf = findField(msgClass, "media_wa_type"); + if (mtf != null) { + try { + mediaType = mtf.getInt(msg); + } catch (Exception ignored) { + } + } + + // 6. Extract Text Body + String textContent = getStr(msg, "text"); + if (textContent == null) + textContent = getStr(msg, "body"); + if (textContent == null) + textContent = getStr(msg, "A0Q"); + + // Safety check: If it's a media message, discard "text" if it looks like a URL + // or Hash + if (mediaType > 0 && textContent != null) { + if (textContent.startsWith("http") || (textContent.length() > 20 && !textContent.contains(" "))) { + textContent = null; + } + } + + // Heuristic search: Only if text is null AND it's NOT a media message + if (textContent == null && mediaType <= 0) { + String bestCandidate = null; + Class cls = msgClass; + while (cls != null && cls != Object.class) { + for (Field f : cls.getDeclaredFields()) { + if (f.getType().equals(String.class)) { + f.setAccessible(true); + try { + String val = (String) f.get(msg); + if (val != null && !val.isEmpty()) { + // Determine if this value is safe (not a URL, not a hash) + boolean isUrl = val.startsWith("http") || val.startsWith("www."); + boolean isHash = val.length() > 20 && !val.contains(" "); + + if (!isUrl && !isHash) { + if (bestCandidate == null || val.length() > bestCandidate.length()) { + if (val.length() > 1) + bestCandidate = val; + } + } + } + } catch (IllegalAccessException e) { + } + } + } + cls = cls.getSuperclass(); + } + if (bestCandidate != null) + textContent = bestCandidate; + } + + // 7. Sender JID + String senderJid = fromMe ? "Me" : chatJid; + Object participant = getObj(msg, "participant"); + if (participant == null) + participant = getObj(msg, "senderJid"); + if (participant == null) + participant = getObj(msg, "A0b"); + if (participant != null) { + String val = participant.toString(); + if (!val.equalsIgnoreCase("false") && !val.equalsIgnoreCase("true")) { + senderJid = val; + } + } + + // 8. Media Details + String mediaPath = null; + Object mf = getObj(msg, "mediaData"); + if (mf == null) + mf = getObj(msg, "mediaFile"); + if (mf instanceof File && ((File) mf).exists()) { + mediaPath = ((File) mf).getAbsolutePath(); + } + + String mediaCaption = getStr(msg, "caption"); + if (mediaCaption == null) + mediaCaption = getStr(msg, "mediaCaption"); + if (mediaCaption == null) + mediaCaption = getStr(msg, "A03"); + + long timestamp = System.currentTimeMillis(); + + // 9. Contact Name Resolution + String contactName = null; + try { + // Priority 1: Current Chat Room Title (Most Reliable as per User Suggestion) + contactName = com.wmods.wppenhacer.xposed.core.WppCore.getCurrentChatTitle(); + + // Priority 2: WaContactWpp Internal Lookup (New Reliable Fallback) + if (contactName == null && chatJid != null) { + try { + XposedBridge.log("WAE: Attempting WaContactWpp lookup for " + chatJid); + FMessageWpp.UserJid userJidObj = new FMessageWpp.UserJid(chatJid); + WaContactWpp waContact = WaContactWpp.getWaContactFromJid(userJidObj); + if (waContact != null) { + contactName = waContact.getDisplayName(); + if (contactName == null || contactName.isEmpty()) { + contactName = waContact.getWaName(); + } + XposedBridge.log("WAE: WaContact result: " + contactName); + } else { + XposedBridge.log("WAE: WaContactWpp returned null for " + chatJid); + } + } catch (Throwable t) { + XposedBridge.log("WAE: WaContactWpp lookup failed: " + t.getMessage()); + } + } + + // Priority 3: Simple ContactsContract lookup (Fallback) + if (contactName == null && chatJid != null && !chatJid.contains("@g.us")) { + contactName = getContactName(context, chatJid); + } + } catch (Throwable t) { + XposedBridge.log("WAE: Error in contact resolution: " + t.getMessage()); + } + + // Capture Package Name + String packageName = context.getPackageName(); + + // Capture original timestamp + long originalTimestamp = 0; + try { + // 1. Try finding 'timestamp' (long) + Field tsField = findField(msgClass, "timestamp"); + if (tsField == null) + tsField = findField(msgClass, "g"); // Common obfuscation + + if (tsField != null) { + Object val = tsField.get(msg); + if (val instanceof Long) { + originalTimestamp = (Long) val; + } + } + + // 2. Fallback: Search for any long field that looks like a timestamp (e.g. > + // 1600000000000) + if (originalTimestamp == 0) { + Class cls = msgClass; + while (cls != null && cls != Object.class) { + for (Field f : cls.getDeclaredFields()) { + if (f.getType() == long.class) { + f.setAccessible(true); + long val = f.getLong(msg); + if (val > 1500000000000L && val < System.currentTimeMillis() + 86400000L) { + originalTimestamp = val; + break; + } + } + } + if (originalTimestamp != 0) + break; + cls = cls.getSuperclass(); + } + } + } catch (Exception e) { + XposedBridge.log("WAE: Error extracting original timestamp: " + e.getMessage()); + } + + // Create and Save + DeletedMessage deletedMessage = new DeletedMessage( + 0, keyId, chatJid, senderJid, timestamp, originalTimestamp, mediaType, textContent, mediaPath, + mediaCaption, fromMe, + contactName, packageName); + + saveToDatabase(context, deletedMessage); + } + + private String getContactName(Context context, String jid) { + if (jid == null) + return null; + String phoneNumber = jid.replace("@s.whatsapp.net", "").replace("@g.us", ""); + if (phoneNumber.contains("@")) + phoneNumber = phoneNumber.split("@")[0]; + + try { + android.net.Uri uri = android.net.Uri.withAppendedPath( + android.provider.ContactsContract.PhoneLookup.CONTENT_FILTER_URI, + android.net.Uri.encode(phoneNumber)); + String[] projection = new String[] { android.provider.ContactsContract.PhoneLookup.DISPLAY_NAME }; + + try (android.database.Cursor cursor = context.getContentResolver().query(uri, projection, null, null, + null)) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getString(0); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private void saveToDatabase(Context context, DeletedMessage message) { + try { + android.content.ContentValues values = new android.content.ContentValues(); + values.put("key_id", message.getKeyId()); + values.put("chat_jid", message.getChatJid()); + values.put("sender_jid", message.getSenderJid()); + values.put("timestamp", message.getTimestamp()); + values.put("original_timestamp", message.getOriginalTimestamp()); + values.put("media_type", message.getMediaType()); + values.put("text_content", message.getTextContent()); + values.put("media_path", message.getMediaPath()); + values.put("media_caption", message.getMediaCaption()); + values.put("is_from_me", message.isFromMe() ? 1 : 0); + values.put("contact_name", message.getContactName()); + values.put("package_name", message.getPackageName()); + + String authority = com.wmods.wppenhacer.BuildConfig.APPLICATION_ID + ".provider"; + android.net.Uri uri = android.net.Uri.parse("content://" + authority + "/deleted_messages"); + context.getContentResolver().insert(uri, values); + XposedBridge.log("WAE: RecoverDeleteForMe saved via Provider: id=" + message.getKeyId() + " text=" + + message.getTextContent()); + } catch (Exception e) { + XposedBridge.log("WAE: Failed to insert to provider: " + e.getMessage()); + e.printStackTrace(); + } + } + + // Scans hierarchy for ALL fields of type, returns first non-null value + private Object getFirstNonNullFieldByType(Object target, Class type) { + if (target == null || type == null) + return null; + Class cls = target.getClass(); + while (cls != null && cls != Object.class) { + for (Field f : cls.getDeclaredFields()) { + if (f.getType().equals(type)) { + f.setAccessible(true); + try { + Object val = f.get(target); + if (val != null) + return val; + } catch (IllegalAccessException e) { + // ignore + } + } + } + cls = cls.getSuperclass(); + } + return null; + } + + private String getFirstNonNullStringField(Object target) { + if (target == null) + return null; + Class cls = target.getClass(); + while (cls != null && cls != Object.class) { + for (Field f : cls.getDeclaredFields()) { + if (f.getType().equals(String.class)) { + f.setAccessible(true); + try { + String val = (String) f.get(target); + if (val != null && !val.isEmpty()) + return val; + } catch (IllegalAccessException e) { + } + } + } + cls = cls.getSuperclass(); + } + return null; + } + + private Field findFieldByType(Class cls, Class type) { + while (cls != null && cls != Object.class) { + for (Field f : cls.getDeclaredFields()) { + if (f.getType().equals(type)) { + f.setAccessible(true); + return f; + } + } + cls = cls.getSuperclass(); + } + return null; + } + + private Field findField(Class cls, String name) { + while (cls != null && cls != Object.class) { + try { + Field f = cls.getDeclaredField(name); + f.setAccessible(true); + return f; + } catch (NoSuchFieldException ignored) { + cls = cls.getSuperclass(); + } + } + return null; + } + + private String getStr(Object obj, String name) { + try { + Field f = findField(obj.getClass(), name); + if (f == null) + return null; + Object v = f.get(obj); + return v instanceof String ? (String) v : null; + } catch (Exception e) { + return null; + } + } + + private Object getObj(Object obj, String name) { + try { + Field f = findField(obj.getClass(), name); + if (f == null) + return null; + return f.get(obj); + } catch (Exception e) { + return null; + } + } + + @Override + public String getPluginName() { + return "Recover Delete For Me"; + } + + public static void restoreMessage(android.content.Context context, DeletedMessage message) { + try { + if (message.getTextContent() != null && !message.getTextContent().isEmpty()) { + android.widget.Toast + .makeText(context, "Message: " + message.getTextContent(), android.widget.Toast.LENGTH_LONG) + .show(); + } else { + android.widget.Toast + .makeText(context, "Media restore not supported yet", android.widget.Toast.LENGTH_SHORT).show(); + } + } catch (Exception e) { + XposedBridge.log("WAE: Restore failed: " + e.getMessage()); + } + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/features/media/CallRecording.java b/app/src/main/java/com/wmods/wppenhacer/xposed/features/media/CallRecording.java new file mode 100644 index 000000000..ab055c04b --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/features/media/CallRecording.java @@ -0,0 +1,454 @@ +package com.wmods.wppenhacer.xposed.features.media; + +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import com.wmods.wppenhacer.xposed.core.Feature; +import com.wmods.wppenhacer.xposed.core.FeatureLoader; +import com.wmods.wppenhacer.xposed.core.devkit.Unobfuscator; +import com.wmods.wppenhacer.xposed.utils.Utils; + +import org.luckypray.dexkit.query.enums.StringMatchType; + +import java.io.File; +import java.io.RandomAccessFile; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XSharedPreferences; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; + +public class CallRecording extends Feature { + + private final AtomicBoolean isRecording = new AtomicBoolean(false); + private final AtomicBoolean isCallConnected = new AtomicBoolean(false); + private AudioRecord audioRecord; + private RandomAccessFile randomAccessFile; + private Thread recordingThread; + private int payloadSize = 0; + private volatile String currentPhoneNumber = null; + private static boolean permissionGranted = false; + + private static final int SAMPLE_RATE = 48000; + private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; + private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; + private static final short CHANNELS = 1; + private static final short BITS_PER_SAMPLE = 16; + + public CallRecording(@NonNull ClassLoader loader, @NonNull XSharedPreferences preferences) { + super(loader, preferences); + } + + @Override + public void doHook() throws Throwable { + if (!prefs.getBoolean("call_recording_enable", false)) { + XposedBridge.log("WaEnhancer: Call Recording is disabled"); + return; + } + + XposedBridge.log("WaEnhancer: Call Recording feature initializing..."); + hookCallStateChanges(); + } + + private void hookCallStateChanges() { + int hooksInstalled = 0; + + try { + var clsCallEventCallback = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.EndsWith, "VoiceServiceEventCallback"); + if (clsCallEventCallback != null) { + XposedBridge.log("WaEnhancer: Found VoiceServiceEventCallback: " + clsCallEventCallback.getName()); + + // Hook ALL methods to discover which ones fire during call + for (Method method : clsCallEventCallback.getDeclaredMethods()) { + final String methodName = method.getName(); + try { + XposedBridge.hookAllMethods(clsCallEventCallback, methodName, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + XposedBridge.log("WaEnhancer: VoiceCallback." + methodName + "()"); + + // Handle call end + if (methodName.equals("fieldstatsReady")) { + isCallConnected.set(false); + stopRecording(); + } + } + }); + hooksInstalled++; + } catch (Throwable ignored) {} + } + + // Hook soundPortCreated with 3 second delay to wait for call connection + XposedBridge.hookAllMethods(clsCallEventCallback, "soundPortCreated", new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + XposedBridge.log("WaEnhancer: soundPortCreated - will record after 3s"); + extractPhoneNumberFromCallback(param.thisObject); + + final Object callback = param.thisObject; + new Thread(() -> { + try { + Thread.sleep(3000); + if (!isRecording.get()) { + XposedBridge.log("WaEnhancer: Starting recording after delay"); + extractPhoneNumberFromCallback(callback); + isCallConnected.set(true); + startRecording(); + } + } catch (Exception e) { + XposedBridge.log("WaEnhancer: Delay error: " + e.getMessage()); + } + }).start(); + } + }); + } + } catch (Throwable e) { + XposedBridge.log("WaEnhancer: Could not hook VoiceServiceEventCallback: " + e.getMessage()); + } + + // Hook VoipActivity onDestroy for call end + try { + var voipActivityClass = Unobfuscator.findFirstClassUsingName(classLoader, StringMatchType.Contains, "VoipActivity"); + if (voipActivityClass != null && Activity.class.isAssignableFrom(voipActivityClass)) { + XposedBridge.log("WaEnhancer: Found VoipActivity: " + voipActivityClass.getName()); + + XposedBridge.hookAllMethods(voipActivityClass, "onDestroy", new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + XposedBridge.log("WaEnhancer: VoipActivity.onDestroy"); + isCallConnected.set(false); + stopRecording(); + } + }); + hooksInstalled++; + } + } catch (Throwable e) { + XposedBridge.log("WaEnhancer: Could not hook VoipActivity: " + e.getMessage()); + } + + XposedBridge.log("WaEnhancer: Call Recording initialized with " + hooksInstalled + " hooks"); + } + + private void extractPhoneNumberFromCallback(Object callback) { + try { + Object callInfo = XposedHelpers.callMethod(callback, "getCallInfo"); + if (callInfo == null) return; + + // Try to get peerJid and resolve LID to phone number + try { + Object peerJid = XposedHelpers.getObjectField(callInfo, "peerJid"); + if (peerJid != null) { + String peerStr = peerJid.toString(); + XposedBridge.log("WaEnhancer: peerJid = " + peerStr); + + // Check if it's a LID format + if (peerStr.contains("@lid")) { + // Try to get phone from the Jid object + try { + Object userMethod = XposedHelpers.callMethod(peerJid, "getUser"); + XposedBridge.log("WaEnhancer: peerJid.getUser() = " + userMethod); + } catch (Throwable ignored) {} + + // Try toPhoneNumber or similar + try { + Object phone = XposedHelpers.callMethod(peerJid, "toPhoneNumber"); + if (phone != null) { + currentPhoneNumber = "+" + phone.toString(); + XposedBridge.log("WaEnhancer: Found phone from toPhoneNumber: " + currentPhoneNumber); + return; + } + } catch (Throwable ignored) {} + } + + // Check if it's already a phone number format + if (peerStr.contains("@s.whatsapp.net") || peerStr.contains("@c.us")) { + String number = peerStr.split("@")[0]; + if (number.matches("\\d{6,15}")) { + currentPhoneNumber = "+" + number; + XposedBridge.log("WaEnhancer: Found phone: " + currentPhoneNumber); + return; + } + } + } + } catch (Throwable ignored) {} + + // Search participants map for phone numbers + try { + Object participants = XposedHelpers.getObjectField(callInfo, "participants"); + if (participants != null) { + XposedBridge.log("WaEnhancer: Participants = " + participants.toString()); + + if (participants instanceof java.util.Map) { + java.util.Map map = (java.util.Map) participants; + for (Object key : map.keySet()) { + String keyStr = key.toString(); + XposedBridge.log("WaEnhancer: Participant key = " + keyStr); + + // Check if key contains phone number + if (keyStr.contains("@s.whatsapp.net") || keyStr.contains("@c.us")) { + String number = keyStr.split("@")[0]; + if (number.matches("\\d{6,15}")) { + // Skip if it's the self number (creatorJid) + Object creatorJid = XposedHelpers.getObjectField(callInfo, "creatorJid"); + if (creatorJid != null && keyStr.equals(creatorJid.toString())) { + continue; + } + currentPhoneNumber = "+" + number; + XposedBridge.log("WaEnhancer: Found phone from participants: " + currentPhoneNumber); + return; + } + } + } + } + } + } catch (Throwable ignored) {} + + } catch (Throwable e) { + XposedBridge.log("WaEnhancer: extractPhoneNumber error: " + e.getMessage()); + } + } + + private void grantVoiceCallPermission() { + if (permissionGranted) return; + + try { + String packageName = FeatureLoader.mApp.getPackageName(); + XposedBridge.log("WaEnhancer: Granting CAPTURE_AUDIO_OUTPUT via root"); + + String[] commands = { + "pm grant " + packageName + " android.permission.CAPTURE_AUDIO_OUTPUT", + "appops set " + packageName + " RECORD_AUDIO allow", + }; + + for (String cmd : commands) { + try { + Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", cmd}); + int exitCode = process.waitFor(); + XposedBridge.log("WaEnhancer: " + cmd + " exit: " + exitCode); + } catch (Exception e) { + XposedBridge.log("WaEnhancer: Root failed: " + e.getMessage()); + } + } + + permissionGranted = true; + } catch (Throwable e) { + XposedBridge.log("WaEnhancer: grantVoiceCallPermission error: " + e.getMessage()); + } + } + + private synchronized void startRecording() { + if (isRecording.get()) { + XposedBridge.log("WaEnhancer: Already recording"); + return; + } + + try { + if (ContextCompat.checkSelfPermission(FeatureLoader.mApp, Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + XposedBridge.log("WaEnhancer: No RECORD_AUDIO permission"); + return; + } + + String packageName = FeatureLoader.mApp.getPackageName(); + String appName = packageName.contains("w4b") ? "WA Business" : "WhatsApp"; + + File parentDir; + if (android.os.Environment.isExternalStorageManager()) { + parentDir = new File(android.os.Environment.getExternalStorageDirectory(), "WA Call Recordings"); + } else { + String settingsPath = prefs.getString("call_recording_path", null); + if (settingsPath != null && !settingsPath.isEmpty()) { + parentDir = new File(settingsPath, "WA Call Recordings"); + } else { + parentDir = new File(FeatureLoader.mApp.getExternalFilesDir(null), "Recordings"); + } + } + + File dir = new File(parentDir, appName + "/Voice"); + if (!dir.exists() && !dir.mkdirs()) { + dir = new File(FeatureLoader.mApp.getExternalFilesDir(null), "Recordings/" + appName + "/Voice"); + dir.mkdirs(); + } + + String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); + String fileName = (currentPhoneNumber != null && !currentPhoneNumber.isEmpty()) + ? "Call_" + currentPhoneNumber.replaceAll("[^+0-9]", "") + "_" + timestamp + ".wav" + : "Call_" + timestamp + ".wav"; + + File file = new File(dir, fileName); + randomAccessFile = new RandomAccessFile(file, "rw"); + randomAccessFile.setLength(0); + randomAccessFile.write(new byte[44]); + + boolean useRoot = prefs.getBoolean("call_recording_use_root", false); + if (useRoot) { + grantVoiceCallPermission(); + } + + int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT); + int bufferSize = minBufferSize * 6; + XposedBridge.log("WaEnhancer: Buffer: " + bufferSize + ", useRoot: " + useRoot); + + int[] audioSources = useRoot + ? new int[]{MediaRecorder.AudioSource.VOICE_CALL, 6, MediaRecorder.AudioSource.VOICE_COMMUNICATION, MediaRecorder.AudioSource.MIC} + : new int[]{6, MediaRecorder.AudioSource.VOICE_COMMUNICATION, MediaRecorder.AudioSource.MIC}; + String[] sourceNames = useRoot + ? new String[]{"VOICE_CALL", "VOICE_RECOGNITION", "VOICE_COMMUNICATION", "MIC"} + : new String[]{"VOICE_RECOGNITION", "VOICE_COMMUNICATION", "MIC"}; + + audioRecord = null; + String usedSource = "none"; + + for (int i = 0; i < audioSources.length; i++) { + try { + XposedBridge.log("WaEnhancer: Trying " + sourceNames[i]); + AudioRecord testRecord = new AudioRecord(audioSources[i], SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, bufferSize); + if (testRecord.getState() == AudioRecord.STATE_INITIALIZED) { + audioRecord = testRecord; + usedSource = sourceNames[i]; + XposedBridge.log("WaEnhancer: SUCCESS " + sourceNames[i]); + break; + } + testRecord.release(); + XposedBridge.log("WaEnhancer: FAILED " + sourceNames[i]); + } catch (Throwable t) { + XposedBridge.log("WaEnhancer: Exception " + sourceNames[i] + ": " + t.getMessage()); + } + } + + if (audioRecord == null) { + XposedBridge.log("WaEnhancer: All audio sources failed"); + return; + } + + isRecording.set(true); + payloadSize = 0; + audioRecord.startRecording(); + XposedBridge.log("WaEnhancer: Recording started (" + usedSource + "): " + file.getAbsolutePath()); + + final int finalBufferSize = bufferSize; + recordingThread = new Thread(() -> { + byte[] buffer = new byte[finalBufferSize]; + XposedBridge.log("WaEnhancer: Recording thread started"); + + while (isRecording.get() && audioRecord != null) { + try { + int read = audioRecord.read(buffer, 0, buffer.length); + if (read > 0) { + synchronized (CallRecording.this) { + if (randomAccessFile != null) { + randomAccessFile.write(buffer, 0, read); + payloadSize += read; + } + } + } else if (read < 0) { + break; + } + } catch (IOException e) { + break; + } + } + XposedBridge.log("WaEnhancer: Recording thread ended, bytes: " + payloadSize); + }, "WaEnhancer-RecordingThread"); + recordingThread.start(); + + if (prefs.getBoolean("call_recording_toast", false)) { + Utils.showToast("Recording started", android.widget.Toast.LENGTH_SHORT); + } + + } catch (Exception e) { + XposedBridge.log("WaEnhancer: startRecording error: " + e.getMessage()); + } + } + + private synchronized void stopRecording() { + if (!isRecording.get()) return; + + isRecording.set(false); + + try { + if (audioRecord != null) { + try { audioRecord.stop(); } catch (Exception ignored) {} + audioRecord.release(); + audioRecord = null; + } + + if (recordingThread != null) { + recordingThread.join(2000); + recordingThread = null; + } + + if (randomAccessFile != null) { + writeWavHeader(); + randomAccessFile.close(); + randomAccessFile = null; + } + + XposedBridge.log("WaEnhancer: Recording stopped, size: " + payloadSize); + + if (prefs.getBoolean("call_recording_toast", false)) { + Utils.showToast(payloadSize > 1000 ? "Recording saved!" : "Recording failed", android.widget.Toast.LENGTH_SHORT); + } + + currentPhoneNumber = null; + } catch (Exception e) { + XposedBridge.log("WaEnhancer: stopRecording error: " + e.getMessage()); + } + } + + private void writeWavHeader() throws IOException { + long totalDataLen = payloadSize + 36; + long byteRate = (long) SAMPLE_RATE * CHANNELS * BITS_PER_SAMPLE / 8; + + randomAccessFile.seek(0); + byte[] header = new byte[44]; + + header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F'; + header[4] = (byte) (totalDataLen & 0xff); + header[5] = (byte) ((totalDataLen >> 8) & 0xff); + header[6] = (byte) ((totalDataLen >> 16) & 0xff); + header[7] = (byte) ((totalDataLen >> 24) & 0xff); + header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; + header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' '; + header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0; + header[20] = 1; header[21] = 0; + header[22] = (byte) CHANNELS; header[23] = 0; + header[24] = (byte) (SAMPLE_RATE & 0xff); + header[25] = (byte) ((SAMPLE_RATE >> 8) & 0xff); + header[26] = (byte) ((SAMPLE_RATE >> 16) & 0xff); + header[27] = (byte) ((SAMPLE_RATE >> 24) & 0xff); + header[28] = (byte) (byteRate & 0xff); + header[29] = (byte) ((byteRate >> 8) & 0xff); + header[30] = (byte) ((byteRate >> 16) & 0xff); + header[31] = (byte) ((byteRate >> 24) & 0xff); + header[32] = (byte) (CHANNELS * BITS_PER_SAMPLE / 8); header[33] = 0; + header[34] = 16; header[35] = 0; + header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a'; + header[40] = (byte) (payloadSize & 0xff); + header[41] = (byte) ((payloadSize >> 8) & 0xff); + header[42] = (byte) ((payloadSize >> 16) & 0xff); + header[43] = (byte) ((payloadSize >> 24) & 0xff); + + randomAccessFile.write(header); + } + + @NonNull + @Override + public String getPluginName() { + return "Call Recording"; + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/features/others/BackupRestore.java b/app/src/main/java/com/wmods/wppenhacer/xposed/features/others/BackupRestore.java new file mode 100644 index 000000000..8b8054c77 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/features/others/BackupRestore.java @@ -0,0 +1,68 @@ +package com.wmods.wppenhacer.xposed.features.others; + +import android.app.Activity; +import android.content.Intent; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; + +import com.wmods.wppenhacer.xposed.core.Feature; +import com.wmods.wppenhacer.xposed.core.devkit.Unobfuscator; +import com.wmods.wppenhacer.xposed.utils.Utils; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XSharedPreferences; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; + +public class BackupRestore extends Feature { + + public BackupRestore(ClassLoader loader, XSharedPreferences preferences) { + super(loader, preferences); + } + + @Override + public String getPluginName() { + return "BackupRestore"; + } + + @Override + public void doHook() throws Exception { + if (!prefs.getBoolean("force_restore_backup_feature", false)) return; + + Class settingsDriveClass = Unobfuscator.loadSettingsGoogleDriveActivity(classLoader); + + XposedHelpers.findAndHookMethod(settingsDriveClass, "onPrepareOptionsMenu", Menu.class, new XC_MethodHook() { + @Override + protected void afterHookedMethod(MethodHookParam param) throws Throwable { + Menu menu = (Menu) param.args[0]; + // Hardcoding string to ensure it appears without resource injection issues + String title = "Force Restore Backup (Experimental)"; + + // Use a high ID to avoid conflicts + if (menu.findItem(10001) == null) { + menu.add(0, 10001, 0, title); + } + } + }); + + XposedHelpers.findAndHookMethod(settingsDriveClass, "onOptionsItemSelected", MenuItem.class, new XC_MethodHook() { + @Override + protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + MenuItem item = (MenuItem) param.args[0]; + if (item.getItemId() == 10001) { + Activity activity = (Activity) param.thisObject; + try { + Class restoreClass = Unobfuscator.loadRestoreBackupActivity(classLoader); + Intent intent = new Intent(activity, restoreClass); + activity.startActivity(intent); + param.setResult(true); + } catch (Exception e) { + Utils.showToast("Error launching restore activity: " + e.getMessage(), Toast.LENGTH_LONG); + XposedBridge.log(e); + } + } + } + }); + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/features/others/Spy.java b/app/src/main/java/com/wmods/wppenhacer/xposed/features/others/Spy.java new file mode 100644 index 000000000..7906baeb9 --- /dev/null +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/features/others/Spy.java @@ -0,0 +1,72 @@ +package com.wmods.wppenhacer.xposed.features.others; + +import android.view.MenuItem; + +import com.wmods.wppenhacer.xposed.core.Feature; +import com.wmods.wppenhacer.xposed.core.devkit.Unobfuscator; +import com.wmods.wppenhacer.xposed.utils.Utils; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import de.robv.android.xposed.XC_MethodHook; +import de.robv.android.xposed.XSharedPreferences; +import de.robv.android.xposed.XposedBridge; +import de.robv.android.xposed.XposedHelpers; +import com.wmods.wppenhacer.xposed.core.WppCore; + +public class Spy extends Feature { + + public Spy(ClassLoader loader, XSharedPreferences preferences) { + super(loader, preferences); + } + + @Override + public void doHook() throws Exception { + // if (!prefs.getBoolean("enable_spy", false)) return; + XposedBridge.log("WAE: Spy Forced Enabled"); + + // ... (Keep existing generic hooks if needed, but focusing on dumps for now) + + dumpMessageStore(classLoader); + dumpCoreMessageStore(classLoader); + } + + // ... (keep logMenuClick and dumpConversationFields) + + private void dumpMessageStore(ClassLoader loader) { + try { + java.lang.reflect.Field f = WppCore.class.getDeclaredField("mCachedMessageStore"); + f.setAccessible(true); + Object store = f.get(null); + if (store != null) { + XposedBridge.log("WAE: mCachedMessageStore class: " + store.getClass().getName()); + for (Method m : store.getClass().getDeclaredMethods()) { + XposedBridge.log("WAE: MessageStore method: " + m.getName() + " " + Arrays.toString(m.getParameterTypes())); + } + } else { + XposedBridge.log("WAE: mCachedMessageStore is null"); + } + } catch (Exception e) { + XposedBridge.log("WAE: MessageStore Dump failed: " + e); + } + } + + private void dumpCoreMessageStore(ClassLoader loader) { + try { + Class cms = Unobfuscator.loadCoreMessageStore(loader); + XposedBridge.log("WAE: CoreMessageStore class: " + cms.getName()); + for (Method m : cms.getDeclaredMethods()) { + // Dump ALL methods to find the right signature + XposedBridge.log("WAE: CMS method: " + m.getName() + " " + Arrays.toString(m.getParameterTypes())); + } + } catch (Exception e) { + XposedBridge.log("WAE: CMS Dump failed: " + e); + } + } + + @Override + public String getPluginName() { + return "Spy Tool"; + } +} diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/spoofer/HookBL.java b/app/src/main/java/com/wmods/wppenhacer/xposed/spoofer/HookBL.java index 73d247b8e..42995f00c 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/spoofer/HookBL.java +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/spoofer/HookBL.java @@ -1,6 +1,6 @@ package com.wmods.wppenhacer.xposed.spoofer; -import android.app.AndroidAppHelper; + import android.app.Application; import android.content.Context; import android.content.pm.PackageManager; @@ -492,7 +492,7 @@ else if ("android.software.device_id_attestation".equals(featureName)) }; try { - Application app = AndroidAppHelper.currentApplication(); + Application app = com.wmods.wppenhacer.xposed.core.FeatureLoader.mApp; Class PackageManagerClass, SharedPreferencesClass; diff --git a/app/src/main/java/com/wmods/wppenhacer/xposed/utils/DesignUtils.java b/app/src/main/java/com/wmods/wppenhacer/xposed/utils/DesignUtils.java index a00033a83..b8a8939fa 100644 --- a/app/src/main/java/com/wmods/wppenhacer/xposed/utils/DesignUtils.java +++ b/app/src/main/java/com/wmods/wppenhacer/xposed/utils/DesignUtils.java @@ -34,24 +34,24 @@ public class DesignUtils { private static SharedPreferences mPrefs; - @SuppressLint("UseCompatLoadingForDrawables") public static Drawable getDrawable(int id) { return Utils.getApplication().getDrawable(id); } - @Nullable public static Drawable getDrawableByName(String name) { var id = Utils.getID(name, "drawable"); - if (id == 0) return null; + if (id == 0) + return null; return DesignUtils.getDrawable(id); } @Nullable public static Drawable getIconByName(String name, boolean isTheme) { var id = Utils.getID(name, "drawable"); - if (id == 0) return null; + if (id == 0) + return null; var icon = DesignUtils.getDrawable(id); if (isTheme && icon != null) { return DesignUtils.coloredDrawable(icon, isNightMode() ? Color.WHITE : Color.BLACK); @@ -59,7 +59,6 @@ public static Drawable getIconByName(String name, boolean isTheme) { return icon; } - @NonNull public static Drawable coloredDrawable(Drawable drawable, int color) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -70,7 +69,6 @@ public static Drawable coloredDrawable(Drawable drawable, int color) { return drawable; } - @SuppressLint("UseCompatLoadingForDrawables") public static Drawable alphaDrawable(Drawable drawable, int primaryTextColor, int i) { Drawable coloredDrawable = DesignUtils.coloredDrawable(drawable, primaryTextColor); @@ -83,25 +81,28 @@ public static Drawable createDrawable(String type, int color) { switch (type) { case "rc_dialog_bg" -> { var border = Utils.dipToPixels(12.0f); - var shapeDrawable = new ShapeDrawable(new RoundRectShape(new float[]{border, border, border, border, 0, 0, 0, 0}, null, null)); + var shapeDrawable = new ShapeDrawable( + new RoundRectShape(new float[] { border, border, border, border, 0, 0, 0, 0 }, null, null)); shapeDrawable.getPaint().setColor(color); return shapeDrawable; } case "selector_bg" -> { var border = Utils.dipToPixels(18.0f); - ShapeDrawable selectorBg = new ShapeDrawable(new RoundRectShape(new float[]{border, border, border, border, border, border, border, border}, null, null)); + ShapeDrawable selectorBg = new ShapeDrawable(new RoundRectShape( + new float[] { border, border, border, border, border, border, border, border }, null, null)); selectorBg.getPaint().setColor(color); return selectorBg; } case "rc_dotline_dialog" -> { var border = Utils.dipToPixels(16.0f); - ShapeDrawable shapeDrawable = new ShapeDrawable(new RoundRectShape(new float[]{border, border, border, border, border, border, border, border}, null, null)); + ShapeDrawable shapeDrawable = new ShapeDrawable(new RoundRectShape( + new float[] { border, border, border, border, border, border, border, border }, null, null)); shapeDrawable.getPaint().setColor(color); return shapeDrawable; } case "stroke_border" -> { float radius = Utils.dipToPixels(18.0f); - float[] outerRadii = new float[]{radius, radius, radius, radius, radius, radius, radius, radius}; + float[] outerRadii = new float[] { radius, radius, radius, radius, radius, radius, radius, radius }; RoundRectShape roundRectShape = new RoundRectShape(outerRadii, null, null); ShapeDrawable shapeDrawable = new ShapeDrawable(roundRectShape); Paint paint = shapeDrawable.getPaint(); @@ -132,7 +133,6 @@ public static int getPrimaryTextColor() { return textColor; } - public static int getUnSeenColor() { var primaryColor = mPrefs.getInt("primary_color", 0); if (shouldUseMonetColors()) { @@ -162,7 +162,8 @@ public static int getPrimarySurfaceColor() { } public static Drawable generatePrimaryColorDrawable(Drawable drawable) { - if (drawable == null) return null; + if (drawable == null) + return null; var primaryColorInt = mPrefs.getInt("primary_color", 0); if (shouldUseMonetColors()) { var monetPrimaryColor = resolveMonetColor(isNightMode() ? "system_accent1_300" : "system_accent1_600"); @@ -180,13 +181,15 @@ public static Drawable generatePrimaryColorDrawable(Drawable drawable) { } public static void setReplacementDrawable(String name, Drawable replacement) { - if (WppXposed.ResParam == null) return; - WppXposed.ResParam.res.setReplacement(Utils.getApplication().getPackageName(), "drawable", name, new XResources.DrawableLoader() { - @Override - public Drawable newDrawable(XResources res, int id) throws Throwable { - return replacement; - } - }); + if (WppXposed.ResParam == null) + return; + WppXposed.ResParam.res.setReplacement(Utils.getApplication().getPackageName(), "drawable", name, + new XResources.DrawableLoader() { + @Override + public Drawable newDrawable(XResources res, int id) throws Throwable { + return replacement; + } + }); } public static boolean isNightMode() { @@ -197,7 +200,6 @@ public static boolean isNightModeBySystem() { return (Utils.getApplication().getResources().getConfiguration().uiMode & 48) == 32; } - public static void setPrefs(SharedPreferences mPrefs) { DesignUtils.mPrefs = mPrefs; } @@ -256,7 +258,8 @@ public static Bitmap drawableToBitmap(Drawable drawable) { return ((BitmapDrawable) drawable).getBitmap(); } - Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); diff --git a/app/src/main/res/drawable/badge_background.xml b/app/src/main/res/drawable/badge_background.xml new file mode 100644 index 000000000..ad2aaa964 --- /dev/null +++ b/app/src/main/res/drawable/badge_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_message_received.xml b/app/src/main/res/drawable/bg_message_received.xml new file mode 100644 index 000000000..57ef707a1 --- /dev/null +++ b/app/src/main/res/drawable/bg_message_received.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_message_sent.xml b/app/src/main/res/drawable/bg_message_sent.xml new file mode 100644 index 000000000..4e83d45b2 --- /dev/null +++ b/app/src/main/res/drawable/bg_message_sent.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/bottom_sheet_background.xml b/app/src/main/res/drawable/bottom_sheet_background.xml new file mode 100644 index 000000000..8a6776862 --- /dev/null +++ b/app/src/main/res/drawable/bottom_sheet_background.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/button_primary_background.xml b/app/src/main/res/drawable/button_primary_background.xml new file mode 100644 index 000000000..1c752e2e2 --- /dev/null +++ b/app/src/main/res/drawable/button_primary_background.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/category_badge_background.xml b/app/src/main/res/drawable/category_badge_background.xml new file mode 100644 index 000000000..cc2a5eac1 --- /dev/null +++ b/app/src/main/res/drawable/category_badge_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/circle_bg.xml b/app/src/main/res/drawable/circle_bg.xml new file mode 100644 index 000000000..6e3fedaab --- /dev/null +++ b/app/src/main/res/drawable/circle_bg.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/circle_button_background.xml b/app/src/main/res/drawable/circle_button_background.xml new file mode 100644 index 000000000..1bad86902 --- /dev/null +++ b/app/src/main/res/drawable/circle_button_background.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/dialog_background.xml b/app/src/main/res/drawable/dialog_background.xml new file mode 100644 index 000000000..4a3a58b97 --- /dev/null +++ b/app/src/main/res/drawable/dialog_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/duration_badge_background.xml b/app/src/main/res/drawable/duration_badge_background.xml new file mode 100644 index 000000000..e9dedd8e0 --- /dev/null +++ b/app/src/main/res/drawable/duration_badge_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/gradient_background.xml b/app/src/main/res/drawable/gradient_background.xml new file mode 100644 index 000000000..d4c58de1d --- /dev/null +++ b/app/src/main/res/drawable/gradient_background.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 000000000..c99238a8f --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 000000000..d5599e06e --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 000000000..5fc26f4d3 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_contacts.xml b/app/src/main/res/drawable/ic_contacts.xml new file mode 100644 index 000000000..51723ec37 --- /dev/null +++ b/app/src/main/res/drawable/ic_contacts.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 000000000..3c4030b03 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_image.xml b/app/src/main/res/drawable/ic_image.xml new file mode 100644 index 000000000..8232c4de2 --- /dev/null +++ b/app/src/main/res/drawable/ic_image.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml new file mode 100644 index 000000000..3d9ff3c76 --- /dev/null +++ b/app/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_person.xml b/app/src/main/res/drawable/ic_person.xml new file mode 100644 index 000000000..a40c2f874 --- /dev/null +++ b/app/src/main/res/drawable/ic_person.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml new file mode 100644 index 000000000..01e830d15 --- /dev/null +++ b/app/src/main/res/drawable/ic_play.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_recording.xml b/app/src/main/res/drawable/ic_recording.xml new file mode 100644 index 000000000..fa12d7cbb --- /dev/null +++ b/app/src/main/res/drawable/ic_recording.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_restore.xml b/app/src/main/res/drawable/ic_restore.xml new file mode 100644 index 000000000..1058f0c70 --- /dev/null +++ b/app/src/main/res/drawable/ic_restore.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 000000000..8ef92f730 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml new file mode 100644 index 000000000..729a1c005 --- /dev/null +++ b/app/src/main/res/drawable/ic_warning.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/rounded_background_outline.xml b/app/src/main/res/drawable/rounded_background_outline.xml new file mode 100644 index 000000000..bd2c6078d --- /dev/null +++ b/app/src/main/res/drawable/rounded_background_outline.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/version_badge_background.xml b/app/src/main/res/drawable/version_badge_background.xml new file mode 100644 index 000000000..75981c148 --- /dev/null +++ b/app/src/main/res/drawable/version_badge_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/layout/activity_call_recording_settings.xml b/app/src/main/res/layout/activity_call_recording_settings.xml new file mode 100644 index 000000000..7a41dca69 --- /dev/null +++ b/app/src/main/res/layout/activity_call_recording_settings.xml @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_deleted_messages.xml b/app/src/main/res/layout/activity_deleted_messages.xml new file mode 100644 index 000000000..666bb2af8 --- /dev/null +++ b/app/src/main/res/layout/activity_deleted_messages.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_message_list.xml b/app/src/main/res/layout/activity_message_list.xml new file mode 100644 index 000000000..f82d0843e --- /dev/null +++ b/app/src/main/res/layout/activity_message_list.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml new file mode 100644 index 000000000..7e73c4a5e --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_audio_player.xml b/app/src/main/res/layout/dialog_audio_player.xml new file mode 100644 index 000000000..4e52787f4 --- /dev/null +++ b/app/src/main/res/layout/dialog_audio_player.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_update_available.xml b/app/src/main/res/layout/dialog_update_available.xml new file mode 100644 index 000000000..d72c55e2d --- /dev/null +++ b/app/src/main/res/layout/dialog_update_available.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_deleted_messages.xml b/app/src/main/res/layout/fragment_deleted_messages.xml new file mode 100644 index 000000000..faec68ce9 --- /dev/null +++ b/app/src/main/res/layout/fragment_deleted_messages.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/app/src/main/res/layout/fragment_recordings.xml b/app/src/main/res/layout/fragment_recordings.xml new file mode 100644 index 000000000..923b2b8fb --- /dev/null +++ b/app/src/main/res/layout/fragment_recordings.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_contact_row.xml b/app/src/main/res/layout/item_contact_row.xml new file mode 100644 index 000000000..8f261feba --- /dev/null +++ b/app/src/main/res/layout/item_contact_row.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_deleted_message.xml b/app/src/main/res/layout/item_deleted_message.xml new file mode 100644 index 000000000..362f3f98a --- /dev/null +++ b/app/src/main/res/layout/item_deleted_message.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_deleted_message_contact.xml b/app/src/main/res/layout/item_deleted_message_contact.xml new file mode 100644 index 000000000..5f2d00a3d --- /dev/null +++ b/app/src/main/res/layout/item_deleted_message_contact.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_message_received.xml b/app/src/main/res/layout/item_message_received.xml new file mode 100644 index 000000000..5e5782750 --- /dev/null +++ b/app/src/main/res/layout/item_message_received.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_message_sent.xml b/app/src/main/res/layout/item_message_sent.xml new file mode 100644 index 000000000..6958765d7 --- /dev/null +++ b/app/src/main/res/layout/item_message_sent.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_recording.xml b/app/src/main/res/layout/item_recording.xml new file mode 100644 index 000000000..817b94396 --- /dev/null +++ b/app/src/main/res/layout/item_recording.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_search_result.xml b/app/src/main/res/layout/item_search_result.xml new file mode 100644 index 000000000..1e2646195 --- /dev/null +++ b/app/src/main/res/layout/item_search_result.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_search_section_header.xml b/app/src/main/res/layout/item_search_section_header.xml new file mode 100644 index 000000000..77571a792 --- /dev/null +++ b/app/src/main/res/layout/item_search_section_header.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/layout/layout_restore_coming_soon.xml b/app/src/main/res/layout/layout_restore_coming_soon.xml new file mode 100644 index 000000000..6acd7839d --- /dev/null +++ b/app/src/main/res/layout/layout_restore_coming_soon.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index eb9822cf9..777ac9b42 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -26,5 +26,10 @@ android:icon="@drawable/ic_dashboard_black_24dp" android:title="@string/perso" /> + + \ No newline at end of file diff --git a/app/src/main/res/menu/header_menu.xml b/app/src/main/res/menu/header_menu.xml index 55a1d7876..8457a59c5 100644 --- a/app/src/main/res/menu/header_menu.xml +++ b/app/src/main/res/menu/header_menu.xml @@ -2,6 +2,13 @@ + + + + + diff --git a/app/src/main/res/menu/menu_deleted_messages.xml b/app/src/main/res/menu/menu_deleted_messages.xml new file mode 100644 index 000000000..8aeec1da7 --- /dev/null +++ b/app/src/main/res/menu/menu_deleted_messages.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/menu/menu_message_list.xml b/app/src/main/res/menu/menu_message_list.xml new file mode 100644 index 000000000..07fb7e4df --- /dev/null +++ b/app/src/main/res/menu/menu_message_list.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index a6988c59a..fdfc0c2ee 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -2,6 +2,7 @@ com.whatsapp + com.whatsapp.w4b android diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index b5d2eeb98..4ffbb4349 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -52,5 +52,11 @@ #212121 #757575 + #9E9E9E #FFFFFF + + #FAFAFA + #FFFFFF + #00796B + #4D2196F3 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1fa07b75d..7be6ea8a8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,6 @@ + Force Restore Backup + Launch the restore backup screen manually. Use this if you skipped restoration during setup. Wa Enhancer WhatsApp Enhancer Module Anti-Revoke Messages @@ -446,5 +448,34 @@ Blocks ads from appearing in WhatsApp Enhanced Locked Chats Improve locked chats by hiding notifications and contacts from the contact list - + Call Recording + Enable Call Recording + Record incoming and outgoing calls (Voice & Video) as audio. + Recordings Output Folder + Record Video Calls (Screen) + Coming soon! + Coming soon! + Recording Mode + Select which calls to record + Record All Calls + Record Unknown Numbers Only + Record Only Selected Contacts + Record All Except Selected + Included Contacts + Excluded Contacts + + Search Features + Search for any feature... + No features found + Try different keywords or browse categories + Start typing to search features + No deleted messages found + Deleted By Me + Restore + View messages deleted by others + Restore Feature + "Coming soon!" + Coming soon! + Enable Spy Tool + Logs internal events to Xposed log for debugging. diff --git a/app/src/main/res/values/strings_recordings.xml b/app/src/main/res/values/strings_recordings.xml new file mode 100644 index 000000000..698ff76c7 --- /dev/null +++ b/app/src/main/res/values/strings_recordings.xml @@ -0,0 +1,69 @@ + + + Recordings + No recordings found + Are you sure you want to delete this recording? + Are you sure you want to delete %d recordings? + Share Recording + Share Recordings + Permission Required + Full File Access is required to manage recordings stored in the root folder. + Grant + Sort By + Name + Date + Duration + Contact + + + List + By Contact + + + %d selected + Select All + + + Recordings for %s + %d recording(s) + + + Play + Pause + Play/Pause + Close + Share + Delete + + + Show Recording Notifications + Show toast when recording starts/stops + + + Recording Settings + Select Recording Mode + Choose how call audio is captured. Root mode provides better quality but requires a rooted device. + + + 🔓 Root Mode (Recommended) + Uses system-level audio capture for the best recording quality. + Records both sides of the conversation + Crystal clear audio quality + Works with all VoIP apps + ⚠️ Requires rooted device with Magisk/SuperSU + + + 📱 Non-Root Mode + Uses standard Android audio APIs. Works on all devices but with limitations. + No root required + Works on any Android device + May only capture your microphone (not the other party) + + + ✓ Advantages + ⚠ Limitations + Changes take effect on the next call. Restart WhatsApp after changing settings for best results. + Root access granted! VOICE_CALL source enabled. + Root access denied. Falling back to non-root mode. + Non-root mode enabled. + diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 000000000..cbed144b6 --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/app/src/main/res/xml/fragment_general.xml b/app/src/main/res/xml/fragment_general.xml index 292eca0c0..a26ed0f5e 100644 --- a/app/src/main/res/xml/fragment_general.xml +++ b/app/src/main/res/xml/fragment_general.xml @@ -35,6 +35,11 @@ app:summary="@string/toast_on_viewed_status_sum" app:title="@string/toast_on_viewed_status" /> + + diff --git a/app/src/main/res/xml/fragment_media.xml b/app/src/main/res/xml/fragment_media.xml index e36a88eaf..c1b16d0a8 100644 --- a/app/src/main/res/xml/fragment_media.xml +++ b/app/src/main/res/xml/fragment_media.xml @@ -65,6 +65,42 @@ app:title="@string/send_video_in_60fps" /> + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/fragment_privacy.xml b/app/src/main/res/xml/fragment_privacy.xml index 980728538..d87d2f705 100644 --- a/app/src/main/res/xml/fragment_privacy.xml +++ b/app/src/main/res/xml/fragment_privacy.xml @@ -41,6 +41,11 @@ app:iconSpaceReserved="false" app:title="@string/conversation"> + + + +
@@ -98,6 +101,7 @@ - `Send Audio as Voice/Audio Note` - `Enable Media Preview` - `Custom Download Location` +- `Force Restore Cloud Backup`
diff --git a/explainer.png b/explainer.png new file mode 100644 index 000000000..d8a0389fd Binary files /dev/null and b/explainer.png differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 27d43a9a3..aefae9115 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ colorpicker = "1.1.0" dexkit = "2.0.7" nav = "2.9.6" kotlin = "2.2.21" - +markwon = "4.6.2" [libraries] androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" } @@ -40,9 +40,9 @@ rikkax-material = { module = "dev.rikka.rikkax.material:material", version = "2. rikkax-material-preference = { module = "dev.rikka.rikkax.material:material-preference", version = "2.0.0" } rikkax-widget-borderview = { module = "dev.rikka.rikkax.widget:borderview", version = "1.1.0" } material = { module = "com.google.android.material:material", version = "1.13.0" } +markwon-core = { module = "io.noties.markwon:core", version.ref = "markwon" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } materialthemebuilder = { id = "dev.rikka.tools.materialthemebuilder", version = "1.5.1" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } - diff --git a/gradlew b/gradlew old mode 100644 new mode 100755