diff --git a/extensions/nothingx/build.gradle.kts b/extensions/nothingx/build.gradle.kts new file mode 100644 index 000000000..bfbf5c815 --- /dev/null +++ b/extensions/nothingx/build.gradle.kts @@ -0,0 +1,15 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) + compileOnly(project(":extensions:nothingx:stub")) +} + +android { + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} \ No newline at end of file diff --git a/extensions/nothingx/src/main/AndroidManifest.xml b/extensions/nothingx/src/main/AndroidManifest.xml new file mode 100644 index 000000000..15e7c2ae6 --- /dev/null +++ b/extensions/nothingx/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/nothingx/src/main/java/app/revanced/extension/nothingx/patches/ShowK1TokensPatch.java b/extensions/nothingx/src/main/java/app/revanced/extension/nothingx/patches/ShowK1TokensPatch.java new file mode 100644 index 000000000..c301ae2fb --- /dev/null +++ b/extensions/nothingx/src/main/java/app/revanced/extension/nothingx/patches/ShowK1TokensPatch.java @@ -0,0 +1,590 @@ +package app.revanced.extension.nothingx.patches; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Application; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Patches to expose the K1 token for Nothing X app to enable pairing with GadgetBridge. + */ +@SuppressWarnings("unused") +public class ShowK1TokensPatch { + + private static final String TAG = "ReVanced"; + private static final String PACKAGE_NAME = "com.nothing.smartcenter"; + private static final String EMPTY_MD5 = "d41d8cd98f00b204e9800998ecf8427e"; + private static final String PREFS_NAME = "revanced_nothingx_prefs"; + private static final String KEY_DONT_SHOW_DIALOG = "dont_show_k1_dialog"; + + // Colors + private static final int COLOR_BG = 0xFF1E1E1E; + private static final int COLOR_CARD = 0xFF2D2D2D; + private static final int COLOR_TEXT_PRIMARY = 0xFFFFFFFF; + private static final int COLOR_TEXT_SECONDARY = 0xFFB0B0B0; + private static final int COLOR_ACCENT = 0xFFFF9500; + private static final int COLOR_TOKEN_BG = 0xFF3A3A3A; + private static final int COLOR_BUTTON_POSITIVE = 0xFFFF9500; + private static final int COLOR_BUTTON_NEGATIVE = 0xFFFF6B6B; + + // Match standalone K1: k1:, K1:, k1>, etc. + private static final Pattern K1_STANDALONE_PATTERN = Pattern.compile("(?i)(?:k1\\s*[:>]\\s*)([0-9a-f]{32})"); + // Match combined r3+k1: format (64 chars = r3(32) + k1(32)) + private static final Pattern K1_COMBINED_PATTERN = Pattern.compile("(?i)r3\\+k1\\s*:\\s*([0-9a-f]{64})"); + + private static volatile boolean k1Logged = false; + private static volatile boolean lifecycleCallbacksRegistered = false; + private static Context appContext; + + /** + * Get K1 tokens from database and log files. + * Call this after the app initializes. + * + * @param context Application context + */ + public static void showK1Tokens(Context context) { + if (k1Logged) { + return; + } + + appContext = context.getApplicationContext(); + + Set allTokens = new LinkedHashSet<>(); + + // First try to get from database. + String dbToken = getK1TokensFromDatabase(); + if (dbToken != null) { + allTokens.add(dbToken); + } + + // Then get from log files. + Set logTokens = getK1TokensFromLogFiles(); + allTokens.addAll(logTokens); + + if (allTokens.isEmpty()) { + return; + } + + // Log all found tokens. + int index = 1; + for (String token : allTokens) { + Log.i(TAG, "#" + index++ + ": " + token.toUpperCase()); + } + + // Register lifecycle callbacks to show dialog when an Activity is ready. + registerLifecycleCallbacks(allTokens); + + k1Logged = true; + } + + /** + * Register ActivityLifecycleCallbacks to show dialog when first Activity resumes. + * + * @param tokens Set of K1 tokens to display + */ + private static void registerLifecycleCallbacks(Set tokens) { + if (lifecycleCallbacksRegistered || !(appContext instanceof Application)) { + return; + } + + Application application = (Application) appContext; + application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + } + + @Override + public void onActivityStarted(Activity activity) { + } + + @Override + public void onActivityResumed(Activity activity) { + // Check if user chose not to show dialog. + SharedPreferences prefs = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + if (prefs.getBoolean(KEY_DONT_SHOW_DIALOG, false)) { + application.unregisterActivityLifecycleCallbacks(this); + lifecycleCallbacksRegistered = false; + return; + } + + // Show dialog on first Activity resume. + if (tokens != null && !tokens.isEmpty()) { + activity.runOnUiThread(() -> showK1TokensDialog(activity, tokens)); + // Unregister after showing + application.unregisterActivityLifecycleCallbacks(this); + lifecycleCallbacksRegistered = false; + } + } + + @Override + public void onActivityPaused(Activity activity) { + } + + @Override + public void onActivityStopped(Activity activity) { + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + } + + @Override + public void onActivityDestroyed(Activity activity) { + } + }); + + lifecycleCallbacksRegistered = true; + } + + /** + * Show dialog with K1 tokens. + * + * @param activity Activity context + * @param tokens Set of K1 tokens + */ + private static void showK1TokensDialog(Activity activity, Set tokens) { + try { + // Create main container. + LinearLayout mainLayout = new LinearLayout(activity); + mainLayout.setOrientation(LinearLayout.VERTICAL); + mainLayout.setBackgroundColor(COLOR_BG); + mainLayout.setPadding(dpToPx(activity, 24), dpToPx(activity, 16), + dpToPx(activity, 24), dpToPx(activity, 16)); + + // Title. + TextView titleView = new TextView(activity); + titleView.setText("K1 Token(s) Found"); + titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); + titleView.setTypeface(Typeface.DEFAULT_BOLD); + titleView.setTextColor(COLOR_TEXT_PRIMARY); + titleView.setGravity(Gravity.CENTER); + mainLayout.addView(titleView, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + )); + + // Subtitle. + TextView subtitleView = new TextView(activity); + subtitleView.setText(tokens.size() == 1 ? "1 token found • Tap to copy" : tokens.size() + " tokens found • Tap to copy"); + subtitleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + subtitleView.setTextColor(COLOR_TEXT_SECONDARY); + subtitleView.setGravity(Gravity.CENTER); + LinearLayout.LayoutParams subtitleParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + subtitleParams.topMargin = dpToPx(activity, 4); + subtitleParams.bottomMargin = dpToPx(activity, 16); + mainLayout.addView(subtitleView, subtitleParams); + + // Scrollable content. + ScrollView scrollView = new ScrollView(activity); + scrollView.setVerticalScrollBarEnabled(false); + LinearLayout.LayoutParams scrollParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + 0, + 1.0f + ); + scrollParams.topMargin = dpToPx(activity, 8); + scrollParams.bottomMargin = dpToPx(activity, 16); + mainLayout.addView(scrollView, scrollParams); + + LinearLayout tokensContainer = new LinearLayout(activity); + tokensContainer.setOrientation(LinearLayout.VERTICAL); + scrollView.addView(tokensContainer); + + // Add each token as a card. + boolean singleToken = tokens.size() == 1; + int index = 1; + for (String token : tokens) { + LinearLayout tokenCard = createTokenCard(activity, token, index++, singleToken); + LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + cardParams.bottomMargin = dpToPx(activity, 12); + tokensContainer.addView(tokenCard, cardParams); + } + + // Info text. + TextView infoView = new TextView(activity); + infoView.setText(tokens.size() == 1 ? "Tap the token to copy it" : "Tap any token to copy it"); + infoView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + infoView.setTextColor(COLOR_TEXT_SECONDARY); + infoView.setGravity(Gravity.CENTER); + infoView.setAlpha(0.7f); + LinearLayout.LayoutParams infoParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + infoParams.topMargin = dpToPx(activity, 8); + mainLayout.addView(infoView, infoParams); + + // Button row. + LinearLayout buttonRow = new LinearLayout(activity); + buttonRow.setOrientation(LinearLayout.HORIZONTAL); + buttonRow.setGravity(Gravity.END); + LinearLayout.LayoutParams buttonRowParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + buttonRowParams.topMargin = dpToPx(activity, 16); + mainLayout.addView(buttonRow, buttonRowParams); + + // "Don't show again" button. + Button dontShowButton = new Button(activity); + dontShowButton.setText("Don't show again"); + dontShowButton.setTextColor(Color.WHITE); + dontShowButton.setBackgroundColor(Color.TRANSPARENT); + dontShowButton.setAllCaps(false); + dontShowButton.setTypeface(Typeface.DEFAULT); + dontShowButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + dontShowButton.setPadding(dpToPx(activity, 16), dpToPx(activity, 8), + dpToPx(activity, 16), dpToPx(activity, 8)); + LinearLayout.LayoutParams dontShowParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + dontShowParams.rightMargin = dpToPx(activity, 8); + buttonRow.addView(dontShowButton, dontShowParams); + + // "OK" button. + Button okButton = new Button(activity); + okButton.setText("OK"); + okButton.setTextColor(Color.BLACK); + okButton.setBackgroundColor(COLOR_BUTTON_POSITIVE); + okButton.setAllCaps(false); + okButton.setTypeface(Typeface.DEFAULT_BOLD); + okButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + okButton.setPadding(dpToPx(activity, 24), dpToPx(activity, 12), + dpToPx(activity, 24), dpToPx(activity, 12)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + okButton.setElevation(dpToPx(activity, 4)); + } + buttonRow.addView(okButton, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + )); + + // Build dialog. + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setView(mainLayout); + + final AlertDialog dialog = builder.create(); + + // Style the dialog with dark background. + if (dialog.getWindow() != null) { + dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent); + } + + dialog.show(); + + // Set button click listeners after dialog is created. + dontShowButton.setOnClickListener(v -> { + SharedPreferences prefs = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + prefs.edit().putBoolean(KEY_DONT_SHOW_DIALOG, true).apply(); + Toast.makeText(activity, "Dialog disabled. Clear app data to re-enable.", + Toast.LENGTH_SHORT).show(); + dialog.dismiss(); + }); + + okButton.setOnClickListener(v -> { + dialog.dismiss(); + }); + + } catch (Exception e) { + Log.e(TAG, "Failed to show K1 dialog", e); + } + } + + /** + * Create a card view for a single token. + */ + private static LinearLayout createTokenCard(Activity activity, String token, int index, boolean singleToken) { + LinearLayout card = new LinearLayout(activity); + card.setOrientation(LinearLayout.VERTICAL); + card.setBackgroundColor(COLOR_TOKEN_BG); + card.setPadding(dpToPx(activity, 16), dpToPx(activity, 12), + dpToPx(activity, 16), dpToPx(activity, 12)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + card.setElevation(dpToPx(activity, 2)); + } + card.setClickable(true); + card.setFocusable(true); + + // Token label (only show if multiple tokens). + if (!singleToken) { + TextView labelView = new TextView(activity); + labelView.setText("Token #" + index); + labelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + labelView.setTextColor(COLOR_ACCENT); + labelView.setTypeface(Typeface.DEFAULT_BOLD); + card.addView(labelView); + } + + // Token value. + TextView tokenView = new TextView(activity); + tokenView.setText(token.toUpperCase()); + tokenView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + tokenView.setTextColor(COLOR_TEXT_PRIMARY); + tokenView.setTypeface(Typeface.MONOSPACE); + tokenView.setLetterSpacing(0.05f); + LinearLayout.LayoutParams tokenParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + if (!singleToken) { + tokenParams.topMargin = dpToPx(activity, 8); + } + card.addView(tokenView, tokenParams); + + // Click to copy. + card.setOnClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard != null) { + clipboard.setText(token.toUpperCase()); + Toast.makeText(activity, "Token copied!", Toast.LENGTH_SHORT).show(); + } + }); + + return card; + } + + /** + * Convert dp to pixels. + */ + private static int dpToPx(Context context, float dp) { + return (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp, + context.getResources().getDisplayMetrics() + ); + } + + /** + * Get K1 tokens from log files. + * Prioritizes pairing K1 tokens over reconnect tokens. + */ + private static Set getK1TokensFromLogFiles() { + Set pairingTokens = new LinkedHashSet<>(); + Set reconnectTokens = new LinkedHashSet<>(); + try { + File logDir = new File("/data/data/" + PACKAGE_NAME + "/files/log"); + if (!logDir.exists() || !logDir.isDirectory()) { + return pairingTokens; + } + + File[] logFiles = logDir.listFiles((dir, name) -> + name.endsWith(".log") || name.endsWith(".log.") || name.matches(".*\\.log\\.\\d+")); + + if (logFiles == null || logFiles.length == 0) { + return pairingTokens; + } + + for (File logFile : logFiles) { + try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) { + String line; + while ((line = reader.readLine()) != null) { + // Determine if this is a pairing or reconnect context. + boolean isPairingContext = line.toLowerCase().contains("watchbind"); + boolean isReconnectContext = line.toLowerCase().contains("watchreconnect"); + + String k1Token = null; + + // First check for combined r3+k1 format (priority). + Matcher combinedMatcher = K1_COMBINED_PATTERN.matcher(line); + if (combinedMatcher.find()) { + String combined = combinedMatcher.group(1); + if (combined.length() == 64) { + // Second half is the actual K1 + k1Token = combined.substring(32).toLowerCase(); + } + } + + // Then check for standalone K1 format (only if not found in combined). + if (k1Token == null) { + Matcher standaloneMatcher = K1_STANDALONE_PATTERN.matcher(line); + if (standaloneMatcher.find()) { + String token = standaloneMatcher.group(1); + if (token != null && token.length() == 32) { + k1Token = token.toLowerCase(); + } + } + } + + // Add to appropriate set. + if (k1Token != null) { + if (isPairingContext && !isReconnectContext) { + pairingTokens.add(k1Token); + } else { + reconnectTokens.add(k1Token); + } + } + } + } catch (Exception e) { + // Skip unreadable files. + } + } + } catch (Exception ex) { + // Fail silently. + } + + // Return pairing tokens first, add reconnect tokens if no pairing tokens found. + if (!pairingTokens.isEmpty()) { + Log.i(TAG, "Found " + pairingTokens.size() + " pairing K1 token(s)"); + return pairingTokens; + } + + if (!reconnectTokens.isEmpty()) { + Log.i(TAG, "Found " + reconnectTokens.size() + " reconnect K1 token(s) (may not work for initial pairing)"); + } + return reconnectTokens; + } + + /** + * Try to get K1 tokens from the database. + */ + private static String getK1TokensFromDatabase() { + try { + File dbDir = new File("/data/data/" + PACKAGE_NAME + "/databases"); + if (!dbDir.exists() || !dbDir.isDirectory()) { + return null; + } + + File[] dbFiles = dbDir.listFiles((dir, name) -> + name.endsWith(".db") && !name.startsWith("google_app_measurement") && !name.contains("firebase")); + + if (dbFiles == null || dbFiles.length == 0) { + return null; + } + + for (File dbFile : dbFiles) { + String token = getK1TokensFromDatabase(dbFile); + if (token != null) { + return token; + } + } + + return null; + } catch (Exception ex) { + return null; + } + } + + /** + * Extract K1 tokens from a database file. + */ + private static String getK1TokensFromDatabase(File dbFile) { + SQLiteDatabase db = null; + try { + db = SQLiteDatabase.openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY); + + // Get all tables. + Cursor cursor = db.rawQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", + null + ); + + List tables = new ArrayList<>(); + while (cursor.moveToNext()) { + tables.add(cursor.getString(0)); + } + cursor.close(); + + // Scan all columns for 32-char hex strings. + for (String table : tables) { + Cursor schemaCursor = null; + try { + schemaCursor = db.rawQuery("PRAGMA table_info(" + table + ")", null); + List columns = new ArrayList<>(); + while (schemaCursor.moveToNext()) { + columns.add(schemaCursor.getString(1)); + } + schemaCursor.close(); + + for (String column : columns) { + Cursor dataCursor = null; + try { + dataCursor = db.query(table, new String[]{column}, null, null, null, null, null); + while (dataCursor.moveToNext()) { + String value = dataCursor.getString(0); + if (value != null && value.length() == 32 && value.matches("[0-9a-fA-F]{32}")) { + // Skip obviously fake tokens (MD5 of empty string). + if (!value.equalsIgnoreCase(EMPTY_MD5)) { + dataCursor.close(); + db.close(); + return value.toLowerCase(); + } + } + } + } catch (Exception e) { + // Skip non-string columns. + } finally { + if (dataCursor != null) { + dataCursor.close(); + } + } + } + } catch (Exception e) { + // Continue to next table. + } finally { + if (schemaCursor != null && !schemaCursor.isClosed()) { + schemaCursor.close(); + } + } + } + + return null; + } catch (Exception ex) { + return null; + } finally { + if (db != null && db.isOpen()) { + db.close(); + } + } + } + + /** + * Reset the logged flag (useful for testing or re-pairing). + */ + public static void resetK1Logged() { + k1Logged = false; + lifecycleCallbacksRegistered = false; + } + + /** + * Reset the "don't show again" preference. + */ + public static void resetDontShowPreference() { + if (appContext != null) { + SharedPreferences prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + prefs.edit().putBoolean(KEY_DONT_SHOW_DIALOG, false).apply(); + } + } +} diff --git a/extensions/nothingx/stub/build.gradle.kts b/extensions/nothingx/stub/build.gradle.kts new file mode 100644 index 000000000..fcadc678c --- /dev/null +++ b/extensions/nothingx/stub/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} \ No newline at end of file diff --git a/extensions/nothingx/stub/src/main/AndroidManifest.xml b/extensions/nothingx/stub/src/main/AndroidManifest.xml new file mode 100644 index 000000000..15e7c2ae6 --- /dev/null +++ b/extensions/nothingx/stub/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/patches/api/patches.api b/patches/api/patches.api index 2cd0b9e94..a593bbf97 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -589,6 +589,14 @@ public final class app/revanced/patches/nfctoolsse/misc/pro/UnlockProPatchKt { public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } +public final class app/revanced/patches/nothingx/misc/extension/SharedExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/nothingx/misc/logk1token/ShowK1TokenPatchsKt { + public static final fun getShowK1TokensPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/nunl/ads/HideAdsPatchKt { public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } @@ -946,8 +954,6 @@ public final class app/revanced/patches/shared/misc/hex/Replacement { public final class app/revanced/patches/shared/misc/litho/filter/LithoFilterPatchKt { public static final fun getAddLithoFilter ()Lkotlin/jvm/functions/Function1; - public static final fun lithoFilterPatch (Lkotlin/jvm/functions/Function1;Lapp/revanced/patcher/Fingerprint;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch; - public static synthetic fun lithoFilterPatch$default (Lkotlin/jvm/functions/Function1;Lapp/revanced/patcher/Fingerprint;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch; } public final class app/revanced/patches/shared/misc/mapping/ResourceElement { diff --git a/patches/src/main/kotlin/app/revanced/patches/nothingx/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/nothingx/misc/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..5f1f78a63 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nothingx/misc/extension/SharedExtensionPatch.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.nothingx.misc.extension + +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch +import app.revanced.patches.shared.misc.extension.extensionHook + +val sharedExtensionPatch = sharedExtensionPatch( + extensionName = "nothingx", + extensionHook { + custom { method, classDef -> + method.name == "onCreate" && classDef.contains("BaseApplication") + } + }, +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/nothingx/misc/logk1token/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/nothingx/misc/logk1token/Fingerprints.kt new file mode 100644 index 000000000..84e5d2c2a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nothingx/misc/logk1token/Fingerprints.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.nothingx.misc.logk1token + +import app.revanced.patcher.fingerprint + +/** + * Fingerprint for the Application onCreate method. + * This is used to trigger scanning for existing log files on app startup. + */ +internal val applicationOnCreateFingerprint = fingerprint { + returns("V") + parameters() + custom { method, classDef -> + // Match BaseApplication onCreate specifically + method.name == "onCreate" && classDef.endsWith("BaseApplication;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/nothingx/misc/logk1token/ShowK1TokenPatchs.kt b/patches/src/main/kotlin/app/revanced/patches/nothingx/misc/logk1token/ShowK1TokenPatchs.kt new file mode 100644 index 000000000..3fd2b9d49 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nothingx/misc/logk1token/ShowK1TokenPatchs.kt @@ -0,0 +1,31 @@ +package app.revanced.patches.nothingx.misc.logk1token + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.nothingx.misc.extension.sharedExtensionPatch + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/nothingx/patches/ShowK1TokensPatch;" + +@Suppress("unused") +val showK1TokensPatch = bytecodePatch( + name = "Show K1 token(s)", + description = "Shows the K1 authentication token(s) in a dialog and logs it to logcat " + + "for pairing with GadgetBridge without requiring root access. " + + "After installing this patch, pair your watch with the Nothing X app and " + + "use the token from the dialog or logcat.", +) { + dependsOn(sharedExtensionPatch) + + compatibleWith("com.nothing.smartcenter"()) + + execute { + // Hook Application.onCreate to get K1 tokens from database and log files. + // This will find K1 tokens that were already written to log files. + // p0 is the Application context in onCreate. + applicationOnCreateFingerprint.method.addInstruction( + 0, + "invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->showK1Tokens(Landroid/content/Context;)V", + ) + } +}