# Speed Tools
An all-in-one Android framework for local pluginization, dynamic skinning and font scaling
中文文档 · English
> **Design goals**: low-intrusion integration, decoupling of multiple business modules, no dependency on Google Play dynamic delivery. > > **Integration**: source dependency is the recommended way now; the legacy Maven coordinate `com.liyihangjson:speed_tools:1.1.1` is no longer recommended. --- ## 📋 Table of Contents - [Features](#-features) - [Architecture](#-architecture) - [Project Structure](#-project-structure) - [Requirements](#-requirements) - [Quick Start: Run the Demo in 10 Minutes](#-quick-start-run-the-demo-in-10-minutes) - [Pluginization Guide](#-pluginization-guide) - [Skinning & Font Scaling Guide](#-skinning--font-scaling-guide) - [API Cheat Sheet](#-api-cheat-sheet) - [FAQ & Troubleshooting](#-faq--troubleshooting) - [Production Recommendations](#-production-recommendations) - [Versions & Compatibility](#-versions--compatibility) - [Related Documents](#-related-documents) --- ## ✨ Features | Feature | Description | Typical use case | |---|---|---| | **Pluginization** | The host dynamically loads uninstalled APKs and launches plugin pages through a proxy | Independent evolution of business modules, plugin-level decoupling | | **Dynamic skinning** | Load a skin-package APK at runtime to replace colors / images / backgrounds | Night mode, seasonal themes, brand customization | | **Font scaling** | Adjust global font size at runtime with persisted user preference | Accessibility, large-font mode | --- ## 🏗️ Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Host APK │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Assets │ │ SpeedUtils │ │ Proxy Activity│ │ │ │ (plugin APK) │───▶│ (copy/load) │───▶│ (lifecycle) │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ SpeedApkManager (singleton) │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ ClassLoader │ │ Resources │ │ PackageInfo │ │ │ │ │ │(memory/file)│ │ (bridging) │ │ (metadata) │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Plugin APK (not installed) │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Plugin page │ │ res/ │ │AndroidManifest│ │ │ │(impl class) │◀───│ (resources) │ │(entry declare)│ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` **Core flow**: 1. The plugin APK is placed in the host `assets/` directory (or an external download directory); 2. At runtime it is copied into the host's private directory (`assets` for development, download + verification for production); 3. **Class loading**: Android 8.0+ uses `InMemoryDexClassLoader` (in-memory loading, avoiding the Android 14+ file permission restriction); older versions fall back to `DexClassLoader`; 4. **Resource bridging**: an `AssetManager` is created via reflection to build the plugin `Resources` context; 5. **Proxy forwarding**: the host navigates through a unified API and a proxy `Activity` forwards lifecycle callbacks to the plugin implementation class. --- ## 📁 Project Structure ``` speed_tools/ ├── lib_speed_tools/ # Core library (plugin loading, proxy, skinning, fonts) ├── module_host_main/ # Sample host app (loads and launches plugins) ├── module_client_one/ # Sample plugin 1 ├── module_client_two/ # Sample plugin 2 ├── theme_demo/ # Skinning and font-scaling demo app ├── black_theme/ # Sample skin package (resources only, no logic) └── lib_img_utils/ # Third-party image library test module ``` | Module | Type | Purpose | |---|---|---| | `lib_speed_tools` | Library | Core capabilities (plugin loading, proxy, skinning, fonts) | | `module_host_main` | App | Sample host (loads and launches plugins) | | `module_client_one` | App | Sample plugin 1 | | `module_client_two` | App | Sample plugin 2 | | `theme_demo` | App | Skinning and font-scaling sample | | `black_theme` | App | Sample skin package | | `lib_img_utils` | Library | Third-party library test module | --- ## 🛠️ Requirements - **JDK**: 17+ - **Android Studio**: latest stable (Koala or newer recommended) - **compileSdk**: 35 - **minSdk**: 21 (Android 5.0) - **targetSdk**: 35 - **AGP**: 8.8.2 - **Gradle**: 8.10+ - **Required**: `android.useAndroidX=true` --- ## 🚀 Quick Start: Run the Demo in 10 Minutes > All steps below can be verified directly in this repository; no extra project is needed. ### Step 1: Build the plugin APKs ```bash # Build the two sample plugins ./gradlew :module_client_one:assembleDebug ./gradlew :module_client_two:assembleDebug # Build the skin package ./gradlew :black_theme:assembleDebug ``` Output paths: - `module_client_one/build/outputs/apk/debug/module_client_one-debug.apk` - `module_client_two/build/outputs/apk/debug/module_client_two-debug.apk` - `black_theme/build/outputs/apk/debug/black_theme-debug.apk` ### Step 2: Place the plugins into the host Copy the two plugin APKs built above into the host `assets` directory: ``` module_host_main/src/main/assets/ ├── module_client_one-debug.apk └── module_client_two-debug.apk ``` Copy the skin-package APK into the `theme_demo` assets directory: ``` theme_demo/src/main/assets/ └── black_theme-debug.apk ``` ### Step 3: Run the host Select the **`module_host_main`** run configuration in Android Studio and run it. After launch the host loads the plugins in the background; once loading succeeds two buttons appear, each opening one plugin page. ### Step 4: Run the skinning demo Select the **`theme_demo`** run configuration and run it. Follow the on-screen steps: switch to the black theme → restore the default → enlarge the font → restore the font. --- ## 🔌 Pluginization Guide ### Overall flow ``` Host prepares the APK (assets / download) │ ▼ Copy into the private directory (resolvePluginApk) │ ▼ Load the plugin (SpeedApkManager.loadApk) │ ▼ Create the plugin ClassLoader + Resources │ ▼ Navigate to the plugin page (SpeedUtils.goActivity) │ ▼ Proxy Activity forwards the lifecycle ``` ### 1. Host dependency **`settings.gradle`** ```gradle include ':lib_speed_tools' ``` **Host `build.gradle`** ```gradle dependencies { implementation project(':lib_speed_tools') } ``` ### 2. Loading a plugin from the host (recommended template) ```java private static final String EXTERNAL_APK_DIR = "/sdcard/Download"; private static final String FIRST_PLUGIN_APK = "module_client_one-debug.apk"; private static final String FIRST_APK_KEY = "first_apk"; private boolean loadPlugin(String key, String assetFileName, String dexOutKey) { // Look into the external directory first, then fall back to copying from assets File apkFile = SpeedUtils.resolvePluginApk( getApplicationContext(), EXTERNAL_APK_DIR, assetFileName); if (apkFile == null) { Log.e(TAG, "Plugin APK not found: " + assetFileName); return false; } return SpeedApkManager.getInstance().loadApk( key, apkFile.getAbsolutePath(), dexOutKey, getApplicationContext()); } ``` **Parameters**: | Parameter | Meaning | Recommendation | |---|---|---| | `key` | Unique plugin identifier | Name it per business domain, e.g. `biz_order_v1` | | `apkPath` | Absolute APK path | Make sure it is readable | | `dexOutKey` | Name of the dex optimization directory | Use a separate directory per plugin to avoid conflicts | ### 3. Navigating to a plugin page ```java SpeedUtils.goActivity(this, "first_apk", null); ``` | Parameter | Description | |---|---| | `activity` | Host Activity | | `"first_apk"` | The plugin key used when loading | | `null` | Class tag; when empty the default entry is used (must be declared in the plugin manifest meta-data) | ### 4. Plugin project conventions **Suggested structure**: ``` plugin module/ ├── src/main/java/ │ └── PluginMainActivity.java # Page implementation │ └── PluginEntry.java # Entry logic (called by the proxy) ├── src/main/res/ # Plugin resources └── src/main/AndroidManifest.xml # Declares the entry meta-data ``` **Entry declaration example** (`AndroidManifest.xml`): ```xml