HTML などの静的ファイルを APK に包んでオフライン閲覧する方法

Sometimes the network is poor in elevators and high‑speed trains, and it’s frustrating when a mobile device can’t load.

Often browsing on a phone means visiting websites that are entirely composed of static files. However, when you click into the homepage, not all resources are downloaded to the device at once, causing subsequent clicks on internal links to wait for loading.

Is it possible to download all resources beforehand for the browser to browse?

One way is to download all html, css, js files to the phone, then start a web server like darkhttpd, and navigate the browser to localhost:8080, but this is a bit cumbersome.

The cross‑platform framework Tauri v2 provides an easier method, as follows:

Create a new project using the vanilla template

pnpm create tauri-app -m pnpm -t vanilla --identifier app.xjtu.rust-doc xjtu-tauri-app-rust-doc
cd xjtu-tauri-app-rust-doc
pnpm install
pnpm tauri android init

Then copy the static resource directory containing index.html into the src directory:

rsync  -av --delete ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/share/doc/rust/html/ ./src/

Refer to Android Code Signing | Tauri to modify the following files, adding the credentials needed for release builds:

src-tauri/gen/android/keystore.properties
password=7
keyAlias=xj
storeFile=/app.jks
src-tauri/gen/android/app/build.gradle.kts
import java.util.Properties
import java.io.FileInputStream

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("rust")
}

val tauriProperties = Properties().apply {
    val propFile = file("tauri.properties")
    if (propFile.exists()) {
        propFile.inputStream().use { load(it) }
    }
}

android {
    compileSdk = 34
    namespace = "app.xjtu.static_app"
    defaultConfig {
        manifestPlaceholders["usesCleartextTraffic"] = "false"
        applicationId = "app.xjtu.static_app"
        minSdk = 24
        targetSdk = 34
        versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
        versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
    }
    signingConfigs {
      create("release") {
          val keystorePropertiesFile = rootProject.file("keystore.properties")
          val keystoreProperties = Properties()
          if (keystorePropertiesFile.exists()) {
              keystoreProperties.load(FileInputStream(keystorePropertiesFile))
          }

          keyAlias = keystoreProperties["keyAlias"] as String
          keyPassword = keystoreProperties["password"] as String
          storeFile = file(keystoreProperties["storeFile"] as String)
          storePassword = keystoreProperties["password"] as String
        }
    }


    buildTypes {
        getByName("debug") {
            manifestPlaceholders["usesCleartextTraffic"] = "true"
            isDebuggable = true
            isJniDebuggable = true
            isMinifyEnabled = false
            packaging {                jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
                jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
                jniLibs.keepDebugSymbols.add("*/x86/*.so")
                jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
            }
        }
        getByName("release") {
            isMinifyEnabled = true
            signingConfig = signingConfigs.getByName("release")
            proguardFiles(
                *fileTree(".") { include("**/*.pro") }
                    .plus(getDefaultProguardFile("proguard-android-optimize.txt"))
                    .toList().toTypedArray()
            )
        }
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
    buildFeatures {
        buildConfig = true
    }
}

rust {
    rootDirRel = "../../../"
}

dependencies {
    implementation("androidx.webkit:webkit:1.6.1")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.8.0")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.4")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}

apply(from = "tauri.build.gradle.kts")

Build the APK:

tauri android build --apk --target aarch64

Demo:

Hidden

On the Play Store there are APKs packaged with some unknown advanced method:
https://play.google.com/store/apps/details?id=com.rust_doc.md_ismail_hosen
https://play.google.com/store/apps/details?id=com.rust_book.example

「いいね!」 2

Thinking like this, your site could also create a static offline version of the app, for browsing only.

Alright, this is an APK that archives all the text, images, and other rich media from your site’s posts together for people to browse. Apart from being a bit large (1650 MiB), the subsequent browsing speed is incredibly fast :flight_departure:

https://assets.xjtu.app/pool/app.xjtu.static-app.apk

P.S. 1: The static website was obtained today using:

wget --adjust-extension --mirror --page-requisites --convert-links   --recursive  --user-agent "Googlebot" https://xjtu.app

The command scraped it.

If you change the browser’s User Agent to Googlebot, you can get the same interface, or you can directly visit https://static.xjtu.app to experience it.

P.S. 2: All your site’s static resources combined total over 1 GiB, compiling takes too long, and this Tauri‑based approach scales badly.