* Add "Recently closed" tab to the tab list A history icon between the smiley and spy icons opens a page listing recently closed normal tabs. Tapping an entry reopens it as a new tab. The page follows the Thumbnail/List/Simple mode. Closed tabs are recorded by a hook in DbUtil.a before their rows are deleted; private tabs are never recorded. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Show "Recently closed" tab in Mini mode too The Mini mode tab list is a separate bottom-sheet dialog (DialogTabMini). Give it the same three-page layout, history icon, reopen path and Delete all handling as DialogTabMain. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Use the tab list components on the Recently closed page Items now come from the same WebTabAdapter, list view and layout managers as the normal and private pages, so they follow the Thumbnail/List/Simple modes in both the full and Mini dialogs. Thumbnails are copied to DbTabThumb rows when a tab closes. Delete all asks for confirmation with DialogDeleteItem. The header icons are centred under the indicator and the history icon stroke is thinner. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Restore tab history and fix empty background on Recently closed The closed tab's saved page stack is copied next to its thumbnail and restored into the reopened tab through the app's own saved-tab loader. The empty page now uses the same background as the other pages. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Save the current tab's history when the tab list opens Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Save tab history for tabs closed without opening the tab list Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
344 lines
12 KiB
Bash
Executable file
344 lines
12 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
SRC="${ROOT_DIR}/sources/java/com/mycompany/app/dialog/ClosedTabsHelper.java"
|
|
OUT_SMALI="${ROOT_DIR}/app/smali_classes3/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)"
|
|
DEX_TOOLS_ZIP="${TOOLS}/dex-tools-v2.4.zip"
|
|
DEX_TOOLS_DIR="${TOOLS}/dex-tools-v2.4"
|
|
BAKSMALI_JAR="${TOOLS}/baksmali.jar"
|
|
|
|
mkdir -p "${TOOLS}" "${OUT_SMALI}"
|
|
|
|
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 [[ ! -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
|
|
|
|
if [[ ! -f "${BAKSMALI_JAR}" ]]; then
|
|
curl -fsSL -o "${BAKSMALI_JAR}" \
|
|
"https://repo1.maven.org/maven2/org/smali/baksmali/2.5.2/baksmali-2.5.2.jar"
|
|
fi
|
|
|
|
STAGE="$(mktemp -d)"
|
|
cleanup() { rm -rf "${STAGE}"; }
|
|
trap cleanup EXIT
|
|
|
|
STUBS="${STAGE}/stubs"
|
|
mkdir -p \
|
|
"${STUBS}/com/mycompany/app/main" \
|
|
"${STUBS}/com/mycompany/app/pref" \
|
|
"${STUBS}/com/mycompany/app/db/book" \
|
|
"${STUBS}/com/mycompany/app/dialog" \
|
|
"${STUBS}/com/mycompany/app/view" \
|
|
"${STUBS}/com/mycompany/app/web" \
|
|
"${STUBS}/androidx/viewpager2/widget"
|
|
|
|
cat > "${STUBS}/com/mycompany/app/main/MainApp.java" <<'EOF'
|
|
package com.mycompany.app.main;
|
|
public final class MainApp {
|
|
public static boolean K1;
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/pref/PrefSync.java" <<'EOF'
|
|
package com.mycompany.app.pref;
|
|
public final class PrefSync {
|
|
public static boolean k;
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/pref/PrefZone.java" <<'EOF'
|
|
package com.mycompany.app.pref;
|
|
public final class PrefZone {
|
|
public static int C;
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/db/book/DbTabThumb.java" <<'EOF'
|
|
package com.mycompany.app.db.book;
|
|
import android.content.Context;
|
|
import android.graphics.Bitmap;
|
|
public class DbTabThumb {
|
|
public static Bitmap d(Context context, long uid) { return null; }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/dialog/DialogTabMain.java" <<'EOF'
|
|
package com.mycompany.app.dialog;
|
|
import android.app.Dialog;
|
|
import android.content.Context;
|
|
public class DialogTabMain extends Dialog {
|
|
public DialogTabMain(Context c) { super(c); }
|
|
public static void u(DialogTabMain dialog, boolean secret) { }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/dialog/DialogTabMini.java" <<'EOF'
|
|
package com.mycompany.app.dialog;
|
|
import android.app.Dialog;
|
|
import android.content.Context;
|
|
public class DialogTabMini extends Dialog {
|
|
public DialogTabMini(Context c) { super(c); }
|
|
public static void D(DialogTabMini dialog, boolean secret) { }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/view/MyButtonRelative.java" <<'EOF'
|
|
package com.mycompany.app.view;
|
|
import android.content.Context;
|
|
import android.widget.RelativeLayout;
|
|
public class MyButtonRelative extends RelativeLayout {
|
|
public MyButtonRelative(Context c) { super(c); }
|
|
public void setBgPreColor(int color) { }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/web/WebNestView.java" <<'EOF'
|
|
package com.mycompany.app.web;
|
|
import android.content.Context;
|
|
import android.webkit.WebView;
|
|
public class WebNestView extends WebView {
|
|
public WebNestView(Context c) { super(c); }
|
|
public final void A(String url, com.mycompany.app.db.book.DbTabState.StateItem state) { }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/web/WebNestFrame.java" <<'EOF'
|
|
package com.mycompany.app.web;
|
|
public class WebNestFrame extends android.view.View {
|
|
public WebNestFrame(android.content.Context c) { super(c); }
|
|
public long i;
|
|
public String getListToStr() { return null; }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/web/WebViewActivity.java" <<'EOF'
|
|
package com.mycompany.app.web;
|
|
import android.app.Activity;
|
|
public class WebViewActivity extends Activity {
|
|
public WebNestView I2;
|
|
public com.mycompany.app.web.WebNestFrame H2;
|
|
public String A8;
|
|
public static String o3() { return null; }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/db/book/DbTabState.java" <<'EOF'
|
|
package com.mycompany.app.db.book;
|
|
import android.content.Context;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.database.sqlite.SQLiteOpenHelper;
|
|
public class DbTabState extends SQLiteOpenHelper {
|
|
public static class StateItem { }
|
|
public static StateItem d(Context context, long uid) { return null; }
|
|
DbTabState(Context c) { super(c, "x", null, 1); }
|
|
public void onCreate(SQLiteDatabase db) { }
|
|
public void onUpgrade(SQLiteDatabase db, int a, int b) { }
|
|
public static DbTabState b(Context context) { return null; }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/androidx/viewpager2/widget/ViewPager2.java" <<'EOF'
|
|
package androidx.viewpager2.widget;
|
|
import android.content.Context;
|
|
import android.view.ViewGroup;
|
|
public class ViewPager2 extends ViewGroup {
|
|
public ViewPager2(Context c) { super(c); }
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) { }
|
|
public void setCurrentItem(int item) { }
|
|
}
|
|
EOF
|
|
|
|
mkdir -p \
|
|
"${STUBS}/androidx/recyclerview/widget" \
|
|
"${STUBS}/androidx/appcompat/widget"
|
|
|
|
cat > "${STUBS}/androidx/recyclerview/widget/RecyclerView.java" <<'EOF'
|
|
package androidx.recyclerview.widget;
|
|
import android.content.Context;
|
|
import android.util.AttributeSet;
|
|
import android.view.ViewGroup;
|
|
public class RecyclerView extends ViewGroup {
|
|
public RecyclerView(Context c, AttributeSet a) { super(c, a); }
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) { }
|
|
public void setLayoutManager(LayoutManager manager) { }
|
|
public void setAdapter(Adapter adapter) { }
|
|
public abstract static class LayoutManager { }
|
|
public abstract static class Adapter<VH extends ViewHolder> { }
|
|
public abstract static class ViewHolder { }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/androidx/recyclerview/widget/LinearLayoutManager.java" <<'EOF'
|
|
package androidx.recyclerview.widget;
|
|
public class LinearLayoutManager extends RecyclerView.LayoutManager {
|
|
public LinearLayoutManager(int orientation) { }
|
|
public void x0(int position) { }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/androidx/recyclerview/widget/GridLayoutManager.java" <<'EOF'
|
|
package androidx.recyclerview.widget;
|
|
public class GridLayoutManager extends LinearLayoutManager {
|
|
public int F;
|
|
public SpanSizeLookup K;
|
|
public GridLayoutManager(int spanCount) { super(1); F = spanCount; }
|
|
public void x1(int spanCount) { F = spanCount; }
|
|
public abstract static class SpanSizeLookup {
|
|
public abstract int c(int position);
|
|
}
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/androidx/appcompat/widget/AppCompatTextView.java" <<'EOF'
|
|
package androidx.appcompat.widget;
|
|
import android.content.Context;
|
|
import android.widget.TextView;
|
|
public class AppCompatTextView extends TextView {
|
|
public AppCompatTextView(Context c) { 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, null); }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/view/MyRoundImage.java" <<'EOF'
|
|
package com.mycompany.app.view;
|
|
import android.content.Context;
|
|
import android.widget.ImageView;
|
|
public class MyRoundImage extends ImageView {
|
|
public MyRoundImage(Context c) { super(c); }
|
|
public void o(int color, int resId) { }
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/dialog/DialogDeleteItem.java" <<'EOF'
|
|
package com.mycompany.app.dialog;
|
|
import android.app.Activity;
|
|
import android.app.Dialog;
|
|
import androidx.appcompat.widget.AppCompatTextView;
|
|
import com.mycompany.app.view.MyRoundImage;
|
|
public class DialogDeleteItem extends Dialog {
|
|
public DialogDeleteItem(Activity activity, DelItemListener listener) { super(activity); }
|
|
public interface DelItemListener {
|
|
void a();
|
|
void b(MyRoundImage image, AppCompatTextView text);
|
|
}
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/pref/PrefZtwo.java" <<'EOF'
|
|
package com.mycompany.app.pref;
|
|
public final class PrefZtwo {
|
|
public static boolean z;
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/main/MainApp.java" <<'EOF'
|
|
package com.mycompany.app.main;
|
|
public final class MainApp {
|
|
public static boolean K1;
|
|
public static int q1;
|
|
public static int F1;
|
|
public static int G1;
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/web/WebTabAdapter.java" <<'EOF'
|
|
package com.mycompany.app.web;
|
|
import android.content.Context;
|
|
import android.view.View;
|
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
import androidx.recyclerview.widget.RecyclerView;
|
|
import com.mycompany.app.view.MyRecyclerView;
|
|
import java.util.List;
|
|
public class WebTabAdapter extends RecyclerView.Adapter<WebTabAdapter.WebTabHolder> {
|
|
public WebTabListener x;
|
|
public WebTabAdapter(Context context, MyRecyclerView list, boolean z, List a, List b, long j, int i, int i2,
|
|
int i3, int i4, boolean z2, LinearLayoutManager manager) { }
|
|
public final WebTabItem H(int position) { return null; }
|
|
public final void Z(int height, int columns, int itemHeight, boolean z) { }
|
|
public final void b0(List a, List b, long j, int i, int i2) { }
|
|
public final void P() { }
|
|
public static class WebTabHolder extends RecyclerView.ViewHolder { }
|
|
public static class WebTabItem {
|
|
public int a;
|
|
public long c;
|
|
public int h;
|
|
public int i;
|
|
public String j;
|
|
public String k;
|
|
}
|
|
public interface WebTabListener {
|
|
void a();
|
|
void b(View view, int position);
|
|
void c(WebTabHolder holder, int position);
|
|
void d(int position);
|
|
void e(WebTabHolder holder, int position);
|
|
}
|
|
}
|
|
EOF
|
|
|
|
cat > "${STUBS}/com/mycompany/app/db/book/DbTabThumb.java" <<'EOF'
|
|
package com.mycompany.app.db.book;
|
|
import android.content.Context;
|
|
import android.database.sqlite.SQLiteDatabase;
|
|
import android.database.sqlite.SQLiteOpenHelper;
|
|
public class DbTabThumb extends SQLiteOpenHelper {
|
|
DbTabThumb(Context c) { super(c, "x", null, 1); }
|
|
public void onCreate(SQLiteDatabase db) { }
|
|
public void onUpgrade(SQLiteDatabase db, int a, int b) { }
|
|
public static DbTabThumb b(Context context) { return null; }
|
|
}
|
|
EOF
|
|
|
|
|
|
echo "Compiling ClosedTabsHelper..."
|
|
javac --release 8 -classpath "${ANDROID_JAR}:${STUBS}" -d "${STAGE}/classes" "${SRC}"
|
|
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[@]}"
|
|
if [[ -x "${DEX_TOOLS_DIR}/d2j-baksmali.sh" ]]; then
|
|
bash "${DEX_TOOLS_DIR}/d2j-baksmali.sh" -f -o "${STAGE}/out" "${STAGE}/dex/classes.dex"
|
|
else
|
|
java -cp "${BAKSMALI_JAR}" org.jf.baksmali.Main d "${STAGE}/dex/classes.dex" -o "${STAGE}/out"
|
|
fi
|
|
# d2j-baksmali prints const/high16 operands as the upper 16 bits in decimal; smali wants the full hex literal.
|
|
python3 -I - "${STAGE}/out" <<'PY'
|
|
import pathlib, re, sys
|
|
for f in pathlib.Path(sys.argv[1]).rglob("*.smali"):
|
|
text = f.read_text()
|
|
fixed = re.sub(r"const/high16 (v\d+), (-?\d+)\b",
|
|
lambda m: "const/high16 %s, 0x%08x" % (m.group(1), (int(m.group(2)) << 16) & 0xFFFFFFFF),
|
|
text)
|
|
if fixed != text:
|
|
f.write_text(fixed)
|
|
PY
|
|
rm -f "${OUT_SMALI}"/ClosedTabsHelper*.smali
|
|
cp -a "${STAGE}/out/com/mycompany/app/dialog/ClosedTabsHelper"* "${OUT_SMALI}/"
|
|
echo "Wrote ${OUT_SMALI}/ClosedTabsHelper*.smali"
|