soul-browser/scripts/compile-redirector.sh
KaKi87 c66b89dab4
Add search to userscripts and redirects (#154)
* Add search to the userscript list and the Redirect rules

The userscript list dialog gets a search button in its header that uses the same search bar as the other book lists. Redirect rules get a search button next to Delete all that swaps the title for a text field and filters rules by description, include and exclude patterns, redirect URL and example URL. Back closes the search first.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Keep the search inline and fix the Redirect rules crash

Redirect rules crashed on open because R$string has no search_menu_title field; use search_url, which exists. In the userscript list the search bar now stops at the header buttons instead of covering the whole header.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Filter the userscript list in place and polish both search bars

In the userscript list, search mode now reads the same script list as the normal view and filters it by name, instead of an empty search-result store. The input shows the Search placeholder. In Redirect rules the input has a rounded background and the back chevron cancels the search before leaving.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Match the Redirect rules search bar to the userscript one

The bar now has the back chevron, input and a clear button on one rounded background with the same height and inset as the userscript bar. The search button comes first among the header buttons.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Keep filtered scripts out of the shared script cache while searching

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Fill the whole Redirect rules search bar with its children

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Hide the Redirect rules title while the search bar is open

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Move userscript Update all into the 3-dot menu

The refresh button is removed from the userscript list header and the menu gets an Update all item that runs the same action.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Close the gap left by the removed refresh button in the userscript header

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 00:40:56 +01:00

638 lines
27 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SRC_HELPER="${ROOT_DIR}/sources/java/com/mycompany/app/web/RedirectorHelper.java"
SRC_ACTIVITY="${ROOT_DIR}/sources/java/com/mycompany/app/setting/RedirectorActivity.java"
SRC_ADAPTER="${ROOT_DIR}/sources/java/com/mycompany/app/setting/RedirectorListAdapter.java"
SRC_PREFS="${ROOT_DIR}/sources/java/com/mycompany/app/setting/RedirectorListPrefs.java"
SRC_DIALOG="${ROOT_DIR}/sources/java/com/mycompany/app/dialog/DialogEditRedirect.java"
OUT_WEB="${ROOT_DIR}/app/smali_classes4/com/mycompany/app/web"
OUT_SETTING="${ROOT_DIR}/app/smali_classes4/com/mycompany/app/setting"
OUT_DIALOG="${ROOT_DIR}/app/smali_classes4/com/mycompany/app/dialog"
TOOLS="${ROOT_DIR}/tools"
ANDROID_HOME="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-${HOME}/android-sdk}}"
ANDROID_JAR="$(ls -1 "${ANDROID_HOME}/platforms"/android-*/android.jar 2>/dev/null | sort -V | tail -1)"
D8="$(find "${ANDROID_HOME}/build-tools" -name d8 -type f 2>/dev/null | sort -V | tail -1)"
APK="$(ls -1 "${ROOT_DIR}/dist"/soul-browser-*-merged-unsigned.apk 2>/dev/null | sort | tail -1 || true)"
if [[ -z "${APK}" || ! -f "${APK}" ]]; then
APK="$(ls -1 /home/kaki/.t3/worktrees/soul-browser/*/dist/soul-browser-*-merged-unsigned.apk 2>/dev/null | sort | tail -1 || true)"
fi
DEX_TOOLS_ZIP="${TOOLS}/dex-tools-v2.4.zip"
DEX_TOOLS_DIR="${TOOLS}/dex-tools-v2.4"
mkdir -p "${TOOLS}" "${OUT_WEB}" "${OUT_SETTING}" "${OUT_DIALOG}"
if [[ ! -f "${ANDROID_JAR}" ]]; then
echo "error: android.jar not found" >&2
exit 1
fi
if [[ -z "${D8}" ]]; then
echo "error: d8 not found" >&2
exit 1
fi
if [[ -z "${APK}" || ! -f "${APK}" ]]; then
echo "error: merged unsigned APK not found; run ./scripts/build.sh first" >&2
exit 1
fi
if [[ ! -f "${DEX_TOOLS_DIR}/d2j-dex2jar.sh" ]]; then
if [[ ! -f "${DEX_TOOLS_ZIP}" ]]; then
echo "Downloading dex-tools v2.4..."
curl -fsSL -o "${DEX_TOOLS_ZIP}" \
"https://github.com/pxb1988/dex2jar/releases/download/v2.4/dex-tools-v2.4.zip"
fi
unzip -qo "${DEX_TOOLS_ZIP}" -d "${TOOLS}"
fi
chmod +x "${DEX_TOOLS_DIR}/"*.sh "${DEX_TOOLS_DIR}/d2j_invoke.sh" 2>/dev/null || true
D2J="${DEX_TOOLS_DIR}/d2j-dex2jar.sh"
STAGE="$(mktemp -d)"
cleanup() { rm -rf "${STAGE}"; }
trap cleanup EXIT
echo "Building classpath from APK dex files..."
CP="${ANDROID_JAR}"
for dex in classes.dex classes2.dex classes3.dex classes4.dex; do
unzip -p "${APK}" "${dex}" > "${STAGE}/${dex}" 2>/dev/null || true
if [[ -s "${STAGE}/${dex}" ]]; then
jar="${STAGE}/${dex%.dex}.jar"
bash "${D2J}" --force "${STAGE}/${dex}" -o "${jar}" >/dev/null
CP="${CP}:${jar}"
fi
done
# Compile-time stubs ahead of APK jars (dex2jar drops no-arg ctors; MainActivity.R shadows resources).
STUBS="${STAGE}/stubs"
mkdir -p "${STUBS}/com/mycompany/app/soulbrowser" \
"${STUBS}/com/mycompany/app/setting" \
"${STUBS}/com/mycompany/app/main" \
"${STUBS}/com/mycompany/app/view"
cat > "${STUBS}/com/mycompany/app/soulbrowser/R.java" <<'EOF'
package com.mycompany.app.soulbrowser;
public final class R {
public static final class string {
public static int redirector_title = 0x7f1205cd;
public static int redirector_on = 0x7f1205ce;
public static int redirector_on_info = 0x7f1205cf;
public static int redirector_rules = 0x7f1205d0;
public static int redirector_add = 0x7f1205d1;
public static int redirector_edit = 0x7f1205d2;
public static int redirector_import = 0x7f1205d3;
public static int redirector_export = 0x7f1205d4;
public static int redirector_include = 0x7f1205d5;
public static int redirector_exclude = 0x7f1205d6;
public static int redirector_to = 0x7f1205d7;
public static int redirector_example = 0x7f1205d8;
public static int redirector_pattern_type = 0x7f1205d9;
public static int redirector_process = 0x7f1205da;
public static int redirector_disabled_rule = 0x7f1205db;
public static int redirector_description = 0x7f1205dc;
public static int redirector_example_result = 0x7f1205dd;
public static int redirector_wildcard = 0x7f1205de;
public static int redirector_regex = 0x7f1205df;
public static int redirector_saved = 0x7f1205e0;
public static int redirector_import_ok = 0x7f1205e1;
public static int redirector_delete = 0x7f1205e2;
public static int redirector_empty = 0x7f1205e3;
public static int apply = 0x7f12005d;
public static int delete = 0x7f120154;
public static int delete_all = 0x7f120155;
public static int del_all_confirm = 0x7f120153;
public static int sort = 0x7f1204a5;
public static int search_url = 0x7f120461;
public static int sort_name = 0x7f1204ab;
public static int show_detail = 0x7f120481;
public static int show_single = 0x7f12048b;
public static int order_descend = 0x7f1203bc;
}
public static final class drawable {
public static int outline_cancel_black_18 = 0x7f080302;
public static int outline_cancel_dark_18 = 0x7f080304;
public static int outline_chevron_left_black_24 = 0x7f080318;
public static int outline_chevron_left_dark_24 = 0x7f080319;
public static int outline_add_black_24 = 0x7f0802d8;
public static int outline_add_dark_24 = 0x7f0802dc;
public static int outline_more_vert_black_24 = 0x7f0803e6;
public static int outline_more_vert_dark_24 = 0x7f0803e8;
public static int outline_search_black_20 = 0x7f080443;
public static int outline_search_dark_20 = 0x7f080445;
public static int outline_delete_black_20 = 0x7f08033f;
public static int outline_delete_dark_20 = 0x7f080341;
public static int baseline_check_circle_black_24 = 0x7f080081;
public static int baseline_check_circle_dark_24 = 0x7f080082;
public static int outline_radio_button_unchecked_black_24 = 0x7f080426;
public static int outline_radio_button_unchecked_dark_24 = 0x7f080427;
public static int edit_cursor = 0x7f080170;
public static int selector_normal = 0x7f080554;
public static int selector_normal_dark = 0x7f080555;
public static int selector_list_back = 0x7f08054e;
public static int selector_list_back_dark = 0x7f080550;
}
}
EOF
# Minimal hierarchy with public no-arg ctors so RedirectorActivity can extend SettingActivity.
cat > "${STUBS}/com/mycompany/app/main/MainActivity.java" <<'EOF'
package com.mycompany.app.main;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
public class MainActivity extends Activity {
public Handler O0;
public MainActivity() {}
@Override public void onCreate(Bundle b) { super.onCreate(b); }
@Override public void onActivityResult(int requestCode, int resultCode, android.content.Intent data) {
super.onActivityResult(requestCode, resultCode, data);
}
public void onBackPressed() { super.onBackPressed(); }
}
EOF
cat > "${STUBS}/com/mycompany/app/setting/CastActivity.java" <<'EOF'
package com.mycompany.app.setting;
import com.mycompany.app.main.MainActivity;
public class CastActivity extends MainActivity {
public CastActivity() {}
}
EOF
cat > "${STUBS}/com/mycompany/app/setting/SettingActivity.java" <<'EOF'
package com.mycompany.app.setting;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import com.mycompany.app.view.MyButtonImage;
import com.mycompany.app.view.MyMainRelative;
import com.mycompany.app.view.MyManagerLinear;
import com.mycompany.app.view.MyRecyclerView;
import java.util.List;
public class SettingActivity extends CastActivity {
public Handler O0;
public MyButtonImage G1;
public MyButtonImage I1;
public MyButtonImage J1;
public MyRecyclerView L1;
public MyManagerLinear M1;
public SettingListAdapter N1;
public MyMainRelative E1;
public androidx.appcompat.widget.AppCompatTextView H1;
public int O1;
public SettingActivity() {}
public List D0() { return null; }
public final void F0(int i, boolean z, boolean z2) {}
public final void M0() {}
@Override public void onCreate(Bundle b) { super.onCreate(b); }
public void onBackPressed() { super.onBackPressed(); }
}
EOF
# Need AppCompatTextView for H1 field type — already stubbed.
cat > "${STUBS}/com/mycompany/app/setting/SettingListAdapter.java" <<'EOF'
package com.mycompany.app.setting;
public class SettingListAdapter {}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyButtonImage.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import android.widget.ImageView;
public class MyButtonImage extends ImageView {
public MyButtonImage(Context c) { super(c); }
public void setBgPreColor(int color) {}
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyRoundItem.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import android.widget.RelativeLayout;
public class MyRoundItem extends RelativeLayout {
public MyRoundItem(Context c) { super(c); }
public final void a(int i) {}
public void setDrawLine(boolean z) {}
public void setRound(int i) {}
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MySwitchView.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import android.widget.FrameLayout;
public class MySwitchView extends FrameLayout {
public MySwitchView(Context c) { super(c); }
public final void b(boolean z, boolean z2) {}
}
EOF
mkdir -p "${STUBS}/androidx/recyclerview/widget" \
"${STUBS}/androidx/appcompat/widget" \
"${STUBS}/androidx/core/widget"
cat > "${STUBS}/androidx/recyclerview/widget/RecyclerView.java" <<'EOF'
package androidx.recyclerview.widget;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
public class RecyclerView extends ViewGroup {
public static final int NO_POSITION = -1;
public RecyclerView(Context c) { super(c); }
public void setAdapter(Adapter adapter) {}
@Override protected void onLayout(boolean c, int l, int t, int r, int b) {}
public static abstract class Adapter<VH extends ViewHolder> {
public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
public abstract void onBindViewHolder(VH holder, int position);
public abstract int getItemCount();
public int getItemViewType(int position) { return 0; }
public void notifyDataSetChanged() {}
}
public static class ViewHolder {
public ViewHolder(View itemView) {}
public final int getAdapterPosition() { return -1; }
}
}
EOF
cat > "${STUBS}/androidx/appcompat/widget/AppCompatTextView.java" <<'EOF'
package androidx.appcompat.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.TextView;
public class AppCompatTextView extends TextView {
public AppCompatTextView(Context c, AttributeSet a) { super(c); }
}
EOF
cat > "${STUBS}/androidx/core/widget/NestedScrollView.java" <<'EOF'
package androidx.core.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
public class NestedScrollView extends FrameLayout {
public NestedScrollView(Context c, AttributeSet a) { super(c); }
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyRecyclerView.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import androidx.recyclerview.widget.RecyclerView;
public class MyRecyclerView extends RecyclerView {
public MyRecyclerView(Context c) { super(c); }
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyManagerLinear.java" <<'EOF'
package com.mycompany.app.view;
public class MyManagerLinear {}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyMainRelative.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import android.widget.RelativeLayout;
public class MyMainRelative extends RelativeLayout {
public MyMainRelative(Context c) { super(c); }
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyPopupAdapter.java" <<'EOF'
package com.mycompany.app.view;
public class MyPopupAdapter {
public static class PopMenuItem {
public PopMenuItem(int id, int titleRes) {}
public PopMenuItem(int id, String title) {}
public PopMenuItem(int id, int titleRes, int unused, boolean checked) {}
}
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyPopupMenu.java" <<'EOF'
package com.mycompany.app.view;
import android.view.View;
import com.mycompany.app.main.MainActivity;
import java.util.ArrayList;
public class MyPopupMenu {
public interface MyPopupListener {
void a();
boolean b(View view, int which);
}
public MyPopupMenu(MainActivity a, View root, View anchor, ArrayList items, boolean dark, MyPopupListener l) {}
public void a() {}
public final boolean d(int id) { return false; }
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyButtonCheck.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import android.view.View;
public class MyButtonCheck extends View {
public boolean D;
public MyButtonCheck(Context c) { super(c); }
public final void p(int a, int b) {}
public final void q(boolean z, boolean z2) { this.D = z; }
public void setBgPreColor(int c) {}
}
EOF
mkdir -p "${STUBS}/com/mycompany/app/dialog"
cat > "${STUBS}/com/mycompany/app/dialog/DialogSetFull.java" <<'EOF'
package com.mycompany.app.dialog;
public class DialogSetFull {
public interface DialogApplyListener { void a(); }
}
EOF
cat > "${STUBS}/com/mycompany/app/dialog/DialogSetMsg.java" <<'EOF'
package com.mycompany.app.dialog;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
public class DialogSetMsg extends Dialog {
public DialogSetMsg(Activity a, int msgRes, int titleRes, DialogSetFull.DialogApplyListener l) { super(a); }
public DialogSetMsg(Activity a, String msg, int titleRes, DialogSetFull.DialogApplyListener l) { super(a); }
@Override public void setOnDismissListener(DialogInterface.OnDismissListener l) { super.setOnDismissListener(l); }
@Override public void dismiss() { super.dismiss(); }
}
EOF
cat > "${STUBS}/com/mycompany/app/dialog/DialogSetSort.java" <<'EOF'
package com.mycompany.app.dialog;
import android.app.Dialog;
import android.content.DialogInterface;
import com.mycompany.app.main.MainActivity;
public class DialogSetSort extends Dialog {
public DialogSetSort(MainActivity a, int type, DialogSetFull.DialogApplyListener l) { super(a); }
@Override public void setOnDismissListener(DialogInterface.OnDismissListener l) { super.setOnDismissListener(l); }
@Override public void dismiss() { super.dismiss(); }
}
EOF
mkdir -p "${STUBS}/com/mycompany/app/pref"
cat > "${STUBS}/com/mycompany/app/pref/PrefList.java" <<'EOF'
package com.mycompany.app.pref;
import android.content.Context;
public class PrefList {
public static int D0;
public static boolean E0;
public static PrefList r(Context c, boolean z) { return new PrefList(); }
public final void n(int i, String key) {}
public final void l(String key, boolean z) {}
public final void a() {}
}
EOF
cat > "${STUBS}/com/mycompany/app/main/MainApp.java" <<'EOF'
package com.mycompany.app.main;
public class MainApp {
public static boolean K1;
public static int E1;
public static int b1;
public static int F1;
public static int G1;
public static int g1;
public static int f1;
public static int h1;
public static int i1;
}
EOF
cat > "${STUBS}/com/mycompany/app/main/MainUtil.java" <<'EOF'
package com.mycompany.app.main;
import android.content.Context;
public class MainUtil {
public static float G(Context c, float v) { return v; }
public static int l1() { return 0; }
}
EOF
# AppCompatTextView already stubbed above.
cat > "${STUBS}/com/mycompany/app/view/MyDialogBottom.java" <<'EOF'
package com.mycompany.app.view;
import android.app.Dialog;
import android.content.Context;
import android.os.Handler;
import android.view.View;
public class MyDialogBottom extends Dialog {
public interface BotViewListener { void a(View view); }
public Handler i;
public boolean f18633c;
public MyDialogBottom(Context c) { super(c); this.i = new Handler(); }
public final void g(View view, BotViewListener l) {}
@Override public void dismiss() { super.dismiss(); }
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyDialogLinear.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import android.widget.LinearLayout;
public class MyDialogLinear extends LinearLayout {
public MyDialogLinear(Context c) { super(c); }
public final void b() {}
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyEditText.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import android.widget.EditText;
public class MyEditText extends EditText {
public MyEditText(Context c) { super(c); }
public void setElineColor(int c) {}
public void c() {}
}
EOF
cat > "${STUBS}/com/mycompany/app/view/MyLineText.java" <<'EOF'
package com.mycompany.app.view;
import android.content.Context;
import android.widget.TextView;
public class MyLineText extends TextView {
public MyLineText(Context c) { super(c); }
public void setLinePad(int p) {}
public void setLineUp(boolean z) {}
public void setFilterColor(int c) {}
public void u() {}
}
EOF
mkdir -p "${STAGE}/stubclasses"
javac --release 11 -classpath "${ANDROID_JAR}" -d "${STAGE}/stubclasses" \
"${STUBS}/androidx/recyclerview/widget/RecyclerView.java" \
"${STUBS}/androidx/appcompat/widget/AppCompatTextView.java" \
"${STUBS}/androidx/core/widget/NestedScrollView.java" \
"${STUBS}/com/mycompany/app/soulbrowser/R.java" \
"${STUBS}/com/mycompany/app/main/MainApp.java" \
"${STUBS}/com/mycompany/app/main/MainUtil.java" \
"${STUBS}/com/mycompany/app/main/MainActivity.java" \
"${STUBS}/com/mycompany/app/setting/CastActivity.java" \
"${STUBS}/com/mycompany/app/view/MyButtonImage.java" \
"${STUBS}/com/mycompany/app/view/MyButtonCheck.java" \
"${STUBS}/com/mycompany/app/view/MyRoundItem.java" \
"${STUBS}/com/mycompany/app/view/MySwitchView.java" \
"${STUBS}/com/mycompany/app/view/MyRecyclerView.java" \
"${STUBS}/com/mycompany/app/view/MyManagerLinear.java" \
"${STUBS}/com/mycompany/app/view/MyMainRelative.java" \
"${STUBS}/com/mycompany/app/view/MyPopupAdapter.java" \
"${STUBS}/com/mycompany/app/view/MyPopupMenu.java" \
"${STUBS}/com/mycompany/app/view/MyDialogBottom.java" \
"${STUBS}/com/mycompany/app/view/MyDialogLinear.java" \
"${STUBS}/com/mycompany/app/view/MyEditText.java" \
"${STUBS}/com/mycompany/app/view/MyLineText.java" \
"${STUBS}/com/mycompany/app/dialog/DialogSetFull.java" \
"${STUBS}/com/mycompany/app/dialog/DialogSetMsg.java" \
"${STUBS}/com/mycompany/app/dialog/DialogSetSort.java" \
"${STUBS}/com/mycompany/app/pref/PrefList.java" \
"${STUBS}/com/mycompany/app/setting/SettingListAdapter.java" \
"${STUBS}/com/mycompany/app/setting/SettingActivity.java"
jar cf "${STAGE}/stub.jar" -C "${STAGE}/stubclasses" .
CP="${STAGE}/stub.jar:${CP}"
echo "Compiling RedirectorHelper + RedirectorActivity + RedirectorListAdapter + DialogEditRedirect..."
mkdir -p "${STAGE}/classes"
javac --release 11 -classpath "${CP}" -d "${STAGE}/classes" \
"${SRC_HELPER}" "${SRC_PREFS}" "${SRC_ADAPTER}" "${SRC_ACTIVITY}" "${SRC_DIALOG}"
mkdir -p "${STAGE}/dex"
mapfile -t CLASS_FILES < <(find "${STAGE}/classes" -name '*.class' -type f)
"${D8}" --min-api 23 --lib "${ANDROID_JAR}" --output "${STAGE}/dex" "${CLASS_FILES[@]}"
bash "${DEX_TOOLS_DIR}/d2j-baksmali.sh" -f -o "${STAGE}/out" "${STAGE}/dex/classes.dex"
find "${STAGE}/out" -name '*.smali' -print0 | xargs -0 sed -i \
-e 's/\\u002d\$\$Nest\$/\-$$Nest$/g' \
-e 's|Lcom/mycompany/app/soulbrowser/R\$|Lnet/kaki87/soul2/testing/R\$|g'
# Fix invalid const/high16 from baksmali (imm16 << 16) -> const with full value.
python3 - <<PY
from pathlib import Path
import re
root = Path("${STAGE}/out")
pat = re.compile(r'const/high16\s+(\w+),\s*(-?0x[0-9a-fA-F]+|-?\d+)')
for p in root.rglob('*.smali'):
t = p.read_text()
def repl(m):
reg, val = m.group(1), m.group(2)
num = int(val, 16) if '0x' in val.lower() else int(val)
full = (num & 0xFFFF) << 16
if full >= 0x80000000:
full -= 0x100000000
return f'const {reg}, {full}'
n, c = pat.subn(repl, t)
if c:
p.write_text(n)
print('fixed const/high16 in', p.name, c)
PY
rm -f "${OUT_WEB}"/RedirectorHelper*.smali
rm -f "${OUT_SETTING}"/RedirectorActivity*.smali
rm -f "${OUT_SETTING}"/RedirectorListAdapter*.smali
rm -f "${OUT_SETTING}"/RedirectorListPrefs*.smali
rm -f "${OUT_DIALOG}"/DialogEditRedirect*.smali
cp -a "${STAGE}/out/com/mycompany/app/web/RedirectorHelper"* "${OUT_WEB}/"
cp -a "${STAGE}/out/com/mycompany/app/setting/RedirectorActivity"* "${OUT_SETTING}/"
cp -a "${STAGE}/out/com/mycompany/app/setting/RedirectorListAdapter"* "${OUT_SETTING}/"
cp -a "${STAGE}/out/com/mycompany/app/setting/RedirectorListPrefs"* "${OUT_SETTING}/"
mkdir -p "${OUT_DIALOG}"
cp -a "${STAGE}/out/com/mycompany/app/dialog/DialogEditRedirect"* "${OUT_DIALOG}/"
# Stub classpath produces a few wrong method refs; rewrite to match shipped smali.
OUT_SETTING_ESC="${OUT_SETTING}" OUT_DIALOG_ESC="${OUT_DIALOG}" python3 - <<'PY'
from pathlib import Path
import os
root = Path(os.environ["OUT_SETTING_ESC"])
for p in list(root.glob("RedirectorActivity*.smali")) + list(root.glob("RedirectorListAdapter*.smali")):
t = p.read_text()
orig = t
t = t.replace(
"invoke-direct { p0 }, Lcom/mycompany/app/setting/SettingActivity;-><init>()V",
"invoke-direct { p0 }, Landroidx/appcompat/app/AppCompatActivity;-><init>()V",
)
t = t.replace(
"invoke-virtual { p0, p1, v0, v0 }, Lcom/mycompany/app/setting/RedirectorActivity;->F0(IZZ)V",
"invoke-virtual { p0, p1, v0, v0 }, Lcom/mycompany/app/setting/SettingActivity;->F0(IZZ)V",
)
t = t.replace(
"invoke-super { p0, p1, p2, p3 }, Lcom/mycompany/app/setting/SettingActivity;->onActivityResult(IILandroid/content/Intent;)V",
"invoke-super { p0, p1, p2, p3 }, Landroidx/fragment/app/FragmentActivity;->onActivityResult(IILandroid/content/Intent;)V",
)
t = t.replace(
"invoke-super { p0 }, Lcom/mycompany/app/setting/SettingActivity;->onBackPressed()V",
"invoke-super { p0 }, Lcom/mycompany/app/main/MainActivity;->onBackPressed()V",
)
t = t.replace(
"invoke-virtual { v2, v3 }, Lcom/mycompany/app/dialog/DialogSetSort;->setOnDismissListener(Landroid/content/DialogInterface$OnDismissListener;)V",
"invoke-virtual { v2, v3 }, Lcom/mycompany/app/view/MyDialogBottom;->setOnDismissListener(Landroid/content/DialogInterface$OnDismissListener;)V",
)
t = t.replace(
"invoke-virtual { v0, v1 }, Lcom/mycompany/app/dialog/DialogSetSort;->setOnDismissListener(Landroid/content/DialogInterface$OnDismissListener;)V",
"invoke-virtual { v0, v1 }, Lcom/mycompany/app/view/MyDialogBottom;->setOnDismissListener(Landroid/content/DialogInterface$OnDismissListener;)V",
)
t = t.replace(
"Lcom/mycompany/app/dialog/DialogSetMsg;->setOnDismissListener(Landroid/content/DialogInterface$OnDismissListener;)V",
"Lcom/mycompany/app/view/MyDialogBottom;->setOnDismissListener(Landroid/content/DialogInterface$OnDismissListener;)V",
)
# Stub Dialog.dismiss → typed dismiss (overrides on MyDialogBottom subclasses).
import re
t = re.sub(
r"(iget-object (v\d+), [^\n]+->mDeleteAllDialog:Lcom/mycompany/app/dialog/DialogSetMsg;\n(?:.*\n){0,6}?)invoke-virtual \{ \2 \}, Landroid/app/Dialog;->dismiss\(\)V",
r"\1invoke-virtual { \2 }, Lcom/mycompany/app/dialog/DialogSetMsg;->dismiss()V",
t,
)
t = re.sub(
r"(iget-object (v\d+), [^\n]+->mSortDialog:Lcom/mycompany/app/dialog/DialogSetSort;\n(?:.*\n){0,6}?)invoke-virtual \{ \2 \}, Landroid/app/Dialog;->dismiss\(\)V",
r"\1invoke-virtual { \2 }, Lcom/mycompany/app/dialog/DialogSetSort;->dismiss()V",
t,
)
t = t.replace(
"invoke-virtual { v1, v0 }, Lcom/mycompany/app/view/MyRecyclerView;->setAdapter(Ljava/lang/Object;)V",
"invoke-virtual { v1, v0 }, Landroidx/recyclerview/widget/RecyclerView;->setAdapter(Landroidx/recyclerview/widget/RecyclerView$Adapter;)V",
)
t = t.replace(
"invoke-virtual { v0, v1 }, Lcom/mycompany/app/view/MyRecyclerView;->setAdapter(Ljava/lang/Object;)V",
"invoke-virtual { v0, v1 }, Landroidx/recyclerview/widget/RecyclerView;->setAdapter(Landroidx/recyclerview/widget/RecyclerView$Adapter;)V",
)
t = t.replace(
"invoke-virtual { v0, v1 }, Lcom/mycompany/app/view/MyRecyclerView;->setAdapter(Landroidx/recyclerview/widget/RecyclerView$Adapter;)V",
"invoke-virtual { v0, v1 }, Landroidx/recyclerview/widget/RecyclerView;->setAdapter(Landroidx/recyclerview/widget/RecyclerView$Adapter;)V",
)
# Soul ships an R8-obfuscated AndroidX RecyclerView (see SettingListAdapter).
# Map standard Adapter/ViewHolder API names to the obfuscated ones.
t = t.replace(
".method public getItemCount()I",
".method public d()I",
)
t = t.replace(
".method public getItemViewType(I)I",
".method public f(I)I",
)
t = t.replace(
".method public bridge synthetic onBindViewHolder(Landroidx/recyclerview/widget/RecyclerView$ViewHolder;I)V",
".method public bridge synthetic n(Landroidx/recyclerview/widget/RecyclerView$ViewHolder;I)V",
)
t = t.replace(
".method public bridge synthetic onCreateViewHolder(Landroid/view/ViewGroup;I)Landroidx/recyclerview/widget/RecyclerView$ViewHolder;",
".method public bridge synthetic o(Landroid/view/ViewGroup;I)Landroidx/recyclerview/widget/RecyclerView$ViewHolder;",
)
t = t.replace(
"invoke-virtual { p0 }, Lcom/mycompany/app/setting/RedirectorListAdapter;->notifyDataSetChanged()V",
"invoke-virtual { p0 }, Landroidx/recyclerview/widget/RecyclerView$Adapter;->g()V",
)
t = t.replace(
"invoke-virtual { p1 }, Lcom/mycompany/app/setting/RedirectorListAdapter$Holder;->getAdapterPosition()I",
"invoke-virtual { p1 }, Landroidx/recyclerview/widget/RecyclerView$ViewHolder;->d()I",
)
t = t.replace(
"invoke-virtual { v0 }, Lcom/mycompany/app/setting/RedirectorListAdapter$Holder;->getAdapterPosition()I",
"invoke-virtual { v0 }, Landroidx/recyclerview/widget/RecyclerView$ViewHolder;->d()I",
)
if t != orig:
p.write_text(t)
print("patched", p.name)
dialog_root = Path(os.environ["OUT_DIALOG_ESC"])
for p in dialog_root.glob("DialogEditRedirect*.smali"):
t = p.read_text()
orig = t
# JADX name f18633c is field `c` in shipped MyDialogBottom smali.
t = t.replace(
"iput-boolean v0, p0, Lcom/mycompany/app/dialog/DialogEditRedirect;->f18633c:Z",
"iput-boolean v0, p0, Lcom/mycompany/app/view/MyDialogBottom;->c:Z",
)
t = t.replace(
"iput-boolean v0, p0, Lcom/mycompany/app/view/MyDialogBottom;->f18633c:Z",
"iput-boolean v0, p0, Lcom/mycompany/app/view/MyDialogBottom;->c:Z",
)
t = t.replace(
"invoke-virtual { p0, v0, v1 }, Lcom/mycompany/app/dialog/DialogEditRedirect;->g(Landroid/view/View;Lcom/mycompany/app/view/MyDialogBottom$BotViewListener;)V",
"invoke-virtual { p0, v0, v1 }, Lcom/mycompany/app/view/MyDialogBottom;->g(Landroid/view/View;Lcom/mycompany/app/view/MyDialogBottom$BotViewListener;)V",
)
t = t.replace(
"Lcom/mycompany/app/dialog/DialogEditRedirect;->i:Landroid/os/Handler;",
"Lcom/mycompany/app/view/MyDialogBottom;->i:Landroid/os/Handler;",
)
t = t.replace(
"Lcom/mycompany/app/view/MyEditText;->setTextCursorDrawable(I)V",
"Landroid/widget/EditText;->setTextCursorDrawable(I)V",
)
if t != orig:
p.write_text(t)
print("patched", p.name)
PY
echo "Wrote ${OUT_WEB}/RedirectorHelper*.smali"
echo "Wrote ${OUT_SETTING}/RedirectorActivity*.smali"
echo "Wrote ${OUT_SETTING}/RedirectorListAdapter*.smali"
echo "Wrote ${OUT_DIALOG}/DialogEditRedirect*.smali"
ls "${OUT_SETTING}"/RedirectorActivity*.smali "${OUT_SETTING}"/RedirectorListAdapter*.smali | wc -l
ls "${OUT_DIALOG}"/DialogEditRedirect*.smali | wc -l