[{"content":"Background This blog started in late 2013, hosted on GitHub Pages with Jekyll. After several theme changes, the most recent setup was jekyll-theme-chirpy 7.1.1.\nAfter accumulating 55 articles, I decided to reassess the tech stack, looking for something simple, elegant, and developer-oriented.\nStatic Blog Ecosystem in 2026 Before making a choice, I compared the current mainstream static site generators (SSGs):\nSSG Language Build Speed Highlights Jekyll Ruby Moderate Native GitHub Pages support, most mature ecosystem Hugo Go Extremely fast (\u0026lt;1ms/page) Single binary, zero dependencies Astro JS/TS Fast Zero JS by default, Island Architecture 11ty JS Fast Flexible, multi-template engine Zola Rust Extremely fast Single binary, built-in Sass/highlighting Why Hugo + PaperMod After evaluation, Hugo + PaperMod was the winner:\nBuild speed — 54 posts built in \u0026lt;100ms, Jekyll takes seconds PaperMod theme — Minimal, elegant, content-focused, perfect dark mode Zero Ruby dependency — Single hugo binary, ready to go Native multilingual support — Hugo\u0026rsquo;s built-in i18n works great for Chinese/English Actively maintained — PaperMod community is active, still receiving updates in 2026 Migration Process 1. Project Structure Change 1 2 3 4 5 6 7 8 # Jekyll structure # Hugo structure ├── _config.yml ├── hugo.yaml ├── _posts/ ├── content/posts/ ├── _data/ ├── static/ ├── _plugins/ ├── themes/PaperMod/ ├── _tabs/ ├── archetypes/ ├── Gemfile └── layouts/ └── assets/ 2. Front Matter Standardization Front matter was inconsistent during the Jekyll era. During migration, everything was unified to:\n1 2 3 4 5 6 7 8 9 10 --- title: \u0026#34;Post Title\u0026#34; date: 2026-06-26 description: \u0026#34;Brief description\u0026#34; author: \u0026#34;haoxiqiang\u0026#34; tags: [\u0026#34;tag1\u0026#34;, \u0026#34;tag2\u0026#34;] categories: [\u0026#34;Category\u0026#34;] ShowToc: true TocOpen: false --- 3. Multilingual Configuration Hugo\u0026rsquo;s multilingual support is intuitive — differentiated by filename suffix:\n1 2 3 content/posts/ ├── my-post.md # Chinese (default language) └── my-post.en.md # English With defaultContentLanguage: zh in the config, English versions are accessible via the /en/ prefix.\n4. GitHub Actions Deployment Using peaceiris/actions-hugo along with GitHub\u0026rsquo;s official Pages deployment actions:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 name: Deploy Hugo site to Pages on: push: branches: [\u0026#34;master\u0026#34;] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: true fetch-depth: 0 - name: Setup Hugo uses: peaceiris/actions-hugo@v3 with: hugo-version: \u0026#39;latest\u0026#39; extended: true - name: Build run: hugo --minify - uses: actions/upload-pages-artifact@v3 with: path: ./public deploy: needs: build runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - uses: actions/deploy-pages@v4 PaperMod Features Out-of-the-box features with PaperMod:\nAuto dark/light mode switching Site search (Fuse.js-based) One-click code copy Reading time estimation Table of contents (TOC) Breadcrumb navigation RSS feed SEO optimization (Open Graph, Twitter Cards) Responsive design Archives page Multilingual support GitHub Pages Best Practices in 2026 Key recommendations for using GitHub Pages today:\nDeployment Method Use GitHub Actions — branch-based deployment is outdated. Actions advantages:\nNot limited to Jekyll; use any SSG Full control over build environment Always using the latest tool versions Performance Optimization Enable minify for compressed output Leverage CDN (GitHub Pages includes Fastly CDN) Use WebP format for images Enable PWA caching for offline access Custom Domain Place a CNAME file in the repository root Configure DNS CNAME pointing to \u0026lt;username\u0026gt;.github.io GitHub auto-provisions HTTPS certificates Content Management Write in Markdown, git push to deploy Use archetypes templates for quick post creation Use draft: true to manage drafts Migration Summary Dimension Before (Jekyll) After (Hugo) Build time ~5s \u0026lt;100ms Dependencies Ruby + Bundler Single binary Theme Chirpy 7.1.1 PaperMod Multilingual Not supported Native support Search None Fuse.js full-text search Post count 55 54 (removed 1 duplicate) The migration took about half a day, mostly spent standardizing front matter. The result is satisfying — build speed went from seconds to milliseconds, the theme is clean and elegant, and multilingual support is ready.\nReferences Hugo Official Documentation Hugo Multilingual Configuration PaperMod Theme GitHub Repository PaperMod Theme Documentation Hugo GitHub Pages Deployment Guide peaceiris/actions-hugo ","permalink":"https://blog.substitute.tech/en/posts/blog-migration-to-hugo/","summary":"\u003ch2 id=\"background\"\u003eBackground\u003c/h2\u003e\n\u003cp\u003eThis blog started in late 2013, hosted on GitHub Pages with Jekyll. After several theme changes, the most recent setup was \u003ca href=\"https://github.com/cotes2020/jekyll-theme-chirpy\"\u003ejekyll-theme-chirpy\u003c/a\u003e 7.1.1.\u003c/p\u003e\n\u003cp\u003eAfter accumulating 55 articles, I decided to reassess the tech stack, looking for something \u003cstrong\u003esimple, elegant, and developer-oriented\u003c/strong\u003e.\u003c/p\u003e\n\u003ch2 id=\"static-blog-ecosystem-in-2026\"\u003eStatic Blog Ecosystem in 2026\u003c/h2\u003e\n\u003cp\u003eBefore making a choice, I compared the current mainstream static site generators (SSGs):\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eSSG\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eLanguage\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eBuild Speed\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eHighlights\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eJekyll\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRuby\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eModerate\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNative GitHub Pages support, most mature ecosystem\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eHugo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eExtremely fast (\u0026lt;1ms/page)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSingle binary, zero dependencies\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eAstro\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eJS/TS\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFast\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eZero JS by default, Island Architecture\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e11ty\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eJS\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFast\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFlexible, multi-template engine\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eZola\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRust\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eExtremely fast\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSingle binary, built-in Sass/highlighting\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"why-hugo--papermod\"\u003eWhy Hugo + PaperMod\u003c/h3\u003e\n\u003cp\u003eAfter evaluation, Hugo + PaperMod was the winner:\u003c/p\u003e","title":"Blog Migration: From Jekyll Chirpy to Hugo PaperMod"},{"content":"The AOSP build process is fairly well understood by now: sync the source code, add device-specific drivers and kernel, and build the target image. I had built AOSP before to debug certain issues, but official support for Pixel 3 XL only goes up to Android 12. Recently, while working on Chromium development, I needed to test WebView on a newer platform, so I used LineageOS 21 for the build instead.\nPrerequisites Assumes a configured AOSP build environment:\nAOSP Setup Guide Codenames, Tags, and Build Numbers Syncing AOSP Source 1 2 3 4 mkdir ~/aosp cd ~/aosp repo init --partial-clone -b android-12.0.0_r34 -u https://android.googlesource.com/platform/manifest repo sync -c -j8 Obtaining Drivers Look up the Pixel 3 XL (codename crosshatch) on the Build Numbers page to find the latest Build ID, e.g., SP1A.210812.016.C2 Download the corresponding drivers from Google Drivers 1 2 3 4 5 6 7 8 9 mkdir vendor_backup \u0026amp;\u0026amp; cd vendor_backup wget https://dl.google.com/dl/android/aosp/google_devices-crosshatch-sp1a.210812.016.c2-a4e274b7.tgz wget https://dl.google.com/dl/android/aosp/qcom-crosshatch-sp1a.210812.016.c2-00a7f1f3.tgz tar xvf qcom-crosshatch-*.tgz tar xvf google_devices-crosshatch-*.tgz ./extract-google_devices-crosshatch.sh ./extract-qcom-crosshatch.sh mv vendor/ ../ Building and Flashing Refer to the Building AOSP documentation:\n1 2 3 4 5 6 7 8 9 10 11 12 13 cd ~/aosp source build/envsetup.sh # Choose Pixel 3 XL (crosshatch) as the build target # user/userdebug/eng select different build types lunch aosp_crosshatch-userdebug # Start the build m # Flash the device export ANDROID_PRODUCT_OUT=\u0026#39;/home/haoxiqiang/workspace/aosp/out/target/product/crosshatch\u0026#39; adb reboot fastboot fastboot flashall -w For differences between user, userdebug, and eng build types, see the official documentation.\nBuilding with LineageOS For newer Android versions, LineageOS provides excellent device support:\n1 2 repo init --partial-clone -u https://github.com/LineageOS/android.git -b lineage-21.0 --git-lfs repo sync -c -j8 See the LineageOS Build Wiki for detailed instructions.\nReferences AOSP Setup Guide AOSP Build Guide Build Numbers Google Drivers for Pixel Downloading the Android Source LineageOS Build Wiki — crosshatch Building Pixel Kernels ","permalink":"https://blog.substitute.tech/en/posts/aosp-build-for-pixel3/","summary":"\u003cp\u003eThe AOSP build process is fairly well understood by now: sync the source code, add device-specific drivers and kernel, and build the target image. I had built AOSP before to debug certain issues, but official support for Pixel 3 XL only goes up to Android 12. Recently, while working on Chromium development, I needed to test WebView on a newer platform, so I used LineageOS 21 for the build instead.\u003c/p\u003e","title":"Build AOSP for Pixel 3 XL"},{"content":"I thought I had a solid understanding of the differences between RSA encryption schemes and their use cases. But while implementing RSA encryption today, I noticed something puzzling: the ciphertext was different every time, even with the same plaintext and public key. This was a blind spot for me. After researching the internals, here is what I found.\nThe Issue The following code uses OpenSSL\u0026rsquo;s EVP API for RSA encryption. No random value is explicitly provided, yet the output differs on every call:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 unsigned char *encode_by_rsa(const char *public_key, unsigned const char *input) { int key_len = (int) strlen(public_key); BIO *bio = BIO_new_mem_buf(public_key, key_len); EVP_PKEY *pKey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); BIO_free_all(bio); EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(pKey, NULL); EVP_PKEY_encrypt_init(ctx); EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING); EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()); EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()); size_t rsa_len = (int) RSA_LENGTH; unsigned char *encrypted_data = malloc(rsa_len + 1); memset(encrypted_data, 0, rsa_len + 1); if (encrypted_data == NULL) { return NULL; } int input_len = (int) strlen((const char *) input); EVP_PKEY_encrypt(ctx, encrypted_data, \u0026amp;rsa_len, input, input_len); encrypted_data[rsa_len] = \u0026#39;\\0\u0026#39;; EVP_PKEY_CTX_free(ctx); EVP_PKEY_free(pKey); return encrypted_data; } The Root Cause: Randomized Padding Whether using an RSA private key for signing or a public key for encryption, the data must first be padded before the cryptographic operation. The padding process introduces pseudo-randomness. This is why the same input with the same key produces different output each time.\nPadding Schemes In standard RSA operations, the data length must be less than the key length. Modern padding schemes (such as OAEP used in the code) serve dual purposes:\nPadding Scheme Standard Characteristics PKCS#1 v1.5 RFC 2313 Legacy padding, widely used historically, but vulnerable to Bleichenbacher padding oracle attacks OAEP PKCS#1 v2.0 (RFC 2437) / v2.1 (RFC 3447) Optimal Asymmetric Encryption Padding, recommended for new applications, introduces randomness PSS PKCS#1 v2.1 For signatures only, also probabilistic OAEP works by prepending a random mask to the data before encryption. This mask is generated using cryptographic primitives (MGF1 in the code) and ensures the ciphertext is different each time — exactly what RSA_PKCS1_OAEP_PADDING triggers in the example.\nPython Sample 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import base64 from Crypto import Random from Crypto.Cipher import PKCS1_v1_5 from Crypto.PublicKey import RSA random_generator = Random.new().read private_key = RSA.generate(2048) public_key = private_key.publickey() def rsa_encrypt(data): key = RSA.importKey(public_key.exportKey()) cipher = PKCS1_v1_5.new(key) cipher_text = base64.b64encode(cipher.encrypt(data)) return cipher_text def rsa_decode(encrypt_text): key = RSA.importKey(private_key.exportKey()) cipher = PKCS1_v1_5.new(key) text = cipher.decrypt(base64.b64decode(encrypt_text), random_generator) return text def main(): data = \u0026#34;Hello, world!\u0026#34; encrypt_text = rsa_encrypt(data.encode()) print(\u0026#34;encrypt_text\u0026#34;, encrypt_text) decrypt_text = rsa_decode(encrypt_text) print(\u0026#34;decrypt_text\u0026#34;, decrypt_text.decode()) if __name__ == \u0026#34;__main__\u0026#34;: main() References RFC 2313 — PKCS #1: RSA Encryption Version 1.5 RFC 2437 — PKCS #1: RSA Cryptography Specifications Version 2.0 RFC 3447 — PKCS #1: RSA Cryptography Specifications Version 2.1 OpenSSL Documentation — RSA_public_encrypt OpenSSL 3.x EVP RSA Documentation Wikipedia — Optimal Asymmetric Encryption Padding ","permalink":"https://blog.substitute.tech/en/posts/openssl-rsa/","summary":"\u003cp\u003eI thought I had a solid understanding of the differences between RSA encryption schemes and their use cases. But while implementing RSA encryption today, I noticed something puzzling: the ciphertext was different every time, even with the same plaintext and public key. This was a blind spot for me. After researching the internals, here is what I found.\u003c/p\u003e\n\u003ch2 id=\"the-issue\"\u003eThe Issue\u003c/h2\u003e\n\u003cp\u003eThe following code uses OpenSSL\u0026rsquo;s EVP API for RSA encryption. No random value is explicitly provided, yet the output differs on every call:\u003c/p\u003e","title":"A Blind Spot in RSA Encryption with OpenSSL"},{"content":"I was working on an overseas application whose build server was originally located in Shanghai. Due to special circumstances, it needed to be migrated abroad. This post documents the Jenkins migration and configuration process.\nPrerequisite: Install JDK 11 Using jenv for multi-version Java management:\n1 2 3 4 5 6 7 8 9 # Install jenv git clone https://github.com/jenv/jenv.git ~/.jenv echo \u0026#39;export PATH=\u0026#34;$HOME/.jenv/bin:$PATH\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.bashrc echo \u0026#39;eval \u0026#34;$(jenv init -)\u0026#34;\u0026#39; \u0026gt;\u0026gt; ~/.bashrc source ~/.bashrc # Enable the export plugin and restart the session jenv enable-plugin export exec $SHELL -l Installing Jenkins on CentOS 1 2 3 4 5 6 # Add Jenkins repository and GPG key sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key # Install yum install jenkins Configuring systemd Service 1 2 3 4 sudo vi /etc/systemd/system/jenkins.service sudo systemctl enable /etc/systemd/system/jenkins.service sudo systemctl start jenkins systemctl status jenkins 1 2 3 4 5 6 7 8 9 10 11 12 13 [Unit] Description=jenkins service After=network.target [Service] Type=simple LimitNOFILE=65536 ExecStart=/usr/bin/jenkins User=work Environment=\u0026#34;JENKINS_PORT=8082\u0026#34; [Install] WantedBy=multi-user.target Configuring Android SDK 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 wget https://dl.google.com/android/repository/commandlinetools-linux-8512546_latest.zip unzip commandlinetools-linux-8512546_latest.zip chmod a+x cmdline-tools/ cd cmdline-tools/bin # Install required components via sdkmanager ./sdkmanager --sdk_root=/home/work/workspace/android_sdk --list ./sdkmanager --install \u0026#39;platforms;android-33\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;build-tools;33.0.0\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;cmdline-tools;7.0\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;build-tools;32.0.0\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;build-tools;31.0.0\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;ndk;25.1.8937393\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;platforms;android-28\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;platforms;android-29\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;platforms;android-30\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;platforms;android-31\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;cmake;3.22.1\u0026#39; --sdk_root=/home/work/workspace/android_sdk ./sdkmanager --install \u0026#39;cmake;3.10.2.4988404\u0026#39; --sdk_root=/home/work/workspace/android_sdk Fixing SSH Key Permission Issues If you encounter Permissions 0664 for '/home/work/.ssh/jenkins_id_rsa' are too open:\n1 chmod 600 ~/.ssh Upgrading Jenkins Always refer to the Jenkins Upgrade Guide for version-specific instructions.\nExample for Debian/Ubuntu (on CentOS, use yum update jenkins instead):\n1 2 3 4 wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add - sudo sh -c \u0026#39;echo deb http://pkg.jenkins.io/debian-stable binary/ \u0026gt; /etc/apt/sources.list.d/jenkins.list\u0026#39; sudo apt-get update sudo apt-get install jenkins References Jenkins Official Installation Guide (Linux) Jenkins Upgrade Guide jenv — Java Version Manager Android Command Line Tools Download Installing Jenkins on Red Hat Distributions ","permalink":"https://blog.substitute.tech/en/posts/jenkins/","summary":"\u003cp\u003eI was working on an overseas application whose build server was originally located in Shanghai. Due to special circumstances, it needed to be migrated abroad. This post documents the Jenkins migration and configuration process.\u003c/p\u003e\n\u003ch2 id=\"prerequisite-install-jdk-11\"\u003ePrerequisite: Install JDK 11\u003c/h2\u003e\n\u003cp\u003eUsing \u003ccode\u003ejenv\u003c/code\u003e for multi-version Java management:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e9\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Install jenv\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003egit clone https://github.com/jenv/jenv.git ~/.jenv\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eecho\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;export PATH=\u0026#34;$HOME/.jenv/bin:$PATH\u0026#34;\u0026#39;\u003c/span\u003e \u0026gt;\u0026gt; ~/.bashrc\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eecho\u003c/span\u003e \u003cspan class=\"s1\"\u003e\u0026#39;eval \u0026#34;$(jenv init -)\u0026#34;\u0026#39;\u003c/span\u003e \u0026gt;\u0026gt; ~/.bashrc\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003esource\u003c/span\u003e ~/.bashrc\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Enable the export plugin and restart the session\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ejenv enable-plugin \u003cspan class=\"nb\"\u003eexport\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003eexec\u003c/span\u003e \u003cspan class=\"nv\"\u003e$SHELL\u003c/span\u003e -l\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"installing-jenkins-on-centos\"\u003eInstalling Jenkins on CentOS\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Add Jenkins repository and GPG key\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Install\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eyum install jenkins\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"configuring-systemd-service\"\u003eConfiguring systemd Service\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo vi /etc/systemd/system/jenkins.service\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo systemctl \u003cspan class=\"nb\"\u003eenable\u003c/span\u003e /etc/systemd/system/jenkins.service\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo systemctl start jenkins\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esystemctl status jenkins\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ini\" data-lang=\"ini\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e[Unit]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eDescription\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003ejenkins service\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eAfter\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003enetwork.target\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e[Service]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eType\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003esimple\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eLimitNOFILE\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e65536\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eExecStart\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e/usr/bin/jenkins\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eUser\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003ework\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eEnvironment\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;JENKINS_PORT=8082\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003e[Install]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003eWantedBy\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s\"\u003emulti-user.target\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"configuring-android-sdk\"\u003eConfiguring Android SDK\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ewget https://dl.google.com/android/repository/commandlinetools-linux-8512546_latest.zip\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eunzip commandlinetools-linux-8512546_latest.zip\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003echmod a+x cmdline-tools/\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003ecd\u003c/span\u003e cmdline-tools/bin\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Install required components via sdkmanager\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk --list\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;platforms;android-33\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;build-tools;33.0.0\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;cmdline-tools;7.0\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;build-tools;32.0.0\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;build-tools;31.0.0\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;ndk;25.1.8937393\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;platforms;android-28\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;platforms;android-29\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;platforms;android-30\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;platforms;android-31\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;cmake;3.22.1\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./sdkmanager --install \u003cspan class=\"s1\"\u003e\u0026#39;cmake;3.10.2.4988404\u0026#39;\u003c/span\u003e --sdk_root\u003cspan class=\"o\"\u003e=\u003c/span\u003e/home/work/workspace/android_sdk\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"fixing-ssh-key-permission-issues\"\u003eFixing SSH Key Permission Issues\u003c/h2\u003e\n\u003cp\u003eIf you encounter \u003ccode\u003ePermissions 0664 for '/home/work/.ssh/jenkins_id_rsa' are too open\u003c/code\u003e:\u003c/p\u003e","title":"Jenkins Setup Notes"},{"content":"During the recent holiday, I reorganized the home network with the goal of streaming 4K content without buffering. Since I was rebuilding the server anyway, I decided to migrate from the old setup to the new shadowsocks-rust implementation.\nSystem Update 1 sudo apt update \u0026amp;\u0026amp; sudo apt upgrade Installing and Configuring SS Option 1: Build from Source via Cargo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Install Rust toolchain curl https://sh.rustup.rs -sSf | sh # Configure Cargo environment (add to .profile / .bash_profile) # CARGO_HOME sets the installation path # target-cpu=native lets rustc optimize for the current CPU CARGO_HOME=/root/cargo RUSTFLAGS=\u0026#34;-C target-cpu=native\u0026#34; source .profile # Install build dependencies sudo apt install build-essential # Install shadowsocks-rust cargo install shadowsocks-rust Option 2: Download Prebuilt Binary 1 2 3 4 wget https://github.com/shadowsocks/shadowsocks-rust/releases/download/v1.14.3/shadowsocks-v1.14.3.x86_64-unknown-linux-gnu.tar.xz tar -xf shadowsocks-v1.14.3.x86_64-unknown-linux-gnu.tar.xz cp ssserver /usr/local/bin chmod a+x /usr/local/bin/ssserver Configuration 1 2 mkdir /etc/shadowsocks vi /etc/shadowsocks/config.json 1 2 3 4 5 6 { \u0026#34;server\u0026#34;: \u0026#34;::\u0026#34;, \u0026#34;server_port\u0026#34;: 8888, \u0026#34;method\u0026#34;: \u0026#34;aes-256-gcm\u0026#34;, \u0026#34;password\u0026#34;: \u0026#34;pw\u0026#34; } Test the server:\n1 ssserver -c /etc/shadowsocks/config.json Setting Up Auto-Start 1 vi /etc/systemd/system/shadowsocks-server.service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [Unit] Description=shadowsocks-rust service After=network.target [Service] Type=simple ExecStart=ssserver -c /etc/shadowsocks/config.json ExecStop=/usr/bin/killall ssserver Restart=always RestartSec=10 StandardOutput=syslog StandardError=syslog SyslogIdentifier=ssserver User=root Group=root [Install] WantedBy=multi-user.target 1 systemctl enable /etc/systemd/system/shadowsocks-server.service Network Optimization (BBR) BBR (Bottleneck Bandwidth and Round-trip propagation time) is a TCP congestion control algorithm developed by Google. It estimates the bottleneck bandwidth and minimum RTT to dynamically adjust the sending rate, outperforming traditional loss-based algorithms in high-latency, high-throughput scenarios.\n1 2 3 4 5 6 7 8 9 # Check if BBR is already enabled lsmod | grep bbr # If not enabled, run the following modprobe tcp_bbr echo \u0026#34;tcp_bbr\u0026#34; \u0026gt;\u0026gt; /etc/modules-load.d/modules.conf echo \u0026#34;net.core.default_qdisc=fq\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf echo \u0026#34;net.ipv4.tcp_congestion_control=bbr\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf sysctl -p BBR should be used with the fq (Fair Queue) qdisc for optimal performance.\nStarting the Service 1 2 3 4 5 6 7 8 9 10 11 sysctl --system systemctl start shadowsocks-server systemctl enable shadowsocks-server # Reload after config changes systemctl daemon-reload systemctl restart shadowsocks-server # Verify status systemctl status shadowsocks-server netstat -tunlp Alternative: x-ui I discovered x-ui through discussions — it\u0026rsquo;s more convenient for most users. Here\u0026rsquo;s a quick setup:\n1 2 # Install x-ui, visit http://ip:54321 bash \u0026lt;(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh) 1 2 3 4 # Install acme.sh and create HTTPS certificate curl https://get.acme.sh | sh -s email=your@email.com ~/.acme.sh/acme.sh --register-account -m your@email.com ~/.acme.sh/acme.sh --issue -d yourdomain --standalone References shadowsocks-rust GitHub Repository shadowsocks-rust Documentation BBR Congestion Control — Google BBR GitHub BBR FAQ x-ui Project acme.sh ","permalink":"https://blog.substitute.tech/en/posts/shadowsocks-rust/","summary":"\u003cp\u003eDuring the recent holiday, I reorganized the home network with the goal of streaming 4K content without buffering. Since I was rebuilding the server anyway, I decided to migrate from the old setup to the new shadowsocks-rust implementation.\u003c/p\u003e\n\u003ch2 id=\"system-update\"\u003eSystem Update\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo apt update \u003cspan class=\"o\"\u003e\u0026amp;\u0026amp;\u003c/span\u003e sudo apt upgrade\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"installing-and-configuring-ss\"\u003eInstalling and Configuring SS\u003c/h2\u003e\n\u003ch3 id=\"option-1-build-from-source-via-cargo\"\u003eOption 1: Build from Source via Cargo\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Install Rust toolchain\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecurl https://sh.rustup.rs -sSf \u003cspan class=\"p\"\u003e|\u003c/span\u003e sh\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Configure Cargo environment (add to .profile / .bash_profile)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# CARGO_HOME sets the installation path\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# target-cpu=native lets rustc optimize for the current CPU\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003eCARGO_HOME\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e/root/cargo\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nv\"\u003eRUSTFLAGS\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;-C target-cpu=native\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nb\"\u003esource\u003c/span\u003e .profile\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Install build dependencies\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo apt install build-essential\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# Install shadowsocks-rust\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecargo install shadowsocks-rust\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"option-2-download-prebuilt-binary\"\u003eOption 2: Download Prebuilt Binary\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ewget https://github.com/shadowsocks/shadowsocks-rust/releases/download/v1.14.3/shadowsocks-v1.14.3.x86_64-unknown-linux-gnu.tar.xz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etar -xf shadowsocks-v1.14.3.x86_64-unknown-linux-gnu.tar.xz\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003ecp ssserver /usr/local/bin\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003echmod a+x /usr/local/bin/ssserver\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"configuration\"\u003eConfiguration\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emkdir /etc/shadowsocks\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003evi /etc/shadowsocks/config.json\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;server\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;::\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;server_port\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e8888\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;method\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;aes-256-gcm\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"nt\"\u003e\u0026#34;password\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;pw\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eTest the server:\u003c/p\u003e","title":"Configuring and Optimizing Shadowsocks Rust"},{"content":"In Android dependency management, it\u0026rsquo;s common to configure multiple remote repositories like jcenter, jitpack, google(), and more. Some large projects, such as \u0026ldquo;Zuiyou\u0026rdquo;, depend on over 10 repositories. This becomes a significant problem during initial project setup, dependency changes, or network issues — troubleshooting builds becomes a nightmare.\nI had noticed this issue long ago but kept putting off addressing it. This post documents the Nexus setup process.\nInstalling and Configuring Nexus Prerequisite: JDK 8+.\n1 2 3 4 5 6 7 8 9 10 11 12 # Download and extract Nexus mkdir /app \u0026amp;\u0026amp; cd /app wget -O nexus.tar.gz https://download.sonatype.com/nexus/3/latest-unix.tar.gz tar -xvf nexus.tar.gz mv nexus-3* nexus # Create a dedicated user adduser nexus # Set ownership chown -R nexus:nexus /app/nexus chown -R nexus:nexus /app/sonatype-work Configure the runtime user:\n1 2 3 vi /app/nexus/bin/nexus.rc # Add the following line run_as_user=\u0026#34;nexus\u0026#34; To customize storage paths, edit the JVM options:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 vi /app/nexus/bin/nexus.vmoptions -Xms2703m -Xmx2703m -XX:MaxDirectMemorySize=2703m -XX:+UnlockDiagnosticVMOptions -XX:+UnsyncloadClass -XX:+LogVMOutput -XX:LogFile=../sonatype-work/nexus3/log/jvm.log -XX:-OmitStackTraceInFastThrow -Djava.net.preferIPv4Stack=true -Dkaraf.home=. -Dkaraf.base=. -Dkaraf.etc=etc/karaf -Djava.util.logging.config.file=etc/karaf/java.util.logging.properties -Dkaraf.data=/nexus/nexus-data -Djava.io.tmpdir=../sonatype-work/nexus3/tmp -Dkaraf.startLocalConsole=false Setting Up Systemd Service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 vi /etc/systemd/system/nexus.service [Unit] Description=nexus service After=network.target [Service] Type=forking LimitNOFILE=65536 User=nexus Group=nexus ExecStart=/app/nexus/bin/nexus start ExecStop=/app/nexus/bin/nexus stop User=nexus Restart=on-abort [Install] WantedBy=multi-user.target 1 2 3 # Enable and start chkconfig nexus on systemctl start nexus Initial Configuration Open http://\u0026lt;ip\u0026gt;:8081 Click Sign In Retrieve the default password: cat /app/sonatype-work/nexus3/admin.password Change the password to complete setup 1 2 systemctl stop nexus systemctl restart nexus Setting Up Proxy Repositories Nexus defines three fundamental repository types:\nType Description group Combines multiple repositories into a single endpoint hosted Stores private artifacts internally proxy Proxies and caches remote sources (e.g., Maven Central) The optimization strategy is to aggregate all remote repositories behind a Nexus group repository, so clients only need to configure a single repository URL. This reduces network requests during dependency resolution and speeds up builds significantly.\nReferences Sonatype Nexus Repository Official Documentation Sonatype Nexus Repository System Requirements Nexus Repository Types Nexus Download Page Running Nexus as a Service ","permalink":"https://blog.substitute.tech/en/posts/nexus/","summary":"\u003cp\u003eIn Android dependency management, it\u0026rsquo;s common to configure multiple remote repositories like \u003ccode\u003ejcenter\u003c/code\u003e, \u003ccode\u003ejitpack\u003c/code\u003e, \u003ccode\u003egoogle()\u003c/code\u003e, and more. Some large projects, such as \u0026ldquo;Zuiyou\u0026rdquo;, depend on over 10 repositories. This becomes a significant problem during initial project setup, dependency changes, or network issues — troubleshooting builds becomes a nightmare.\u003c/p\u003e\n\u003cp\u003eI had noticed this issue long ago but kept putting off addressing it. This post documents the Nexus setup process.\u003c/p\u003e\n\u003ch2 id=\"installing-and-configuring-nexus\"\u003eInstalling and Configuring Nexus\u003c/h2\u003e\n\u003cp\u003ePrerequisite: JDK 8+.\u003c/p\u003e","title":"Optimizing Android Builds with Self-Hosted Nexus Repository"},{"content":"Network instability at the office was affecting work, so I set up a Shadowsocks server on my VPS for source code pulls. The steps below work on most Linux distributions and have been tested on Ubuntu 16.04 and 18.04.\n2024 Update: The Python version of shadowsocks used in this guide is no longer maintained. The actively maintained implementation is shadowsocks-rust, which offers better performance and support for modern encryption protocols. For a fresh setup, refer to the shadowsocks-rust documentation. The original Python-based steps are preserved below for reference.\nSystem Preparation 1 apt update \u0026amp;\u0026amp; apt upgrade -y Install and Configure Shadowsocks (Python Edition) Installation 1 2 apt install python3-pip -y pip3 install https://github.com/shadowsocks/shadowsocks/archive/master.zip Configuration 1 2 mkdir /etc/shadowsocks vi /etc/shadowsocks/config.json 1 2 3 4 5 6 7 8 9 10 { \u0026#34;server\u0026#34;:\u0026#34;::\u0026#34;, \u0026#34;server_port\u0026#34;:8888, \u0026#34;local_address\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;local_port\u0026#34;:1080, \u0026#34;password\u0026#34;:\u0026#34;your-password\u0026#34;, \u0026#34;timeout\u0026#34;:300, \u0026#34;method\u0026#34;:\u0026#34;aes-256-cfb\u0026#34;, \u0026#34;fast_open\u0026#34;: true } Firewall Rules 1 2 3 4 5 iptables -I INPUT -p tcp --dport 8888 -j ACCEPT iptables -I INPUT -p udp --dport 8888 -j ACCEPT # If using UFW: ufw allow 8888 Quick Test 1 ssserver -c /etc/shadowsocks/config.json Enable BBR Acceleration BBR (Bottleneck Bandwidth and Round-trip propagation time) is Google\u0026rsquo;s TCP congestion control algorithm that significantly improves network throughput.\n1 2 3 4 5 6 7 8 9 # Check if BBR is already loaded lsmod | grep bbr # If not loaded, run: modprobe tcp_bbr echo \u0026#34;tcp_bbr\u0026#34; \u0026gt;\u0026gt; /etc/modules-load.d/modules.conf echo \u0026#34;net.core.default_qdisc=fq\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf echo \u0026#34;net.ipv4.tcp_congestion_control=bbr\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf sysctl -p Systemd Service for Auto-Start 1 vi /etc/systemd/system/shadowsocks-server.service 1 2 3 4 5 6 7 8 9 10 11 12 [Unit] Description=Shadowsocks Server After=network.target [Service] ExecStartPre=/bin/sh -c \u0026#39;iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8888\u0026#39; ExecStartPre=/bin/sh -c \u0026#39;iptables -t nat -A PREROUTING -p udp --dport 443 -j REDIRECT --to-port 8888\u0026#39; ExecStart=/usr/local/bin/ssserver -c /etc/shadowsocks/config.json Restart=on-abort [Install] WantedBy=multi-user.target Kernel Network Parameter Tuning Create /etc/sysctl.d/local.conf for further network optimization:\n1 vi /etc/sysctl.d/local.conf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 # max open files fs.file-max = 51200 # max read/write buffer net.core.rmem_max = 67108864 net.core.wmem_max = 67108864 net.core.rmem_default = 65536 net.core.wmem_default = 65536 # max processor input queue net.core.netdev_max_backlog = 4096 # max backlog net.core.somaxconn = 4096 # resist SYN flood attacks net.ipv4.tcp_syncookies = 1 # reuse timewait sockets when safe net.ipv4.tcp_tw_reuse = 1 # turn off fast timewait sockets recycling net.ipv4.tcp_tw_recycle = 0 # short FIN timeout net.ipv4.tcp_fin_timeout = 30 # short keepalive time net.ipv4.tcp_keepalive_time = 1200 # outbound port range net.ipv4.ip_local_port_range = 10000 65000 # max SYN backlog net.ipv4.tcp_max_syn_backlog = 4096 # max timewait sockets held by system simultaneously net.ipv4.tcp_max_tw_buckets = 5000 # turn on TCP Fast Open on both client and server side net.ipv4.tcp_fastopen = 3 # TCP receive/write buffer (min, default, max) net.ipv4.tcp_rmem = 4096 87380 67108864 net.ipv4.tcp_wmem = 4096 65536 67108864 # turn on path MTU discovery net.ipv4.tcp_mtu_probing = 1 # BBR congestion control net.ipv4.tcp_congestion_control = bbr Start Shadowsocks 1 2 3 4 5 6 7 8 9 10 11 sysctl --system systemctl start shadowsocks-server systemctl enable shadowsocks-server # Reload after any config changes systemctl daemon-reload systemctl restart shadowsocks-server # Verify status systemctl status shadowsocks-server netstat -tunlp References Shadowsocks Official GitHub (Python) Shadowsocks-rust (Recommended Replacement) Google BBR Congestion Control TCP BBR Documentation - kernel.org ArchLinux Wiki: Shadowsocks ","permalink":"https://blog.substitute.tech/en/posts/shadowsocks/","summary":"\u003cp\u003eNetwork instability at the office was affecting work, so I set up a Shadowsocks server on my VPS for source code pulls. The steps below work on most Linux distributions and have been tested on Ubuntu 16.04 and 18.04.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e2024 Update:\u003c/strong\u003e The Python version of \u003ccode\u003eshadowsocks\u003c/code\u003e used in this guide is no longer maintained. The actively maintained implementation is \u003ca href=\"https://github.com/shadowsocks/shadowsocks-rust\"\u003eshadowsocks-rust\u003c/a\u003e, which offers better performance and support for modern encryption protocols. For a fresh setup, refer to the \u003ca href=\"https://github.com/shadowsocks/shadowsocks-rust\"\u003eshadowsocks-rust documentation\u003c/a\u003e. The original Python-based steps are preserved below for reference.\u003c/p\u003e","title":"Shadowsocks Setup and Optimization"},{"content":"Three quick tips: handling the back button in DialogFragment, chmod permission reference, and the underscore problem in SSL hostnames.\nDialogFragment Back Button Handling DialogFragment has no direct override for the back button. Two approaches were commonly used.\nApproach 1: Override in onCreateDialog 1 2 3 4 5 6 7 8 9 @Override public Dialog onCreateDialog(Bundle savedInstanceState) { return new Dialog(getActivity(), getTheme()){ @Override public void onBackPressed() { // Handle back press here } }; } Approach 2: Via onKeyListener 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public void onResume() { super.onResume(); Dialog dialog = getDialog(); if (dialog != null) { dialog.setOnKeyListener(this); } } @Override public void onPause() { super.onPause(); Dialog dialog = getDialog(); if (dialog != null) { dialog.setOnKeyListener(null); } } Modern Approach: OnBackPressedDispatcher (AndroidX) For newer versions, use AndroidX\u0026rsquo;s OnBackPressedDispatcher. Since Fragment 1.6.1, DialogFragment returns a ComponentDialog by default, which has its own OnBackPressedDispatcher:\n1 2 3 4 5 6 7 8 9 10 class MyDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return super.onCreateDialog(savedInstanceState).also { dialog -\u0026gt; val componentDialog = dialog as ComponentDialog componentDialog.onBackPressedDispatcher.addCallback(componentDialog) { // Custom back press handling } } } } See Android Developer: ComponentDialog and OnBackPressedDispatcher for details.\nchmod Permission Reference Linux file permissions in octal notation:\nPermission Value Description --- 0 No permissions --x 1 Execute only -w- 2 Write only -wx 3 Write + execute r-- 4 Read only r-x 5 Read + execute rw- 6 Read + write rwx 7 Read + write + execute The three digits represent: Owner / Group / Others.\nCommon Permission Combinations 1 2 3 4 5 6 sudo chmod 600 file # Owner read+write only (private key, config) sudo chmod 644 file # Owner rw, group+others r (standard file permission) sudo chmod 700 file # Owner full access (script, private key) sudo chmod 755 file # Owner full, others r+x (executable, directory) sudo chmod 666 file # Everyone read+write (rare) sudo chmod 777 file # Everyone full access (risky, avoid) Security advice: Avoid 777. Use 600 for config files, 755 for executables, and 644 for regular files.\nThe Underscore Problem in SSL Hostnames Some domains used in our project contained underscores (_), which caused javax.net.ssl.SSLHandshakeException during HTTPS connections — hostnames simply don\u0026rsquo;t allow underscores.\nSpecification Per RFC 952 and RFC 1123, each hostname label may only contain ASCII letters, digits, and hyphens (-). Underscores are not permitted. Affected record types include A, AAAA, MX, and CNAME.\nUnderscores aren\u0026rsquo;t completely banned in DNS — RFC 2181 Section 11 allows arbitrary binary content in DNS labels — but hostnames have stricter rules. RFC 2782 deliberately introduced underscore prefixes in SRV records (e.g., _sip._tcp.example.com) to avoid conflicts with hostnames.\nSummary Hostnames (URLs, SSL certificates): no underscores allowed SRV records and service discovery: underscores required as prefix Fix: replace _ with - or other compliant characters in domain names Discussion at StackOverflow: The use of the underscore in host names.\nReferences Android Developer: ComponentDialog Android Developer: OnBackPressedDispatcher RFC 952 - Hostname Specifications RFC 1123 - Hostname Requirements RFC 2181 Section 11 - DNS Name Syntax RFC 2782 - SRV Record Specification StackOverflow: The use of the underscore in host names Wikipedia: Hostname - Restrictions ","permalink":"https://blog.substitute.tech/en/posts/%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86%E4%B8%89/","summary":"\u003cp\u003eThree quick tips: handling the back button in DialogFragment, chmod permission reference, and the underscore problem in SSL hostnames.\u003c/p\u003e\n\u003ch2 id=\"dialogfragment-back-button-handling\"\u003eDialogFragment Back Button Handling\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eDialogFragment\u003c/code\u003e has no direct override for the back button. Two approaches were commonly used.\u003c/p\u003e\n\u003ch3 id=\"approach-1-override-in-oncreatedialog\"\u003eApproach 1: Override in onCreateDialog\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e9\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eDialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eonCreateDialog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eBundle\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003esavedInstanceState\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eDialog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003egetActivity\u003c/span\u003e\u003cspan class=\"p\"\u003e(),\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003egetTheme\u003c/span\u003e\u003cspan class=\"p\"\u003e()){\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eonBackPressed\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e            \u003c/span\u003e\u003cspan class=\"c1\"\u003e// Handle back press here\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"approach-2-via-onkeylistener\"\u003eApproach 2: Via onKeyListener\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eonResume\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003esuper\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eonResume\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eDialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003egetDialog\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e!=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esetOnKeyListener\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003ethis\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003eonPause\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003esuper\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eonPause\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eDialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003egetDialog\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e!=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003edialog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003esetOnKeyListener\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003enull\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"modern-approach-onbackpresseddispatcher-androidx\"\u003eModern Approach: OnBackPressedDispatcher (AndroidX)\u003c/h3\u003e\n\u003cp\u003eFor newer versions, use AndroidX\u0026rsquo;s \u003ccode\u003eOnBackPressedDispatcher\u003c/code\u003e. Since Fragment 1.6.1, \u003ccode\u003eDialogFragment\u003c/code\u003e returns a \u003ccode\u003eComponentDialog\u003c/code\u003e by default, which has its own \u003ccode\u003eOnBackPressedDispatcher\u003c/code\u003e:\u003c/p\u003e","title":"Android \u0026 Linux Tips Collection #3"},{"content":"When building a photo album feature similar to WeChat, I needed to read photos and videos with multi-folder switching — and it had to be faster than WeChat. After some research, the MediaStore approach proved most suitable. Since I hadn\u0026rsquo;t used it much before, this post serves as a record.\nThe GROUP BY Hack in ContentResolver ContentResolver.query() does not expose a groupBy parameter (unlike SQLiteQueryBuilder.query()), but you can achieve a similar effect by embedding GROUP BY directly into the selection argument.\nThe trick relies on the fact that ContentResolver wraps the selection in parentheses during SQL compilation, producing WHERE ( ... ). By closing the parenthesis early in the selection string, you can append a GROUP BY clause.\n1 2 3 4 5 6 // Normal — selection becomes WHERE (mime_type IS NOT NULL) MediaStore.Images.ImageColumns.MIME_TYPE + \u0026#34; IS NOT NULL \u0026#34; // Hack — close the parenthesis and inject GROUP BY MediaStore.Images.ImageColumns.MIME_TYPE + \u0026#34; IS NOT NULL \u0026#34; + \u0026#34;) GROUP BY (\u0026#34; + MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME; The resulting SQL looks like:\n1 WHERE (1=1) AND (mime_type IS NOT NULL) GROUP BY (bucket_display_name) ORDER BY ... Warning: This approach may break on Android 10 (API 29) and above. The system MediaProvider injects additional conditions such as is_pending=0, is_trashed=0, and volume_name IN (...), which can incorrectly place the GROUP BY inside the WHERE clause. As of Android 14+, this hack is confirmed broken.\nModern Approach: Android 11+ Bundle Parameters Starting with Android 11 (API 30), ContentResolver.query() supports structured query parameters via Bundle, eliminating the need for the hack above.\n1 2 3 4 5 6 7 8 9 10 11 12 Bundle queryArgs = new Bundle(); queryArgs.putStringArray( ContentResolver.QUERY_ARG_GROUP_COLUMNS, new String[]{MediaStore.Images.Media.BUCKET_DISPLAY_NAME} ); Cursor cursor = contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, queryArgs, null ); Prefer QUERY_ARG_GROUP_COLUMNS (structured) over QUERY_ARG_SQL_GROUP_BY (raw SQL) for forward compatibility. The Provider indicates which arguments were honored via the Cursor\u0026rsquo;s EXTRA_HONORED_ARGS.\nWorkaround for Android 10 If you still need to support API 29, consider these alternatives:\nUse ContentResolver.query(uri, projection, selection, selectionArgs, sortOrder) with a subquery inline in the selection Client-side grouping — fetch all results and group in memory Custom ContentProvider — take full control of the SQL query logic Caveats The traditional selection hack behaves inconsistently across OEMs and Android versions — test thoroughly. If your minSdkVersion is 30+, prioritize the Bundle approach. When implementing a custom ContentProvider, use SQLiteQueryBuilder.query() directly — it natively supports groupBy and having parameters. References Android Developers: Access media files from shared storage Android Developers: ContentResolver StackOverflow: How to query from MEDIA provider with \u0026ldquo;group by\u0026rdquo; option? Android Source: SQLiteQueryBuilder.java Android Source: MediaProvider ","permalink":"https://blog.substitute.tech/en/posts/android%E7%9A%84mediastore/","summary":"\u003cp\u003eWhen building a photo album feature similar to WeChat, I needed to read photos and videos with multi-folder switching — and it had to be faster than WeChat. After some research, the \u003ccode\u003eMediaStore\u003c/code\u003e approach proved most suitable. Since I hadn\u0026rsquo;t used it much before, this post serves as a record.\u003c/p\u003e\n\u003ch2 id=\"the-group-by-hack-in-contentresolver\"\u003eThe GROUP BY Hack in ContentResolver\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eContentResolver.query()\u003c/code\u003e does not expose a \u003ccode\u003egroupBy\u003c/code\u003e parameter (unlike \u003ccode\u003eSQLiteQueryBuilder.query()\u003c/code\u003e), but you can achieve a similar effect by embedding \u003ccode\u003eGROUP BY\u003c/code\u003e directly into the \u003ccode\u003eselection\u003c/code\u003e argument.\u003c/p\u003e","title":"Android MediaStore: GROUP BY via ContentResolver"},{"content":"Before working with HTTPS, I recommend reading Android Training: Security with SSL. Many companies have adopted full-site HTTPS, but not all implementations are correct. This post records some issues I encountered.\nPrerequisite knowledge:\nSymmetric encryption Asymmetric encryption Certificate Formats Extension Description .DER Binary certificate, usually uses .cer or .crt extension .PEM Base64-encoded X.509v3 certificate, starts with ---BEGIN... .CRT / .CER Essentially the same; .CRT is more aligned with Microsoft standards .key Public/private key file processed with PKCS #8 Generating Self-Signed Certificates with OpenSSL Generate Private Key 1 2 3 4 5 $ openssl genrsa -out key.pem 1024 Generating RSA private key, 1024 bit long modulus ....................++++++ .....................++++++ e is 65537 (0x10001) Create Certificate Signing Request (CSR) 1 2 3 4 5 6 7 8 9 10 $ openssl req -new -key key.pem -out request.pem You are about to be asked to enter information that will be incorporated into your certificate request. ----- Country Name (2 letter code) [AU]:CN State or Province Name (full name) [Some-State]:Beijing Locality Name (eg, city) []:Beijing Organization Name (eg, company) [Internet Widgits Pty Ltd]:Hxq Common Name (e.g. server FQDN or YOUR name) []:hao Email Address []:haoxiqiang@live.com The Common Name must match the server hostname, as required by the SSL-RFC specification.\nGenerate Self-Signed Certificate 1 $ openssl x509 -req -days 30 -in request.pem -signkey key.pem -out certificate.pem Fetch Certificate from an Existing Server 1 echo | openssl s_client -connect hostname:443 2\u0026gt;\u0026amp;1 | sed -ne \u0026#39;/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p\u0026#39; \u0026gt; hostname.pem Using Certificates in Android Android typically only recognizes BKS (Bouncy Castle KeyStore) certificates, so conversion is required.\nConvert PEM to BKS 1 2 3 4 5 6 7 8 keytool -importcert -v \\ -trustcacerts \\ -alias 0 \\ -file \u0026lt;(openssl x509 -in hostname.pem) \\ -keystore $CERTSTORE -storetype BKS \\ -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \\ -providerpath bcprov-jdk16-1.46.jar \\ -storepass password Related tools:\nBouncy Castle \u0026ndash; Java crypto library providing BKS support Portecle \u0026ndash; GUI certificate management tool Loading a BKS Certificate in Code 1 2 3 4 5 6 7 InputStream inputStream = context.getResources().openRawResource(res); KeyStore keyStore = KeyStore.getInstance(\u0026#34;BKS\u0026#34;); keyStore.load(inputStream, password.toCharArray()); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); SSLContext sslContext = SSLContext.getInstance(\u0026#34;TLS\u0026#34;); sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); If you prefer not to use resource files, you can also load text-form certificates via String -\u0026gt; InputStream.\nCertificate Format Conversion 1 2 3 4 5 6 7 8 9 10 11 12 # PEM to DER $ openssl x509 -outform der -in certificate.pem -out certificate.der # PEM to P7B $ openssl crl2pkcs7 -nocrl -certfile certificate.cer -out certificate.p7b -certfile CAcert.cer # PEM to PFX $ openssl pkcs12 -export -out certificate.pfx -inkey privateKey.key -in certificate.crt -certfile CAcert.crt # DER to PEM $ openssl x509 -inform der -in certificate.cer -out certificate.pem # P7B to PEM $ openssl pkcs7 -print_certs -in certificate.p7b -out certificate.cer # PFX to PEM $ openssl pkcs12 -in certificate.pfx -out certificate.cer -nodes Viewing Certificate Information 1 2 3 openssl x509 -in cert.pem -text -noout openssl x509 -in cert.cer -text -noout openssl x509 -in cert.crt -text -noout Common Issues SSLPeerUnverifiedException: Hostname not verified This exception occurs when the Common Name in a self-signed certificate doesn\u0026rsquo;t match the server domain. You can work around it with a custom HostnameVerifier, but never set verify() to always return true:\n1 2 3 4 5 6 7 8 HostnameVerifier hostnameVerifier = new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier(); return hv.verify(\u0026#34;example.com\u0026#34;, session); } }; Reference: Hostname Not Verified Issue\nReferences Security with HTTPS and SSL | Android Developers HttpsURLConnection | Android Developers Bouncy Castle Portecle | SourceForge OpenSSL genrsa Documentation OpenSSL req Documentation OpenSSL x509 Documentation ","permalink":"https://blog.substitute.tech/en/posts/https/","summary":"\u003cp\u003eBefore working with HTTPS, I recommend reading \u003ca href=\"https://developer.android.com/training/articles/security-ssl.html\"\u003eAndroid Training: Security with SSL\u003c/a\u003e. Many companies have adopted full-site HTTPS, but not all implementations are correct. This post records some issues I encountered.\u003c/p\u003e","title":"HTTPS Notes"},{"content":"Second batch of collected issues, mainly covering WebView memory management, cookie synchronization, and other development details.\nWebView Memory Leaks Avoid declaring WebView directly in XML layout, as the WebView may hold a reference to the Activity\u0026rsquo;s Context even after the Activity is destroyed, preventing memory release. Correct approaches:\nUse ApplicationContext 1 WebView webView = new WebView(getApplicationContext()); Proper Lifecycle Handling in Fragments 1 2 3 4 5 6 @Override public void onDetach() { super.onDetach(); webView.removeAllViews(); webView.destroy(); } Process Management via the android:process Attribute You can assign different components to separate processes in the manifest:\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;application android:process=\u0026#34;com.processkill.p1\u0026#34;\u0026gt; \u0026lt;activity android:name=\u0026#34;com.processkill.A\u0026#34; android:process=\u0026#34;com.processkill.p2\u0026#34;\u0026gt; \u0026lt;/activity\u0026gt; \u0026lt;activity android:name=\u0026#34;com.processkill.B\u0026#34; android:process=\u0026#34;com.processkill.p3\u0026#34;\u0026gt; \u0026lt;/activity\u0026gt; \u0026lt;/application\u0026gt; Avoiding Memory Leaks from Static Drawables Romain Guy wrote a classic article: Avoid Memory Leaks on Android. While somewhat dated now \u0026ndash; Drawable.setCallback() has used WeakReference since Android 4.0.1 \u0026ndash; holding Drawables via static fields remains a bad practice.\nThe Android framework itself clears the previous drawable reference when setting a new background:\n1 2 3 4 5 6 7 8 /* * Regardless of whether we\u0026#39;re setting a new background or not, we want * to clear the previous drawable. */ if (mBackground != null) { mBackground.setCallback(null); unscheduleDrawable(mBackground); } WebView URL Parameter Encoding When appending parameters to a URL, the values must be URL-encoded. The old NameValuePair approach has been deprecated since API 23. You can implement it yourself:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 JSONObject json = new JSONObject(); Iterator\u0026lt;String\u0026gt; keys = json.keys(); StringBuilder stringBuilder = new StringBuilder(); try { while (keys.hasNext()) { String key = keys.next(); String value = json.optString(key); if (value != null) { stringBuilder.append(URLEncoder.encode(key, \u0026#34;UTF-8\u0026#34;)) .append(\u0026#34;=\u0026#34;) .append(URLEncoder.encode(value, \u0026#34;UTF-8\u0026#34;)); } } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } The underlying logic is the same as NameValuePair \u0026ndash; both do two toString() calls internally.\nWebView Cookie Synchronization When using OkHttpClient, you need to manually sync cookies to WebView\u0026rsquo;s CookieManager. I spent some time debugging with packet capture before realizing the issue.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Uri uri = API.getUri(); HttpUrl httpUrl = new HttpUrl.Builder() .scheme(uri.getScheme()) .host(uri.getHost()) .build(); OkHttpClient okHttpClient = ClientManager.getInstance(); CookieJar cookieJar = okHttpClient.cookieJar(); List\u0026lt;Cookie\u0026gt; cookies = cookieJar.loadForRequest(httpUrl); for (Cookie cookie : cookies) { if (cookie != null) { String cookieString = cookie.name() + \u0026#34;=\u0026#34; + cookie.value() + \u0026#34;; domain=\u0026#34; + cookie.domain(); cookieManager.setCookie(httpUrl.toString(), cookieString); } } The key insight: CookieManager.setCookie() takes a full URL as its first parameter, not just the host:\n1 2 3 4 5 6 7 8 9 10 /** * Sets a cookie for the given URL. Any existing cookie with the same host, * path and name will be replaced with the new cookie. The cookie being set * will be ignored if it is expired. * * @param url the URL for which the cookie is to be set * @param value the cookie as a string, using the format of the \u0026#39;Set-Cookie\u0026#39; * HTTP response header */ public abstract void setCookie(String url, String value); References CookieManager | Android Developers Avoid Memory Leaks on Android - Romain Guy Android WebView Memory Leak - Chromium Code Review ","permalink":"https://blog.substitute.tech/en/posts/%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86%E4%BA%8C/","summary":"\u003cp\u003eSecond batch of collected issues, mainly covering WebView memory management, cookie synchronization, and other development details.\u003c/p\u003e","title":"Android Issues Roundup (Part 2)"},{"content":"I\u0026rsquo;ve been working on a Zhihu Daily client to evaluate RxJava\u0026rsquo;s benefits and trade-offs in a real project. RxJava has gained significant traction abroad, with language bindings spanning Java, JavaScript, C#, Scala, Clojure, C++, Python, Ruby, Kotlin, Swift, and more.\nReactive Extensions (Rx) is an event-based asynchronous programming framework built on the Observer pattern. It abstracts common concerns like thread scheduling, synchronization, thread safety, concurrent data structures, and non-blocking I/O.\nFor a great introduction, read RxJava for Android Developers (in Chinese) by Zhu Kai.\nSchedulers RxJava provides several Schedulers to control execution threads:\nScheduler Use Case Notes Schedulers.immediate() Current thread Default \u0026ndash; runs inline Schedulers.newThread() Dedicated thread Creates a new thread per subscription, no reuse Schedulers.io() I/O operations File/db/network. Cached thread pool with no cap. Reuses idle threads. Not for CPU-bound work Schedulers.computation() CPU-bound work Fixed pool sized to core count. Not for I/O AndroidSchedulers.mainThread() Android UI thread For UI updates Use subscribeOn() and observeOn() to control threading:\nsubscribeOn(): Specifies the thread for the source Observable (where OnSubscribe fires). Only the first call in the chain takes effect. observeOn(): Specifies the thread for downstream operators (where Subscriber receives events). Can be called multiple times, each switching threads for subsequent operations. Creating Observables Operator Description Create Create Observable by calling observer methods programmatically Defer Defer creation until subscription; fresh Observable per observer Empty / Never / Throw Precise, limited-behavior Observables From Convert arrays or Iterables into Observable Interval Emit sequential integers at timed intervals Just Convert object(s) into an emitting Observable Range Emit a range of sequential integers Repeat Emit items repeatedly Start Emit return value of a function Timer Emit single item after a delay Transforming Observables Operator Description Buffer Gather items into bundles periodically, then emit the bundle FlatMap Transform each emission into an Observable, merge all results GroupBy Divide Observable into groups by key Map Apply function to each emission Scan Apply function sequentially, emit each successive intermediate value Window Like Buffer, but emits Observables instead of collections Currently building a real app with RxAndroid. While the basics are straightforward, real-world usage has nuances worth exploring. More to follow.\nReferences RxJava GitHub Repository ReactiveX Official Documentation RxJava 3.x Javadoc ReactiveX Scheduler Documentation RxAndroid GitHub Repository RxJava Tutorial for Android Developers (in Chinese) ","permalink":"https://blog.substitute.tech/en/posts/rxjava/","summary":"\u003cp\u003eI\u0026rsquo;ve been working on a Zhihu Daily client to evaluate RxJava\u0026rsquo;s benefits and trade-offs in a real project. RxJava has gained significant traction abroad, with language bindings spanning Java, JavaScript, C#, Scala, Clojure, C++, Python, Ruby, Kotlin, Swift, and more.\u003c/p\u003e","title":"Building an App with RxJava"},{"content":"After reading some issue roundups on Xiaoke\u0026rsquo;s Blog, I realized I\u0026rsquo;ve encountered many of the same problems. Here\u0026rsquo;s a collected summary.\nFragment State Restoration When a FragmentActivity is recreated, the system restores all Fragment lists from the FragmentManager and re-adds them to the activity. However, the Fragment instances are restored without their internal state \u0026ndash; you must manually handle state restoration in onCreate or onRestoreInstanceState. The key difference is that onRestoreInstanceState is called after onStart, so choose based on timing needs.\nFragmentActivity State Restoration Source 1 2 3 4 5 6 7 // Inside onCreate() if (savedInstanceState != null) { Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); // FragmentManager.restoreAllState re-adds previously saved // Fragments to the FragmentManager and restores the BackStack mFragments.restoreAllState(p, nc != null ? nc.fragments : null); } FragmentActivity State Saving Source 1 2 3 4 5 6 7 8 @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Parcelable p = mFragments.saveAllState(); if (p != null) { outState.putParcelable(FRAGMENTS_TAG, p); } } Google recommends creating and initializing Fragments in onCreate only when savedInstanceState is null:\n1 2 3 4 5 6 7 8 if (savedInstanceState == null) { DetailsFragment details = new DetailsFragment(); details.setArguments(getIntent().getExtras()); getFragmentManager() .beginTransaction() .add(android.R.id.content, details) .commit(); } Related discussion: Failure Delivering Result onActivityResult\nBackground and Selector Require Real Drawables On certain Samsung and Motorola devices, using pseudo-drawables defined in colors.xml results in a solid black background. You must use actual image drawables or defined shapes instead:\n1 2 \u0026lt;color name=\u0026#34;mail_published_time_color\u0026#34;\u0026gt;#bcbcbc\u0026lt;/color\u0026gt; \u0026lt;drawable name=\u0026#34;ab_bg_black\u0026#34;\u0026gt;#aa191919\u0026lt;/drawable\u0026gt; Showing a Dialog in onActivityResult When calling other apps via startActivityForResult, you may need to process results asynchronously and show a dialog. However, calling Dialog.show() directly inside onActivityResult throws an error \u0026ndash; onActivityResult is called before onResume, and at that point the FragmentManager believes its state is already saved and refuses to commit transactions.\nSimilarly, you cannot commit FragmentTransaction operations after onSaveInstanceState has been called. Both issues affect DialogFragment usage.\nTwo correct approaches:\nOption 1: Show the dialog in onPostResume. Option 2: Set a flag (e.g., mPendingShowDialog = true) in onActivityResult, then check it in onResume. This applies not just to Dialogs but to any Fragment transaction or commit. If you must commit after onSaveInstanceState, use commitAllowingStateLoss. ListView Empty Area Display Issue When a ListView is set to MATCH_PARENT but has too few items to fill the space, the ListView shrinks to fit its content, leaving the empty area with the default background color. Fix this by setting the overscroll footer to transparent in your theme:\n1 \u0026lt;item name=\u0026#34;android:overScrollFooter\u0026#34;\u0026gt;@android:color/transparent\u0026lt;/item\u0026gt; Or directly in the layout:\n1 android:overScrollFooter=\u0026#34;@android:color/transparent\u0026#34; Reference: Background Color ListView - Stack Overflow\nDrawable Shape Solid Fill Issue When \u0026lt;solid\u0026gt; has no color set, the theoretical default is transparent. However, on some Samsung devices it renders as black. Always set it explicitly:\n1 2 3 4 5 6 7 8 \u0026lt;shape xmlns:android=\u0026#34;http://schemas.android.com/apk/res/android\u0026#34; android:shape=\u0026#34;rectangle\u0026#34;\u0026gt; \u0026lt;solid android:color=\u0026#34;@android:color/transparent\u0026#34;/\u0026gt; \u0026lt;stroke android:width=\u0026#34;1dp\u0026#34; android:color=\u0026#34;@color/soft_white\u0026#34;/\u0026gt; \u0026lt;corners android:radius=\u0026#34;1dp\u0026#34;/\u0026gt; \u0026lt;/shape\u0026gt; WebView Word Wrap Issue Starting from Android 4.4 (API 19), WebView does not automatically break long lines. You can try setting the layout algorithm in code:\n1 settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING); TEXT_AUTOSIZING heuristically boosts paragraph font sizes to improve readability in overview mode on wide-viewport layouts. However, many web pages still require handling at the HTML level:\n1 \u0026lt;pre style=\u0026#34;word-wrap: break-word; white-space: pre-wrap;\u0026#34;\u0026gt; Using the Dialer\u0026rsquo;s SecretCode Feature Android\u0026rsquo;s dialer supports special Secret Codes that launch custom Intents. When a user enters *#*#CODE#*#*, the system broadcasts android.provider.Telephony.SECRET_CODE or (API 29+) android.telephony.action.SECRET_CODE.\n1 2 3 4 5 6 \u0026lt;receiver android:name=\u0026#34;.receiver.DiagnoserReceiver\u0026#34;\u0026gt; \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.provider.Telephony.SECRET_CODE\u0026#34;/\u0026gt; \u0026lt;data android:scheme=\u0026#34;android_secret_code\u0026#34; android:host=\u0026#34;111222\u0026#34;/\u0026gt; \u0026lt;/intent-filter\u0026gt; \u0026lt;/receiver\u0026gt; 1 2 3 4 5 6 7 8 9 10 public class DiagnoserReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { if (\u0026#34;android.provider.Telephony.SECRET_CODE\u0026#34;.equals(intent.getAction())) { Intent i = new Intent(Intent.ACTION_MAIN); i.setClass(context, Diagnoser.class); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(i); } } } More details: Create a secret doorway to your app.\nImporting an Existing Git Repo Into Another The recommended approach is Git Subtree Merge:\n1 2 3 4 5 6 7 git remote add rack_remote git@github.com:schacon/rack.git git fetch rack_remote git merge -s ours --no-commit rack_remote/master git read-tree --prefix=rack/ -u rack_remote/master git commit -m \u0026#34;Imported rack as a subtree.\u0026#34; # Track upstream updates later: git pull -s subtree rack_remote master Git automatically recognizes the subtree root directory, so no need to specify the prefix in subsequent merges.\nReference: About Git Subtree Merges\nCardView Ripple Effect (Android Lollipop) The AppCompat support library omits the ripple effect. To see ripples, use Android 5.0+ and test on a compatible device. The official AppCompat v7 explanation:\n\u0026ldquo;Why are there no ripples on pre-Lollipop? A lot of what allows RippleDrawable to run smoothly is Android 5.0\u0026rsquo;s new RenderThread. To optimize for performance on previous versions of Android, we\u0026rsquo;ve left RippleDrawable out for now.\u0026rdquo;\nUse android:foreground (not background) pointing to selectableItemBackground, to avoid conflicts with CardView\u0026rsquo;s shadow and corner radius rendering.\n1 2 3 4 5 6 7 8 \u0026lt;android.support.v7.widget.CardView android:layout_width=\u0026#34;match_parent\u0026#34; android:layout_height=\u0026#34;wrap_content\u0026#34; android:layout_gravity=\u0026#34;center\u0026#34; android:foreground=\u0026#34;?android:attr/selectableItemBackground\u0026#34; android:clickable=\u0026#34;true\u0026#34;\u0026gt; ... \u0026lt;/android.support.v7.widget.CardView\u0026gt; Mac OS X Cursor Movement and Text Editing Shortcuts Reference: Apple official keyboard shortcut documentation.\nCursor movement shortcuts:\nControl-F: Forward one character Control-B: Backward one character Control-P: Previous line (up) Control-N: Next line (down) Control-A: Move to beginning of line (Ahead) Control-E: Move to end of line (End) Text editing shortcuts:\nControl-H: Delete character before cursor Control-D: Delete character after cursor Control-K: Delete from cursor to end of line Control-Shift-A: Select from cursor to beginning of line Control-Shift-E: Select from cursor to end of line Switching Git Versions Xcode bundles its own Git. If you have installed another version and want to switch to the Homebrew-managed one, use symlinks:\n1 2 3 cd /Applications/Xcode.app/Contents/Developer/usr/bin sudo mv ./git ./git-xcode-usr-bin sudo ln -s /usr/local/bin/git ./git References Saving State with Fragments | Android Developers Failure Delivering Result - Stack Overflow WebSettings.LayoutAlgorithm | Android Developers Background Color ListView - Stack Overflow Android Shape Solid Default Color - Stack Overflow Create a Secret Doorway to Your App About Git Subtree Merges | GitHub RippleDrawable | Android Developers Mac Keyboard Shortcuts | Apple Support TelephonyManager.ActionSecretCode | Android Developers ","permalink":"https://blog.substitute.tech/en/posts/%E9%97%AE%E9%A2%98%E6%95%B4%E7%90%86/","summary":"\u003cp\u003eAfter reading some issue roundups on Xiaoke\u0026rsquo;s Blog, I realized I\u0026rsquo;ve encountered many of the same problems. Here\u0026rsquo;s a collected summary.\u003c/p\u003e","title":"Android Development Issues and Solutions"},{"content":"While building a project, I discovered the shrinkResources attribute, which removes unused resources from the APK. Here is how to configure it and what to watch out for.\nBasic Configuration shrinkResources requires minifyEnabled to be true, because resource shrinking runs after ProGuard/R8 removes unused code:\n1 2 3 4 5 6 7 8 9 10 android { ... buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile(\u0026#39;proguard-android.txt\u0026#39;), \u0026#39;proguard-rules.pro\u0026#39; } } } Inspecting Results Use --info logging to see overall reduction:\n1 2 3 4 5 6 ./gradlew clean assembleRelease --info \u0026gt; build-output.txt ... :android:shrinkDebugResources Removed unused resources: Binary resource data reduced from 2570KB to 1711KB: Removed 33% ... View which specific files were skipped:\n1 2 3 4 $ ./gradlew clean assembleDebug --info | grep \u0026#34;Skipped unused resource\u0026#34; Skipped unused resource res/anim/abc_fade_in.xml: 396 bytes Skipped unused resource res/anim/abc_fade_out.xml: 396 bytes Skipped unused resource res/anim/abc_slide_in_bottom.xml: 400 bytes Keeping or Discarding Specific Resources Resources referenced via reflection (common in third-party SDKs) can be mistakenly removed. Modern versions support keep.xml for fine-grained control.\nStrict Mode In res/raw/keep.xml:\n1 2 3 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;resources xmlns:tools=\u0026#34;http://schemas.android.com/tools\u0026#34; tools:shrinkMode=\u0026#34;strict\u0026#34; /\u0026gt; Keeping Specific Resources 1 2 3 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;resources xmlns:tools=\u0026#34;http://schemas.android.com/tools\u0026#34; tools:keep=\u0026#34;@layout/umeng_*, @drawable/umeng_*, @anim/umeng_*\u0026#34; /\u0026gt; Forcing Removal 1 2 3 4 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; \u0026lt;resources xmlns:tools=\u0026#34;http://schemas.android.com/tools\u0026#34; tools:shrinkMode=\u0026#34;safe\u0026#34; tools:discard=\u0026#34;@layout/unused2\u0026#34; /\u0026gt; The build system also skips resources referenced via URLs like file:///android_res/drawable/ic_plus.png.\nLanguage and Density Configuration For apps that do not need multi-language support, use resConfigs to keep only specific languages or densities:\n1 2 3 4 5 6 android { defaultConfig { ... resConfigs \u0026#34;en\u0026#34;, \u0026#34;zh\u0026#34; } } You can also restrict to specific densities like \u0026quot;nodpi\u0026quot;, \u0026quot;hdpi\u0026quot;.\nSummary Enable shrinkResources even if your app is relatively clean. The log output helps you understand which resources are in use and which might be incorrectly removed.\nReferences Customize which resources to keep Enable app optimization ","permalink":"https://blog.substitute.tech/en/posts/resourceshrinking%E8%B5%84%E6%BA%90%E6%B8%85%E7%90%86/","summary":"\u003cp\u003eWhile building a project, I discovered the \u003ccode\u003eshrinkResources\u003c/code\u003e attribute, which removes unused resources from the APK. Here is how to configure it and what to watch out for.\u003c/p\u003e","title":"Android Resource Shrinking"},{"content":"I had long wanted to write a tree view demo. Previously, I used TreeViewList (a ListView wrapper) or ExpandableListView for multi-level menus. Eventually I realized there is no need for custom views \u0026ndash; just use RecyclerView and manage the data flattening yourself.\nThe core idea: recursively flatten a nested data structure into a linear list, then use notifyItemRangeInserted / notifyItemRangeRemoved to handle expand and collapse.\nKey Techniques 1. Unified Data Format Convert all data sources into a tree structure with children for easy recursion:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 { \u0026#34;tree\u0026#34;: { \u0026#34;children\u0026#34;: [ { \u0026#34;available\u0026#34;: true, \u0026#34;children\u0026#34;: [], \u0026#34;id\u0026#34;: \u0026#34;548005da36ec3532c4a18391\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;First Round Review\u0026#34; } ], \u0026#34;id\u0026#34;: \u0026#34;gaozhongshuxue\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;High School Math\u0026#34; } } 2. Recursion-Friendly Entity The entity class must be able to store all its child nodes:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Course { @Expose @SerializedName(\u0026#34;id\u0026#34;) public String id; @Expose @SerializedName(\u0026#34;name\u0026#34;) public String name; public int level; public boolean open; public String parentId; public LinkedList\u0026lt;Course\u0026gt; children = new LinkedList\u0026lt;\u0026gt;(); public boolean hasChild() { return children != null \u0026amp;\u0026amp; children.size() \u0026gt; 0; } public void addChildren(LinkedList\u0026lt;Course\u0026gt; children) { this.children.clear(); this.children.addAll(children); } } 3. Recursive Data Construction 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private void createTree(Course container, JSONArray children, String parentId, int level) throws JSONException { if (children != null) { int size = children.length(); LinkedList\u0026lt;Course\u0026gt; tree = new LinkedList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; size; i++) { JSONObject item = children.getJSONObject(i); Course course = new Course(); course.id = item.getString(\u0026#34;id\u0026#34;); course.name = item.getString(\u0026#34;name\u0026#34;); course.level = level; course.open = false; course.parentId = parentId; JSONArray subChildren = item.getJSONArray(\u0026#34;children\u0026#34;); createTree(course, subChildren, course.id, level + 1); tree.add(course); if (container == null) { mData.add(course); } } if (container != null) { container.addChildren(tree); } } } 4. Expand/Collapse on Click When an item is clicked, check if it has children, then expand or collapse. The code uses notifyItemRangeInserted / notifyItemRangeRemoved for animation.\nWhy these methods over notifyDataSetChanged: the range-specific notifications preserve RecyclerView\u0026rsquo;s default animations, giving users a smooth visual transition during expand/collapse. notifyDataSetChanged would refresh the entire list, losing animations and incurring higher performance cost.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public boolean dispatchClick(LinkedList\u0026lt;Course\u0026gt; container, Course course) { if (container == null || course == null) { return false; } if (course.hasChild()) { int insertPosition = container.indexOf(course) + 1; if (course.open) { size = 0; removeAllChildren(container, course); notifyItemRangeRemoved(insertPosition, size); } else { course.open = true; container.addAll(insertPosition, course.children); notifyItemRangeInserted(insertPosition, course.children.size()); } return true; } return false; } private void removeAllChildren(LinkedList\u0026lt;Course\u0026gt; container, Course course) { course.open = false; int childrenSize = course.children.size(); for (Course child : course.children) { if (child.hasChild() \u0026amp;\u0026amp; child.open) { child.open = false; removeAllChildren(container, child); } } size += childrenSize; container.removeAll(course.children); } 5. Custom Animation (Optional) Default RecyclerView animations may not show the expand effect clearly. Extend SimpleItemAnimator for a custom look:\n1 2 3 4 5 6 7 8 9 @Override public boolean animateAdd(final RecyclerView.ViewHolder holder) { resetAnimation(holder); ViewCompat.setTranslationY(holder.itemView, -(holder.itemView.getMeasuredHeight() / 2)); ViewCompat.setAlpha(holder.itemView, 0); mPendingAdditions.add(holder); return true; } Source Code Full example: GitHub: Haoxiqiang/TreeView\nReferences RecyclerView SimpleItemAnimator ","permalink":"https://blog.substitute.tech/en/posts/treeview%E6%A0%91%E5%BD%A2%E8%8F%9C%E5%8D%95/","summary":"\u003cp\u003eI had long wanted to write a tree view demo. Previously, I used \u003ccode\u003eTreeViewList\u003c/code\u003e (a \u003ccode\u003eListView\u003c/code\u003e wrapper) or \u003ccode\u003eExpandableListView\u003c/code\u003e for multi-level menus. Eventually I realized there is no need for custom views \u0026ndash; just use \u003ccode\u003eRecyclerView\u003c/code\u003e and manage the data flattening yourself.\u003c/p\u003e\n\u003cp\u003eThe core idea: recursively flatten a nested data structure into a linear list, then use \u003ccode\u003enotifyItemRangeInserted\u003c/code\u003e / \u003ccode\u003enotifyItemRangeRemoved\u003c/code\u003e to handle expand and collapse.\u003c/p\u003e","title":"Tree View Menu with RecyclerView"},{"content":"A colleague asked me today what this text meant:\nLorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid veniam harum quas similique quis, quisquam tempore iste. Alias soluta dolorum ratione, dolorem quia corrupti, suscipit eveniet dolor dignissimos voluptas laborum debitis ea fugiat aliquam rem ullam minus a aliquid pariatur!\nAt first glance it looked like English, but I could not understand it. After some searching, it turned out to be Latin. Why would a programmer write something like this?\nWhat Is Lorem Ipsum? Lorem ipsum has been used in Western printing and design since the 15th century. With the rise of digital typesetting, this centuries-old placeholder text became popular again. Since it starts with \u0026ldquo;Lorem ipsum,\u0026rdquo; it is commonly used for title testing and referred to as Lorem ipsum (or Lipsum for short).\nA common Lorem ipsum passage:\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nA Bit of History People used to think this was just meaningless text designed to let viewers focus on typography and layout. However, Latin scholar Richard McClintock discovered that Lorem ipsum originates from Cicero\u0026rsquo;s De finibus bonorum et malorum (45 BC):\nNeque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit\u0026hellip;\n(No one loves pain itself, nor seeks it, nor wants it, for it is pain\u0026hellip;)\nSome later versions replace letters with K, W, Z (which do not exist in Latin) to match the letter frequency of modern English, or add words like \u0026ldquo;zzril\u0026rdquo; and \u0026ldquo;takimata.\u0026rdquo;\nIn short: it is placeholder text for checking layouts during design. Looks fancy, but it is just filler.\nReferences Wikipedia: Lorem ipsum Wikipedia: De finibus bonorum et malorum ","permalink":"https://blog.substitute.tech/en/posts/loremipsum%E4%B9%B1%E6%95%B0%E5%81%87%E6%96%87/","summary":"\u003cp\u003eA colleague asked me today what this text meant:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eLorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid veniam harum quas similique quis, quisquam tempore iste. Alias soluta dolorum ratione, dolorem quia corrupti, suscipit eveniet dolor dignissimos voluptas laborum debitis ea fugiat aliquam rem ullam minus a aliquid pariatur!\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Lorem Ipsum — Placeholder Text"},{"content":"Basic commands are well documented elsewhere. Here are some operations I\u0026rsquo;ve found useful in real projects.\nResources Git Official Documentation Pro Git Book Codecademy Git Tutorial Delete a Remote Branch Since Git v1.7.0, the recommended syntax is:\n1 2 3 4 5 # Recommended (v1.7.0+) $ git push origin --delete \u0026lt;branchName\u0026gt; # Legacy syntax (push nothing to the target branch) $ git push origin :\u0026lt;branchName\u0026gt; See git-push docs.\nUndo the Last Commit 1 2 3 4 5 $ git commit ... # (1) Commit $ git reset --soft HEAD~1 # (2) Undo commit, keep working tree changes # \u0026lt;\u0026lt; edit files \u0026gt;\u0026gt; # (3) Make corrections $ git add .... # (4) Stage changes $ git commit -c ORIG_HEAD # (5) Recommit with original message Reference: Stack Overflow.\nNote: If you only need to fix a commit message, use git commit --amend instead. It is simpler and preserves the commit history.\nCherry-Pick a Specific Commit When working with multiple branches, it is easy to accidentally modify the wrong branch. Instead of manually reapplying changes:\n1 $ git cherry-pick commit-hash References Git Official Docs - git-push Git Official Docs - git-reset Git Official Docs - git-cherry-pick Stack Overflow: How do you undo the last commit? GitHub Docs: Pushing commits to a remote repository ","permalink":"https://blog.substitute.tech/en/posts/git%E7%9A%84%E4%B8%80%E4%BA%9B%E6%93%8D%E4%BD%9C/","summary":"\u003cp\u003eBasic commands are well documented elsewhere. Here are some operations I\u0026rsquo;ve found useful in real projects.\u003c/p\u003e","title":"Some Git Operations"},{"content":"We use Tencent Bugly for crash monitoring in our project. Most crash reporting tools are similar in functionality; Bugly was chosen for its clean statistics dashboard and reliable brand. Here is a curated collection of Bugly\u0026rsquo;s technical blog posts covering common crash scenarios and analysis approaches.\nArticle Collection Full System Stack Crash — What Is It? (link may be dead) — PDF backup How FileDescriptor Leaks Cause Crashes (link may be dead) — PDF backup Common Android Native Crashes and Causes (link may be dead) — PDF backup Android Stack Traces Disfigured — The Truth Revealed (link may be dead) — PDF backup What Exactly Is ANR (link may be dead) — PDF backup Reading ANR Trace Files (link may be dead) — PDF backup UnsatisfiedLinkError Analysis (link may be dead) — PDF backup java.lang.NoSuchMethodError Analysis (link may be dead) — PDF backup References Bugly Android SDK Guide Bugly Professional Edition — Crash Docs Tencent Cloud APM Pro ","permalink":"https://blog.substitute.tech/en/posts/%E5%A6%82%E4%BD%95%E6%A0%B9%E6%8D%AE%E9%94%99%E8%AF%AF%E6%97%A5%E5%BF%97%E8%A7%A3%E5%86%B3%E9%97%AE%E9%A2%98/","summary":"\u003cp\u003eWe use Tencent Bugly for crash monitoring in our project. Most crash reporting tools are similar in functionality; Bugly was chosen for its clean statistics dashboard and reliable brand. Here is a curated collection of Bugly\u0026rsquo;s technical blog posts covering common crash scenarios and analysis approaches.\u003c/p\u003e","title":"Solving Problems from Error Logs — Bugly Blog Collection"},{"content":"While debugging memory leaks, I often need to modify field values via reflection. I came across a great Stack Overflow answer worth documenting.\nCan We Modify private static final Fields? Assuming no SecurityManager is preventing it, you can use setAccessible to bypass private, then remove the final modifier to actually modify a private static final field.\nExample 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.lang.reflect.*; public class EverythingIsTrue { static void setFinalStatic(Field field, Object newValue) throws Exception { field.setAccessible(true); Field modifiersField = Field.class.getDeclaredField(\u0026#34;modifiers\u0026#34;); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() \u0026amp; ~Modifier.FINAL); field.set(null, newValue); } public static void main(String args[]) throws Exception { setFinalStatic(Boolean.class.getField(\u0026#34;FALSE\u0026#34;), true); System.out.format(\u0026#34;Everything is %s\u0026#34;, false); // \u0026#34;Everything is true\u0026#34; } } If no SecurityException is thrown, this prints \u0026ldquo;Everything is true\u0026rdquo;.\nHow It Works The primitive boolean values true and false in main are autoboxed to Boolean.TRUE and Boolean.FALSE Reflection changes public static final Boolean.FALSE to refer to the same object as Boolean.TRUE Consequently, false autoboxes to Boolean.TRUE Everything that was \u0026ldquo;false\u0026rdquo; now reads as \u0026ldquo;true\u0026rdquo; Bitwise Manipulation 1 field.getModifiers() \u0026amp; ~Modifier.FINAL field.getModifiers() gets the modifier bitmask ~Modifier.FINAL inverts the FINAL bit The AND operation clears the FINAL bit Caveats This behavior depends on the SecurityManager configuration JLS 17.5.3 states: final fields can be changed via reflection, but this only has reasonable semantics when an object is constructed, its final fields are updated, and the object is not made visible to other threads until updates are complete If a final field is initialized to a compile-time constant, changes may not be observable because the constant is inlined at compile time The specification allows aggressive optimization of final fields, including reordering reads with modifications not happening in the constructor See also JLS 15.28 Constant Expression: this technique is unlikely to work with a private static final boolean primitive because it is inlined as a compile-time constant.\nReferences Stack Overflow: Change private static final field using Java reflection JLS 17.5.3: Subsequent Modification of Final Fields JLS 15.28: Constant Expression Stack Overflow: Using reflection to change static final File.separatorChar Stack Overflow: How to limit setAccessible to legitimate uses ","permalink":"https://blog.substitute.tech/en/posts/javareflection/","summary":"\u003cp\u003eWhile debugging memory leaks, I often need to modify field values via reflection. I came across a great Stack Overflow answer worth documenting.\u003c/p\u003e","title":"Java Reflection: Modifying Private Static Final Fields"},{"content":"While integrating Volley into a project, I encountered several common issues worth documenting. The following does not apply to Android 2.3 and below.\nAdding Request Parameters Override getParams:\n1 2 3 4 @Override protected Map\u0026lt;String, String\u0026gt; getParams() { return mParams; } Using Cookies HttpURLConnection natively supports cookie management via CookieHandler:\n1 2 CookieHandler.setDefault(new CookieManager()); CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL)); Cookies are carried in request headers and parsed from set-cookie in responses. See the HttpURLConnection documentation.\nCache.Entry NullPointerException 1 2 3 Attempt to read from field com.android.volley.Cache$Entry com.android.volley.Response.cacheEntry on a null object reference at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:126) Cause: Passing null as the second parameter in parseNetworkResponse causes a NPE when NetworkDispatcher reads the cache entry.\nFix: Always return valid cache headers.\n1 2 3 4 5 @Override protected Response\u0026lt;String\u0026gt; parseNetworkResponse(NetworkResponse response) { // ... return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response)); } statusCode NullPointerException 1 int statusCode = statusLine.getStatusCode(); // NullPointerException This can occur when constructor parameters (e.g., listener) are null. Ensure all required parameters are non-null.\nSetting Timeout 1 2 3 4 myRequest.setRetryPolicy(new DefaultRetryPolicy( MY_SOCKET_TIMEOUT_MS, DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)); References Volley Overview Volley on GitHub Custom Requests HttpURLConnection ","permalink":"https://blog.substitute.tech/en/posts/volley%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%AD%E7%9A%84%E5%87%A0%E4%B8%AA%E9%97%AE%E9%A2%98%E7%9A%84%E5%8E%9F%E5%9B%A0/","summary":"\u003cp\u003eWhile integrating Volley into a project, I encountered several common issues worth documenting. The following does not apply to Android 2.3 and below.\u003c/p\u003e","title":"Common Issues When Customizing Volley"},{"content":" This article is compiled from the official Gson User Guide. Gson\u0026rsquo;s performance is competitive with other JSON frameworks, and being Google-maintained makes it a solid choice for Java/Android JSON processing.\nGson is a Java library developed by Google for serializing Java objects to JSON and deserializing JSON strings back to Java objects.\nPerformance and Scalability Benchmark data from a desktop system (dual opteron, 8GB RAM, 64-bit Ubuntu), reproducible via PerformanceTest:\nStrings: Deserialization of 25MB+ strings works without issues Large collections: Serialization of 1.4M objects, deserialization of 87K objects Gson 1.4 increased array deserialization limits from 80KB to 11MB Basic Usage Primitives 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // Serialization Gson gson = new Gson(); gson.toJson(1); ==\u0026gt; prints 1 gson.toJson(\u0026#34;abcd\u0026#34;); ==\u0026gt; prints \u0026#34;abcd\u0026#34; gson.toJson(new Long(10)); ==\u0026gt; prints 10 int[] values = { 1 }; gson.toJson(values); ==\u0026gt; prints [1] // Deserialization int one = gson.fromJson(\u0026#34;1\u0026#34;, int.class); Integer one = gson.fromJson(\u0026#34;1\u0026#34;, Integer.class); Long one = gson.fromJson(\u0026#34;1\u0026#34;, Long.class); Boolean false = gson.fromJson(\u0026#34;false\u0026#34;, Boolean.class); String str = gson.fromJson(\u0026#34;\\\u0026#34;abc\\\u0026#34;\u0026#34;, String.class); String anotherStr = gson.fromJson(\u0026#34;[\\\u0026#34;abc\\\u0026#34;]\u0026#34;, String.class); Object Examples 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class BagOfPrimitives { private int value1 = 1; private String value2 = \u0026#34;abc\u0026#34;; private transient int value3 = 3; BagOfPrimitives() { // no-args constructor } } // Serialization BagOfPrimitives obj = new BagOfPrimitives(); Gson gson = new Gson(); String json = gson.toJson(obj); ==\u0026gt; json is {\u0026#34;value1\u0026#34;:1,\u0026#34;value2\u0026#34;:\u0026#34;abc\u0026#34;} // Deserialization BagOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class); ==\u0026gt; obj2 is just like obj Key Points private fields are recommended No annotations needed — all fields in the class and its superclasses are included by default transient fields are excluded automatically null handling: During serialization, null fields are omitted During deserialization, missing keys result in null field values synthetic fields are excluded Inner, anonymous, and local class references to outer classes are ignored Nested Classes Gson can serialize/deserialize static nested classes easily, but cannot auto-deserialize non-static inner classes because their no-arg constructors need an outer class reference.\n1 2 3 4 5 6 7 8 // NOTE: Class B cannot be correctly serialized by default public class A { public String a; class B { public String b; public B() { } } } Solutions:\nDefine B as static class B Provide a custom InstanceCreator (works but not recommended) Arrays 1 2 3 4 5 6 7 8 9 10 Gson gson = new Gson(); int[] ints = {1, 2, 3, 4, 5}; String[] strings = {\u0026#34;abc\u0026#34;, \u0026#34;def\u0026#34;, \u0026#34;ghi\u0026#34;}; // Serialization gson.toJson(ints); ==\u0026gt; prints [1,2,3,4,5] gson.toJson(strings); ==\u0026gt; prints [\u0026#34;abc\u0026#34;, \u0026#34;def\u0026#34;, \u0026#34;ghi\u0026#34;] // Deserialization int[] ints2 = gson.fromJson(\u0026#34;[1,2,3,4,5]\u0026#34;, int[].class); Multi-dimensional arrays are also supported.\nCollections 1 2 3 4 5 6 7 Gson gson = new Gson(); Collection\u0026lt;Integer\u0026gt; ints = Lists.immutableList(1,2,3,4,5); String json = gson.toJson(ints); ==\u0026gt; json is [1,2,3,4,5] Type collectionType = new TypeToken\u0026lt;Collection\u0026lt;Integer\u0026gt;\u0026gt;(){}.getType(); Collection\u0026lt;Integer\u0026gt; ints2 = gson.fromJson(json, collectionType); Note: Java\u0026rsquo;s type erasure requires TypeToken to specify generic types for deserialization.\nGenerics Type erasure causes loss of generic type information:\n1 2 3 4 5 6 7 8 class Foo\u0026lt;T\u0026gt; { T value; } Gson gson = new Gson(); Foo\u0026lt;Bar\u0026gt; foo = new Foo\u0026lt;Bar\u0026gt;(); gson.toJson(foo); // May not serialize foo.value correctly gson.fromJson(json, foo.getClass()); // Cannot deserialize to Bar Use TypeToken:\n1 2 3 Type fooType = new TypeToken\u0026lt;Foo\u0026lt;Bar\u0026gt;\u0026gt;() {}.getType(); gson.toJson(foo, fooType); gson.fromJson(json, fooType); Mixed Type Collections For ['hello', 5, {name: 'GREETINGS', source: 'guest'}]:\nThree approaches:\nRecommended: Parse each element individually using JsonParser Register a type adapter for Collection.class Use Collection\u0026lt;MyCollectionMemberType\u0026gt; with a registered adapter Built-in Type Support Gson provides built-in serializers/deserializers for java.net.URL, java.net.URI, plus Joda-Time types (DateTime, Instant) via custom converters.\nCustom Serialization/Deserialization 1 2 3 4 5 GsonBuilder gson = new GsonBuilder(); gson.registerTypeAdapter(MyType2.class, new MyTypeAdapter()); gson.registerTypeAdapter(MyType.class, new MySerializer()); gson.registerTypeAdapter(MyType.class, new MyDeserializer()); gson.registerTypeAdapter(MyType.class, new MyInstanceCreator()); Pretty Printing Default output is compact. For readable formatting:\n1 2 Gson gson = new GsonBuilder().setPrettyPrinting().create(); String jsonOutput = gson.toJson(someObject); Null Object Support By default, null fields are omitted. To include them:\n1 Gson gson = new GsonBuilder().serializeNulls().create(); Versioning Use @Since annotation for versioned field control:\n1 Gson gson = new GsonBuilder().setVersion(1.0).create(); Field Exclusion Built-in mechanisms:\nModifier-based: Exclude transient and static by default @Expose annotation: Whitelist fields for serialization Custom ExclusionStrategy: Full programmatic control Field Naming Use @SerializedName for custom field names, or FieldNamingPolicy for global naming conventions.\nReferences Gson GitHub Repository Gson User Guide Gson API Javadoc Joda-Time ","permalink":"https://blog.substitute.tech/en/posts/gsonexamples/","summary":"\u003cblockquote\u003e\n\u003cp\u003eThis article is compiled from the official Gson User Guide. Gson\u0026rsquo;s performance is competitive with other JSON frameworks, and being Google-maintained makes it a solid choice for Java/Android JSON processing.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eGson is a Java library developed by Google for serializing Java objects to JSON and deserializing JSON strings back to Java objects.\u003c/p\u003e","title":"Gson User Guide"},{"content":" Timeliness note: The mirror URLs in this article were valid for a specific period. Both TUNA (Tsinghua) and USTC (Hefei) AOSP mirror addresses have changed multiple times. Always consult each mirror\u0026rsquo;s official help page for current URLs.\nAOSP source is enormous (~70GB), making downloads from Google\u0026rsquo;s official servers via VPN painfully slow. Chinese mirrors offer much faster speeds.\nReplacing the Remote URL Replace https://android.googlesource.com/ with the TUNA mirror:\n1 git://aosp.tuna.tsinghua.edu.cn/android/ For an existing checkout, modify the aosp remote fetch URL in .repo/manifest.xml:\n1 2 3 4 5 6 7 \u0026lt;manifest\u0026gt; \u0026lt;remote name=\u0026#34;aosp\u0026#34; - fetch=\u0026#34;https://android.googlesource.com\u0026#34; + fetch=\u0026#34;git://aosp.tuna.tsinghua.edu.cn/android/\u0026#34; review=\u0026#34;android-review.googlesource.com\u0026#34; /\u0026gt; \u0026lt;remote name=\u0026#34;github\u0026#34; Download the repo Tool 1 2 3 4 mkdir ~/bin PATH=~/bin:$PATH curl https://storage.googleapis.com/git-repo-downloads/repo \u0026gt; ~/bin/repo chmod a+x ~/bin/repo Initialize the Working Directory 1 2 3 4 5 mkdir WORKING_DIRECTORY cd WORKING_DIRECTORY repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest ## For a specific version (e.g. Android 5.1.1): repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest -b android-5.1.1_r3 If gerrit.googlesource.com is unreachable, edit ~/bin/repo and change the REPO_URL line:\n1 REPO_URL = \u0026#39;https://gerrit-googlesource.lug.ustc.edu.cn/git-repo\u0026#39; Sync the Source Tree 1 repo sync Run this command repeatedly to keep the source tree up to date.\nSwitching an Existing Repo to a Chinese Mirror Edit .repo/manifests.git/config, change:\n1 url = https://android.googlesource.com/platform/manifest to:\n1 url = git://mirrors.ustc.edu.cn/aosp/platform/manifest This method also works for syncing CyanogenMod code from TUNA.\nReferences AOSP Official Site Tsinghua TUNA AOSP Mirror Help USTC AOSP Mirror Help ","permalink":"https://blog.substitute.tech/en/posts/android%E6%BA%90%E7%A0%81%E4%B8%8B%E8%BD%BD/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eTimeliness note\u003c/strong\u003e: The mirror URLs in this article were valid for a specific period. Both TUNA (Tsinghua) and USTC (Hefei) AOSP mirror addresses have changed multiple times. Always consult each mirror\u0026rsquo;s official help page for current URLs.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eAOSP source is enormous (~70GB), making downloads from Google\u0026rsquo;s official servers via VPN painfully slow. Chinese mirrors offer much faster speeds.\u003c/p\u003e","title":"Downloading Android Source Code"},{"content":"While using Google Messenger, I noticed it has a \u0026ldquo;set as default SMS app\u0026rdquo; feature. Here\u0026rsquo;s how it works.\nStarting from Android 4.4 KitKat, Google introduced the default SMS app mechanism. The official reasoning:\nSome of you have built SMS apps using hidden APIs — a practice we discourage because hidden APIs may be changed or removed and new devices are not tested against them for compatibility. So, to provide you with a fully supported set of APIs for building SMS apps.\nIn short, if you build well and have enough users, Google takes notice and provides official support.\nAPI Overview Two new intents were introduced in Android 4.4:\nSMS_DELIVER_ACTION — for SMS WAP_PUSH_DELIVER_ACTION — for MMS Making Your App the Default 1 2 3 Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT); intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, getPackageName()); startActivity(intent); Complete Implementation: Register and Unregister 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // Check if we are the default SMS app currentPackageName = getPackageName(); String defaultSmsApp = Telephony.Sms.getDefaultSmsPackage(this); if (currentPackageName != null \u0026amp;\u0026amp; !currentPackageName.equals(defaultSmsApp)) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(DEDAULTSMS, defaultSmsApp); editor.apply(); } // Register as default register.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT); intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, getPackageName()); startActivity(intent); } }); // Restore the previous default unregister.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { SharedPreferences sharedPreferences = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()); String defaultSmsApp = sharedPreferences.getString(DEDAULTSMS, null); if (defaultSmsApp != null) { Intent intent = new Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT); intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, defaultSmsApp); startActivity(intent); } else { Toast.makeText(getApplicationContext(), \u0026#34;failed unregister\u0026#34;, Toast.LENGTH_SHORT).show(); } } }); Key Logic On startup, save the current default SMS app\u0026rsquo;s package name to SharedPreferences When the user clicks \u0026ldquo;register\u0026rdquo;, set the current app as default When the user clicks \u0026ldquo;unregister\u0026rdquo;, restore the previously saved default app Note for Android Q (10) and above: Google introduced the RoleManager API. Use RoleManager.createRequestRoleIntent(RoleManager.ROLE_SMS) instead of ACTION_CHANGE_DEFAULT for newer versions. The above implementation still works across most Android versions.\nReferences Telephony API Reference Telephony.Sms.Intents - ACTION_CHANGE_DEFAULT Getting Your SMS Apps Ready for KitKat ","permalink":"https://blog.substitute.tech/en/posts/defaultapp/","summary":"\u003cp\u003eWhile using Google Messenger, I noticed it has a \u0026ldquo;set as default SMS app\u0026rdquo; feature. Here\u0026rsquo;s how it works.\u003c/p\u003e\n\u003cp\u003eStarting from Android 4.4 KitKat, Google introduced the default SMS app mechanism. The official reasoning:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eSome of you have built SMS apps using hidden APIs — a practice we discourage because hidden APIs may be changed or removed and new devices are not tested against them for compatibility. So, to provide you with a fully supported set of APIs for building SMS apps.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eIn short, if you build well and have enough users, Google takes notice and provides official support.\u003c/p\u003e","title":"Make Your App the Default SMS App"},{"content":"A collection of subtle but important details I noticed while reading the Android developer documentation.\nFragmentTransaction: Using the replace Method replace() removes all existing fragments in the container before adding the new one. Combined with addToBackStack(), the user can navigate back to the previous fragment.\n1 2 3 4 FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.fragment_container, newFragment); transaction.addToBackStack(null); transaction.commit(); Note: addToBackStack(null) pushes the entire transaction (not a single fragment) onto the back stack. Pressing back reverses all operations in the transaction.\nReference: FragmentTransaction API\nBaseColumns in Database Contract Classes Table contract inner classes should implement BaseColumns to automatically include the _ID and _COUNT constants.\n1 2 3 4 5 6 7 8 /* Inner class that defines the table contents */ public static abstract class FeedEntry implements BaseColumns { public static final String TABLE_NAME = \u0026#34;entry\u0026#34;; public static final String COLUMN_NAME_ENTRY_ID = \u0026#34;entryid\u0026#34;; public static final String COLUMN_NAME_TITLE = \u0026#34;title\u0026#34;; public static final String COLUMN_NAME_SUBTITLE = \u0026#34;subtitle\u0026#34;; ... } Reference: BaseColumns API\nValidating Implicit Intents Before Launching Always verify that at least one activity can handle an implicit Intent, or the app will crash with ActivityNotFoundException.\n1 2 3 4 5 6 7 8 9 10 PackageManager packageManager = getPackageManager(); List activities = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); boolean isIntentSafe = activities.size() \u0026gt; 0; // Using a Chooser Intent chooser = Intent.createChooser(intent, title); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(chooser); } Key differences:\nqueryIntentActivities() returns all matching activities resolveActivity() returns the best matching activity (or Chooser) Reference: Intents and Intent Filters\nExposing an Activity to Third-Party Apps Declare intent-filters in the Manifest for other apps to call your Activity:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;activity android:name=\u0026#34;ShareActivity\u0026#34;\u0026gt; \u0026lt;!-- Handle sending SMS --\u0026gt; \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.intent.action.SENDTO\u0026#34;/\u0026gt; \u0026lt;category android:name=\u0026#34;android.intent.category.DEFAULT\u0026#34;/\u0026gt; \u0026lt;data android:scheme=\u0026#34;sms\u0026#34; /\u0026gt; \u0026lt;data android:scheme=\u0026#34;smsto\u0026#34; /\u0026gt; \u0026lt;/intent-filter\u0026gt; \u0026lt;!-- Handle sharing text/images --\u0026gt; \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.intent.action.SEND\u0026#34;/\u0026gt; \u0026lt;category android:name=\u0026#34;android.intent.category.DEFAULT\u0026#34;/\u0026gt; \u0026lt;data android:mimeType=\u0026#34;image/*\u0026#34;/\u0026gt; \u0026lt;data android:mimeType=\u0026#34;text/plain\u0026#34;/\u0026gt; \u0026lt;/intent-filter\u0026gt; \u0026lt;/activity\u0026gt; Handle the incoming intent based on type:\n1 2 3 4 5 6 7 8 9 10 11 12 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); Uri data = intent.getData(); if (intent.getType().indexOf(\u0026#34;image/\u0026#34;) != -1) { // Handle image data } else if (intent.getType().equals(\u0026#34;text/plain\u0026#34;)) { // Handle text } } Return results to the caller:\n1 2 3 4 Intent result = new Intent(\u0026#34;com.example.RESULT_ACTION\u0026#34;, Uri.parse(\u0026#34;content://result_uri\u0026#34;)); setResult(Activity.RESULT_OK, result); finish(); Correct Handler Usage If you create a Handler with new Handler() in an outer class, its Looper depends on the current thread. Without specifying a Looper, there\u0026rsquo;s no guarantee post(Runnable) runs on the main thread.\nRecommended singleton pattern:\n1 private static Handler INSTANCE = new Handler(Looper.getMainLooper()); Always use Looper.getMainLooper() to explicitly target the main thread.\nReference: Handler API\nWebView Automatically Requests Favicon on Every Page Load WebView sends these requests regardless of whether the page has a favicon (even from iframes):\n1 2 3 \u0026#34;GET /favicon.ico HTTP/1.1\u0026#34; 404 183 \u0026#34;GET /apple-touch-icon-precomposed.png HTTP/1.1\u0026#34; 404 197 \u0026#34;GET /apple-touch-icon.png HTTP/1.1\u0026#34; 404 189 Workaround Use a data URI to avoid unnecessary network requests:\n1 \u0026lt;link rel=\u0026#34;icon\u0026#34; href=\u0026#34;data:;base64,iVBORw0KGgo=\u0026#34;\u0026gt; This works across Safari, Chrome, and Firefox.\nBackground: Browsers request favicons after page load by default. Android WebView, being based on Chromium, exhibits the same behavior.\nReferences:\nChromium Issue 131567 html5-boilerplate Issue 1103 StackOverflow: Prevent favicon request ","permalink":"https://blog.substitute.tech/en/posts/%E6%9C%80%E8%BF%91%E5%8F%91%E7%8E%B0%E7%9A%84%E5%87%A0%E4%B8%AA%E6%B3%A8%E6%84%8F%E7%82%B9/","summary":"\u003cp\u003eA collection of subtle but important details I noticed while reading the Android developer documentation.\u003c/p\u003e","title":"Notes from Reading Android Documentation"},{"content":" Historical note: This article was written in 2015 based on the old Dart toolchain (Dart Editor + Dartium). Dart has since moved entirely to the Flutter ecosystem and the dart CLI toolchain. Content is kept for reference only.\nYou can launch and debug Android web applications built with Dart without pre-compiling to JavaScript. To do this, you need Dart Editor and Dart Content Shell. Dart Content Shell is automatically installed on the Android device.\nPrerequisites An Android device (phone or tablet) USB cable Chrome browser installed on both computer and Android device Part 1: Setting Up Your Environment Step 1: Set up your computer Download and install Dart Editor: Download Dart Editor (Note: this download page is no longer maintained)\nStep 2: Set up your Android device Enable USB debugging: Set up remote debugging\nStep 3: Connect the device Connect your device via USB: Connect your device via USB\nStep 4: Set up port forwarding Unless you are on a home network, you may need to set up port forwarding. See: Remote Debugging on Android with Chrome\nStep 5: Launch your app on the Android device Right-click an HTML file in Dart Editor, and select the mobile run option:\nTroubleshooting:\nIf using port forwarding, make sure Chrome is running If Dart Editor still cannot detect your device (shows \u0026ldquo;No phone or USB development phone found\u0026rdquo;), try unplugging and re-plugging the device If you see either dialog below, follow the on-screen instructions: Step 6: Debug your app With your app running on the Android device, set breakpoints in Dart Editor. When you see \u0026ldquo;remote\u0026rdquo; connected in the Debugger view (Tools \u0026gt; Debugger), you can start debugging.\nFAQ What apps are downloaded to the Android device?\nIn the first Dart Editor session, two apps are downloaded:\nDart Content Shell — a streamlined Chromium with the Dart VM, where your app runs during testing Connection test app — detects problems accessing the web server What is port forwarding, and why do I need it?\nWhat\u0026rsquo;s the difference between \u0026ldquo;Pub serve over USB\u0026rdquo; and \u0026ldquo;Embedded server over WiFi network\u0026rdquo;?\nIn the Manage Launches dialog:\nPub serve over USB: Port forwarding over USB. Recommended when behind a firewall, on public WiFi, or when the computer and device are on different networks Embedded server over WiFi network: Uses the embedded server in Dart Editor. Suitable for home networks without a firewall, no port forwarding needed Part 2: Running Sample Code Start the Editor Navigate to the extracted folder and double-click DartEditor.\nGet the sample code 1 git clone https://github.com/dart-lang/one-hour-codelab.git Open the sample In Dart Editor, choose File \u0026gt; Open Existing Folder and navigate to the cloned directory.\nRun the skeleton app Open 1-blankbadge, right-click piratebadge.html, and select Run in Dartium.\nSee: Dart Codelab\nReferences Dart official documentation Dart lang archive Chrome Remote Debugging ","permalink":"https://blog.substitute.tech/en/posts/dart/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eHistorical note\u003c/strong\u003e: This article was written in 2015 based on the old Dart toolchain (Dart Editor + Dartium). Dart has since moved entirely to the Flutter ecosystem and the \u003ccode\u003edart\u003c/code\u003e CLI toolchain. Content is kept for reference only.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eYou can launch and debug Android web applications built with Dart without pre-compiling to JavaScript. To do this, you need \u003cstrong\u003eDart Editor\u003c/strong\u003e and \u003cstrong\u003eDart Content Shell\u003c/strong\u003e. Dart Content Shell is automatically installed on the Android device.\u003c/p\u003e","title":"Dart Web Apps on Android"},{"content":"Cache replacement policies determine which entries to evict when the cache is full. They have a direct impact on system performance. Here is an overview of common cache eviction algorithms.\nAlgorithm Overview Algorithm Full Name Core Idea Belady Belady\u0026rsquo;s Algorithm Evict the item not needed for the longest time in the future (theoretical optimum, not realizable) LRU Least Recently Used Evict the least recently accessed item MRU Most Recently Used Evict the most recently accessed item PLRU Pseudo-LRU Approximate LRU using bitsets instead of linked lists for lower overhead RR Random Replacement Random eviction SLRU Segmented LRU Split cache into probation and protected segments 2-way 2-Way Set Associative Each cache set has two slots Direct-mapped Direct-Mapped Cache Each memory address maps to exactly one cache line LFU Least-Frequently Used Evict the least frequently accessed item LIRS Low Inter-reference Recency Set Distinguish hot/cold data by reuse distance, outperforms LRU in many workloads ARC Adaptive Replacement Cache Dynamically balances between LRU (recency) and LFU (frequency) CAR Clock with Adaptive Replacement Clock-based approximation of ARC MQ Multi Queue Multi-level queue management, each with its own replacement policy Least Recently Used (LRU) LRU is the most widely used cache eviction strategy: when the cache is full and a new item must be inserted, evict the item that has gone the longest without being accessed.\nImplementation Key Points Get: Move the accessed key to the head of the list Put: If the key doesn\u0026rsquo;t exist and space is insufficient, evict from the tail until space is available, then insert at the head Common Implementations LinkedHashMap: Java\u0026rsquo;s LinkedHashMap with accessOrder=true provides a ready-made LRU cache Linked List + HashMap: Doubly-linked list + hash map for O(1) read/write LruCache: Android\u0026rsquo;s LruCache\u0026lt;K, V\u0026gt; class Trade-offs Pros: Exploits temporal locality, simple to implement Cons: Sequential/looping access patterns with a working set slightly larger than the cache cause thrashing References Wikipedia: Cache Replacement Policies - LRU PDF: LRU Cache Implementation in Java ARC (Adaptive Replacement Cache) Proposed by IBM Research, ARC adaptively balances between recency (LRU) and frequency (LFU) to achieve better hit rates across diverse workloads.\nWikipedia: Adaptive Replacement Cache Original Paper: ARC - A Self-Tuning, Low Overhead Replacement Cache LIRS (Low Inter-reference Recency Set) LIRS distinguishes between \u0026ldquo;hot\u0026rdquo; and \u0026ldquo;cold\u0026rdquo; data using reuse distance rather than simple access time for eviction decisions, widely used in databases and storage systems.\nWikipedia: LIRS Original Paper: LIRS - An Efficient Low Inter-reference Recency Set Note: Originally written as study notes while learning cache algorithms. Content will be continuously improved.\n","permalink":"https://blog.substitute.tech/en/posts/cachealgorithms/","summary":"\u003cp\u003eCache replacement policies determine which entries to evict when the cache is full. They have a direct impact on system performance. Here is an overview of common cache eviction algorithms.\u003c/p\u003e","title":"Cache Algorithms"},{"content":"A collection of obscure Android development issues and their solutions.\n1. Android Studio Fails to Compile a New Project 1 2 Error:Execution failed for task :app:mergeDebugResources. \u0026gt; Crunching Cruncher ic_launcher.png failed, see logs Root cause: In early versions of Android Studio, an incomplete resource directory structure could trigger this error.\nFix: Create a drawable-hdpi or drawable-xhdpi folder.\n2. Meilan Note Cannot Connect via ADB When your device won\u0026rsquo;t connect over USB despite all normal conditions being met, you may need to add the USB Vendor ID.\nSolution (macOS):\nFind the Vendor ID via System Information or system_profiler SPUSBDataType Edit ~/.android/adb_usb.ini and append 0x2a45 (Meizu\u0026rsquo;s vendor ID) Restart ADB and reconnect: 1 2 adb kill-server adb start-server See: Android ADB Documentation\n3. App Installation Fails Due to Signature Mismatch 1 Package com.yuexue.tifenapp signatures do not match the previously installed version; ignoring! Root cause: The installed version has a different signature than the one you\u0026rsquo;re trying to install. This commonly happens when switching between debug and release signing keys.\nFix: Uninstall the conflicting version first:\n1 adb uninstall com.yuexue.tifenapp Key notes:\nadb -e targets an emulator; without the -e flag ADB picks the only connected device To uninstall a system app from Google Play, use adb shell pm uninstall -k --user 0 \u0026lt;pkg\u0026gt; (requires root) ","permalink":"https://blog.substitute.tech/en/posts/recent-strangeproblem/","summary":"\u003cp\u003eA collection of obscure Android development issues and their solutions.\u003c/p\u003e","title":"Strange Problems I Encountered Recently"},{"content":"Android provides several options for persisting data, each suited to different use cases. Choosing the right storage method is critical for your app\u0026rsquo;s performance, security, and user experience.\nStorage Options Overview Storage Method Best For Private? Removed on Uninstall? SharedPreferences Simple key-value data Yes Yes Internal Storage App-private files Yes Yes External Storage Shareable files No Only getExternalFilesDir() SQLite Databases Structured data Yes Yes Network Connection Server-side data — No Content Providers Cross-app data sharing Varies Varies SharedPreferences Used for storing simple key-value pairs in an XML file. Supports basic data types and String.\nWriting commit() writes synchronously to disk, while apply() updates the in-memory cache immediately and writes to disk asynchronously — prefer apply():\n1 2 3 SharedPreferences.Editor editor = settings.edit(); editor.putBoolean(\u0026#34;silentMode\u0026#34;, mSilentMode); editor.apply(); // use apply instead of commit Reading Use getXX(key, defaultValue):\n1 2 boolean silentMode = settings.getBoolean(\u0026#34;silentMode\u0026#34;, false); String name = settings.getString(\u0026#34;name\u0026#34;, \u0026#34;\u0026#34;); Note: In 2019, Google introduced Jetpack DataStore, a modern, type-safe, asynchronous replacement for SharedPreferences built on Kotlin Coroutines and Flow.\nInternal Storage vs External Storage Key Differences Internal Storage\nAlways available Private to your app by default Removed when the user uninstalls the app Best for data that should not be accessed by other apps External Storage\nNot always available (may be removed when mounted as USB storage) Files are world-readable Only files in getExternalFilesDir() are removed on uninstall Best for files that need to be shared or accessed via a computer Internal Storage Operations 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // Method 1: getFilesDir() File file = new File(context.getFilesDir(), filename); // Method 2: openFileOutput() String filename = \u0026#34;myfile\u0026#34;; FileOutputStream outputStream; try { outputStream = openFileOutput(filename, Context.MODE_PRIVATE); outputStream.write(\u0026#34;Hello world!\u0026#34;.getBytes()); outputStream.close(); } catch (Exception e) { e.printStackTrace(); } // Method 3: Cache files public File getTempFile(Context context, String url) { File file; try { String fileName = Uri.parse(url).getLastPathSegment(); file = File.createTempFile(fileName, null, context.getCacheDir()); } catch (IOException e) { // Error while creating file } return file; } External Storage Operations Always check the storage state before operating:\n1 2 3 4 5 6 7 8 9 10 11 12 // Check if external storage is available for read and write public boolean isExternalStorageWritable() { String state = Environment.getExternalStorageState(); return Environment.MEDIA_MOUNTED.equals(state); } // Check if external storage is at least readable public boolean isExternalStorageReadable() { String state = Environment.getExternalStorageState(); return Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state); } Querying Free Space API 8+ provides File.getFreeSpace() and File.getTotalSpace(). Checking available space before writing can prevent out-of-space errors. However, the system doesn\u0026rsquo;t guarantee you can actually use all the space getFreeSpace() reports — if you need significantly less than the free space, or the system is under 90% capacity, it\u0026rsquo;s generally safe.\nYou aren\u0026rsquo;t required to check available space before saving. An alternative approach is to write the file and catch an IOException. This is useful when you don\u0026rsquo;t know the exact file size in advance (e.g., converting a PNG to JPEG changes the size unpredictably).\nSQLite Databases Android includes a built-in SQLite database engine for structured data. Use SQLiteOpenHelper to manage database creation and version migration.\nModern Android development recommends Room — a persistence library that provides compile-time query validation, automatic migration, and Coroutine support on top of SQLite.\nNetwork Connection Store data on a remote server via network connections, suitable for cross-device access and cloud synchronization. Use libraries like Retrofit or OkHttp for network operations.\nContent Providers ContentProvider is Android\u0026rsquo;s cross-app data sharing mechanism. It encapsulates the underlying storage (SQLite, files, network, etc.) and exposes data through a URI-based interface.\nReferences Data and file storage overview — Android Developers Shared storage overview — Android Developers Jetpack DataStore — Android Developers Room Persistence Library — Android Developers Content Provider Basics — Android Developers ","permalink":"https://blog.substitute.tech/en/posts/androiddatastorage/","summary":"\u003cp\u003eAndroid provides several options for persisting data, each suited to different use cases. Choosing the right storage method is critical for your app\u0026rsquo;s performance, security, and user experience.\u003c/p\u003e","title":"Android Data Storage"},{"content":"An App Widget is a miniature application view that can be embedded into other applications (such as the Home screen) and receive periodic updates. Common examples include weather widgets and music player controls.\nArchitecture An App Widget consists of three core components:\nAppWidgetProviderInfo: metadata defining the widget (layout, update frequency, size), declared in XML AppWidgetProvider: extends BroadcastReceiver, handles widget lifecycle events View Layout: the widget\u0026rsquo;s UI layout, built using RemoteViews Key Lifecycle Methods Method Trigger onUpdate() At each update interval; the only required method onAppWidgetOptionsChanged() When the widget is resized onEnabled(Context) When the first widget instance is created onDisabled(Context) When the last widget instance is removed onDeleted(Context, int[]) When widget instances are deleted How to Create a Widget 1. Define Widget Metadata res/xml/example_appwidget_info.xml:\n1 2 3 4 5 6 7 \u0026lt;appwidget-provider xmlns:android=\u0026#34;http://schemas.android.com/apk/res/android\u0026#34; android:minWidth=\u0026#34;250dp\u0026#34; android:minHeight=\u0026#34;40dp\u0026#34; android:updatePeriodMillis=\u0026#34;86400000\u0026#34; android:initialLayout=\u0026#34;@layout/example_appwidget\u0026#34; android:resizeMode=\u0026#34;horizontal|vertical\u0026#34; android:widgetCategory=\u0026#34;home_screen\u0026#34; /\u0026gt; 2. Create the Layout Widget layouts use RemoteViews, which supports a limited set of views (FrameLayout, LinearLayout, RelativeLayout, GridLayout, Button, TextView, ImageView, etc.):\nres/layout/example_appwidget.xml:\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;LinearLayout xmlns:android=\u0026#34;http://schemas.android.com/apk/res/android\u0026#34; android:layout_width=\u0026#34;match_parent\u0026#34; android:layout_height=\u0026#34;match_parent\u0026#34; android:orientation=\u0026#34;horizontal\u0026#34;\u0026gt; \u0026lt;TextView android:id=\u0026#34;@+id/example_text\u0026#34; android:layout_width=\u0026#34;0dp\u0026#34; android:layout_height=\u0026#34;match_parent\u0026#34; android:layout_weight=\u0026#34;1\u0026#34; android:text=\u0026#34;Hello Widget!\u0026#34; /\u0026gt; \u0026lt;/LinearLayout\u0026gt; 3. Implement the Provider 1 2 3 4 5 6 7 8 9 10 11 public class ExampleAppWidgetProvider extends AppWidgetProvider { public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { for (int appWidgetId : appWidgetIds) { RemoteViews views = new RemoteViews( context.getPackageName(), R.layout.example_appwidget); views.setTextViewText(R.id.example_text, \u0026#34;Updated!\u0026#34;); appWidgetManager.updateAppWidget(appWidgetId, views); } } } 4. Declare in AndroidManifest 1 2 3 4 5 6 7 8 \u0026lt;receiver android:name=\u0026#34;.ExampleAppWidgetProvider\u0026#34;\u0026gt; \u0026lt;intent-filter\u0026gt; \u0026lt;action android:name=\u0026#34;android.appwidget.action.APPWIDGET_UPDATE\u0026#34; /\u0026gt; \u0026lt;/intent-filter\u0026gt; \u0026lt;meta-data android:name=\u0026#34;android.appwidget.provider\u0026#34; android:resource=\u0026#34;@xml/example_appwidget_info\u0026#34; /\u0026gt; \u0026lt;/receiver\u0026gt; Limitations Custom Views or View subclasses cannot be used — only views supported by RemoteViews Update frequency is managed by the system; updatePeriodMillis has a minimum of 30 minutes ListView and GridView widgets require a RemoteViewsService for data provision References Create a simple widget — Android Developers App Widgets Overview — Android Developers AppWidgetProvider API Reference App Widget Design Guidelines Jetpack Glance (Compose-based Widgets) ","permalink":"https://blog.substitute.tech/en/posts/androidappwidgets/","summary":"\u003cp\u003eAn App Widget is a miniature application view that can be embedded into other applications (such as the Home screen) and receive periodic updates. Common examples include weather widgets and music player controls.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"appwidget\" loading=\"lazy\" src=\"/images/appwidget.png\"\u003e\u003c/p\u003e","title":"Android App Widgets"},{"content":" Note: This article was written in 2015. Some toolchain details and version numbers are outdated and provided for historical reference only. For current Android development, please refer to Android Studio and the official Android Developers documentation.\nA setup checklist for the Android development environment. Tools without specified versions should use the latest available at the time.\nDownload Locations Google Android Developer (official): https://developer.android.com/sdk/index.html (link may be outdated; use the new entry point) AndroidDevTools (mirror for users who cannot access Google): http://www.androiddevtools.cn/ (link may be dead) Required Tools JDK: JDK 7+ recommended. (Note: Since 2019, Android requires JDK 11+.) Gradle: The core build system for Android. Download an offline distribution to speed up builds by configuring a local path in the IDE. Android SDK Tools: The latest SDK typically includes proxy configuration. Use domestic mirrors if downloads fail. Android Studio: Always use the latest stable version. Other tools: Choose as needed. Notes As of 2015, Eclipse is strongly discouraged. Android Studio has a significant productivity advantage.\nThis assessment still holds true today — Android Studio, as the official IDE, far surpasses Eclipse ADT in code editing, debugging, performance profiling, and build management.\nReferences Android Studio Download Android SDK Documentation Gradle Releases ","permalink":"https://blog.substitute.tech/en/posts/androiddeveloperrequirements/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eNote\u003c/strong\u003e: This article was written in 2015. Some toolchain details and version numbers are outdated and provided for historical reference only. For current Android development, please refer to \u003ca href=\"https://developer.android.com/studio\"\u003eAndroid Studio\u003c/a\u003e and the \u003ca href=\"https://developer.android.com/\"\u003eofficial Android Developers documentation\u003c/a\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Android Developer Requirements"},{"content":"Design Patterns are reusable solutions to common problems in object-oriented design. The 23 classic patterns were systematized by the Gang of Four (GoF: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) in their 1994 book Design Patterns: Elements of Reusable Object-Oriented Software.\nThis article is compiled from a blog post by smallnest and a StackOverflow discussion on GoF design pattern implementations in the Java API.\nView PDF Overview\nDesign patterns are not code libraries — they are conceptual frameworks for solving problems. The key is understanding when to use a pattern, not just how to implement it.\nCreational Patterns Creational patterns abstract the instantiation process, making a system independent of how its objects are created, composed, and represented.\nAbstract Factory Recognizable by: creational methods returning the factory itself, which in turn creates another abstract/interface type.\nJava API Examples:\njavax.xml.parsers.DocumentBuilderFactory#newInstance() javax.xml.transform.TransformerFactory#newInstance() javax.xml.xpath.XPathFactory#newInstance() Builder Recognizable by: creational methods returning the instance itself (fluent API).\nJava API Examples:\njava.lang.StringBuilder#append() (unsynchronized) java.lang.StringBuffer#append() (synchronized) java.nio.ByteBuffer#put() (also CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer) javax.swing.GroupLayout.Group#addComponent() All implementations of java.lang.Appendable Factory Method Recognizable by: creational methods returning an implementation of an abstract/interface type.\nJava API Examples:\njava.util.Calendar#getInstance() java.util.ResourceBundle#getBundle() java.text.NumberFormat#getInstance() java.nio.charset.Charset#forName() java.net.URLStreamHandlerFactory#createURLStreamHandler(String) (returns singleton per protocol) Prototype Recognizable by: creational methods returning a different instance of itself with the same properties.\nJava API Examples:\njava.lang.Object#clone() (class must implement java.lang.Cloneable) Singleton Recognizable by: creational methods returning the same instance every time.\nJava API Examples:\njava.lang.Runtime#getRuntime() java.awt.Desktop#getDesktop() Structural Patterns Structural patterns focus on how classes and objects are composed to form larger structures.\nAdapter Recognizable by: creational methods taking an instance of a different abstract/interface type and returning an implementation of own/another abstract/interface type that decorates/overrides the given instance.\nJava API Examples:\njava.util.Arrays#asList() java.io.InputStreamReader(InputStream) (returns a Reader) java.io.OutputStreamWriter(OutputStream) (returns a Writer) javax.xml.bind.annotation.adapters.XmlAdapter#marshal() and #unmarshal() Bridge Recognizable by: creational methods taking an instance of a different abstract/interface type and returning an implementation that delegates/uses the given instance.\nJava API Examples:\nJDBC, JNDI, JCE are the classic examples. No direct JDK class comes to mind. java.util.Collections#newSetFromMap() and singletonXXX() methods come close.\nComposite Recognizable by: behavioral methods taking an instance of the same abstract/interface type into a tree structure.\nJava API Examples:\njava.awt.Container#add(Component) (all over Swing) javax.faces.component.UIComponent#getChildren() (all over JSF) Decorator Recognizable by: creational methods taking an instance of the same abstract/interface type and adding additional behavior.\nJava API Examples:\nAll subclasses of java.io.InputStream, OutputStream, Reader, Writer have constructors taking the same type java.util.Collections checkedXXX(), synchronizedXXX(), unmodifiableXXX() methods javax.servlet.http.HttpServletRequestWrapper and HttpServletResponseWrapper Facade Recognizable by: behavioral methods that internally use instances of different independent abstract/interface types.\nJava API Examples:\njavax.faces.context.FacesContext — internally uses LifeCycle, ViewHandler, NavigationHandler, etc. javax.faces.context.ExternalContext — internally uses ServletContext, HttpSession, HttpServletRequest, etc. Flyweight Recognizable by: creational methods returning a cached instance (the \u0026ldquo;multiton\u0026rdquo; idea).\nJava API Examples:\njava.lang.Integer#valueOf(int) (also Boolean, Byte, Character, Short, Long) String constant pool Proxy Recognizable by: creational methods returning an implementation that delegates/uses a different implementation.\nJava API Examples:\njava.lang.reflect.Proxy java.rmi.* — the entire API Behavioral Patterns Behavioral patterns focus on how objects distribute responsibilities and interact.\nChain of Responsibility Recognizable by: behavioral methods that (indirectly) invoke the same method in another implementation of the same type in a queue.\nJava API Examples:\njava.util.logging.Logger#log() javax.servlet.Filter#doFilter() Command Recognizable by: behavioral methods in an abstract/interface type that invoke a method in a different type, encapsulated during creation.\nJava API Examples:\nAll implementations of java.lang.Runnable All implementations of javax.swing.Action Interpreter Recognizable by: behavioral methods returning a structurally different instance/type of the given instance/type.\nJava API Examples:\njava.util.Pattern java.text.Normalizer All subclasses of java.text.Format All subclasses of javax.el.ELResolver Iterator Recognizable by: behavioral methods sequentially returning instances of a different type from a queue.\nJava API Examples:\nAll implementations of java.util.Iterator All implementations of java.util.Enumeration Mediator Recognizable by: behavioral methods taking an instance of a different abstract/interface type and delegating/using it.\nJava API Examples:\njava.util.Timer (all scheduleXXX() methods) java.util.concurrent.Executor#execute() java.util.concurrent.ExecutorService (invokeXXX() and submit() methods) java.util.concurrent.ScheduledExecutorService (all scheduleXXX() methods) java.lang.reflect.Method#invoke() Memento Recognizable by: behavioral methods that internally change the state of the whole instance.\nJava API Examples:\njava.util.Date (setters change the internal long value) All implementations of java.io.Serializable All implementations of javax.faces.component.StateHolder Observer (Publish/Subscribe) Recognizable by: behavioral methods that invoke a method on another abstract/interface type instance, depending on own state.\nJava API Examples:\njava.util.Observer / java.util.Observable (rarely used in practice) All implementations of java.util.EventListener (all over Swing) javax.servlet.http.HttpSessionBindingListener javax.servlet.http.HttpSessionAttributeListener javax.faces.event.PhaseListener State Recognizable by: behavioral methods that change behavior depending on the instance\u0026rsquo;s state, which can be controlled externally.\nJava API Examples:\njavax.faces.lifecycle.LifeCycle#execute() (behavior depends on the current JSF lifecycle phase) Strategy Recognizable by: behavioral methods in an abstract/interface type that invoke a method in a different abstract/interface type passed in as a method argument.\nJava API Examples:\njava.util.Comparator#compare() (executed by Collections#sort()) javax.servlet.http.HttpServlet — service() and all doXXX() methods javax.servlet.Filter#doFilter() Template Method Recognizable by: behavioral methods in an abstract type that already have a \u0026ldquo;default\u0026rdquo; behavior defined.\nJava API Examples:\nAll non-abstract methods of java.io.InputStream, OutputStream, Reader, Writer All non-abstract methods of java.util.AbstractList, AbstractSet, AbstractMap javax.servlet.http.HttpServlet — all doXXX() methods default to HTTP 405 Visitor Recognizable by: two different abstract/interface types that each have methods taking the other type; one calls the other\u0026rsquo;s method to execute the desired strategy.\nJava API Examples:\njavax.lang.model.element.AnnotationValue and AnnotationValueVisitor javax.lang.model.element.Element and ElementVisitor javax.lang.model.type.TypeMirror and TypeVisitor References Original post: smallnest/Design Patterns Cheatsheet Design Patterns: Elements of Reusable Object-Oriented Software — GoF CMU Course: \u0026ldquo;23 Patterns in 80 Minutes\u0026rdquo; by Josh Bloch GoF Design Patterns in Java — Alibaba Cloud Java Design Patterns — Wikipedia ","permalink":"https://blog.substitute.tech/en/posts/designpatterns/","summary":"\u003cp\u003eDesign Patterns are reusable solutions to common problems in object-oriented design. The 23 classic patterns were systematized by the Gang of Four (GoF: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) in their 1994 book \u003cem\u003eDesign Patterns: Elements of Reusable Object-Oriented Software\u003c/em\u003e.\u003c/p\u003e\n\u003cp\u003eThis article is compiled from a blog post by \u003ca href=\"https://github.com/smallnest\"\u003esmallnest\u003c/a\u003e and a StackOverflow discussion on GoF design pattern implementations in the Java API.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"javagofdesignpatterns\" loading=\"lazy\" src=\"/images/javagofdesignpatterns.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"../source/pdf/design-pattern-scard.pdf\"\u003eView PDF Overview\u003c/a\u003e\u003c/p\u003e","title":"Design Patterns"},{"content":"Chrome DevTools supports remote debugging of web pages and WebView applications on Android devices. This feature is invaluable for mobile front-end development and hybrid app development, allowing you to use the full suite of desktop Chrome DevTools to debug mobile pages.\nRequirements Chrome 32+ installed on your development machine A USB cable to connect your Android device For browser debugging: Android 4.0+ and Chrome for Android For WebView debugging: Android 4.4+ (KitKat) with WebView configured for debugging Step-by-step Guide 1. Connect Your Device Enable Developer Options on your device (Settings \u0026gt; About Phone \u0026gt; tap Build Number repeatedly), ensure USB debugging is enabled, and connect via USB.\n2. Open chrome://inspect Navigate to chrome://inspect in desktop Chrome and verify that Discover USB devices is checked. On first connection, your device may show an authorization prompt — tap OK.\n3. Inspect Browser Tabs Chrome will list open tabs from your device. Click inspect to open DevTools.\n4. Debug WebViews in Your App To enable WebView debugging in your app, add this code:\n1 2 3 if (Build.VERSION.SDK_INT \u0026gt;= Build.VERSION_CODES.KITKAT) { WebView.setWebContentsDebuggingEnabled(true); } It\u0026rsquo;s recommended to guard this with a debug-build check or compile-time flag for production builds.\n5. Screencast Click the Screencast icon in DevTools to see the device screen in real-time and interact with it directly from your desktop.\nDebugging Tips Press F5 (or Cmd+R on Mac) to reload a remote page from DevTools Use the Network panel to view the waterfall under actual mobile cellular conditions Use the Timeline panel to analyze rendering and CPU usage — mobile hardware is significantly slower than your development machine If your device can\u0026rsquo;t reach your development server, use Port Forwarding or Virtual Host Mapping Port Forwarding Your phone often can\u0026rsquo;t reach your development server directly (different networks, corporate VPN, etc.). Port forwarding creates a TCP listener on your device that tunnels traffic through USB to your development machine.\nSteps:\nOpen chrome://inspect Click Port Forwarding Set Device port (default 8080) — the port your device will listen on Set Host to your dev server\u0026rsquo;s IP and port (port must be 1024-65535) Check Enable port forwarding Click Done Virtual Host Mapping For debugging with custom domain names:\nInstall a proxy on your dev machine (e.g., Charles Proxy or Squid) Configure port forwarding: Device port = 9000, Host = localhost:\u0026lt;proxy-port\u0026gt; Configure the Android device\u0026rsquo;s Wi-Fi proxy to localhost:9000 Troubleshooting Device not appearing in chrome://inspect\nWindows: install the correct USB driver — see OEM USB Drivers Connect the device directly (not through a USB hub) Verify USB debugging is enabled and authorized on the device Desktop Chrome version must be newer than Chrome for Android on the device; try Chrome Canary If still failing: revoke USB debugging authorizations on the device and reconnect Browser tabs not showing\nOpen Chrome on the device and load the page first, then refresh chrome://inspect WebViews not showing\nConfirm setWebContentsDebuggingEnabled(true) is called in your app Open the app with the WebView on the device, then refresh chrome://inspect Can\u0026rsquo;t access dev server from device\nUse port forwarding or set up a virtual host mapping As a last resort, you can fall back to the legacy adb workflow.\nReferences Remote debugging WebViews — Chrome DevTools Debugging Web Apps — Android Developers OEM USB Drivers — Android Developers Chrome DevTools Remote Debugging ","permalink":"https://blog.substitute.tech/en/posts/remotedebuggingonandroidwithchrome/","summary":"\u003cp\u003eChrome DevTools supports remote debugging of web pages and WebView applications on Android devices. This feature is invaluable for mobile front-end development and hybrid app development, allowing you to use the full suite of desktop Chrome DevTools to debug mobile pages.\u003c/p\u003e","title":"Remote Debugging on Android with Chrome"},{"content":"Bézier curves are widely used in computer graphics to model smooth curves. They were independently developed by French engineers Pierre Bézier (Renault) and Paul de Casteljau (Citroën) in the 1960s.\nMathematical Definition A Bézier curve is defined by a vector function B(t) that traces from control points P0, P1, ..., Pn, with parameter t in [0, 1]. The general formula of degree n is:\n$$\\mathbf{B}(t) = \\sum_{i=0}^{n} \\binom{n}{i} t^{i} (1-t)^{n-i} \\mathbf{P}_i \\quad , \\quad t \\in [0, 1]$$\nCommon Degrees Linear (1st degree) Bézier Curve Two control points P0 and P1 — simply a straight line between them:\n$$\\mathbf{B}(t)=\\mathbf{P}_0 + (\\mathbf{P}_1-\\mathbf{P}_0)t=(1-t)\\mathbf{P}_0 + t\\mathbf{P}_1 \\mbox{ , } t \\in [0,1]$$\n1 2 3 private float getLinearBezier(float t, float p0, float p1) { return (1 - t) * p0 + t * p1; } Quadratic (2nd degree) Bézier Curve Three control points P0, P1, P2:\n$$\\mathbf{B}(t) = (1 - t)^{2}\\mathbf{P}_0 + 2t(1 - t)\\mathbf{P}_1 + t^{2}\\mathbf{P}_2 \\mbox{ , } t \\in [0,1]$$\nCubic (3rd degree) Bézier Curve Four control points P0, P1, P2, P3 — the most commonly used in practice:\n$$\\mathbf{B}(t) = (1 - t)^3\\mathbf{P}_0 + 3t(1 - t)^2\\mathbf{P}_1 + 3t^2(1 - t)\\mathbf{P}_2 + t^3\\mathbf{P}_3 \\quad , \\quad t \\in [0, 1]$$\nKey Properties Endpoint interpolation: The curve always passes through the first and last control points: B(0) = P0, B(1) = Pn Tangent property: The curve is tangent to P0-\u0026gt;P1 at the start and Pn-1-\u0026gt;Pn at the end Convex hull property: The entire curve lies within the convex hull of its control points Affine invariance: Applying affine transformations (translation, rotation, scaling) to control points is equivalent to transforming the curve itself Subdivision: A Bézier curve can be split into two curves of the same degree (via de Casteljau\u0026rsquo;s algorithm) Usage in Android In Android, Bézier curves are commonly used for:\nCustom interpolators: Define animation curves via Bézier control points Path animations: Use Path.quadTo() (quadratic) and Path.cubicTo() (cubic) for curved paths Gesture tracking: Draw touch trajectories in custom Views Vector graphics: Define curved shapes in VectorDrawable resources 1 2 3 4 5 6 7 // Cubic Bézier curve in Android Path Path path = new Path(); path.moveTo(startX, startY); path.cubicTo(control1X, control1Y, control2X, control2Y, endX, endY); // Quadratic Bézier curve path.quadTo(controlX, controlY, endX, endY); References Bézier Curve — Wolfram MathWorld Bézier curve — Wikipedia Path API Reference — Android Developers VectorDrawable — Android Developers de Casteljau\u0026rsquo;s algorithm — Wikipedia ","permalink":"https://blog.substitute.tech/en/posts/beziercurvepractice/","summary":"\u003cp\u003eBézier curves are widely used in computer graphics to model smooth curves. They were independently developed by French engineers Pierre Bézier (Renault) and Paul de Casteljau (Citroën) in the 1960s.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Bézier_3_big\" loading=\"lazy\" src=\"/images/Be%CC%81zier_3_big.svg.png\"\u003e\u003cimg alt=\"Bézier_3_big_gif\" loading=\"lazy\" src=\"/images/Be%CC%81zier_3_big.gif\"\u003e\u003c/p\u003e","title":"Bezier Curve Practice"},{"content":"Property Animation is a powerful animation framework introduced in Android 3.0 (API 11). Unlike the older View Animation (tween animation) system, property animation actually changes the properties of the target object, not just its rendered appearance.\nWhy Property Animation? The legacy View Animation system (android.view.animation.Animation) had a fundamental flaw: it only changed where a View was drawn, not its actual properties. For example, if you animated a Button to move to the right side of the screen, clicking the original position would still trigger the button — because the View\u0026rsquo;s clickable region never moved. Property animation solves this by directly modifying the actual property values (like x, y, alpha) of the target object.\nCore APIs The core classes reside in the android.animation package:\nObjectAnimator The most commonly used class. It directly animates a named property of a target object:\n1 2 3 ObjectAnimator anim = ObjectAnimator.ofFloat(foo, \u0026#34;alpha\u0026#34;, 0f, 1f); anim.setDuration(1000); anim.start(); ValueAnimator A lower-level class that animates values without applying them to any specific object. You must provide an update listener to apply each frame\u0026rsquo;s value:\n1 2 3 4 5 6 7 8 9 10 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (float) animation.getAnimatedValue(); view.setAlpha(value); } }); animator.setDuration(300); animator.start(); AnimatorSet Used to orchestrate multiple animations — playing them together, sequentially, or with delays:\n1 2 3 4 AnimatorSet set = new AnimatorSet(); set.play(anim1).before(anim2); set.play(anim2).with(anim3); set.start(); Configurable Properties Property Description Default Duration How long the animation runs 300ms TimeInterpolator Controls the rate of change AccelerateDecelerateInterpolator RepeatCount Number of times to repeat 0 (no repeat) RepeatMode RESTART or REVERSE RESTART StartDelay Delay before animation starts 0 FrameRefreshDelay Frame refresh interval (rarely changed) 10ms Note: The actual frame rate depends not only on the configured delay but also on system performance and resource usage.\nInterpolators Android provides several built-in interpolators:\nLinearInterpolator — constant speed AccelerateDecelerateInterpolator — accelerate then decelerate AccelerateInterpolator — accelerate only DecelerateInterpolator — decelerate only BounceInterpolator — bounce effect at the end AnticipateOvershootInterpolator — pull back then overshoot the target You can also implement TimeInterpolator to create custom interpolators.\nViewPropertyAnimator For simple multi-property view animations, ViewPropertyAnimator provides a fluent API:\n1 2 3 4 5 view.animate() .alpha(0.5f) .translationX(200f) .setDuration(1000) .start(); Animation Listeners Use AnimatorListener or the convenience class AnimatorListenerAdapter to react to animation events:\n1 2 3 4 5 6 anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { // Handle post-animation logic } }); References Property Animation Overview — Android Developers Animation Resources — Android Developers ObjectAnimator API Reference ValueAnimator API Reference AnimatorSet API Reference ViewPropertyAnimator API Reference ","permalink":"https://blog.substitute.tech/en/posts/androidpropertyanimation/","summary":"\u003cp\u003e\u003ccode\u003eProperty Animation\u003c/code\u003e is a powerful animation framework introduced in Android 3.0 (API 11). Unlike the older View Animation (tween animation) system, property animation actually changes the properties of the target object, not just its rendered appearance.\u003c/p\u003e","title":"Android Property Animation"},{"content":"While debugging a recent bug, I ran into a peculiar issue: android.database.sqlite.SQLiteFullException: database or disk is full (code 13). After extensive research without a definitive root cause, I am sharing my findings here in the hope that others with similar experiences can help shed light on this.\nInitially, I suspected the disk was full, but the screenshot above suggested otherwise. Next, I wondered if SQLite had a size limit. A classic Stack Overflow discussion covers this: maximum-number-of-rows-in-a-sqlite-table\nIn July 2011 the sqlite3 limits page was updated to define the practical limits\u0026hellip;The theoretical maximum number of rows in a table is 2^64 (about 1.8e+19). This limit is unreachable since the maximum database size of 14 terabytes will be reached first. A 14 terabytes database can hold no more than approximately 1e+13 rows\u0026hellip;So with a max database size of 14 terabytes you\u0026rsquo;d be lucky to get ~1 Trillion rows since if you actually had a useful table with data in it the number of rows would be constrained by the size of the data.\nMy data volume was nowhere near those numbers. I then checked if it was a platform-specific issue by looking at the Comparison of relational database management systems on Wikipedia, but that was not the answer either. So I systematically went through the official documentation at Limits In SQLite. Here is a summary of the key limits.\nKey SQLite Limits String / BLOB Length Maximum of 2^31 - 1 bytes (2,147,483,647 bytes, approximately 2 GB).\nNumber of Columns Default maximum is 2000, adjustable to 32767 at compile time.\nSQL Statement Length Default limit is 1,000,000 bytes, configurable up to 1,073,741,824 bytes (1 GB).\nTables in a JOIN Supports up to 64 tables in a single join query.\nExpression Tree Depth Default limit is 1000; set to 0 for no enforced limit.\nFunction Arguments Default maximum is 100 parameters.\nCompound SELECT Terms Default is 500.\nLIKE / GLOB Pattern Length Default is 50,000 bytes. SQLite notes that the time complexity is O(N^2), where N is the total character count.\nHost Parameters in a Single SQL Statement Default maximum is 999.\nTrigger Recursion Depth Supported since v3.6.18; default is 1000 as of v3.7.0.\nAttached Databases Default maximum is 10, with an absolute maximum of 125.\nPages in a Database File Typically set to 1,073,741,823, with a maximum of 2,147,483,646. Combined with the maximum page size of 65,536 bytes, this yields a maximum database size of about 140 TB. Note: Starting from SQLite 3.45.0, the page count limit increased to 2^32 - 2, corresponding to a maximum of approximately 281 TB.\nRows in a Table Theoretical maximum is 2^64 (approximately 1.8e+19), but the file size limit is reached first.\nMaximum Database Size Every database consists of one or more \u0026ldquo;pages.\u0026rdquo; Page sizes are powers of two between 512 and 65,536 bytes. The maximum database file is 2,147,483,646 pages. At the maximum page size of 65,536 bytes, this translates to approximately 140 TB (128 TiB).\nInvestigation Directions Given that SQLite\u0026rsquo;s own limits far exceed any realistic scenario for my case, the focus should shift elsewhere. Two directions worth exploring.\nDatabase Size and VACUUM If you delete a lot of data but the database file does not shrink — this is not a bug. SQLite adds the freed space to an internal \u0026ldquo;free-list\u0026rdquo; and reuses it on the next insert, but it does not return space to the operating system.\nTo reclaim space, run the VACUUM command:\n1 VACUUM; VACUUM rebuilds the database from scratch, producing a minimal file with an empty free-list. However, it requires up to 2x the original file size in temporary disk space and can be slow.\nAs an alternative, enable auto-vacuum mode:\n1 PRAGMA auto_vacuum = FULL; -- 1: fully auto-vacuum The trade-off: auto-vacuum reclaims free pages automatically but can cause more fragmentation and does not compact partially filled pages the way VACUUM does.\nDatabase Maximum Size (Android Platform) If the database comes from a third-party library or framework layer, it may have an explicit size limit. Check it with:\n1 2 SQLiteDatabase db = getWritableDatabase(); long maxSize = db.getMaximumSize(); // get current maximum capacity If needed, call setMaximumSize() to relax the limit, or add compatibility handling.\nRelated discussion: Bugly SQLiteFullException (link may no longer be valid)\nIf you have had similar experiences or know of other possible causes, please share.\nReferences Limits In SQLite (sqlite.org) VACUUM Command (sqlite.org) Maximum number of rows in a SQLite table (Stack Overflow) Comparison of relational database management systems (Wikipedia) SQLiteFullException (Android Developers) Performance characteristics of SQLite with very large database files (Stack Overflow) ","permalink":"https://blog.substitute.tech/en/posts/limitsinsqlite/","summary":"\u003cp\u003eWhile debugging a recent bug, I ran into a peculiar issue: \u003ccode\u003eandroid.database.sqlite.SQLiteFullException: database or disk is full (code 13)\u003c/code\u003e. After extensive research without a definitive root cause, I am sharing my findings here in the hope that others with similar experiences can help shed light on this.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"limitsinsqlite01\" loading=\"lazy\" src=\"/images/limitsinsqlite01.png\"\u003e\u003c/p\u003e","title":"SQLite Limits Explained: Tracing a SQLiteFullException"},{"content":"This article covers the four most fundamental steps in learning Go: writing your first program, creating a utility library, writing unit tests, and using remote packages. The content follows the original GOPATH workflow from the early Go 1.x era.\nNote: Go Modules were introduced in Go 1.11 and became the default in Go 1.16, effectively replacing GOPATH mode. For new projects, use go mod init to create a module instead of manually setting up GOPATH. The examples below preserve the original style for reference.\nHello, Go After installing the Go pkg, configure environment variables. It is recommended to create a go directory under your home folder for development:\n1 2 export GOPATH=$HOME/go export PATH=$PATH:$GOPATH/bin Although the documentation does not strictly require these steps, skipping them makes debugging much more painful. Follow Go\u0026rsquo;s convention to create the directory structure. Create the path info.haoxiqiang/first/hello under $GOPATH/src, then create hello.go:\n1 2 3 4 5 6 7 package main import \u0026#34;fmt\u0026#34; func main() { fmt.Printf(\u0026#34;hello, go\\n\u0026#34;) } To run this, you need at least one package named main. There are three ways to execute it:\nRun directly: go run hello.go Build and install (omitting the src path): go install info.haoxiqiang/first/hello Run go install directly in the source directory A successful run produces no error output, and an executable is generated in $GOPATH/bin. You can run it via cd $GOPATH/bin \u0026amp;\u0026amp; ./hello or from anywhere with $GOPATH/bin/hello.\nCreating a Library Real-world projects often use utility classes or third-party libraries. Here is how to create and use a basic Go library:\n1 2 mkdir -p $GOPATH/src/info.haoxiqiang/tool/stringutil vi util1.go 1 2 3 4 5 6 7 8 9 package stringutil func Reverse(s string) string { r := []rune(s) for i, j := 0, len(s)-1; i \u0026lt; len(s)/2; i, j = i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r) } The package name is what you use when importing. Usage:\n1 2 3 4 5 6 7 8 9 10 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;info.haoxiqiang/tool/stringutil\u0026#34; ) func main() { fmt.Printf(stringutil.Reverse(\u0026#34;123456789\\n\u0026#34;)) } Unit Testing Go includes a lightweight built-in testing framework. Test files should follow the xxx_test.go naming convention:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package stringutil import \u0026#34;testing\u0026#34; func TestReverse(t *testing.T) { cases := []struct { in, want string }{ {\u0026#34;Hello, world\u0026#34;, \u0026#34;dlrow ,olleH\u0026#34;}, {\u0026#34;Hello, 世界\u0026#34;, \u0026#34;界世 ,olleH\u0026#34;}, {\u0026#34;\u0026#34;, \u0026#34;\u0026#34;}, } for _, c := range cases { got := Reverse(c.in) if got != c.want { t.Errorf(\u0026#34;Reverse(%q) == %q, want %q\u0026#34;, c.in, got, c.want) } } } Run the tests:\n1 2 go test info.haoxiqiang/tool/stringutil ok info.haoxiqiang/tool/stringutil\t0.005s This table-driven test pattern is widely adopted in the Go community. By defining a struct slice to cover multiple test cases, it stays concise and easy to extend.\nRemote Packages Go natively supports remote packages. Fetch an official example:\n1 go get github.com/golang/example/hello This creates the corresponding directory structure and files under $GOPATH/src. Running $GOPATH/bin/hello displays Hello, Go examples!.\nYou can also reference remote packages directly in import statements. The go get command handles fetching, building, and installing automatically:\n1 2 3 4 import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/golang/example/stringutil\u0026#34; ) 1 2 3 go get info.haoxiqiang/first/hello go install info.haoxiqiang/first/hello bin/hello Source code: https://github.com/Haoxiqiang/go-practise\nReferences How to Write Go Code (golang.org) Effective Go (golang.org) A Tour of Go (tour.golang.org) Go Testing Package (pkg.go.dev) Go Modules Reference (go.dev) Go Wiki: GOPATH (go.googlesource.com) Migrating to Go Modules (go.dev) ","permalink":"https://blog.substitute.tech/en/posts/go1/","summary":"\u003cp\u003eThis article covers the four most fundamental steps in learning Go: writing your first program, creating a utility library, writing unit tests, and using remote packages. The content follows the original GOPATH workflow from the early Go 1.x era.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eNote\u003c/strong\u003e: Go Modules were introduced in Go 1.11 and became the default in Go 1.16, effectively replacing GOPATH mode. For new projects, use \u003ccode\u003ego mod init\u003c/code\u003e to create a module instead of manually setting up GOPATH. The examples below preserve the original style for reference.\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Getting Started with Go: First Program and Basic Practices"},{"content":"The notification system is a critical channel for user-app interaction on Android — it keeps users informed of important events even when the app is not in the foreground, such as incoming messages or calendar reminders. Notifications received a major overhaul in Android 4.1 (Jelly Bean) and continued to see refinements through Android 5.0 (Lollipop). Since 4.1, Android supports action buttons at the bottom of notifications, allowing users to perform common tasks directly without opening the app.\nNote: This article was written for the Android 4.1–5.0 era APIs. Starting from Android 8.0 (API 26), all notifications must be assigned to a Notification Channel. Android 13 (API 33) introduced the POST_NOTIFICATIONS runtime permission. The code examples below use NotificationCompat for backward compatibility; visual results may vary across devices.\nBasic Usage All examples use android.support.v4.app.NotificationCompat. Creating a basic notification takes only a few lines:\n1 2 3 4 5 6 7 8 9 10 public static void showNotification(Context context, int mNotificationId) { NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle(\u0026#34;SimpleNotification\u0026#34;) .setContentText(\u0026#34;Hello World! This is the first notification.\u0026#34;); NotificationManager mNotifyMgr = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); mNotifyMgr.notify(mNotificationId, mBuilder.build()); } Click Behavior and Activity Navigation To navigate to an in-app page when the user taps the notification, attach a PendingIntent:\n1 2 3 4 5 Intent resultIntent = new Intent(context, ResultActivity.class); PendingIntent resultPendingIntent = PendingIntent.getActivity( context, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT); // ... mBuilder.setContentIntent(resultPendingIntent); One detail often overlooked: android:excludeFromRecents controls whether the Activity appears in the recent tasks list.\n1 2 3 4 \u0026lt;activity android:name=\u0026#34;.ResultActivity\u0026#34; android:launchMode=\u0026#34;singleTask\u0026#34; android:taskAffinity=\u0026#34;\u0026#34; android:excludeFromRecents=\u0026#34;false\u0026#34;/\u0026gt; There are two common scenarios for notification-driven Activity navigation.\nRegular Activity (with Back Stack) Use when the notification launches an Activity that is part of the app\u0026rsquo;s normal workflow — the user should be able to press back to return to the previous screen. Build a proper back stack using TaskStackBuilder:\n1 2 3 4 5 6 7 8 9 Intent resultIntent = new Intent(context, ParentActivity.class); TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); // Add the back stack stackBuilder.addParentStack(ParentActivity.class); // Add the Intent to the top of the stack stackBuilder.addNextIntent(resultIntent); // Obtain a PendingIntent containing the full back stack PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); Special Activity (No Back Stack) Use when the Activity is only reachable from the notification — it serves as an extension of the notification, displaying information that doesn\u0026rsquo;t fit in the notification itself:\n1 2 3 4 5 Intent notifyIntent = new Intent(); notifyIntent.setComponent(new ComponentName(context, NewTaskActivity.class)); notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent notifyPendingIntent = PendingIntent.getActivity( context, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT); Updating and Canceling Notifications Avoid creating a brand new notification every time. Instead, update an existing one — either modify its content or append new information. To make a notification updatable, assign a unique ID when publishing via NotificationManager.notify(ID, notification). To update, rebuild the NotificationCompat.Builder and re-publish with the same ID:\n1 mBuilder.setNumber(20); // The notification badge will show 20 A notification is removed under the following conditions:\nThe user manually clears it or taps \u0026ldquo;Clear All\u0026rdquo; (if the notification is dismissable) setAutoCancel() was set and the user tapped the notification NotificationManager.cancel(ID) was called — this also removes ongoing notifications NotificationManager.cancelAll() was called — removes all previously published notifications Notification Styles Android 4.1 introduced Big Views, allowing notifications to display richer content.\nBigTextStyle For displaying lengthy text:\n1 2 3 4 5 6 .setStyle(new NotificationCompat.BigTextStyle() .setBigContentTitle(\u0026#34;BigContentTitle\u0026#34;) .setSummaryText(\u0026#34;SummaryText\u0026#34;) .bigText(\u0026#34;I\u0026#39;m a big text message\u0026#34;)) .addAction(R.mipmap.ic_stat_dismiss, \u0026#34;dismiss\u0026#34;, notifyPendingIntent) .addAction(R.mipmap.ic_stat_snooze, \u0026#34;snooze\u0026#34;, notifyPendingIntent); BigPictureStyle For displaying a large image:\n1 2 3 4 .setStyle(new NotificationCompat.BigPictureStyle() .setBigContentTitle(\u0026#34;BigContentTitle\u0026#34;) .setSummaryText(\u0026#34;SummaryText\u0026#34;) .bigPicture(bitmapDrawable.getBitmap())); InboxStyle For displaying a list of multiple messages:\n1 2 3 4 5 6 7 .setStyle(new NotificationCompat.InboxStyle() .setBigContentTitle(\u0026#34;BigContentTitle\u0026#34;) .setSummaryText(\u0026#34;SummaryText\u0026#34;) .addLine(\u0026#34;aaaaaaaaaaaaaaaaa\u0026#34;) .addLine(\u0026#34;bbbbbbbbbbbbbbbbb\u0026#34;) .addLine(\u0026#34;ccccccccccccccccc\u0026#34;) .addLine(\u0026#34;ddddddddddddddddd\u0026#34;)); Progress Bar Notifications Notifications can include a progress bar. If you can estimate the total duration and current progress, use determinate mode to show a percentage. Otherwise, use indeterminate mode for a continuous animation.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // Start a lengthy operation in a background thread new Thread( new Runnable() { @Override public void run() { int incr; for (incr = 0; incr \u0026lt;= 100; incr += 5) { mBuilder.setProgress(100, incr, false); mNotifyManager.notify(mNotificationId, mBuilder.build()); try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { Log.d(\u0026#34;showNotificationWithDeterminate\u0026#34;, \u0026#34;sleep failure\u0026#34;); } } mBuilder.setContentText(\u0026#34;Download complete\u0026#34;) .setProgress(0, 0, false); // Remove the progress bar mNotifyManager.notify(mNotificationId, mBuilder.build()); } }).start(); // Indeterminate mode // .setProgress(0, 0, true); References Android Notification Overview (developer.android.com) Create a Notification (developer.android.com) Start an Activity from a Notification (developer.android.com) NotificationCompat API Reference (developer.android.com) NotificationCompat.Builder API Reference (developer.android.com) TaskStackBuilder API Reference (developer.android.com) Notifications Design Guide (developer.android.com) ","permalink":"https://blog.substitute.tech/en/posts/androidnotification/","summary":"\u003cp\u003eThe notification system is a critical channel for user-app interaction on Android — it keeps users informed of important events even when the app is not in the foreground, such as incoming messages or calendar reminders. Notifications received a major overhaul in Android 4.1 (Jelly Bean) and continued to see refinements through Android 5.0 (Lollipop). Since 4.1, Android supports action buttons at the bottom of notifications, allowing users to perform common tasks directly without opening the app.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eNote\u003c/strong\u003e: This article was written for the Android 4.1–5.0 era APIs. Starting from Android 8.0 (API 26), all notifications must be assigned to a Notification Channel. Android 13 (API 33) introduced the \u003ccode\u003ePOST_NOTIFICATIONS\u003c/code\u003e runtime permission. The code examples below use \u003ccode\u003eNotificationCompat\u003c/code\u003e for backward compatibility; visual results may vary across devices.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cimg alt=\"notification01\" loading=\"lazy\" src=\"/images/notification01.jpg\"\u003e\u003c/p\u003e","title":"Android Notification Guide"},{"content":" Note: This post was written in 2015. Some links may no longer be valid. The recommendations reflect the ecosystem at that time, but the core learning path and methodology remain relevant.\nForeword We all start somewhere. For any technology, everyone begins with Hello World. What puzzles me is why some people improve 100% in a year while others only 20% — or even stagnate. Is it IQ? In my opinion, it is more likely a matter of learning method.\nWhile on my wedding leave, I was writing blog posts and searching for learning materials. I found that relying solely on Baidu was terribly inefficient — at least half the time was wasted on useless pages. I decided to spend a few evenings organizing my bookmarks, making sure most resources are accessible from within China. The following is a curated list for beginner-to-intermediate Android developers. As for advanced-level topics — I\u0026rsquo;m not quite qualified to speak on those yet.\nAndroid Beginners For newcomers, I recommend buying a solid introductory book and browsing through Android fundamentals. Most Android books on the market are similar in quality — Turing Books (Turing Publishing) puts out decent ones. I received Android Programming: The Big Nerd Ranch Guide (link may no longer be available) from Turing Education, and I bought Professional Android 4 Application Development myself. Either works — just keep it on your desk and flip through it. If your English is good enough and you can bypass the firewall, go straight to the Android API Guides — most books are just translations or extensions of this content.\nBeginners must write lots of code. When I was learning, I went through the examples on the eoe forums (link may no longer be available) and rewrote them from scratch — well, maybe a few hundred examples, not literally thousands.\nOnce you can write simple apps, start reading other people\u0026rsquo;s code. I began by fixing bugs in an open-source course schedule app (link may no longer be available). Reading code, fixing issues bit by bit — that sense of accomplishment kept me going.\nWhen you reach the point where you can understand most code, it is time to learn Android\u0026rsquo;s design guidelines:\nMaterial Design Guidelines (Chinese Translation) (may load slowly due to Google Fonts) Getting Started: Android\u0026rsquo;s basic characteristics and standard UI naming conventions Style: Design principles — especially useful for screen adaptation: icon sizes, spacing, which folder to put different resolution images in Patterns: When and how to use Android elements — notifications, navigation, etc. Components: Basic widget usage — progress bars, activity indicators, etc. Another essential resource is the Chinese translation of Google\u0026rsquo;s official training course: Android Training Course in Chinese\nAndroid Intermediate At this stage, writing a typical app feels effortless — what you need is time, not new skills. You\u0026rsquo;re comfortable with most Android APIs: databases, networking, custom Views. Now it is time to level up through blogs.\nRecommended CSDN Bloggers Blogger Blog Known For Hongyang blog.csdn.net/lmj623565791 Practical tutorials, MOOC courses Guo Lin blog.csdn.net/guolin_blog Author of The First Line of Code Ren Yugang blog.csdn.net/singwhatiwanna APK dynamic loading framework, Baidu Mr.Simple blog.csdn.net/bboyfeiyu Source code analysis, HTTP framework tutorials AigeStudio blog.csdn.net/aigestudio Deep custom View series Android_Tutor blog.csdn.net/android_tutor Detailed beginner tutorials Code Samples 23CODE (link may no longer be available) — great examples, updated irregularly APKBUS (link may no longer be available) — like iOS\u0026rsquo;s code4app, sample code collection xiulian (修炼源码) (link may no longer be available) — personal site, good examples godcoder (link may no longer be available) — sample code collection Other Resources IBM DeveloperWorks Java Community — where I learned HashMap\u0026hellip; stormzhang — Android Studio tutorial series Google Android Training Course in Chinese (GitHub) Q\u0026amp;A Stack Overflow — the programmer\u0026rsquo;s essential tool. Search Chinese forums for simple questions; hit Stack Overflow for the tough ones. Android Advanced At this stage\u0026hellip; well, I don\u0026rsquo;t have much to write — my sophistication level is about to drop.\nGit: The modern VCS, essential for interviews at internet companies Git Cheat Sheet (Chinese) Git Complete Guide (link may no longer be available) GitHub: Your resume booster, open-source essential GitHub Cheat Sheet Chen Hao - CoolShell — interesting tech topics and deep thoughts Luo Shengyang — deep Android source code analysis, for those who want to understand how Android works internally Programming Coder Weekly Categorized — organized by language and topic Android Open Project Collection — curated by Trinea The Art of Programming: Interview and Algorithms Awesome Android UI — curated list of Android UI/UX libraries Gear Advice I recommend non-.NET developers use a Mac. Check out Chi Jianqiang\u0026rsquo;s Mac tutorials. The main advantages:\nNative command-line environment Wide selection of quality software Easy setup — can dabble in iOS development Battery life — 9 hours by 2015 standards For IDEs, most companies in 2015 still used Eclipse + JDK 1.6. I personally recommended Android Studio + JDK 1.8 — it was clearly the future. Today, Android Studio is the official standard; Eclipse is no longer recommended.\nNotes All resources above were verified at the time of publication. You are free to share them without attribution — my goal in posting this is to help every Android developer. It took me about an afternoon to compile these links, and I hope they help you.\nI also ask that content aggregators respect the hard work of the bloggers listed above. Even if you share just a simple Activity after your own efforts, that carries its own pride. Thank you.\nReferences Android Developer Guide Material Design Guidelines Google Android Training Course in Chinese Awesome Android UI ","permalink":"https://blog.substitute.tech/en/posts/android%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99%E6%8C%87%E5%8D%97/","summary":"\u003cblockquote\u003e\n\u003cp\u003eNote: This post was written in 2015. Some links may no longer be valid. The recommendations reflect the ecosystem at that time, but the core learning path and methodology remain relevant.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"foreword\"\u003eForeword\u003c/h2\u003e\n\u003cp\u003eWe all start somewhere. For any technology, everyone begins with Hello World. What puzzles me is why some people improve 100% in a year while others only 20% — or even stagnate. Is it IQ? In my opinion, it is more likely a matter of learning method.\u003c/p\u003e\n\u003cp\u003eWhile on my wedding leave, I was writing blog posts and searching for learning materials. I found that relying solely on Baidu was terribly inefficient — at least half the time was wasted on useless pages. I decided to spend a few evenings organizing my bookmarks, making sure most resources are accessible from within China. The following is a curated list for beginner-to-intermediate Android developers. As for advanced-level topics — I\u0026rsquo;m not quite qualified to speak on those yet.\u003c/p\u003e","title":"Android Learning Resources Guide"},{"content":"Note: This post was written in 2015. Some mirror URLs may have changed or become unavailable. As of 2025, the recommended mirrors for Chinese developers are Tsinghua TUNA, USTC, and other university mirrors, or simply using Android Studio\u0026rsquo;s built-in SDK Manager.\nFor developers behind the Great Firewall of China, accessing Google\u0026rsquo;s servers directly is slow or unreliable. This post covers mirror-based solutions for:\nAndroid SDK updates Android Studio downloads AOSP source code downloads Android NDK downloads Chinese Mirror Sites Chinese universities and cloud providers host mirrors of Google\u0026rsquo;s Android download servers, offering much faster speeds.\nSDK Manager Proxy (Historical Approach, ~2015) Open Android SDK Manager and configure the proxy:\nMirror URL: http://ubuntu.buct.edu.cn/ or http://ubuntu.buct.cn/ Port: 80 (default) NDK Downloads (Historical Approach) NDK mirror — kept in sync with Google\u0026rsquo;s official NDK releases at the time.\nRecommended Approach (2025) AOSP Source Mirrors Mirror URL Status Tsinghua TUNA https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest Actively maintained, monthly archive available USTC (Hefei) https://mirrors.ustc.edu.cn/aosp/platform/manifest Synced from TUNA, long-term maintenance SUSTech (Shenzhen) https://mirrors.sustech.edu.cn/AOSP/ Newer option For the initial AOSP sync (about 80 GB), use TUNA\u0026rsquo;s monthly archive:\n1 2 3 4 5 6 7 8 9 10 11 12 13 # Download repo tool from Tsinghua mirror curl https://mirrors.tuna.tsinghua.edu.cn/git/git-repo -o ~/bin/repo chmod +x ~/bin/repo # Download monthly archive (recommended for first sync) wget https://mirrors.tuna.tsinghua.edu.cn/aosp-monthly/aosp-latest.tar tar xf aosp-latest.tar cd aosp repo sync -j4 # Or initialize a specific branch directly repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-14.0.0_r33 repo sync -j4 SDK Downloads Android Studio\u0026rsquo;s built-in SDK Manager now works reasonably well within China — it uses Tencent CDN acceleration internally. Additional mirrors:\nTencent SDK Mirror: Built into Android Studio for Chinese users Alibaba Cloud Mirror: mirrors.aliyun.com References Android Studio Official Download Android NDK Official Download AOSP Source Code Tsinghua TUNA AOSP Mirror Help Page USTC AOSP Mirror Help Page ","permalink":"https://blog.substitute.tech/en/posts/%E8%BD%BB%E6%9D%BE%E6%90%9E%E5%AE%9Aandroid%E7%9A%84%E7%8E%AF%E5%A2%83%E6%BA%90%E7%A0%81%E5%B7%A5%E5%85%B7/","summary":"\u003cp\u003e\u003cstrong\u003eNote\u003c/strong\u003e: This post was written in 2015. Some mirror URLs may have changed or become unavailable. As of 2025, the recommended mirrors for Chinese developers are Tsinghua TUNA, USTC, and other university mirrors, or simply using Android Studio\u0026rsquo;s built-in SDK Manager.\u003c/p\u003e\n\u003cp\u003eFor developers behind the Great Firewall of China, accessing Google\u0026rsquo;s servers directly is slow or unreliable. This post covers mirror-based solutions for:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAndroid SDK updates\u003c/li\u003e\n\u003cli\u003eAndroid Studio downloads\u003c/li\u003e\n\u003cli\u003eAOSP source code downloads\u003c/li\u003e\n\u003cli\u003eAndroid NDK downloads\u003c/li\u003e\n\u003c/ul\u003e","title":"Setting Up Android Environment, Source Code, and Tools Using Chinese Mirrors"},{"content":" 1 2 android.support.v4.app.Fragment$InstantiationException: Unable to instantiate fragment make sure class name exists, is public, and has an empty constructor that is public This error used to be rare, but recently it spiked. After investigation, here are the causes and solutions.\nRoot Cause The Android framework recreates Fragments via reflection during state restoration. The following patterns break that:\nFragment is a non-static inner class — Java inner classes hold an implicit reference to the outer class, so the compiler generates a constructor with the outer class parameter, leaving no empty constructor Passing parameters via constructors — the system only calls the no-argument constructor; custom constructors are not used during restoration No public empty constructor declared — even if you rely on the compiler-generated default, ensure it is declared public (package-private visibility can fail in certain scenarios) How to Reproduce The easiest trigger is an Activity configuration change: screen rotation (portrait \u0026lt;-\u0026gt; landscape), returning from a phone call, etc. The system destroys and recreates the Activity along with its Fragments, calling Fragment.instantiate() on each. If a public no-arg constructor is not found, InstantiationException is thrown.\nSolution 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // Always have a public empty constructor public class CourseFragment extends BaseThemeFragment { public CourseFragment() { super(); } } // Pass initialization parameters via setArguments public static PageFragment newInstance(int currentResId) { PageFragment fragment = new PageFragment(); Bundle args = new Bundle(); args.putInt(CURRENT_RES, currentResId); fragment.setArguments(args); return fragment; } Key rule: always use setArguments(Bundle) for Fragment initialization parameters — never custom constructors. The system automatically restores the Arguments bundle when recreating the Fragment.\nWhy Explicitly Declare an Empty Constructor Fragment.instantiate() uses Class.newInstance() via reflection, which requires a public no-argument constructor accessible to the class loader. Non-static inner classes carry an implicit outer class reference, making their constructors incompatible with Class.newInstance().\nAlternative: FragmentFactory (AndroidX) If you\u0026rsquo;re using AndroidX, you can register a custom FragmentFactory to bypass the empty constructor requirement:\n1 2 3 4 5 6 7 8 9 10 supportFragmentManager.fragmentFactory = new FragmentFactory() { @NonNull @Override public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) { return switch (className) { case \u0026#34;com.example.MyFragment\u0026#34; -\u0026gt; new MyFragment(\u0026#34;custom arg\u0026#34;); default -\u0026gt; super.instantiate(classLoader, className); }; } }; That said, the newInstance() static factory pattern remains the simplest and most recommended approach for most cases.\nReferences Fragment Official Documentation — \u0026ldquo;All subclasses of Fragment must include a public no-argument constructor\u0026rdquo; Fragment.InstantiationException API Reference FragmentFactory API Reference Do fragments really need an empty constructor? Fragment InstantiationException — no empty constructor ","permalink":"https://blog.substitute.tech/en/posts/android-fragmentinstantiationexception/","summary":"\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-gdscript3\" data-lang=\"gdscript3\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eandroid\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003esupport\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003ev4\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eapp\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFragment\u003c/span\u003e\u003cspan class=\"o\"\u003e$\u003c/span\u003e\u003cspan class=\"n\"\u003eInstantiationException\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eUnable\u003c/span\u003e \u003cspan class=\"n\"\u003eto\u003c/span\u003e \u003cspan class=\"n\"\u003einstantiate\u003c/span\u003e \u003cspan class=\"n\"\u003efragment\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emake\u003c/span\u003e \u003cspan class=\"n\"\u003esure\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"n\"\u003eexists\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eis\u003c/span\u003e \u003cspan class=\"n\"\u003epublic\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"n\"\u003ehas\u003c/span\u003e \u003cspan class=\"n\"\u003ean\u003c/span\u003e \u003cspan class=\"n\"\u003eempty\u003c/span\u003e \u003cspan class=\"n\"\u003econstructor\u003c/span\u003e \u003cspan class=\"n\"\u003ethat\u003c/span\u003e \u003cspan class=\"n\"\u003eis\u003c/span\u003e \u003cspan class=\"n\"\u003epublic\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eThis error used to be rare, but recently it spiked. After investigation, here are the causes and solutions.\u003c/p\u003e","title":"Android Fragment$InstantiationException: Causes and Solutions"},{"content":"HashMap is one of the most frequently asked topics in Java interviews — it effectively tests a candidate\u0026rsquo;s understanding of data structures and engineering trade-offs. This article breaks down its core design and implementation details.\nStorage Structure HashMap is backed by a table array, where each element is a Node\u0026lt;K,V\u0026gt; (called HashMapEntry in JDK 7). When put is called, the key\u0026rsquo;s hashCode is computed and mapped to an array index via hash \u0026amp; (table.length - 1). If multiple keys hash to the same index (hash collision), entries are stored as a linked list at that bucket.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override public V put(K key, V value) { if (key == null) { return putValueForNullKey(value); } int hash = secondaryHash(key); HashMapEntry\u0026lt;K, V\u0026gt;[] tab = table; int index = hash \u0026amp; (tab.length - 1); for (HashMapEntry\u0026lt;K, V\u0026gt; e = tab[index]; e != null; e = e.next) { if (e.hash == hash \u0026amp;\u0026amp; key.equals(e.key)) { preModify(e); V oldValue = e.value; e.value = value; return oldValue; } } // No entry for (non-null) key is present; create one modCount++; if (size++ \u0026gt; threshold) { tab = doubleCapacity(); index = hash \u0026amp; (tab.length - 1); } addNewEntry(key, value, hash, index); return null; } void addNewEntry(K key, V value, int hash, int index) { table[index] = new HashMapEntry\u0026lt;K, V\u0026gt;(key, value, hash, table[index]); } The code above is from Android AOSP\u0026rsquo;s implementation of JDK 7 HashMap. The logic is straightforward: traverse the linked list looking for an existing key — overwrite on match, or append a new node.\nHash Function The raw key.hashCode() goes through an additional perturbation to mix higher bits into the lower bits, reducing collision probability.\nAndroid AOSP / JDK 7 Implementation Multiple XOR-shift operations:\n1 2 3 int hash = key.hashCode(); hash ^= (hash \u0026gt;\u0026gt;\u0026gt; 20) ^ (hash \u0026gt;\u0026gt;\u0026gt; 12); hash ^= (hash \u0026gt;\u0026gt;\u0026gt; 7) ^ (hash \u0026gt;\u0026gt;\u0026gt; 4); JDK 8 Implementation Simplified to a single XOR: (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16). The red-black tree addition in JDK 8 reduces linked-list lookup cost, so the simpler hash function suffices.\n1 2 3 4 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); } Index Calculation: hash \u0026amp; (length - 1) The array length is always a power of 2 by design. length - 1 in binary is all low-bit 1s, making hash \u0026amp; (length - 1) equivalent to hash % length — but bitwise AND is much faster than modulo, and the result always falls in [0, length-1].\nh \u0026amp; (length - 1) example (length=16, so n-1 = 1111):\nhash (binary) \u0026amp; length-1 (1111) = Result 8 (0100) \u0026amp; 15 (1111) = 0100 (8) 9 (0101) \u0026amp; 15 (1111) = 0101 (9) 16 (10000) \u0026amp; 15 (1111) = 0000 (0) 17 (10001) \u0026amp; 15 (1111) = 0001 (1) If length is not a power of 2 (e.g., 15), length - 1 is 1110. The last bit is always 0, making odd indices never used — wasting space and increasing collisions.\nResizing Trigger Condition Resizing triggers when the number of elements exceeds capacity * loadFactor. Default loadFactor = 0.75, default capacity 16, so the first threshold is 16 * 0.75 = 12.\nWhy 0.75? It is a time-space trade-off. A higher load factor (e.g., 1.0) uses space efficiently but increases collisions and lookup cost. A lower factor (e.g., 0.5) speeds lookups but wastes memory. 0.75 is JDK\u0026rsquo;s empirically chosen sweet spot.\nResize Process The capacity doubles (e.g., 2 * 16 = 32), and each entry\u0026rsquo;s index is recomputed. This rehash is the single most expensive operation in HashMap.\nJDK 7 used head insertion during rehash (in transfer()), which can create circular linked lists under concurrent access — leading to infinite loops. JDK 8 switched to tail insertion, fixing this issue.\nPre-allocation Advice If you plan to store 1000 entries, new HashMap(1000) sets capacity to 1024 (nearest power of 2). But 1024 * 0.75 = 768 \u0026lt; 1000, so a resize will still occur. The better choice is new HashMap(2048), ensuring capacity * 0.75 \u0026gt; 1000.\nJDK 8+ Key Improvements JDK 8 brought major changes:\nFeature JDK 7 JDK 8 Entry name HashMapEntry Node + TreeNode subclass Collision handling Linked list only (O(n)) Linked list + Red-black tree (O(log n)) Treeify threshold N/A TREEIFY_THRESHOLD = 8 Hash function 4 perturbations 1 perturbation Insertion strategy Head insertion Tail insertion Initialization Array allocated at construction Lazy: allocated on first put Red-Black Tree Conversion When a linked list exceeds TREEIFY_THRESHOLD = 8 and the array length is at least MIN_TREEIFY_CAPACITY = 64, the list is converted to a red-black tree, improving worst-case lookup from O(n) to O(log n). If the array length is below 64, HashMap resizes first instead of treeifying.\nThe threshold of 8 is based on the Poisson distribution: under random hash codes and a 0.75 load factor, the probability of a bucket reaching 8 entries is approximately 0.00000006 — so 8 is a sufficiently conservative choice.\nWhy Untreeify Threshold Is 6 UNTREEIFY_THRESHOLD = 6 leaves a buffer of 2 between treeify and untreeify thresholds, preventing frequent conversions when the count oscillates near the boundary.\nThread Safety HashMap is not thread-safe. Concurrent writes can cause circular linked lists (JDK 7\u0026rsquo;s head insertion during rehash), lost updates, and infinite loops. For concurrent access, use:\nConcurrentHashMap: segmented locks (JDK 7) or CAS + synchronized (JDK 8) Collections.synchronizedMap(): simple wrapper, lower throughput HashTable: table-wide lock, not recommended References OpenJDK HashMap.java Source (JDK 8) JDK 17 HashMap Documentation How Java HashMaps Work - Internal Mechanics Explained (freeCodeCamp) Android AOSP libcore HashMap Source HashMap Collisions and How JDK Handles It (Dev.to) ","permalink":"https://blog.substitute.tech/en/posts/javahashmap%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/","summary":"\u003cp\u003eHashMap is one of the most frequently asked topics in Java interviews — it effectively tests a candidate\u0026rsquo;s understanding of data structures and engineering trade-offs. This article breaks down its core design and implementation details.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"hashmap01\" loading=\"lazy\" src=\"/images/hashmap01.jpg\"\u003e\u003c/p\u003e","title":"How Java HashMap Works Internally"},{"content":"The box-shadow property in CSS3 is highly versatile. When used creatively, it can produce rich visual effects with pure CSS. Here are several practical techniques.\nDirectional Shadows By adjusting offsets and spread radius, shadows can be limited to a specific direction.\nTop Right Bottom Left 1 2 3 4 5 6 7 8 9 10 11 12 .drop-shadow.top { box-shadow: 0 -4px 2px -2px rgba(0,0,0,0.4); } .drop-shadow.right { box-shadow: 4px 0 2px -2px rgba(0,0,0,0.4); } .drop-shadow.bottom { box-shadow: 0 4px 2px -2px rgba(0,0,0,0.4); } .drop-shadow.left { box-shadow: -4px 0 2px -2px rgba(0,0,0,0.4); } Key point: a negative spread-radius (fourth parameter) cancels shadow diffusion on non-target sides, making single-side shadows possible.\nEmphasis Effects Zero-offset shadows with large spread radius simulate glow or border effects.\nDark Light Inset Border 1 2 3 4 5 6 7 8 9 10 11 12 .emphasize-dark { box-shadow: 0 0 5px 2px rgba(0,0,0,.35); } .emphasize-light { box-shadow: 0 0 0 10px rgba(255,255,255,.25); } .emphasize-inset { box-shadow: inset 0 0 7px 4px rgba(255,255,255,.5); } .emphasize-border { box-shadow: inset 0 0 0 7px rgba(255,255,255,.5); } Dark/Light: blur radius + spread radius create a glow effect Inset: the inset keyword creates inner shadow, simulating a concave look Border: when spread-radius is 7px with no blur, inset shadow appears as an inner border Gradient Overlays Combining background-image gradients with semi-transparent box-shadow creates subtle depth.\nLight Linear Dark Linear Light Radial Dark Radial 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 .gradient-light-linear { background-image: linear-gradient( rgba(255,255,255,.5), rgba(255,255,255,0)); } .gradient-dark-linear { background-image: linear-gradient( rgba(0,0,0,.25), rgba(0,0,0,0)); } .gradient-light-radial { background-image: radial-gradient( center 0, circle farthest-corner, rgba(255,255,255,0.4), rgba(255,255,255,0)); } .gradient-dark-radial { background-image: radial-gradient( center 0, circle farthest-corner, rgba(0,0,0,0.15), rgba(0,0,0,0)); } Note: vendor prefixes have been removed — modern browsers all support the standard linear-gradient / radial-gradient syntax.\nRounded Shadows Combining border-radius with shadows yields more natural card-like visuals.\nLight Heavy Full Barrel 1 2 3 4 5 6 7 8 9 10 11 12 .light-rounded { border-radius: 3px; } .heavy-rounded { border-radius: 8px; } .full-rounded { border-radius: 50%; } .barrel-rounded { border-radius: 20px/60px; } border-radius supports separate horizontal and vertical radii using /. The barrel-rounded example demonstrates this elliptical effect.\nEmbossed Shadows Combining inset shadows with regular box shadows simulates embossed or debossed button effects.\nLight Heavy 1 2 3 4 5 6 7 8 9 10 11 .embossed-light { border: 1px solid rgba(0,0,0,0.05); box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); } .embossed-heavy { border: 1px solid rgba(0,0,0,0.05); box-shadow: inset 0 2px 3px rgba(255,255,255,0.3), inset 0 -2px 3px rgba(0,0,0,0.3), 0 1px 1px rgba(255,255,255,0.9); } How it works: an inset highlight on the top edge combined with an inset darken on the bottom edge, plus a subtle drop shadow, creates the illusion of raised or indented surfaces.\nReferences box-shadow | MDN Basic Ready to Use CSS Styles | Tympanus Box-shadow generator | MDN ","permalink":"https://blog.substitute.tech/en/posts/css-box-shadows/","summary":"\u003cp\u003eThe \u003ccode\u003ebox-shadow\u003c/code\u003e property in CSS3 is highly versatile. When used creatively, it can produce rich visual effects with pure CSS. Here are several practical techniques.\u003c/p\u003e","title":"CSS Box Shadows Techniques"},{"content":"While working on an image upload feature, I encountered a runtime exception on certain devices:\n1 2 3 4 5 6 7 8 9 java.io.FileNotFoundException: /mnt/sdcard/Android/data/com.xxxxxx.android/files/xxxx open failed: EBUSY (Device or resource busy) at libcore.io.IoBridge.open(IoBridge.java:406) at java.io.FileOutputStream.\u0026lt;init\u0026gt;(FileOutputStream.java:88) ... Caused by: libcore.io.ErrnoException: open failed: EBUSY (Device or resource busy) at libcore.io.Posix.open(Native Method) at libcore.io.BlockGuardOs.open(BlockGuardOs.java:110) ... Root Cause The EBUSY error is related to Android\u0026rsquo;s filesystem behavior, particularly on FAT32 volumes. A common scenario: you delete a file and immediately try to create a new file with the same name. Even though the file has been removed, the filesystem\u0026rsquo;s dentry cache or file lock hasn\u0026rsquo;t been released yet, causing the new file creation to fail.\nSolution The simplest fix: rename the file or directory before deleting it.\n1 2 3 4 final File to = new File( file.getAbsolutePath() + System.currentTimeMillis()); file.renameTo(to); to.delete(); Why this works: renameTo() changes the file\u0026rsquo;s name reference in the filesystem. The original filename is freed, and the data blocks are now referenced by the new temporary name. Deleting the renamed file succeeds because the original filename is no longer locked.\nAdditional Notes EBUSY typically occurs in these scenarios:\nMultiple processes referencing the same file A file is deleted but its reference is not released FAT32 filesystems on external SD cards (most commonly reported) References StackOverflow: open failed EBUSY ","permalink":"https://blog.substitute.tech/en/posts/android-ebusy-exception/","summary":"\u003cp\u003eWhile working on an image upload feature, I encountered a runtime exception on certain devices:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e9\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ejava\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eFileNotFoundException\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003emnt\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003esdcard\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003eAndroid\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003edata\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003ecom\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003exxxxxx\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eandroid\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003efiles\u003c/span\u003e\u003cspan class=\"o\"\u003e/\u003c/span\u003e\u003cspan class=\"n\"\u003exxxx\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eopen\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003efailed\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eEBUSY\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDevice\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eor\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ebusy\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eat\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003elibcore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eIoBridge\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eopen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIoBridge\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ejava\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003e406\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eat\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ejava\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eFileOutputStream\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003einit\u003c/span\u003e\u003cspan class=\"o\"\u003e\u0026gt;\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eFileOutputStream\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ejava\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003e88\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e...\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eCaused\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eby\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003elibcore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eErrnoException\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eopen\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003efailed\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eEBUSY\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eDevice\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eor\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eresource\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003ebusy\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eat\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003elibcore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ePosix\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eopen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eNative\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eMethod\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"n\"\u003eat\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003elibcore\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eio\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eBlockGuardOs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eopen\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eBlockGuardOs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003ejava\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003e110\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e...\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e","title":"Android EBUSY Exception"},{"content":"I came across stormzhang\u0026rsquo;s Tencent interview questions and decided to work through them, documenting my answers and thought process.\nTopics covered:\nAndroid fundamentals (processes, Activities, Handler) Screen density and resource qualifiers Thread synchronization in Java System design (resumable downloads, network architecture) Interview questions:\nHow to draw a stamp pattern How to implement text stroke and shadow effects Can different Activities of the same app run in separate processes? If so, give an example. What thread synchronization mechanisms exist in Java? Give examples. Explain Handler, Looper, and HandlerThread. What do dp, dip, dpi, px, sp mean? How are they converted? What do layout-sw600dp and layout-h600dp mean? List Activity launch modes with your understanding and use cases. How to design a resumable file download system. XML layout: how to center two TextViews horizontally in a RelativeLayout. Design a system that fetches data and images from the network and loads them into a list — draw the client architecture and analyze. 3. Can different Activities of the same app run in separate processes? Yes. Normally all Activities in an application run in the same process. However, setting the android:process attribute on an Activity forces it to run in its own process.\nNaming rules for android:process:\nPrefix with : (e.g., :first.process): creates a private process (prefixed with the app\u0026rsquo;s package name) Start with a lowercase letter (e.g., com.example.shared): creates a global process that other apps can also use 1 2 3 4 5 6 7 \u0026lt;application android:icon=\u0026#34;@drawable/icon\u0026#34; android:label=\u0026#34;@string/app_name\u0026#34;\u0026gt; \u0026lt;activity android:name=\u0026#34;.MainActivity\u0026#34; android:process=\u0026#34;:first.process\u0026#34; /\u0026gt; \u0026lt;activity android:name=\u0026#34;.SubActivity\u0026#34; android:process=\u0026#34;:second.process\u0026#34; /\u0026gt; \u0026lt;/application\u0026gt; Two Activities from the same app can run in different processes yet belong to the same task — this demonstrates the power of Android\u0026rsquo;s task management. It allows isolating independent modules into separate processes, reducing coupling, without worrying about inter-process communication details. The implementation involves ActivityRecord and ProcessRecord scheduling managed by ActivityManagerService.\nReference: android:process | Android Developers Reference: Processes and threads overview | Android Developers\n6. dp, dip, dpi, px, sp — Definitions and Conversion Unit Meaning Description px Pixel Physical screen pixel in Inch Physical measurement mm Millimeter Physical measurement pt Point 1/72 inch dp / dip Density-independent pixel At 160dpi, 1dp = 1px sp Scale-independent pixel Like dp, but respects user font size preference Conversion between dp and px:\n1 2 3 4 5 6 7 8 9 10 11 public static int dip2px(Context context, float dipValue) { final float scale = context.getResources() .getDisplayMetrics().density; return (int)(dipValue * scale + 0.5f); } public static int px2dip(Context context, float pxValue) { final float scale = context.getResources() .getDisplayMetrics().density; return (int)(pxValue / scale + 0.5f); } Recommendation: use sp for text, dp for everything else.\nResource Directory Qualifiers swdp (e.g., layout-sw600dp): smallest width — the smaller of width/height. Does not change when the device orientation changes. wdp (e.g., layout-w600dp): current screen width — changes with orientation. hdp (e.g., layout-h600dp): current screen height — changes with orientation. The official docs recommend avoiding this qualifier, since scrollable content makes height less predictable than width. References Tencent Interview - stormzhang android:process | Android Developers Processes and threads overview | Android Developers ","permalink":"https://blog.substitute.tech/en/posts/tencent-interview/","summary":"\u003cp\u003eI came across \u003ca href=\"http://stormzhang.com/android/other/2014/05/03/tencent-interview/\"\u003estormzhang\u0026rsquo;s Tencent interview questions\u003c/a\u003e and decided to work through them, documenting my answers and thought process.\u003c/p\u003e\n\u003cp\u003eTopics covered:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eAndroid fundamentals (processes, Activities, Handler)\u003c/li\u003e\n\u003cli\u003eScreen density and resource qualifiers\u003c/li\u003e\n\u003cli\u003eThread synchronization in Java\u003c/li\u003e\n\u003cli\u003eSystem design (resumable downloads, network architecture)\u003c/li\u003e\n\u003c/ul\u003e","title":"Tencent Interview"},{"content":"What is a Lambda Expression? Lambda expressions are a core feature introduced in Java 8, enabling more concise code. Here is the most straightforward example:\n1 2 3 4 5 6 7 8 9 10 Runnable runnable1 = new Runnable() { @Override public void run() { System.out.println(\u0026#34;runnable1 start!!!\u0026#34;); } }; Runnable runnable2 = () -\u0026gt; System.out.println(\u0026#34;runnable2 start!!!\u0026#34;); runnable1.run(); runnable2.run(); Both blocks are equivalent, but the lambda version is a single line. The basic form is () -\u0026gt; expression or () -\u0026gt; { statements; }.\nWith Parameters, No Return Value 1 2 3 4 5 6 7 8 9 JButton testButton = new JButton(\u0026#34;Test Button\u0026#34;); testButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { System.out.println(\u0026#34;Click Detected by Anon Class\u0026#34;); } }); testButton.addActionListener(e -\u0026gt; System.out .println(\u0026#34;Click Detected by Lambda Listner\u0026#34;)); Parentheses can be omitted for single parameters: e -\u0026gt; expression.\nWith Parameters and Return Value 1 2 3 4 5 6 7 8 9 10 11 12 List\u0026lt;Person\u0026gt; personList = Person.createShortList(); // Anonymous inner class Collections.sort(personList, new Comparator\u0026lt;Person\u0026gt;() { public int compare(Person p1, Person p2) { return p1.getSurName().compareTo(p2.getSurName()); } }); // Lambda version Collections.sort(personList, (Person p1, Person p2) -\u0026gt; p1.getSurName().compareTo(p2.getSurName())); For multiple parameters with a return value: (p1, p2) -\u0026gt; { return expression; }. Parameter types can be omitted when inferable.\n@FunctionalInterface An interface with a single abstract method is treated as a functional interface by Java 8, even without the @FunctionalInterface annotation. Adding the annotation makes the intent explicit and triggers a compiler error if the contract is violated.\nCommon functional interfaces include Runnable, Comparator, and ActionListener.\nBuilt-in Functional Interfaces Java 8 provides standard functional interfaces in java.util.function:\nInterface Description Predicate\u0026lt;T\u0026gt; Takes T, returns boolean Consumer\u0026lt;T\u0026gt; Takes T, returns void Function\u0026lt;T, R\u0026gt; Takes T, returns R Supplier\u0026lt;T\u0026gt; Takes nothing, returns T (factory pattern) UnaryOperator\u0026lt;T\u0026gt; Takes T, returns T (unary operation) BinaryOperator\u0026lt;T\u0026gt; Takes two T, returns T (binary operation) Stream API and Collection Operations Combined with the Stream API, lambdas enable highly readable data processing:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 List\u0026lt;Person\u0026gt; pl = Person.createShortList(); // forEach pl.forEach(p -\u0026gt; p.printWesternName()); pl.forEach(Person::printEasternName); pl.forEach(p -\u0026gt; { System.out.println(p.printCustom( r -\u0026gt; \u0026#34;Name: \u0026#34; + r.getGivenName())); }); // filter + forEach pl.stream().filter(p -\u0026gt; p.getAge() \u0026gt; 16) .forEach(Person::printWesternName); // filter + collect (create new list) Predicate\u0026lt;Person\u0026gt; allDraftees = p -\u0026gt; p.getAge() \u0026gt;= 18 \u0026amp;\u0026amp; p.getAge() \u0026lt;= 25 \u0026amp;\u0026amp; p.getGender() == Gender.MALE; List\u0026lt;Person\u0026gt; pilotList = pl .stream() .filter(allDraftees) .collect(Collectors.toList()); // mapToInt + sum Predicate\u0026lt;Person\u0026gt; allPilots = p -\u0026gt; p.getAge() \u0026gt;= 23 \u0026amp;\u0026amp; p.getAge() \u0026lt;= 65; long totalAge = pl .stream() .filter(allPilots) .mapToInt(p -\u0026gt; p.getAge()) .sum(); // parallelStream + average OptionalDouble averageAge = pl .parallelStream() .filter(allPilots) .mapToDouble(p -\u0026gt; p.getAge()) .average(); Generic Processing Pipeline The following example combines Predicate, Function, and Consumer into a generic processing pipeline. Readability decreases with nesting, but understanding each interface\u0026rsquo;s role makes the pattern clear:\n1 2 3 4 5 6 7 8 9 10 11 12 public static \u0026lt;X, Y\u0026gt; void processElements( Iterable\u0026lt;X\u0026gt; source, Predicate\u0026lt;X\u0026gt; tester, Function\u0026lt;X, Y\u0026gt; mapper, Consumer\u0026lt;Y\u0026gt; block) { for (X p : source) { if (tester.test(p)) { Y data = mapper.apply(p); block.accept(data); } } } References Lambda Expressions (Oracle Official Tutorial) Lambda-QuickStart (Oracle OBE) java.util.function Package (Java SE 8) Project Lambda (OpenJDK) Lambda Expressions (JLS 15.27) ","permalink":"https://blog.substitute.tech/en/posts/java-8-%E7%9A%84-lambda/","summary":"\u003ch2 id=\"what-is-a-lambda-expression\"\u003eWhat is a Lambda Expression?\u003c/h2\u003e\n\u003cp\u003eLambda expressions are a core feature introduced in Java 8, enabling more concise code. Here is the most straightforward example:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eRunnable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003erunnable1\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eRunnable\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"nd\"\u003e@Override\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"kt\"\u003evoid\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"nf\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e        \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;runnable1 start!!!\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e    \u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e};\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eRunnable\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003erunnable2\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"o\"\u003e-\u0026gt;\u003c/span\u003e\u003cspan class=\"w\"\u003e \u003c/span\u003e\u003cspan class=\"n\"\u003eSystem\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eout\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003eprintln\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;runnable2 start!!!\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003erunnable1\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003erunnable2\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"na\"\u003erun\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\u003cspan class=\"w\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eBoth blocks are equivalent, but the lambda version is a single line. The basic form is \u003ccode\u003e() -\u0026gt; expression\u003c/code\u003e or \u003ccode\u003e() -\u0026gt; { statements; }\u003c/code\u003e.\u003c/p\u003e","title":"Lambda Expressions in Java 8"},{"content":"Ever since getting a Nexus device, I never wanted to plug in a USB cable again. Android supports ADB debugging over WiFi, making cable-free development a reality.\nPrerequisites Phone and computer on the same local network Developer Options and USB Debugging enabled on the phone ADB tools installed on the computer Legacy Method (Android 10 and below) Connect your phone via USB and switch ADB to TCP/IP mode:\n1 2 3 4 5 6 7 8 9 10 11 12 # Make sure adb is running in USB mode $ adb usb restarting in USB mode # Verify the device is detected $ adb devices List of devices attached ######## device # Restart adb in TCP/IP mode on port 5555 $ adb tcpip 5555 restarting in TCP mode port: 5555 Find your phone\u0026rsquo;s IP address (Settings \u0026gt; About phone \u0026gt; Status \u0026gt; IP address), then connect wirelessly:\n1 2 3 4 5 6 7 8 # Connect to the device over WiFi $ adb connect your_ip connected to #.#.#.#:5555 # Remove USB cable and confirm the device is still accessible $ adb devices List of devices attached #.#.#.#:5555 device Now you can debug wirelessly without any cables.\nTroubleshooting If the connection drops or fails:\nMake sure both devices are on the same network subnet Restart ADB and try again: 1 2 3 4 $ adb kill-server $ adb start-server $ adb tcpip 5555 $ adb connect your_ip If it still doesn\u0026rsquo;t work, plug the USB cable back in and repeat the full process.\nAndroid 11+ Native Wireless Debugging Android 11 introduced a Wireless debugging option that requires no initial USB connection:\nEnable Developer Options and Wireless Debugging Select \u0026ldquo;Pair device with pairing code\u0026rdquo; On your computer, run: 1 2 3 $ adb pair IP_address:pairing_port # Enter the 6-digit pairing code $ adb connect IP_address:connection_port This method uses TLS-encrypted communication, offering better security than the legacy adb tcpip approach.\nReferences Run apps on a hardware device | Android Developers Android Debug Bridge (adb) | Android Developers ADB over Wi-Fi and Ethernet | ChromeOS Developers ADB Wifi Architecture | AOSP ","permalink":"https://blog.substitute.tech/en/posts/android%E4%BD%BF%E7%94%A8wireless%E8%B0%83%E8%AF%95/","summary":"\u003cp\u003eEver since getting a Nexus device, I never wanted to plug in a USB cable again. Android supports ADB debugging over WiFi, making cable-free development a reality.\u003c/p\u003e","title":"Wireless Debugging on Android"},{"content":"SQLite is a lightweight relational database engine built into Android, ideal for local data persistence. The android.database.sqlite package provides everything you need without additional setup.\nSQLiteOpenHelper Basics To create and open a database, extend SQLiteOpenHelper. The constructor takes a database name and version number. The system checks if the database already exists — if so, it opens it; otherwise, it creates a new one and calls onCreate.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class DBHelper extends SQLiteOpenHelper { private static final String TAG = \u0026#34;DBHelper\u0026#34;; private static final String DATABASE_NAME = \u0026#34;contacts.db\u0026#34;; private static final int DATABASE_VERSION = 1; public DBHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } /** * Called when the database is created for the first time. */ @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { Log.i(TAG, \u0026#34;[\u0026#34; + DATABASE_NAME + \u0026#34; v.\u0026#34; + DATABASE_VERSION + \u0026#34;]\u0026#34;); // TODO: Create tables here } /** * Called when the DATABASE_VERSION is increased. */ @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { Log.i(TAG, \u0026#34;Upgrading database[\u0026#34; + DATABASE_NAME + \u0026#34; v.\u0026#34; + newVersion + \u0026#34;]\u0026#34;); } } Note that the database is not created or opened until getWritableDatabase() or getReadableDatabase() is called. onUpgrade is triggered when the version number increases — handle schema migrations here.\nSingleton Pattern and Thread Safety A SQLiteOpenHelper subclass returns the same SQLiteDatabase instance. This means calling close() from any thread closes all database instances in your app. Be mindful of open/close timing.\nBest practices:\nUse a singleton pattern to manage your SQLiteOpenHelper instance Avoid opening and closing database connections frequently across different parts of your app Unless data volume is enormous, consolidate data into a single database rather than managing multiple SQLiteOpenHelper instances File Location and Security Database files are private to your app, stored at /data/data/(packageName)/database/. However:\nThe database file is not encrypted — anyone with root access can read it To export the database on a non-rooted device, copy it to a public location from your app For sensitive data, consider SQLCipher or Android\u0026rsquo;s EncryptedDatabase References SQLiteOpenHelper | Android Developers Save data using SQLite | Android Developers SQLite Official Documentation Using the SQLite Database on Android and SQLiteOpenHelper ","permalink":"https://blog.substitute.tech/en/posts/androids-sqlite/","summary":"\u003cp\u003eSQLite is a lightweight relational database engine built into Android, ideal for local data persistence. The \u003ccode\u003eandroid.database.sqlite\u003c/code\u003e package provides everything you need without additional setup.\u003c/p\u003e","title":"Android's SQLite"},{"content":"The Palette library, part of Android Support Library (now migrated to AndroidX palette), extracts representative colors from a Bitmap. In Material Design, deriving UI colors from imagery is a common pattern, and Palette makes this straightforward.\nSupported Color Profiles Palette provides six color profiles covering vibrant to muted tones:\nProfile Description Vibrant Bright, saturated Vibrant Dark Dark variant of vibrant Vibrant Light Light variant of vibrant Muted Subdued, desaturated Muted Dark Dark variant of muted Muted Light Light variant of muted Each profile corresponds to a Palette.Swatch object that provides RGB/HSL values, population count, and recommended text colors for overlaying on the swatch.\nBasic Usage Palette supports both synchronous and asynchronous generation:\n1 2 3 4 5 6 7 8 9 // Synchronous generation (run on background thread) public static Palette generate (Bitmap bitmap); public static Palette generate (Bitmap bitmap, int numColors); // Asynchronous generation public static AsyncTask\u0026lt;Bitmap, Void, Palette\u0026gt; generateAsync ( Bitmap bitmap, Palette.PaletteAsyncListener listener); public static AsyncTask\u0026lt;Bitmap, Void, Palette\u0026gt; generateAsync ( Bitmap bitmap, int numColors, Palette.PaletteAsyncListener listener); The synchronous generate method completes in tens of milliseconds. The async variant runs on a background thread via AsyncTask and delivers results through a callback.\nAbout the numColors Parameter numColors controls the number of colors extracted:\nFor landscapes, 12-16 colors suffice. For images dominated by faces, increase to 24-32 for better detail.\nSmaller values yield faster generation; larger values produce finer color granularity. Default is 16.\nExample The following example extracts colors from a Bitmap and applies them to the ActionBar and a set of views:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 Bitmap bitmap = BitmapFactory.decodeResource( getResources(), R.drawable.strictdroid); Palette.generateAsync(bitmap, new PaletteAsyncListener() { @Override public void onGenerated(Palette palette) { getSupportActionBar().setBackgroundDrawable( new ColorDrawable( palette.getVibrantSwatch().getRgb())); for (int i = 0; i \u0026lt; viewList.size(); i++) { View view = viewList.get(i); switch (i) { case 0: view.setBackgroundColor( palette.getDarkMutedColor(Color.BLACK)); break; case 1: view.setBackgroundColor( palette.getDarkVibrantColor(Color.BLACK)); break; case 2: view.setBackgroundColor( palette.getLightMutedColor(Color.BLACK)); break; case 3: view.setBackgroundColor( palette.getLightVibrantColor(Color.BLACK)); break; case 4: view.setBackgroundColor( palette.getMutedColor(Color.BLACK)); break; case 5: view.setBackgroundColor( palette.getVibrantColor(Color.BLACK)); break; } } } }); Each getXxxColor() method accepts a default color parameter, used when the requested profile is not present in the image.\nMigration to AndroidX The original v7 Palette library has migrated to AndroidX:\n1 implementation(\u0026#34;androidx.palette:palette:1.0.0\u0026#34;) The API remains largely the same. The Builder pattern (Palette.from(bitmap).generate()) is recommended for new code, offering customization options like maximumColorCount(), addFilter(), and resizeBitmapArea().\nReferences Select colors with the Palette API | Android Developers Palette API Reference | Android Developers Palette.Builder API Reference | Android Developers Source code: PaletteDemo ","permalink":"https://blog.substitute.tech/en/posts/androids-palette/","summary":"\u003cp\u003eThe Palette library, part of Android Support Library (now migrated to AndroidX \u003ccode\u003epalette\u003c/code\u003e), extracts representative colors from a Bitmap. In Material Design, deriving UI colors from imagery is a common pattern, and Palette makes this straightforward.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"palette01\" loading=\"lazy\" src=\"/images/palette01.png\"\u003e\u003c/p\u003e","title":"Android's Palette"},{"content":"Parallax scrolling is a common UI effect where different layers move at different speeds during scrolling, creating a sense of depth.\nImplementation The core idea is to listen for RecyclerView scroll events, dynamically translate the Header based on scroll distance, and clip the overflow to maintain correct layout.\n1. Listen to Scroll and Translate Header Use setOnScrollListener to capture scroll distance, then apply a translationY offset to the Header:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 RecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (mHeader != null) { RecyclerView.ViewHolder holder = RecyclerView.findViewHolderForPosition(0); if (holder != null) { int startTop = holder.itemView.getTop(); float ofCalculated = startTop * SCROLL_MULTIPLIER; ViewCompat.setTranslationY(mHeader, -ofCalculated); // ... } } } }); SCROLL_MULTIPLIER controls the parallax speed — values less than 1 make the Header move slower than the content, achieving the layered effect.\n2. Clip the Overflow The Header visually moves but its layout bounds remain unchanged, so we need to clip the overflow:\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Override protected void dispatchDraw(Canvas canvas) { Log.i(\u0026#34;dispatchDraw\u0026#34;, \u0026#34;mOffset=\u0026#34; + mOffset + \u0026#34; ;getLeft()=\u0026#34; + getLeft() + \u0026#34; ;getRight()=\u0026#34; + getRight() + \u0026#34; ;getTop()=\u0026#34; + getTop() + \u0026#34; ;getBottom()=\u0026#34; + getBottom()); canvas.clipRect(new Rect(getLeft(), getTop(), getRight(), getBottom() + mOffset)); super.dispatchDraw(canvas); } public void setClipY(int offset) { mOffset = offset; invalidate(); } By overriding dispatchDraw, we clip the Canvas before drawing, hiding the portion of the Header that extends beyond the container.\n3. Calculate Scroll Progress The current scroll progress can be computed as startTop / mHeader.getHeight(), useful for driving other coordinated effects like opacity transitions.\nReferences Source code AndroidRecyclerViewDemo android-parallax-recyclerview RecyclerView - Android Developers ","permalink":"https://blog.substitute.tech/en/posts/androids-recyclerview2-%E8%A7%86%E5%B7%AE%E6%BB%9A%E5%8A%A8/","summary":"\u003cp\u003eParallax scrolling is a common UI effect where different layers move at different speeds during scrolling, creating a sense of depth.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"parallaxrecycler\" loading=\"lazy\" src=\"/images/parallaxrecycler.gif\"\u003e\u003c/p\u003e","title":"Android RecyclerView (Part 2): Parallax Scrolling"},{"content":"RecyclerView is a more flexible replacement for ListView. According to the official documentation, it efficiently maintains a limited collection of scrolling data items, and is recommended when your Views need to interact with user gestures and network data.\nKey Advantages RecyclerView simplifies View display and data handling in two main areas:\nLayout positioning — Managed by LayoutManager Item animations — Built-in add/remove animations with customization support Basic Usage RecyclerView requires a LayoutManager and an Adapter (extending RecyclerView.Adapter). The LayoutManager handles item positioning, recycling, and reuse, avoiding unnecessary overhead like findViewById.\nBuilt-in LayoutManagers:\nLayoutManager Effect LinearLayoutManager Vertical or horizontal scrolling list GridLayoutManager Grid layout StaggeredGridLayoutManager Staggered grid layout Animations RecyclerView enables add/remove animations by default. To customize, extend RecyclerView.ItemAnimator and call RecyclerView.setItemAnimator().\nReference implementation: RecyclerViewItemAnimators\nClick Handling RecyclerView does not have an onItemClickListener like ListView. The reason is that the original onItemClickListener was often confusing — RecyclerView has no strict row or column concept, so each Item View handles its own click events.\nFurther discussion: Why doesn\u0026rsquo;t RecyclerView have onItemClickListener()?\nLayoutManager Details LinearLayoutManager Behaves like ListView by default, with additional configuration options:\n1 LinearLayoutManager(Context context, int orientation, boolean reverseLayout) orientation is HORIZONTAL or VERTICAL; reverseLayout controls reverse ordering.\n1 mLayoutManager.setStackFromEnd(true); setStackFromEnd(true) displays items starting from the bottom, but has no effect when the data set changes.\nGridLayoutManager 1 2 3 mLayoutManager = new GridLayoutManager(this, 2, GridLayoutManager.VERTICAL, false); mRecyclerView.setLayoutManager(mLayoutManager); dataSet.addAll(Arrays.asList(LETTERS)); Two constructors:\nGridLayoutManager(Context context, int spanCount) — default vertical layout, spanCount controls columns GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) — same options as LinearLayoutManager References Source code AndroidRecyclerViewDemo Create dynamic lists with RecyclerView - Android Developers RecyclerView API Reference ","permalink":"https://blog.substitute.tech/en/posts/androids-recyclerview/","summary":"\u003cp\u003eRecyclerView is a more flexible replacement for ListView. According to the official documentation, it efficiently maintains a limited collection of scrolling data items, and is recommended when your Views need to interact with user gestures and network data.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"RecyclerView01\" loading=\"lazy\" src=\"/images/RecyclerView01.png\"\u003e\u003c/p\u003e","title":"Android RecyclerView"},{"content":"Canvas clipping limits the drawing region: only content within the clipped area is visible.\nRegion.Op Clip Modes Canvas supports combining multiple clip regions through Region.Op:\nEnum Value Description DIFFERENCE(0) Final region is the part of region1 NOT in region2 INTERSECT(1) Final region is the intersection of region1 and region2 UNION(2) Final region is the union of region1 and region2 XOR(3) Final region is the non-overlapping parts of region1 and region2 REVERSE_DIFFERENCE(4) Final region is the part of region2 NOT in region1 REPLACE(5) Final region is region2 Complete Example The following code demonstrates the effect of each clip mode:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 private void drawScene(Canvas canvas) { canvas.clipRect(0, 0, 100 * factor, 100 * factor); canvas.drawColor(Color.WHITE); mPaint.setColor(Color.RED); canvas.drawLine(0, 0, 100 * factor, 100 * factor, mPaint); mPaint.setColor(Color.GREEN); canvas.drawCircle(30 * factor, 70 * factor, 30 * factor, mPaint); mPaint.setColor(Color.BLUE); canvas.drawText(\u0026#34;Clipping\u0026#34;, 100 * factor, 30 * factor, mPaint); } @Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.GRAY); canvas.save(); canvas.translate(10 * factor, 10 * factor); drawScene(canvas); canvas.restore(); canvas.save(); canvas.translate(160 * factor, 10 * factor); canvas.clipRect(10 * factor, 10 * factor, 90 * factor, 90 * factor); canvas.clipRect(30 * factor, 30 * factor, 70 * factor, 70 * factor, Region.Op.DIFFERENCE); drawScene(canvas); canvas.restore(); canvas.save(); canvas.translate(10 * factor, 160 * factor); mPath.reset(); mPath.addCircle(50 * factor, 50 * factor, 50 * factor, Path.Direction.CCW); canvas.clipPath(mPath, Region.Op.REPLACE); drawScene(canvas); canvas.restore(); canvas.save(); canvas.translate(160 * factor, 160 * factor); canvas.clipRect(0, 0, 60 * factor, 60 * factor); canvas.clipRect(40 * factor, 40 * factor, 100 * factor, 100 * factor, Region.Op.UNION); drawScene(canvas); canvas.restore(); canvas.save(); canvas.translate(10 * factor, 310 * factor); canvas.clipRect(0, 0, 60 * factor, 60 * factor); canvas.clipRect(40 * factor, 40 * factor, 100 * factor, 100 * factor, Region.Op.XOR); drawScene(canvas); canvas.restore(); canvas.save(); canvas.translate(160 * factor, 310 * factor); canvas.clipRect(0, 0, 60 * factor, 60 * factor); canvas.clipRect(40 * factor, 40 * factor, 100 * factor, 100 * factor, Region.Op.REVERSE_DIFFERENCE); drawScene(canvas); canvas.restore(); } Using canvas.save() and canvas.restore() preserves and restores the Canvas state, ensuring each clip operation only affects the current drawing scope.\nReferences roamer\u0026rsquo; blog - Canvas Clip Source code Blog02 Canvas - Android Developers ","permalink":"https://blog.substitute.tech/en/posts/android-canvas2/","summary":"\u003cp\u003eCanvas clipping limits the drawing region: only content within the clipped area is visible.\u003c/p\u003e","title":"Android Canvas (Part 2): Clipping"},{"content":"A View renders its content by \u0026ldquo;drawing\u0026rdquo; — using a canvas (Canvas) and a paintbrush (Paint). The Canvas obtained through the onDraw method is directly composited onto the View.\nObtaining a Canvas There are three ways to get a Canvas instance:\nOverride View.onDraw — The Canvas is passed in as a parameter by the system Direct construction — Canvas c = new Canvas(Bitmap) or construct then call setBitmap Surface locking — Call SurfaceHolder.lockCanvas() Canvas Transformations Canvas provides transformation methods for translation, rotation, scaling, and more:\nrotate scale translate skew Canvas Layers The following content is adapted from roamer\u0026rsquo; blog\nA Canvas can be thought of as a drawing board where all drawing operations (drawBitmap, drawCircle, etc.) take place. The canvas also defines properties like Matrix and color.\nFor complex rendering needs (e.g., multi-layer animations, map overlays), Canvas supports layers. By default there is one layer. You can draw hierarchically using saveLayerXXX to create intermediate layers and restore to return.\nLayers are managed as a stack:\nAdapted from roamer\u0026rsquo; blog\nPush a new Layer onto the stack with saveLayer or savaLayerAlpha; pop with restore or restoreToCount. While a Layer is on the stack, all DrawXXX calls affect it. When the Layer is popped, its content is composited onto the parent layer or the Canvas, optionally with a transparency value.\nComplete Drawing Example The following example demonstrates various Canvas drawing operations:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 public class CanvasView extends View { private Paint arcPaint; private RectF rectF; private Shader mShader; private Path mPath; private Bitmap bitmap; public CanvasView(Context context) { super(context); } public CanvasView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG); arcPaint.setColor(Color.BLUE); rectF = new RectF(260, 70, 290, 115); mShader = new LinearGradient(0, 0, 100, 100, new int[] {Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.LTGRAY}, null, Shader.TileMode.REPEAT); mPath = new Path(); mPath.reset(); bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Background fill canvas.drawARGB(255, 0, 180, 255); canvas.drawColor(Color.RED); // TODO PorterDuff.Mode — needs further study canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); canvas.drawRGB(255, 255, 255); arcPaint.setColor(Color.RED); canvas.drawText(\u0026#34;Circle:\u0026#34;, 10, 50, arcPaint); canvas.drawCircle(100, 45, 10, arcPaint); arcPaint.setAntiAlias(true); canvas.drawCircle(150, 45, 20, arcPaint); canvas.drawText(\u0026#34;Lines \u0026amp; Arcs:\u0026#34;, 10, 100, arcPaint); arcPaint.setColor(Color.GREEN); canvas.drawLine(160, 90, 210, 90, arcPaint); canvas.drawLine(210, 75, 270, 55, arcPaint); arcPaint.setStyle(Paint.Style.STROKE); canvas.drawCircle(300, 100, 50, arcPaint); canvas.drawArc(rectF, 180, 180, false, arcPaint); rectF.set(310, 70, 340, 115); canvas.drawArc(rectF, 180, 180, false, arcPaint); rectF.set(290, 110, 310, 125); canvas.drawArc(rectF, 0, 180, false, arcPaint); canvas.drawText(\u0026#34;Rectangles:\u0026#34;, 50, 150, arcPaint); arcPaint.setColor(Color.GRAY); arcPaint.setStyle(Paint.Style.FILL); canvas.drawRect(160, 135, 210, 185, arcPaint); canvas.drawRect(215, 150, 260, 210, arcPaint); canvas.drawText(\u0026#34;Sector \u0026amp; Oval:\u0026#34;, 10, 250, arcPaint); arcPaint.setShader(mShader); rectF.set(150, 190, 290, 310); canvas.drawArc(rectF, 200, 130, true, arcPaint); rectF.set(300, 200, 420, 300); canvas.drawOval(rectF, arcPaint); canvas.drawText(\u0026#34;Triangle:\u0026#34;, 10, 380, arcPaint); mPath.moveTo(100, 330); mPath.lineTo(150, 430); mPath.lineTo(180, 350); mPath.close(); canvas.drawPath(mPath, arcPaint); // Hexagon arcPaint.reset(); arcPaint.setColor(Color.LTGRAY); arcPaint.setStyle(Paint.Style.STROKE); mPath.reset(); mPath.moveTo(280, 400); mPath.lineTo(300, 400); mPath.lineTo(310, 410); mPath.lineTo(300, 420); mPath.lineTo(280, 420); mPath.lineTo(270, 410); mPath.close(); canvas.drawPath(mPath, arcPaint); // Rounded rectangle arcPaint.setStyle(Paint.Style.FILL); arcPaint.setColor(Color.LTGRAY); arcPaint.setAntiAlias(true); canvas.drawText(\u0026#34;Rounded Rectangle:\u0026#34;, 10, 450, arcPaint); rectF.set(180, 430, 300, 470); canvas.drawRoundRect(rectF, 20, 15, arcPaint); // Bezier curve canvas.drawText(\u0026#34;Bezier Curve:\u0026#34;, 10, 310, arcPaint); arcPaint.reset(); arcPaint.setStyle(Paint.Style.STROKE); arcPaint.setColor(Color.GREEN); mPath.reset(); mPath.moveTo(180, 310); mPath.quadTo(250, 250, 200, 350); canvas.drawPath(mPath, arcPaint); // Points arcPaint.setStyle(Paint.Style.FILL); canvas.drawText(\u0026#34;Points:\u0026#34;, 10, 520, arcPaint); canvas.drawPoint(60, 520, arcPaint); canvas.drawPoints(new float[] {60, 550, 65, 560, 70, 570}, arcPaint); // Bitmap canvas.drawBitmap(bitmap, 350, 350, arcPaint); } } References roamer\u0026rsquo; blog - Canvas Guide Source code Canvas(1) Canvas - Android Developers Paint - Android Developers Create a custom drawing - Android Developers ","permalink":"https://blog.substitute.tech/en/posts/android-canvas1/","summary":"\u003cp\u003eA View renders its content by \u0026ldquo;drawing\u0026rdquo; — using a canvas (\u003ccode\u003eCanvas\u003c/code\u003e) and a paintbrush (\u003ccode\u003ePaint\u003c/code\u003e). The \u003ccode\u003eCanvas\u003c/code\u003e obtained through the \u003ccode\u003eonDraw\u003c/code\u003e method is directly composited onto the View.\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Canvas01\" loading=\"lazy\" src=\"/images/android_canvas01.png\"\u003e\u003c/p\u003e","title":"Android Canvas (Part 1)"},{"content":"When a ViewGroup measures its child views, it specifies a measurement mode through MeasureSpec. Understanding these three modes is fundamental to custom View layout.\nMeasurement Modes Mode Description Trigger EXACTLY Precise value Child View width/height is a specific value or match_parent AT_MOST Maximum bound Child View width/height is wrap_content UNSPECIFIED No constraint Common in AdapterView, ScrollView child height EXACTLY The parent specifies an exact size for the child. This occurs when the child\u0026rsquo;s layout_width or layout_height is a fixed value or match_parent. The child must render within the given dimensions.\nAT_MOST The child is constrained to a maximum value. This occurs when wrap_content is set. The child should calculate its own size based on content, but cannot exceed the parent\u0026rsquo;s upper bound.\nUNSPECIFIED No constraints at all. The parent imposes no size restrictions, and the child can take any dimensions it needs. This is common in scrollable containers like ScrollView and ListView.\nReferences Source code BlogCode01 Creating Custom Views - Android Developers View.MeasureSpec - Android Developers ","permalink":"https://blog.substitute.tech/en/posts/customview2/","summary":"","title":"Android Custom View (Part 2): Measurement Modes"},{"content":"When you first start writing custom Views in Android, it\u0026rsquo;s common to feel unsure where to begin. Generally, there are two approaches:\nFrom scratch: Extend View and implement the appearance entirely through calculation and drawing. Extend an existing View: Add child views to an existing component, or override methods to change its behavior. Subclassing View Here is the simplest example of extending View:\n1 2 3 4 5 6 7 8 9 10 public class SimpleView extends View { public SimpleView(Context context) { super(context); } public SimpleView(Context context, AttributeSet attrs) { super(context, attrs); } } If you don\u0026rsquo;t need XML attributes, you can omit the AttributeSet constructor — but then the View cannot be used in XML layouts.\nCustom Attributes AttributeSet parses XML attributes into an array for code access. Attribute types must be declared in advance, typically in res/values/attrs.xml:\n1 2 3 4 5 6 7 \u0026lt;declare-styleable name=\u0026#34;SimpleView\u0026#34;\u0026gt; \u0026lt;attr name=\u0026#34;showContent\u0026#34; format=\u0026#34;boolean\u0026#34; /\u0026gt; \u0026lt;attr name=\u0026#34;showPosition\u0026#34; format=\u0026#34;enum\u0026#34;\u0026gt; \u0026lt;enum name=\u0026#34;left\u0026#34; value=\u0026#34;0\u0026#34; /\u0026gt; \u0026lt;enum name=\u0026#34;right\u0026#34; value=\u0026#34;1\u0026#34; /\u0026gt; \u0026lt;/attr\u0026gt; \u0026lt;/declare-styleable\u0026gt; Supported types: boolean, string, dimension, enum, fraction, reference, color. You can also use | to specify multiple types.\nUsing Custom Attributes in XML 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;RelativeLayout xmlns:android=\u0026#34;http://schemas.android.com/apk/res/android\u0026#34; xmlns:tools=\u0026#34;http://schemas.android.com/tools\u0026#34; xmlns:hxq=\u0026#34;http://schemas.android.com/apk/res/blog.haoxiqiang\u0026#34; android:layout_width=\u0026#34;match_parent\u0026#34; android:layout_height=\u0026#34;match_parent\u0026#34; tools:context=\u0026#34;blog.haoxiqiang.MainActivity\u0026#34; \u0026gt; \u0026lt;blog.haoxiqiang.SimpleView android:layout_width=\u0026#34;wrap_content\u0026#34; android:layout_height=\u0026#34;wrap_content\u0026#34; hxq:showContent=\u0026#34;true\u0026#34; hxq:showPosition=\u0026#34;right\u0026#34; android:text=\u0026#34;@string/hello_world\u0026#34; /\u0026gt; \u0026lt;/RelativeLayout\u0026gt; The namespace format: http://schemas.android.com/apk/res/[your package name] or http://schemas.android.com/apk/res/auto.\nComplete Example: View with Circle and Text This custom View draws a blue circle with optional text. showContent toggles the text, and showPosition controls whether it draws on the left or right side of the screen.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package blog.haoxiqiang; public class SimpleView extends View { // attr private boolean showContent = false; private int position = 0; // param private Paint mCirclePaint; private Paint mTextPaint; private final int radius = 100; private int width = 0; private boolean initLayout = false; public SimpleView(Context context) { super(context); init(context); } public SimpleView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SimpleView); if (typedArray != null) { showContent = typedArray.getBoolean(R.styleable.SimpleView_showContent, false); position = typedArray.getInt(R.styleable.SimpleView_showPosition, 0); } typedArray.recycle(); init(context); } private void init(Context context) { mCirclePaint = new Paint(); mCirclePaint.setColor(Color.BLUE); mCirclePaint.setStrokeWidth(8); mTextPaint = new Paint(); mTextPaint.setColor(Color.RED); mTextPaint.setStrokeWidth(5); mTextPaint.setTextSize(35.0f); this.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { if (!initLayout) { width = SimpleView.this.getWidth(); Log.i(\u0026#34;onGlobalLayout\u0026#34;, \u0026#34;initLayout:\u0026#34; + width); SimpleView.this.invalidate(); initLayout = true; } } }); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int x = position == 0 ? radius : width - 2 * radius; x = x \u0026lt; 0 ? 0 : x; canvas.drawCircle(x, 100, radius, mCirclePaint); if (showContent) { canvas.drawText(\u0026#34;SimpleView\u0026#34;, x, 100, mTextPaint); } } } The next part will discuss how onMeasure works.\nReferences Source code BlogCode01 Creating Custom Views - Android Developers Create custom view components - Android Developers ","permalink":"https://blog.substitute.tech/en/posts/customview1/","summary":"\u003cp\u003eWhen you first start writing custom Views in Android, it\u0026rsquo;s common to feel unsure where to begin. Generally, there are two approaches:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eFrom scratch\u003c/strong\u003e: Extend \u003ccode\u003eView\u003c/code\u003e and implement the appearance entirely through calculation and drawing.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eExtend an existing View\u003c/strong\u003e: Add child views to an existing component, or override methods to change its behavior.\u003c/li\u003e\n\u003c/ol\u003e","title":"Android Custom View (Part 1)"},{"content":"This is the first post of this blog. I referenced many Jekyll templates at the time, adapted a custom theme inspired by Pure, and hosted it on GitHub.\nSource code: haoxiqiang-template\nJekyll Overview Jekyll is a static site generator that converts plain text (Markdown, Liquid templates) into a complete static website without requiring a database. Combined with GitHub Pages, it provides free blog hosting.\nFeatures Code Highlighting Jekyll has built-in syntax highlighting powered by Rouge:\n1 2 3 4 5 def print_hi(name) puts \u0026#34;Hi, #{name}\u0026#34; end print_hi(\u0026#39;Tom\u0026#39;) #=\u0026gt; prints \u0026#39;Hi, Tom\u0026#39; LaTeX Math Rendering Set latex: true in the page front matter to enable MathJax rendering:\n$$ \\begin{aligned} \\dot{x} \u0026amp;= \\sigma(y-x) \\ \\dot{y} \u0026amp;= \\rho x - y - xz \\ \\dot{z} \u0026amp;= -\\beta z + xy \\end{aligned} $$\n$$ a^2 + b^2 = c^2 $$\nWhen $a \\ne 0$, there are two solutions to $ax^2 + bx + c = 0$.\nExcerpt Separator Use `\n` to mark the split point between the article summary and the full content.\nTechnical Dependencies Tool Description Version Jekyll Static blog generator 2.5.2 MathJax LaTeX formula rendering 2.4.0 Duoshuo Comment system (link may no longer be active) 1.0.0 Disqus Comment system 1.0.0 haoxiqiang-template Blog theme 1.0.0 References Jekyll Official Documentation MathJax Official Documentation GitHub Pages ","permalink":"https://blog.substitute.tech/en/posts/welcome/","summary":"\u003cp\u003eThis is the first post of this blog. I referenced many Jekyll templates at the time, adapted a custom theme inspired by \u003ccode\u003ePure\u003c/code\u003e, and hosted it on GitHub.\u003c/p\u003e\n\u003cp\u003eSource code: \u003ca href=\"https://github.com/Haoxiqiang/haoxiqiang-template\"\u003ehaoxiqiang-template\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"jekyll-overview\"\u003eJekyll Overview\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://jekyllrb.com/\"\u003eJekyll\u003c/a\u003e is a static site generator that converts plain text (Markdown, Liquid templates) into a complete static website without requiring a database. Combined with \u003ca href=\"https://pages.github.com/\"\u003eGitHub Pages\u003c/a\u003e, it provides free blog hosting.\u003c/p\u003e\n\u003ch2 id=\"features\"\u003eFeatures\u003c/h2\u003e\n\u003ch3 id=\"code-highlighting\"\u003eCode Highlighting\u003c/h3\u003e\n\u003cp\u003eJekyll has built-in syntax highlighting powered by Rouge:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-ruby\" data-lang=\"ruby\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eprint_hi\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nb\"\u003eputs\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;Hi, \u003c/span\u003e\u003cspan class=\"si\"\u003e#{\u003c/span\u003e\u003cspan class=\"nb\"\u003ename\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eprint_hi\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s1\"\u003e\u0026#39;Tom\u0026#39;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e#=\u0026gt; prints \u0026#39;Hi, Tom\u0026#39;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"latex-math-rendering\"\u003eLaTeX Math Rendering\u003c/h3\u003e\n\u003cp\u003eSet \u003ccode\u003elatex: true\u003c/code\u003e in the page front matter to enable MathJax rendering:\u003c/p\u003e\n\u003cp\u003e$$\n\\begin{aligned} \\dot{x} \u0026amp;= \\sigma(y-x) \\\n\\dot{y} \u0026amp;= \\rho x - y - xz \\\n\\dot{z} \u0026amp;= -\\beta z + xy \\end{aligned}\n$$\u003c/p\u003e\n\u003cp\u003e$$\na^2 + b^2 = c^2\n$$\u003c/p\u003e\n\u003cp\u003eWhen $a \\ne 0$, there are two solutions to $ax^2 + bx + c = 0$.\u003c/p\u003e\n\u003ch3 id=\"excerpt-separator\"\u003eExcerpt Separator\u003c/h3\u003e\n\u003cp\u003eUse `\u003c/p\u003e","title":"Building a Blog with Jekyll"},{"content":"About Me I\u0026rsquo;m Haoxiqiang, a software engineer working across multiple domains.\nMain areas of expertise:\nChromium — Browser engine development Android — App-level and system-level development Server — Backend services and infrastructure Contact GitHub: haoxiqiang Email: haoxiqiang@live.com About This Blog This blog documents my technical notes and thoughts from practice. Built with Hugo, themed with PaperMod, hosted on GitHub Pages.\n","permalink":"https://blog.substitute.tech/en/about/","summary":"about","title":"About"}]