Skip to content
SmartXiaoMingPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

blauser

让 PC 网页在手机上真正能用。

📦 下载 APK · 1.8 MB · Android 5.0+ · MIT 协议

一个 Android 浏览器,只专注做一件事:那些没有做移动端适配的网页,在手机上也得好用。

手机上打开这类网站,体验通常是灾难 —— 字小到要放大镜、排版整个错位、 按钮挤在一起点不中、表格横向溢出。它们本来就是按 1280px 的桌面屏设计的, 而手机浏览器的默认行为是「按移动端 viewport 渲染」,两边对不上。

Blauser 反过来做:按 PC 的宽度渲染,再整体缩放贴合手机屏。 同时伪装 window.screen.width,让响应式站点主动吐出 PC 布局。 于是后台管理系统、老论坛、文档站、政府网站这些「只在电脑上能看」的页面,手机上也能用了。

PC 页面适配 新标签页 悬浮球菜单 深色模式

左起:PC 页面缩放 · 新标签页 · 悬浮球菜单 · 深色模式


它和普通手机浏览器有什么不同

1. 不是「缩放到适应」,是「按桌面布局渲染后再缩放」

普通浏览器把桌面页硬塞进 375px 宽,结果是断行错乱、侧边栏盖住正文。 Blauser 让页面按 1280px(或你指定的宽度)正常排版 —— 该并排的并排、该分栏的分栏 —— 然后等比缩放到手机屏。你看到的是完整的桌面布局,只是小了一号,双指放大即可看清。

而且它会判断:页面自己声明了 width=device-width(说明它做了移动端适配), 就完全不干预,保持原生渲染。所以淘宝、知乎这类站点仍然是正常的移动端体验, 只有真正的 PC 页面才被接管。

2. 界面上只有一个控件

没有状态栏、没有地址栏、没有标签栏、没有底部导航 —— 网页铺满整屏。 所有操作(后退、刷新、收藏、历史、复制链接、设置……)都从屏幕边缘那个 32dp 宽的半球进出。 短按弹菜单,长按直达设置,可拖动、自动贴边、位置记忆、空闲时半透明不挡内容。 无痕模式下这个半球会变成深色 —— 它是界面上唯一能承载「这个标签不留痕」这个信息的地方。

3. 标签页就是系统任务

每个标签跑在独立的 Android task 里,会出现在系统「最近任务」中各占一张卡片。 切换标签 = 切换任务,用系统的手势就能完成,不需要在浏览器里再找一层标签列表。 而且因为这些 task 共享同一个进程,cookie / localStorage 照常共享,登录态不会丢。

4. 一个网站一套设置

有些站点只在特定配置下才正常 —— 只认桌面 UA、必须横屏、要特定语言。 与其来回改全局设置,不如按域名记一份覆盖:离开这个站点自动失效, 没覆盖的项跟随全局。还提供「桌面模式 / 移动模式」一键预设。

5. 该有的浏览器能力都不缺

文件上传、下载、摄像头 / 麦克风 / 定位、三方 cookie(很多站点的内嵌登录靠它)、 深色模式、添加到桌面。不用因为「某个功能它没有」而临时切回别的浏览器。


功能一览

显示

  • PC 页面自动适配:自动判断页面类型,PC 页面按桌面宽度渲染后整体缩放
  • 分辨率可选:自动(默认)/ 跟随手机 / 1280 × 720 / 1366 × 768 / 1024 × 768 / 800 × 600 / 自定义
    • 预设档只定宽度:按该宽度排版,再整体缩放贴合手机屏宽,纵向照常滚动。 同时伪装 window.screen.width,响应式网站会据此输出 PC 布局
    • 自定义可以精确填「宽 × 高」。填了高度就按这个比例模拟一块屏幕: 等比缩放、四周留白(下图就是自定义 1280 × 720 的效果)。 高度留空表示不限高,退化成只按宽度缩放
  • UserAgent 可选:Android 手机 / 桌面 Chrome / iPhone Safari / 自定义
  • 缩放开关:默认关闭,而且是按网站设的 —— 单指点按和双指捏合都不会缩放页面。 同一个浏览器里「看文档要能放大」和「刷信息流怕误触」会同时存在,全局一个开关满足不了, 所以在悬浮球菜单里单独切(菜单项文字会带出当前是开还是关),本站设置里也有勾选项
  • 深色模式:跟随系统
  • 屏幕方向:跟随系统 / 竖屏 / 横屏
  • 语言:同时作用于 Accept-Language 请求头和 navigator.language
  • 搜索引擎:百度 / Google,默认百度。作用于地址栏里不像网址的输入
  • 以上每一项都可以按域名单独覆盖(缩放本身就是按域名的)

自定义 1280×720 的留白效果
自定义分辨率 1280 × 720:按 16:9 渲染,上下留白,内容不变形

操作

  • 悬浮球:贴长边的半球 —— 竖屏贴左右、横屏自动横过来贴上下; 可拖动、自动贴边、位置记忆、空闲半透明。贴顶边时会避开刘海
    • 短按展开菜单:后退 / 前进 / 刷新 / 允许缩放 / 首页 / 新建无痕标签 / 收藏本页 / 收藏夹 / 历史记录 / 复制链接 / 分享 / 添加到桌面 / 本站设置 / 全局设置 / 审查元素
    • 长按直达设置
    • 无历史时「后退 / 前进」置灰;空白标签上「收藏 / 复制链接 / 分享 / 添加到桌面 / 本站设置」置灰 —— 这些都要有网页才有意义
    • 已收藏时「收藏本页」变成「取消收藏」
    • 设备不支持无痕(见下)时,那一项不显示而不是置灰 —— 摆一个永远点不动的灰色项只会让人困惑
  • 新标签页 / 首页(定制页面):
    • 地址输入框 —— 输入网址或搜索词;自动补全协议, localhost:3000 这类本地地址补 http://,其余补 https://
    • 打开的标签页列表,点击即切换;无痕标签带「无痕」角标
    • 收藏的网址列表,点击打开;若该网址已经开着,则切过去而不是重复开
    • 上面两个列表、以及收藏夹 / 历史记录对话框里,长按任意一行即复制该行网址
  • 无效证书:证书校验失败时不再一律拒绝,而是弹窗让你选「取消」还是 「继续访问(不安全)」。选继续会永久记住这个域名的这张证书,之后不再询问; 证书换了会重新问。撤销入口有两个:本站设置里的「证书例外」一行 (只在该域名有例外时出现),或全局设置 →「证书例外」清单(逐条删除 / 清空)

隐私

  • 无痕标签:独立存储,cookie / localStorage / 缓存与普通标签完全隔离, 关闭后整份数据销毁。也不写浏览历史。 依赖 WebView 的多 profile 能力,条件有两个:Android 9 及以上, 且 WebView 本身支持多 profile(较新版本的 WebView)。 两者缺一就不显示这个入口 —— 而不是做个「不记历史但 cookie 照样共享」的假的。 实际判断走 WebViewFeature.isFeatureSupported(MULTI_PROFILE),两个条件都包含。
  • 浏览历史:记录访问过的页面,同一网址只留一条并置顶,上限 500 条, 可一键清空(清空有二次确认)。无痕标签不写历史。

网页能力

  • 文件上传(<input type="file">,支持多选)
  • 下载(走系统 DownloadManager,透传 cookie 与 UA,完成后可在系统「下载」里看到)
  • 摄像头 / 麦克风 / 定位(网页请求时才向你申请权限)
  • 三方 cookie
  • 添加到桌面:把当前网页固定成桌面快捷方式,点击以独立任务打开

界面语言

中文 / English,跟随系统。文案全部走 strings.xml,加一门语言只需再加一份 values-<语言>/strings.xml。

系统集成

  • 注册为系统默认浏览器,其他 App 点链接可以直接用这个打开(会开新标签,不打断当前页面)
  • 注册为本地 HTML 文件的打开方式:在文件管理器里点 .html / .htm / .xhtml 文件时可以选择用本 App 打开。file:// 与 Android 7+ 的 content:// 都支持 (后者会先复制到应用缓存,再按本地文件加载)
  • 应用内检查更新:悬浮球菜单里有「检查更新」,每天首次启动时也会静默检查一次 GitHub Releases;发现新版本后可下载 APK 并调起系统安装器

更多截图

设置面板 历史记录 自定义错误页

设置面板 · 历史记录 · 自定义错误页(按错误码给出中文原因,替代系统那张 “net::ERR_…”)


安装

直接装 APK

到 Releases 页下载最新版 · 1.8 MB

  • 需要 Android 5.0(API 21)及以上
  • 没有上架任何应用商店,首次安装需在「设置 → 安全」里允许安装未知来源的应用
  • Release 页面里附有 APK 的签名 SHA-256,可自行核对

装好后可以去系统「设置 → 应用 → 默认应用 → 浏览器应用」里把它设成默认浏览器。

自行构建

git clone https://github.com/SmartXiaoMing/blauser.git
cd blauser
./gradlew assembleRelease
# 产物:app/build/outputs/apk/release/blauser-<版本号>.apk
adb install -r app/build/outputs/apk/release/blauser-*.apk

环境要求:JDK 17+、Android SDK(local.properties 里写 sdk.dir=...)。 Gradle 用仓库自带的 wrapper 即可,不需要另装。


使用

  • 打开网页:点悬浮球 → 首页 → 输入网址或搜索词
  • 新开标签:首页里点「空白页」那一行,或在网页里点 target="_blank" 链接
  • 切换标签:用系统的最近任务手势(每个标签是一张独立卡片), 或者从首页的「打开的标签页」列表里点
  • 关闭标签:首页列表里点该行右侧的 ×,或者在最近任务里划掉
  • 收藏:悬浮球 → 收藏本页
  • 复制链接:悬浮球 → 复制链接(复制当前页);首页的标签 / 收藏列表、 收藏夹、历史记录里长按任意一行则复制那一行的网址
  • 自签名 / 过期证书的站点:证书警告里选「继续访问」,该域名的这张证书会被永久记住; 想反悔就去本站设置或全局设置 →「证书例外」里撤销
  • 针对某个网站改设置:悬浮球 → 本站设置
  • 审查元素:悬浮球 → 审查元素(仅 debug 包开启远程调试)

已知限制

这些是设计上明确接受的取舍,不是 bug。

场景 页面状态是否保留 说明
切换标签页、切到后台再回来 ✅ 完整保留 DOM / JS 变量 / 滚动位置 / 表单输入都在
屏幕旋转 / 改字体大小 / 改语言 ✅ 完整保留 已声明 configChanges,Activity 不重建
切换深色模式 ⚠️ 页面会重载 不重建 Activity 就没法给界面换肤。前进后退历史与滚动位置会保住
后台标签被系统回收 ❌ 页面会重载 见下。URL、前进后退历史、滚动位置可恢复,DOM 与表单内容不行
主动刷新 / 渲染进程被回收 ❌ 会重载 符合预期

为什么后台标签会被回收:标签改成独立 task 后,后台 task 的 Activity 处于 stopped 状态, 内存紧张时系统可以销毁它。这与「切回来绝不重载」不可兼得 —— 把所有标签的 WebView 塞进同一个前台 Activity 可以 100% 保证不重载,但那样就没法在最近任务里分卡片了。 本项目选择后者,并明确接受前者的代价。

被回收后能恢复到什么程度:URL、前进后退历史(WebView.saveState)、滚动位置。 恢复不了 DOM 和表单内容 —— WebView 没有序列化页面状态的接口, 硬做只能自己拦截每个输入框,代价远大于收益。

其它限制:

  • 无痕标签需要 Android 9 及以上,且 WebView 支持多 profile。两个条件缺一都做不出 真隔离(WebView 的多 profile 是随 WebView 版本走的,跟系统版本不完全同步)—— 与其做个「假装无痕」,不如不提供这个入口
  • 滚动位置是尽力而为:页面重载后要等排版稳定(图片撑开高度、懒加载补内容), 恢复得太早会被后续布局吃掉。代码里是固定延迟 300ms,极端页面可能不准
  • 证书例外按「域名 + 证书指纹」记,而且不参与备份:换设备 / 重装后会重新问一遍。 这是刻意的 —— 一份用户看不见的信任清单不该跟着备份走
  • 撤销证书例外要重启 App 才生效。同一次运行里 WebView 自己记着那次放行 (见「安全上的取舍」),而这份记忆没有公开接口能清掉。所以撤销之后: 记录立刻没了,但当前这次运行访问该域名仍然不会问;重启 App 后恢复询问

技术方案

  • 语言:Kotlin + viewBinding。配色与文案全部走资源 (colors.xml + values-night + strings.xml),没有散落的硬编码
  • 最低版本:Android 5.0(API 21),targetSdk 34

标签模型

一个 MainActivity 实例 = 一个标签页 = 一个 Android task。 开新标签用 Intent(FLAG_ACTIVITY_NEW_DOCUMENT or FLAG_ACTIVITY_MULTIPLE_TASK) 启动新实例, 系统即为它创建独立 task 与最近任务卡片。

每个标签的启动 Intent 带一个唯一的 data URI(blauser://tab/<uuid>)作为稳定标识 —— 实测 task 的 baseIntent 会保留它,因此可以据此枚举标签、moveToFront() 切过去、 finishAndRemoveTask() 关掉。

这个标识必须唯一:外部 App 用 ACTION_VIEW 调起时,Intent 的 data 是网址本身, 不能拿来当标签标识 —— 同一个网址被打开两次会撞成同一个 key, 导致两个标签的状态互相覆盖、关一个还关俩。所以这类 Intent 不会被就地加载, 而是转交给一个带 uuid 的正经标签页。

缩放原理

注入 JS 改写 viewport meta 并计算 scale;固定档位模式下同时用 Object.defineProperty 伪装 window.screen.width / availWidth。

「自动」档在 DOMContentLoaded 判断(此时 <head> 已解析完,比 onPageFinished 早得多, 能明显减少重排闪烁):

页面类型 判定 处理
有 width=device-width 移动端适配页 不干预,保持原生渲染
无 viewport meta 或固定宽度 PC 页面 按 1280 渲染后整体缩放贴合屏幕

安全上的取舍

  • 混合内容用 MIXED_CONTENT_COMPATIBILITY_MODE:放行 http 图片等被动资源, 但拦下 http 脚本
  • allowFileAccess 默认关闭,仅本地 HTML 标签页临时打开:地址栏仍然不放行 任意 file:// 输入;打开本地 HTML 时需要读同目录的 CSS / 图片,所以只跟着主文档 地址开关(跳到网页立即收回)。allowContentAccess 保持关闭
  • 更新包只接受 GitHub Releases 的 https 地址:重定向每一跳都要求 https; 下载后校验包名、版本号和 versionCode,release 提供 SHA-256 时还会校验摘要; 签名一致性交给系统安装器(覆盖安装时签名不符会被拒绝)
  • 外部协议跳转要求用户手势:没有手势的 weixin:// / intent:// 跳转一律拦下 —— 网页静默拉起 App 是最常见的骚扰与诱导手段(Chrome 同样策略)。 弹窗(window.open)也只放行用户点出来的
  • intent:// 另做了三处加固: 清掉解析出来的 component / selector(否则网页能借本 App 的身份去拉起别的应用里 没有导出的组件,即 Google 所说的 Intent Redirection); startActivity 连 SecurityException 一起捕获(目标未导出或带 android:permission 时抛的是它,只捕 ActivityNotFoundException 会让恶意网页直接搞崩 App); browser_fallback_url 必须是 http/https —— 它完全由网页控制, 放行 javascript: 会在当前页面上下文里执行脚本
  • 证书例外逐条记录,而不是给一个「忽略证书错误」的总开关:存的是 域名 + 证书指纹(见 SslExceptionStore)。只按域名记的话,这个域名以后任何 证书都会被放行 —— 用户当初接受的明明是「那张自签证书」,不是「这个域名以后可以被 随便冒充」。绑上指纹后,证书一换就会重新问。取不到指纹时宁可不记 (本次放行、下次再问),也不退化成整站无条件放行
    • WebView 自己有一份进程内的记忆:放行过一次的域名,在同一次运行里不会再回调 onReceivedSslError(实测:撤销例外后另开一个标签访问同一域名,TLS 握手确实 重新失败,但回调根本没发生,页面直接加载)。这份记忆没有公开接口可读可清, 进程一退就没了 —— 所以「永久记住」必须由应用自己落盘,两者是互补的: 进程内靠 WebView,跨进程靠这份记录
    • 例外是按域名记的,不区分端口:同一台机器上的 https://nas:8443 与 wss://nas:9443 共用一个例外。证书本来就是按主机签的,分端口记只会让人 在同一台机器上被问很多次
    • 无痕标签里不落盘,只放行本次 —— 那正是「无痕」的意义
  • 网页里的 wss:// 也能被「继续访问」救回来(实测通过)。WebView 会把非主文档的 证书错误一并交给应用 —— 这是它和 Chrome 的刻意差异,Chromium 为此专门加了 resource type 管线(提交说明:"the Chromium based webview needs to process and pass SslErrors to the application even for the non-Main-Frame resource types")。 对话框会写清出错的是「这个网页请求的资源」,并且拒绝时不会用错误页盖掉页面 —— 子资源出错时页面本身是好的。代价是页面上一个坏证书的图片也可能弹一次窗
  • allowBackup="false" + 备份规则全排除:应用数据目录里有 WebView 的 cookie / localStorage,那是用户登录态,不该跟着云备份或换机迁移走

开发

app/src/main/java/com/blauser/browser/
├── MainActivity.kt          # 一个实例 = 一个标签页;生命周期、覆盖层、回调分发
├── TabRegistry.kt           # 标签 = task 的枚举 / 切换 / 关闭 + 各标签状态登记
├── WebViewFactory.kt        # 唯一构造 WebView 的地方(WebSettings + 两个 client)
├── PageScaler.kt            # viewport 缩放与 screen 伪装
├── UrlHelper.kt             # 地址栏输入解析(纯函数)
├── SettingsManager.kt       # 全局设置持久化
├── SiteSettingsManager.kt   # 按域名覆盖全局设置
├── SslExceptionStore.kt     # 「这个域名的这张证书我认了」(域名 + 指纹,可撤销)
├── SettingsDialogs.kt       # 全局设置 / 本站设置两个面板
├── PageActions.kt           # 收藏 / 复制 / 分享 / 桌面快捷方式 / 审查元素
├── DownloadHandler.kt       # 下载(走系统 DownloadManager)
├── FloatingBallView.kt      # 半球悬浮球与下拉菜单(菜单项数据驱动)
├── BookmarkManager.kt       # 收藏夹存储(含旧数据迁移)
└── UrlListDialog.kt         # 通用网址列表对话框(收藏夹 / 历史 / 证书例外共用)

资源目录:

app/src/main/res/
├── values/colors.xml          # 浅色配色 token
├── values-night/colors.xml    # 深色配色 token(与上面一一对应)
├── values/strings.xml         # 全部用户可见文案
├── xml/data_extraction_rules.xml, backup_rules.xml   # 备份全排除
└── layout/ drawable/          # 布局与图标(颜色全部引用 @color token)

常用命令

./gradlew assembleDebug       # 调试包
./gradlew assembleRelease     # 发布包(R8 + 资源压缩)
./gradlew testDebugUnitTest   # 单元测试
./gradlew lintDebug           # 静态检查(有 error 会中断构建)

发布签名从 local.properties 读取(该文件不进版本库),不配的话自动回落到 debug 签名:

sdk.dir=/path/to/android-sdk
RELEASE_STORE_FILE=../release.jks
RELEASE_STORE_PASSWORD=...
RELEASE_KEY_ALIAS=...
RELEASE_KEY_PASSWORD=...

测试

单元测试覆盖纯逻辑部分:地址栏输入解析、缩放脚本生成、UA 解析、收藏夹 JSON 读写与容错。

./gradlew testDebugUnitTest

UI 与 WebView 行为没有自动化测试,改动后需要在真机上过一遍。

调试技巧

# 查看当前有几个标签(= 几个 task)
adb shell dumpsys activity activities | grep -oE "Task\{[a-f0-9]+ #[0-9]+ type=standard A=[0-9]+:com.blauser.browser" \
  | sed -E 's/.*#([0-9]+).*/\1/' | sort -un

# WebView 远程调试(debug 包自动开启,release 包关闭)
adb forward tcp:9222 localabstract:webview_devtools_remote:$(adb shell pidof com.blauser.browser)
curl -s http://localhost:9222/json    # 每个标签页是一个独立的 page target

图标

小飞象启动图标由 tools/gen_icon.py 生成(几何定义是唯一来源,同时输出自适应图标的 矢量 XML 和 API 21-25 用的各级 PNG)。改图标设计时改该脚本重新生成即可。


开源协议

MIT


已完成

  • 多语言支持(中文 / English,跟随系统)
  • 浏览历史(去重置顶、上限 500、一键清空)
  • 无痕标签(Android 9+,独立 profile 真隔离)
  • 标签被系统回收后的状态恢复(URL + 前进后退历史 + 滚动位置)
  • 文件上传 / 下载 / 摄像头麦克风定位权限 / 三方 cookie
  • 深色模式
  • 按域名覆盖设置
  • 无效证书自己说了算(拒绝 / 继续访问并永久记住,按域名 + 指纹记,可撤销)
  • 添加书签、多标签页、标签出现在系统最近任务中
  • 打开本地 HTML 文件(注册为 .html / .htm 的系统打开方式)
  • 应用内检查更新 / 下载 APK 升级(GitHub Releases,每天启动时自动检查一次)

不在计划内

写在这里是为了不让人反复提同一个建议 —— 这几项都是想清楚了不做的,不是没想到。

  • 广告 / 跟踪拦截:shouldInterceptRequest 拦每个请求会明显拖慢页面加载, 而维护一份可用的规则表是个持续投入的工程。想拦广告就用专门的扩展或 DNS。
  • 标签页分组 / 批量关闭:标签就是系统最近任务里的卡片, 系统自己提供了切换和批量清理,再叠一层是重复建设。
  • 恢复表单内容:见上方「已知限制」,WebView 没有序列化页面状态的接口。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages