diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml
index 6813d6dd0..d76d31a61 100644
--- a/.github/workflows/build-binaries.yml
+++ b/.github/workflows/build-binaries.yml
@@ -17,13 +17,17 @@ on:
paths:
- 'external/**'
+permissions:
+ contents: write # Required to git push compiled binaries and create GitHub Releases
+ actions: read
+
jobs:
build-busybox:
name: Build Busybox
runs-on: ubuntu-latest
strategy:
matrix:
- arch: [arm, arm64, mips, x86]
+ arch: [arm, arm64, x86]
include:
- arch: arm
target: arm-linux-gnueabihf
@@ -33,10 +37,6 @@ jobs:
target: aarch64-linux-gnu
cc: aarch64-linux-gnu-gcc
strip: aarch64-linux-gnu-strip
- - arch: mips
- target: mips-linux-gnu
- cc: mips-linux-gnu-gcc
- strip: mips-linux-gnu-strip
- arch: x86
target: i686-linux-gnu
cc: i686-linux-gnu-gcc
@@ -52,9 +52,66 @@ jobs:
sudo apt-get install -y \
build-essential \
wget \
- gcc-${{ matrix.target }} \
+ unzip \
file
+ - name: Set up Android NDK
+ run: |
+ NDK_VERSION="r26d"
+ echo "Downloading Android NDK ${NDK_VERSION}..."
+
+ # Download with retries and better error handling
+ for i in {1..3}; do
+ if wget -q "https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip"; then
+ echo "✓ Downloaded android-ndk-${NDK_VERSION}-linux.zip (attempt $i)"
+ break
+ fi
+ echo "Download attempt $i failed, retrying..."
+ sleep 5
+ done
+
+ # Verify download
+ if [ ! -f "android-ndk-${NDK_VERSION}-linux.zip" ]; then
+ echo "❌ Failed to download NDK after 3 attempts"
+ exit 1
+ fi
+
+ echo "Extracting NDK..."
+ unzip -q android-ndk-${NDK_VERSION}-linux.zip
+
+ # Verify extraction
+ if [ ! -d "android-ndk-${NDK_VERSION}" ]; then
+ echo "❌ NDK extraction failed - directory not found"
+ ls -la
+ exit 1
+ fi
+
+ export ANDROID_NDK_ROOT="$(pwd)/android-ndk-${NDK_VERSION}"
+ echo "✓ NDK setup complete: ${ANDROID_NDK_ROOT}"
+ echo "ANDROID_NDK_ROOT=${ANDROID_NDK_ROOT}" >> $GITHUB_ENV
+
+ # Set up NDK toolchain paths (including NDK_AR for llvm-ar)
+ case "${{ matrix.arch }}" in
+ arm64)
+ echo "NDK_CC=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang" >> $GITHUB_ENV
+ echo "NDK_STRIP=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" >> $GITHUB_ENV
+ echo "NDK_AR=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" >> $GITHUB_ENV
+ echo "NDK_TARGET=aarch64-linux-android" >> $GITHUB_ENV
+ ;;
+ arm)
+ echo "NDK_CC=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi28-clang" >> $GITHUB_ENV
+ echo "NDK_STRIP=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" >> $GITHUB_ENV
+ echo "NDK_AR=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" >> $GITHUB_ENV
+ echo "NDK_TARGET=armv7a-linux-androideabi" >> $GITHUB_ENV
+ ;;
+ x86)
+ echo "NDK_CC=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android28-clang" >> $GITHUB_ENV
+ echo "NDK_STRIP=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" >> $GITHUB_ENV
+ echo "NDK_AR=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar" >> $GITHUB_ENV
+ echo "NDK_TARGET=i686-linux-android" >> $GITHUB_ENV
+ ;;
+ esac
+
- name: Download Busybox source
run: |
BUSYBOX_VERSION="${{ github.event.inputs.busybox_version || '1.36.1' }}"
@@ -77,66 +134,200 @@ jobs:
run: |
cd "$BUSYBOX_DIR"
- # Start with minimal config to avoid cross-compilation issues
- make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.target }}- allnoconfig
-
- # Enable only essential features for AFWall+
- cat >> .config << EOF
- CONFIG_STATIC=y
- CONFIG_INSTALL_NO_USR=y
- CONFIG_BUSYBOX=y
- CONFIG_BUSYBOX_EXEC_PATH="/system/xbin/busybox"
- CONFIG_ASH=y
- CONFIG_SH_IS_ASH=y
- CONFIG_BASH_IS_NONE=y
- CONFIG_CAT=y
- CONFIG_CHMOD=y
- CONFIG_CP=y
- CONFIG_ECHO=y
- CONFIG_GREP=y
- CONFIG_KILL=y
- CONFIG_KILLALL=y
- CONFIG_LS=y
- CONFIG_MKDIR=y
- CONFIG_MV=y
- CONFIG_PS=y
- CONFIG_RM=y
- CONFIG_TEST=y
- CONFIG_WHICH=y
- CONFIG_IPTABLES=y
- CONFIG_IP6TABLES=y
- CONFIG_FEATURE_PREFER_APPLETS=n
- CONFIG_FEATURE_SH_STANDALONE=y
- EOF
+ # Use the basic Android default config (more stable than android_ndk_defconfig)
+ cp configs/android_defconfig .config
+
+ # Fix the cross-compiler prefix for NDK
+ sed -i 's/CONFIG_CROSS_COMPILER_PREFIX=.*/CONFIG_CROSS_COMPILER_PREFIX=""/' .config
+
+ # Process the initial config
+ yes '' | make ARCH=${{ matrix.arch }} oldconfig
+
+ # Disable problematic applets for Android bionic libc compatibility
+ echo "Disabling problematic applets for Android..."
+ sed -i 's/CONFIG_SWAPON=y/# CONFIG_SWAPON is not set/g' .config
+ sed -i 's/CONFIG_SWAPOFF=y/# CONFIG_SWAPOFF is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_SWAPON_DISCARD=y/# CONFIG_FEATURE_SWAPON_DISCARD is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_SWAPON_PRI=y/# CONFIG_FEATURE_SWAPON_PRI is not set/g' .config
+
+ # Disable problematic archival features
+ sed -i 's/CONFIG_TAR=y/# CONFIG_TAR is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_TAR_TO_COMMAND=y/# CONFIG_FEATURE_TAR_TO_COMMAND is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_TAR_FROM=y/# CONFIG_FEATURE_TAR_FROM is not set/g' .config
+ sed -i 's/CONFIG_AR=y/# CONFIG_AR is not set/g' .config
+ sed -i 's/CONFIG_DPKG=y/# CONFIG_DPKG is not set/g' .config
+ sed -i 's/CONFIG_DPKG_DEB=y/# CONFIG_DPKG_DEB is not set/g' .config
+ sed -i 's/CONFIG_RPM=y/# CONFIG_RPM is not set/g' .config
+ sed -i 's/CONFIG_RPM2CPIO=y/# CONFIG_RPM2CPIO is not set/g' .config
+
+ # Disable features that require crypt.h (not available in Android bionic)
+ sed -i 's/CONFIG_LOGIN=y/# CONFIG_LOGIN is not set/g' .config
+ sed -i 's/CONFIG_PASSWD=y/# CONFIG_PASSWD is not set/g' .config
+ sed -i 's/CONFIG_SU=y/# CONFIG_SU is not set/g' .config
+ sed -i 's/CONFIG_SULOGIN=y/# CONFIG_SULOGIN is not set/g' .config
+ sed -i 's/CONFIG_VLOCK=y/# CONFIG_VLOCK is not set/g' .config
+ sed -i 's/CONFIG_CRYPTPW=y/# CONFIG_CRYPTPW is not set/g' .config
+ sed -i 's/CONFIG_CHPASSWD=y/# CONFIG_CHPASSWD is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_SHADOWPASSWDS=y/# CONFIG_FEATURE_SHADOWPASSWDS is not set/g' .config
+
+ # Disable other bionic-incompatible features
+ sed -i 's/CONFIG_FEATURE_UTMP=y/# CONFIG_FEATURE_UTMP is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_WTMP=y/# CONFIG_FEATURE_WTMP is not set/g' .config
+ sed -i 's/CONFIG_LAST=y/# CONFIG_LAST is not set/g' .config
+ sed -i 's/CONFIG_USERS=y/# CONFIG_USERS is not set/g' .config
+ sed -i 's/CONFIG_WHO=y/# CONFIG_WHO is not set/g' .config
+ sed -i 's/CONFIG_W=y/# CONFIG_W is not set/g' .config
+
+ # Disable networking features that require crypt.h
+ sed -i 's/CONFIG_FTPD=y/# CONFIG_FTPD is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_FTP_AUTHENTICATION=y/# CONFIG_FEATURE_FTP_AUTHENTICATION is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_HTTPD_AUTH_MD5=y/# CONFIG_FEATURE_HTTPD_AUTH_MD5 is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_HTTPD_BASIC_AUTH=y/# CONFIG_FEATURE_HTTPD_BASIC_AUTH is not set/g' .config
+ sed -i 's/CONFIG_MKPASSWD=y/# CONFIG_MKPASSWD is not set/g' .config
+
+ # Reprocess config
+ yes '' | make ARCH=${{ matrix.arch }} oldconfig
+
+ # Fix x86 register pressure issue in TLS code
+ if [ "${{ matrix.arch }}" = "x86" ]; then
+ echo "Disabling ALL TLS and crypto features for x86 due to register pressure"
+
+ # Disable all TLS/SSL-related features comprehensively
+ sed -i 's/CONFIG_TLS=y/# CONFIG_TLS is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_TLS_SHA1=y/# CONFIG_FEATURE_TLS_SHA1 is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_WGET_HTTPS=y/# CONFIG_FEATURE_WGET_HTTPS is not set/g' .config
+ sed -i 's/CONFIG_FTPS=y/# CONFIG_FTPS is not set/g' .config
+ sed -i 's/CONFIG_SHA1SUM=y/# CONFIG_SHA1SUM is not set/g' .config
+ sed -i 's/CONFIG_SHA256SUM=y/# CONFIG_SHA256SUM is not set/g' .config
+ sed -i 's/CONFIG_SHA512SUM=y/# CONFIG_SHA512SUM is not set/g' .config
+ sed -i 's/CONFIG_MD5SUM=y/# CONFIG_MD5SUM is not set/g' .config
+
+ # Disable SSL client utilities that depend on TLS
+ sed -i 's/CONFIG_SSL_CLIENT=y/# CONFIG_SSL_CLIENT is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_SSL_CLIENT=y/# CONFIG_FEATURE_SSL_CLIENT is not set/g' .config
+
+ # Disable wget entirely if it depends on TLS (safer approach)
+ sed -i 's/CONFIG_WGET=y/# CONFIG_WGET is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_WGET_STATUSBAR=y/# CONFIG_FEATURE_WGET_STATUSBAR is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_WGET_AUTHENTICATION=y/# CONFIG_FEATURE_WGET_AUTHENTICATION is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_WGET_LONG_OPTIONS=y/# CONFIG_FEATURE_WGET_LONG_OPTIONS is not set/g' .config
+ sed -i 's/CONFIG_FEATURE_WGET_TIMEOUT=y/# CONFIG_FEATURE_WGET_TIMEOUT is not set/g' .config
+
+ # Disable any crypto/hash utilities that might link to TLS code
+ sed -i 's/CONFIG_BASE64=y/# CONFIG_BASE64 is not set/g' .config
+ sed -i 's/CONFIG_UUENCODE=y/# CONFIG_UUENCODE is not set/g' .config
+ sed -i 's/CONFIG_UUDECODE=y/# CONFIG_UUDECODE is not set/g' .config
+
+ # Multiple reprocessing to ensure all dependencies are resolved
+ yes '' | make ARCH=${{ matrix.arch }} oldconfig
+ yes '' | make ARCH=${{ matrix.arch }} oldconfig
+
+ # Remove ALL TLS/SSL-related source files to prevent any compilation
+ echo "Removing all TLS/SSL source files for x86..."
+ for pattern in "tls*.c" "ssl_client.c"; do
+ find networking -name "$pattern" -type f | while read tls_file; do
+ if [ -f "$tls_file" ]; then
+ echo "Removing $tls_file"
+ rm "$tls_file"
+ touch "$tls_file"
+ fi
+ done
+ done
+ fi
+
+ # Show what's enabled for debugging
+ echo "=== Essential applets for AFWall+ ==="
+ grep -E "CONFIG_(PING|IFCONFIG|LS|ECHO|CAT|GREP|FEATURE_INSTALLER)=" .config || true
- # Apply minimal config (use yes to answer all prompts with default)
- yes '' | make ARCH=${{ matrix.arch }} CROSS_COMPILE=${{ matrix.target }}- oldconfig
+ echo "=== Problematic features (should be disabled) ==="
+ grep -E "CONFIG_(SWAPON|TAR_TO_COMMAND|DPKG|RPM|TLS).*=" .config || true
- # Fix cross-compilation issues for all architectures
- # Replace the problematic off_t size check that fails during cross-compilation
+ # Fix cross-compilation and duplicate symbol issues
+ echo "Applying Android compatibility fixes..."
+
+ # Fix off_t size check
if grep -q "struct BUG_off_t_size_is_misdetected" include/libbb.h; then
sed -i '/struct BUG_off_t_size_is_misdetected/,/};/c\
- /* Cross-compilation fix: off_t size check disabled for all architectures */' include/libbb.h
- echo "Applied cross-compilation fix for ${{ matrix.arch }}"
+ /* Cross-compilation fix: off_t size check disabled */' include/libbb.h
+ fi
+
+ # Fix FAST_FUNC calling convention
+ sed -i 's/__attribute__((regparm(3),stdcall))/__attribute__((regparm(3)))/g' include/platform.h
+
+ # Remove strchrnul conflicts
+ sed -i '/extern char \*strchrnul.*FAST_FUNC;/d' include/platform.h
+ sed -i '/char\* FAST_FUNC strchrnul/,/^}/d' libbb/platform.c
+
+ # Create stub for data_extract_to_command to fix linking
+ if [ ! -f "archival/libarchive/data_extract_to_command.c.bak" ]; then
+ cp archival/libarchive/data_extract_to_command.c archival/libarchive/data_extract_to_command.c.bak
+ cat > archival/libarchive/data_extract_to_command.c << 'EOF'
+ /* Stub for data_extract_to_command when FEATURE_TAR_TO_COMMAND is disabled */
+ #include "libbb.h"
+ #include "bb_archive.h"
+
+ void FAST_FUNC data_extract_to_command(archive_handle_t *archive_handle)
+ {
+ bb_simple_error_msg_and_die("--to-command feature not compiled");
+ }
+ EOF
+ # Force compile the stub by enabling it in Kbuild temporarily
+ sed -i 's/lib-\$(CONFIG_FEATURE_TAR_TO_COMMAND)/lib-y/' archival/libarchive/Kbuild
+ fi
+
+ # Create stub for pw_encrypt to fix linking issues when password features are disabled
+ if [ ! -f "libbb/pw_encrypt.c.bak" ]; then
+ cp libbb/pw_encrypt.c libbb/pw_encrypt.c.bak
+ cat > libbb/pw_encrypt.c << 'EOF'
+ /* Stub for pw_encrypt when crypt.h features are disabled */
+ #include "libbb.h"
+
+ char* FAST_FUNC pw_encrypt(const char *clear, const char *salt, int cleanup)
+ {
+ bb_simple_error_msg_and_die("password encryption not supported");
+ return NULL;
+ }
+ EOF
+ fi
+
+ # Fix duplicate syscall symbols
+ if [ -f "libbb/missing_syscalls.c" ]; then
+ echo "Fixing duplicate syscall symbols..."
+ sed -i '/pid_t.*getsid/,/^}/d' libbb/missing_syscalls.c
+ sed -i '/int.*adjtimex/,/^}/d' libbb/missing_syscalls.c
+ sed -i '/int.*sethostname/,/^}/d' libbb/missing_syscalls.c
+ sed -i '/getsid.*(/d' libbb/missing_syscalls.c
+ sed -i '/adjtimex.*(/d' libbb/missing_syscalls.c
+ sed -i '/sethostname.*(/d' libbb/missing_syscalls.c
+ fi
+
+ if [ -f "include/libbb.h" ]; then
+ sed -i '/extern.*getsid.*(/d' include/libbb.h
+ sed -i '/extern.*adjtimex.*(/d' include/libbb.h
+ sed -i '/extern.*sethostname.*(/d' include/libbb.h
fi
- name: Build Busybox
run: |
cd "$BUSYBOX_DIR"
- # Build with explicit compiler settings
+ # Build with Android NDK
make ARCH=${{ matrix.arch }} \
- CROSS_COMPILE=${{ matrix.target }}- \
- CC=${{ matrix.cc }} \
+ CC="${NDK_CC}" \
+ AR="${NDK_AR}" \
HOSTCC=gcc \
HOSTCXX=g++ \
+ STRIP="${NDK_STRIP}" \
+ CFLAGS="-static -DHAVE_STRCHRNUL" \
+ EXTRA_CFLAGS="-DHAVE_STRCHRNUL" \
+ LDFLAGS="-static" \
-j$(nproc)
# Verify binary
file busybox
# Strip and copy
- ${{ matrix.strip }} busybox
+ ${NDK_STRIP} busybox
cp busybox ../busybox_${{ matrix.arch }}
- name: Upload Busybox artifact
@@ -150,7 +341,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- arch: [arm, arm64, mips, x86]
+ arch: [arm, arm64, x86]
include:
- arch: arm
target: arm-linux-gnueabihf
@@ -162,11 +353,6 @@ jobs:
cc: aarch64-linux-gnu-gcc
strip: aarch64-linux-gnu-strip
configure_host: aarch64-linux-gnu
- - arch: mips
- target: mips-linux-gnu
- cc: mips-linux-gnu-gcc
- strip: mips-linux-gnu-strip
- configure_host: mips-linux-gnu
- arch: x86
target: i686-linux-gnu
cc: i686-linux-gnu-gcc
@@ -183,13 +369,67 @@ jobs:
sudo apt-get install -y \
build-essential \
wget \
- gcc-${{ matrix.target }} \
+ unzip \
pkg-config \
autoconf \
automake \
libtool \
file
+ - name: Set up Android NDK
+ run: |
+ NDK_VERSION="r26d"
+ echo "Downloading Android NDK ${NDK_VERSION}..."
+
+ # Download with retries and better error handling
+ for i in {1..3}; do
+ if wget -q "https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip"; then
+ echo "✓ Downloaded android-ndk-${NDK_VERSION}-linux.zip (attempt $i)"
+ break
+ fi
+ echo "Download attempt $i failed, retrying..."
+ sleep 5
+ done
+
+ # Verify download
+ if [ ! -f "android-ndk-${NDK_VERSION}-linux.zip" ]; then
+ echo "❌ Failed to download NDK after 3 attempts"
+ exit 1
+ fi
+
+ echo "Extracting NDK..."
+ unzip -q android-ndk-${NDK_VERSION}-linux.zip
+
+ # Verify extraction
+ if [ ! -d "android-ndk-${NDK_VERSION}" ]; then
+ echo "❌ NDK extraction failed - directory not found"
+ ls -la
+ exit 1
+ fi
+
+ export ANDROID_NDK_ROOT="$(pwd)/android-ndk-${NDK_VERSION}"
+ echo "✓ NDK setup complete: ${ANDROID_NDK_ROOT}"
+ echo "ANDROID_NDK_ROOT=${ANDROID_NDK_ROOT}" >> $GITHUB_ENV
+
+ # Set up NDK toolchain paths
+ case "${{ matrix.arch }}" in
+ arm64)
+ echo "NDK_CC=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android28-clang" >> $GITHUB_ENV
+ echo "NDK_STRIP=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" >> $GITHUB_ENV
+ echo "NDK_TARGET=aarch64-linux-android" >> $GITHUB_ENV
+ ;;
+ arm)
+ echo "NDK_CC=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi28-clang" >> $GITHUB_ENV
+ echo "NDK_STRIP=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" >> $GITHUB_ENV
+ echo "NDK_TARGET=armv7a-linux-androideabi" >> $GITHUB_ENV
+ ;;
+ x86)
+ echo "NDK_CC=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android28-clang" >> $GITHUB_ENV
+ echo "NDK_STRIP=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" >> $GITHUB_ENV
+ echo "NDK_TARGET=i686-linux-android" >> $GITHUB_ENV
+ ;;
+ esac
+
- name: Download iptables source
run: |
IPTABLES_VERSION="${{ github.event.inputs.iptables_version || '1.8.10' }}"
@@ -212,33 +452,67 @@ jobs:
run: |
cd "$IPTABLES_DIR"
- # Set cross-compilation environment
- export CC=${{ matrix.cc }}
- export STRIP=${{ matrix.strip }}
- export CFLAGS="-static -Os -DANDROID"
+ # Set Android NDK cross-compilation environment
+ export CC="${NDK_CC}"
+ export STRIP="${NDK_STRIP}"
+
+ # Set architecture-specific TLS flags and fix lock path
+ case "${{ matrix.arch }}" in
+ arm64)
+ export CFLAGS="-static -Os -DANDROID -D_GNU_SOURCE -Wno-logical-op -Wno-unknown-warning-option -fno-emulated-tls -DXTABLES_LOCK_DIR='\"/data/local/tmp\"'"
+ ;;
+ *)
+ export CFLAGS="-static -Os -DANDROID -D_GNU_SOURCE -Wno-logical-op -Wno-unknown-warning-option -DXTABLES_LOCK_DIR='\"/data/local/tmp\"'"
+ ;;
+ esac
+
export LDFLAGS="-static"
export PKG_CONFIG_PATH=""
- # Configure for cross-compilation
+ # Configure for Android cross-compilation
./configure \
- --host=${{ matrix.configure_host }} \
+ --host=${NDK_TARGET} \
--enable-static \
--disable-shared \
--disable-nftables \
--disable-bpf-compiler \
--disable-connlabel \
--disable-devel \
- --with-kernel=/usr/include
+ --with-kernel=/usr/include \
+ --disable-libipq \
+ --with-xtlockdir=/data/local/tmp
- name: Build iptables
run: |
cd "$IPTABLES_DIR"
- export CC=${{ matrix.cc }}
- export STRIP=${{ matrix.strip }}
- export CFLAGS="-static -Os -DANDROID"
+ export CC="${NDK_CC}"
+ export STRIP="${NDK_STRIP}"
+
+ # Set architecture-specific TLS flags and fix lock path
+ case "${{ matrix.arch }}" in
+ arm64)
+ export CFLAGS="-static -Os -DANDROID -D_GNU_SOURCE -Wno-logical-op -Wno-unknown-warning-option -fno-emulated-tls -DXTABLES_LOCK_DIR='\"/data/local/tmp\"'"
+ ;;
+ *)
+ export CFLAGS="-static -Os -DANDROID -D_GNU_SOURCE -Wno-logical-op -Wno-unknown-warning-option -DXTABLES_LOCK_DIR='\"/data/local/tmp\"'"
+ ;;
+ esac
+
export LDFLAGS="-static"
+ # Remove only the cgroup extension (has O_PATH macro conflict)
+ rm -f extensions/libxt_cgroup.c
+
+ # Fix prioritynames issue in LOG extension for Android compatibility
+ sed -i 's/prioritynames\[i\]\.c_name/0/g; s/prioritynames\[i\]\.c_val/0/g' extensions/libxt_LOG.c
+
+ # Fix hardcoded lock path for Android - replace /run/xtables.lock with /data/local/tmp/xtables.lock
+ find . -name "*.c" -o -name "*.h" | xargs grep -l "/run/xtables.lock" | while read file; do
+ sed -i 's|/run/xtables\.lock|/data/local/tmp/xtables.lock|g' "$file"
+ echo "Fixed lock path in $file"
+ done
+
# Build
make -j$(nproc) V=1
@@ -257,8 +531,8 @@ jobs:
file "../iptables_${{ matrix.arch }}"
file "../ip6tables_${{ matrix.arch }}"
- ${{ matrix.strip }} "../iptables_${{ matrix.arch }}"
- ${{ matrix.strip }} "../ip6tables_${{ matrix.arch }}"
+ ${NDK_STRIP} "../iptables_${{ matrix.arch }}"
+ ${NDK_STRIP} "../ip6tables_${{ matrix.arch }}"
- name: Upload iptables artifacts
uses: actions/upload-artifact@v4
@@ -273,24 +547,21 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- arch: [arm, arm64, mips, x86]
+ arch: [arm, arm64, x86]
+ # nflog targets API 29: from API 29 on, the NDK's crt objects give the executable's TLS
+ # segment the alignment Bionic requires (64 bytes on arm64, 32 on arm). Android 16
+ # refuses to start the API-28 build ("executable's TLS segment is underaligned").
+ # busybox/iptables have no TLS segment and keep API 28.
include:
- arch: arm
- target: arm-linux-gnueabihf
- cc: arm-linux-gnueabihf-gcc
- strip: arm-linux-gnueabihf-strip
+ ndk_cc_suffix: armv7a-linux-androideabi29-clang
+ min_tls_align: 32
- arch: arm64
- target: aarch64-linux-gnu
- cc: aarch64-linux-gnu-gcc
- strip: aarch64-linux-gnu-strip
- - arch: mips
- target: mips-linux-gnu
- cc: mips-linux-gnu-gcc
- strip: mips-linux-gnu-strip
+ ndk_cc_suffix: aarch64-linux-android29-clang
+ min_tls_align: 64
- arch: x86
- target: i686-linux-gnu
- cc: i686-linux-gnu-gcc
- strip: i686-linux-gnu-strip
+ ndk_cc_suffix: i686-linux-android29-clang
+ min_tls_align: 0
steps:
- name: Checkout repository
@@ -302,89 +573,82 @@ jobs:
sudo apt-get install -y \
build-essential \
wget \
- gcc-${{ matrix.target }} \
- pkg-config \
- file \
- libnetfilter-log-dev
+ unzip \
+ file
+
+ - name: Set up Android NDK
+ run: |
+ NDK_VERSION="r26d"
+ echo "Downloading Android NDK ${NDK_VERSION}..."
+
+ for i in {1..3}; do
+ if wget -q "https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip"; then
+ echo "✓ Downloaded android-ndk-${NDK_VERSION}-linux.zip (attempt $i)"
+ break
+ fi
+ echo "Download attempt $i failed, retrying..."
+ sleep 5
+ done
+
+ if [ ! -f "android-ndk-${NDK_VERSION}-linux.zip" ]; then
+ echo "❌ Failed to download NDK after 3 attempts"
+ exit 1
+ fi
+
+ echo "Extracting NDK..."
+ unzip -q android-ndk-${NDK_VERSION}-linux.zip
+
+ export ANDROID_NDK_ROOT="$(pwd)/android-ndk-${NDK_VERSION}"
+ echo "ANDROID_NDK_ROOT=${ANDROID_NDK_ROOT}" >> $GITHUB_ENV
+ echo "NDK_CC=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/${{ matrix.ndk_cc_suffix }}" >> $GITHUB_ENV
+ echo "NDK_STRIP=${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip" >> $GITHUB_ENV
+ echo "✓ NDK setup complete"
- name: Build nflog from AFWall+ source
run: |
cd external/nflog
- # Compile nflog using AFWall+ source code
- ${{ matrix.cc }} -static \
+ # Use Android NDK clang — produces Android-native binaries that avoid
+ # AV false-positive heuristics triggered by GNU cross-compiled ELFs
+ ${NDK_CC} -static \
-I. -Ilibmnl \
+ -D_GNU_SOURCE -DANDROID \
+ -Os \
-o nflog_${{ matrix.arch }} \
nflog.c attr.c callback.c nlmsg.c socket.c
- # Verify and strip
+ # Verify the binary
file nflog_${{ matrix.arch }}
- ${{ matrix.strip }} nflog_${{ matrix.arch }}
+ ${NDK_STRIP} nflog_${{ matrix.arch }}
ls -la nflog_${{ matrix.arch }}
+ - name: Check TLS segment alignment
+ run: |
+ # Bionic refuses to run executables whose TLS segment is aligned below this
+ # (issues #1505/#1506: nflog built for API 28 had 8 on arm64).
+ min=${{ matrix.min_tls_align }}
+ readelf="${ANDROID_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf"
+ align=$("$readelf" -lW external/nflog/nflog_${{ matrix.arch }} | awk '/ TLS /{print $NF}')
+ if [ -z "$align" ]; then
+ echo "✓ no TLS segment"
+ elif [ $((align)) -lt "$min" ]; then
+ echo "❌ TLS segment alignment is $align, needs at least $min"
+ exit 1
+ else
+ echo "✓ TLS segment alignment $align (minimum $min)"
+ fi
+
- name: Upload nflog artifact
uses: actions/upload-artifact@v4
with:
name: nflog-${{ matrix.arch }}
path: external/nflog/nflog_${{ matrix.arch }}
- build-run-pie:
- name: Build run_pie (${{ matrix.arch }})
- runs-on: ubuntu-latest
- strategy:
- matrix:
- include:
- - arch: arm
- target: arm-linux-gnueabihf
- cc: arm-linux-gnueabihf-gcc
- strip: arm-linux-gnueabihf-strip
- - arch: arm64
- target: aarch64-linux-gnu
- cc: aarch64-linux-gnu-gcc
- strip: aarch64-linux-gnu-strip
- - arch: mips
- target: mips-linux-gnu
- cc: mips-linux-gnu-gcc
- strip: mips-linux-gnu-strip
- - arch: x86
- target: i686-linux-gnu
- cc: i686-linux-gnu-gcc
- strip: i686-linux-gnu-strip
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Set up build environment
- run: |
- sudo apt-get update
- sudo apt-get install -y \
- build-essential \
- gcc-${{ matrix.target }} \
- file
-
- - name: Build run_pie from AFWall+ source
- run: |
- cd external/run_pie
-
- # Compile run_pie using AFWall+ source code
- ${{ matrix.cc }} -static -ldl -o run_pie_${{ matrix.arch }} run_pie.c
-
- # Verify the binary
- file run_pie_${{ matrix.arch }}
- ${{ matrix.strip }} run_pie_${{ matrix.arch }}
- ls -la run_pie_${{ matrix.arch }}
-
- - name: Upload run_pie artifact
- uses: actions/upload-artifact@v4
- with:
- name: run-pie-${{ matrix.arch }}
- path: external/run_pie/run_pie_${{ matrix.arch }}
collect-binaries:
name: Collect and Update Binaries
runs-on: ubuntu-latest
- needs: [build-busybox, build-iptables, build-nflog, build-run-pie]
+ needs: [build-busybox, build-iptables, build-nflog]
steps:
- name: Checkout repository
@@ -400,7 +664,7 @@ jobs:
# Create binaries directory for testing (separate from res/raw)
mkdir -p binaries
- for arch in arm arm64 mips x86; do
+ for arch in arm arm64 x86; do
# Busybox
if [ -f "artifacts/busybox-${arch}/busybox_${arch}" ]; then
cp "artifacts/busybox-${arch}/busybox_${arch}" "binaries/"
@@ -425,11 +689,6 @@ jobs:
echo "✓ nflog_${arch} -> binaries/"
fi
- # run_pie
- if [ -f "artifacts/run-pie-${arch}/run_pie_${arch}" ]; then
- cp "artifacts/run-pie-${arch}/run_pie_${arch}" "binaries/"
- echo "✓ run_pie_${arch} -> binaries/"
- fi
done
echo "Binary inventory:"
@@ -453,7 +712,7 @@ jobs:
echo "No new binaries to commit"
else
# Create commit message
- git commit -m "Update cross-compiled binaries - busybox v${{ github.event.inputs.busybox_version || '1.36.1' }}, iptables v${{ github.event.inputs.iptables_version || '1.8.10' }} for arm/arm64/mips/x86. Ready for testing in /binaries directory."
+ git commit -m "Update cross-compiled binaries - busybox v${{ github.event.inputs.busybox_version || '1.36.1' }}, iptables v${{ github.event.inputs.iptables_version || '1.8.10' }} for arm/arm64/x86. Ready for testing in /binaries directory."
# Push changes back to repository
git push
@@ -480,11 +739,10 @@ jobs:
name: AFWall+ Cross-compiled Binaries ${{ github.event.inputs.release_tag }}
body: |
Cross-compiled binaries for AFWall+:
- - busybox (arm, arm64, mips, x86)
- - iptables (arm, arm64, mips, x86)
- - ip6tables (arm, arm64, mips, x86)
- - nflog (arm, arm64, mips, x86)
- - run_pie (arm, arm64, mips, x86)
+ - busybox (arm, arm64, x86)
+ - iptables (arm, arm64, x86)
+ - ip6tables (arm, arm64, x86)
+ - nflog (arm, arm64, x86)
Built from commit: ${{ github.sha }}
files: binaries/*
diff --git a/.gitignore b/.gitignore
index 8392555bf..032aeaa53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,8 @@
.DS_Store
+make-donate.sh
+
# Files for the dex VM
*.dex
@@ -17,15 +19,8 @@ out/
build/
build.xml
-# Gradle wrapper
-gradlew
-gradlew.bat
-gradle-wrapper.jar
-gradle-wrapper.properties
-
# Gradle
.gradle/
-gradle/
# Log Files
*.log
diff --git a/Changelog.md b/Changelog.md
index fa1d929a9..1afedd289 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,6 +1,117 @@
AFWall+ Changelog
==================
+AFWall+ v4.1.0
+
+ Added: Custom rules at the application level - direct per-app allow/block rules
+ Added: Custom theme maker with pre-built themes (Amber, Ocean, Forest, Slate, Plum, Black) plus a high-contrast light theme
+ Added: Multi-user / work-profile app detection and improved app search
+ Added: Option to show package name alongside app name in the list
+ Improved: Firewall rule generation - IPv6 control traffic (ICMPv6 RS/RA/NS/NA), loopback routing, LAN discovery (multicast/broadcast/mDNS/SSDP), tethered DHCP replies, Tor redirect ordering, reject-chain logging
+ Improved: Root apply reliability - consistent failure handling; success now waits for both IPv4 and IPv6 rule application to complete
+ Improved: Log service reliability - watchdog restarts a dead log watcher, batched log writes
+ Improved: Multiple LAN subnets now route correctly to WAN (Issue #1362)
+ Fixed: Missing system apps caused by removal of QUERY_ALL_PACKAGES
+ Fixed: Import/export - removed filter that hid valid AFWall+ backup files
+ Fixed: Widget bugs - repeated toggle callback reuse, activity reference leak
+ Reverted: Per-app localhost blocking (Issue #1421) - reverted pending a more reliable approach
+ Updated: Target SDK 36 (Android 16) support
+
+AFWall+ v4.0.1
+
+ Fixed boot rules not being applied (#1438)
+ Fixed app search not working (#1445)
+ Fixed pull to refresh when list is empty (#1439)
+ Updated Magisk binary location (#1437)
+ Relaxed sanitize rule to allow existing custom rules
+ Added back button to PreferencesActivity
+ Optimized pattern handling and memory management
+ Added build scripts for F-Droid (#1441)
+
+AFWall+ v4.0.0
+
+🚀 Major Features & Enhancements
+
+🎯 Rule Management & Stability
+
+- Fixed critical rule application issues - Resolved iptables command failures and cascade errors
+- Improved error handling - Smart selective error handling prevents unnecessary fallbacks
+- Enhanced chain management - Better synchronization prevents race conditions
+- Owner module compatibility - Automatic detection and fallback for devices without owner
+ iptables module
+
+🎨 Material Design Overhaul
+
+- Modernized UI - Material Design enhancements for rules, help, and custom scripts views
+- Revamped help section - Complete redesign with better organization and moved legends
+- Visual widget indicators - Added pulse animations and visual feedback for toggle widgets
+- Improved layouts - Fixed layout issues for devices with merged status bars
+
+📊 Enhanced Logging System
+
+- Better log details view - Enhanced display with allow/deny address information
+- Improved UID detection - Better handling of special UIDs (including uid -100) and bug fixes
+- Seamless log target switching - Dynamic switching between LOG and NFLOG in preferences
+- NFLOG improvements - Better NFLOG and LOG handling with updated binaries
+
+🔒 Security Enhancements
+
+- Upgraded encryption - Migrated from DES to AES for better security
+- Enhanced security utilities - New SecureCrypto and SecurityUtil classes
+- Input validation improvements - Better validation and security checks
+
+🛠 Platform & Compatibility
+
+📱 Android Support
+
+- Android 16 preparation - Updated build configuration for future Android support
+- Binary updates - Cross-compiled binaries: busybox v1.36.1, iptables v1.8.10
+- Architecture support - Added ARM64 binaries and improved architecture detection
+- GitHub Actions CI - Automated binary builds and improved CI/CD pipeline
+
+🔧 Bug Fixes & Stability
+
+- Export/Import fixes - Resolved bugs when handling large numbers of files (#1399, #1401)
+- Build error fixes - Fixed app:tint and other build-related issues
+- Service leak fixes - Improved thread safety and fixed service connection leaks
+- USB tethering support - Added auto-detection and support for USB tethering
+- DNS forwarding - Improved DNS handling for tethering scenarios
+
+🔧 Technical Improvements
+
+⚡ Performance & Threading
+
+- Thread safety - Improved synchronization and thread-safe operations
+- Better exception handling - More robust error handling and recovery
+- Optimized rule processing - Faster and more reliable rule application
+
+🛠 Developer Experience
+
+- Code cleanup - Extensive refactoring and code organization improvements
+- CI/CD enhancements - Updated GitHub Actions and automated workflows
+- Binary management - Automated cross-compilation and binary distribution
+
+📋 Specific Issue Fixes
+
+- #1386 - Default chain rules only applied when necessary (with smart revert)
+- #1410 - Fallback on default commands when needed
+- #1423, #1382 - Various stability and functionality fixes
+- #1400 - Layout fixes for merged status bar screens
+- #1399 - Export only enabled rule types
+- #1169 - Export/import rule improvements
+
+⚠️ Breaking Changes
+
+- Security upgrade - DES encryption deprecated in favor of AES
+- Removed legacy views - Old unsupported view components removed
+- Binary updates - Requires newer binaries for optimal performance
+
+🙏 Contributors
+
+- @getgo-nobugs - Syntax fixes and improvements
+- @NeroProtagonist - CI/CD updates (upload-artifact@v4)
+- @Fry-kun - Multiple fixes: typos, layout improvements, export fixes, ARM64 NFLOG binary (initial version)
+
Version 3.6.0
* Updated libraries and SDK (33)
diff --git a/README.md b/README.md
index 29f35494e..99d691991 100644
--- a/README.md
+++ b/README.md
@@ -2,35 +2,74 @@
[](https://github.com/ukanth/afwall/actions) [](https://crowdin.net/project/afwall)    
+[](https://github.com/ukanth/afwall/actions/workflows/build-binaries.yml)
+
> **Your Privacy, Your Control** - AFWall+ gives you complete control over which apps can access the internet on your Android device.
+---
+
+
+## Support AFWall+ Development
+
+AFWall+ is developed and maintained by volunteers. If you find it useful, please consider supporting the project:
+
+### How to Donate
+
+**Why donate?** AFWall+ is free and open-source. Your support helps:
+- Continue development and add new features
+- Fix bugs and keep the app stable
+- Support more Android versions and devices
+- Maintain documentation and help the community
+
+**Donation options:**
+- **PayPal**: [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6E4VZTULRB8GU)
+- **Google Play**: Purchase the [unlocker key](https://play.google.com/store/apps/details?id=dev.ukanth.ufirewall.donate) for extra features
+- **Amazon Gift Cards**: `cumakt+amazon at gmail.com` (not preferred)
+- **Bitcoin**: `bc1q54nf3y9zmdcpasxx9sywkprd6309rfhav3mape`
+- **Ethereum**: `0x5e65649C2B26eD816fCeD25a8E507C90D4b1D697`
+
+After donating, please send your receipt to contact@portgenix.com to receive an unlocker. Please allow 1-2 days for a response.
+
+### Other Ways to Help
+- Star this repository
+- Report bugs and test new features
+- Contribute translations on [Crowdin](http://crowdin.net/project/afwall)
+- Improve documentation
+- Help other users in forums
+
+---
+
-## 🔥 What is AFWall+?
+
+## What is AFWall+?
**AFWall+ (Android Firewall+)** is a powerful, open-source firewall application for rooted Android devices. Built on Linux's robust `iptables` framework, AFWall+ provides **granular network control** at the system level - something impossible with standard Android permissions.
-### 🎯 **Core Purpose**
-- **Block unwanted network access** by apps, even when they have internet permission
-- **Prevent data leaks** and unauthorized background connections
-- **Monitor network activity** with comprehensive logging
-- **Save battery and data** by controlling which apps can connect when
-- **Enhance privacy** by blocking tracking and analytics
-### 🛡️ **How It Works**
-AFWall+ operates at the **Linux kernel level** using `iptables` rules to:
-1. **Intercept all network requests** before they leave your device
-2. **Apply custom firewall rules** based on your preferences
-3. **Allow or block connections** per app, per network type (WiFi, mobile, VPN)
-4. **Log blocked attempts** for monitoring and analysis
+### Core Purpose
+- Block unwanted network access by apps, even if they have internet permission
+- Prevent data leaks and unauthorized background connections
+- Monitor network activity with comprehensive logging
+- Save battery and data by controlling which apps can connect
+- Enhance privacy by blocking tracking and analytics
-This approach is **far more powerful** than app-level solutions because it works regardless of how apps try to connect to the internet.
+
+### How It Works
+AFWall+ operates at the Linux kernel level using `iptables` rules to:
+1. Intercept all network requests before they leave your device
+2. Apply custom firewall rules based on your preferences
+3. Allow or block connections per app, per network type (WiFi, mobile, VPN)
+4. Log blocked attempts for monitoring and analysis
+
+This approach is much more powerful than app-level solutions because it works regardless of how apps try to connect to the internet.
---
-## 📥 Download
+
+## Download
@@ -44,102 +83,117 @@ This approach is **far more powerful** than app-level solutions because it works
-📋 **Release Notes**: Check the [changelog](https://github.com/ukanth/afwall/blob/beta/Changelog.md) for what's new in each version.
+
+**Release Notes**: Check the [changelog](https://github.com/ukanth/afwall/blob/beta/Changelog.md) for what's new in each version.
---
-## 🌟 Key Features
-
-### 🔐 **Granular Control**
-- **Per-app network rules** - Allow/block individual apps
-- **Network type filtering** - Different rules for WiFi, mobile data, VPN, tethering
-- **IPv4 & IPv6 support** - Complete protocol coverage
-- **Custom rule scripting** - Advanced users can write custom iptables rules
-
-### 🎛️ **User Experience**
-- **Clean, intuitive interface** - Easy to understand app list with clear allow/block controls
-- **Quick search & filtering** - Find apps instantly, sort by name, install date, or permissions
-- **Bulk operations** - Enable/disable rules for multiple apps at once
-- **Profile management** - Switch between different rule sets (home, work, travel)
-
-### 📊 **Monitoring & Logging**
-- **Real-time network monitoring** - See which apps are trying to connect
-- **Detailed connection logs** - Track blocked attempts with timestamps and destinations
-- **Notification system** - Get alerts for blocked connection attempts
-- **Export/import rules** - Backup your configuration or share with others
-
-### 🔧 **Advanced Features**
-- **Boot protection** - Apply rules before apps start (prevents data leaks during startup)
-- **Startup delay management** - Robust boot rule application with network change handling
-- **Multi-user support** - Different profiles for different Android users
-- **Tasker/Locale integration** - Automate firewall based on conditions
-- **Password protection** - Secure your firewall settings
-- **Tor and VPN detection** - Special handling for privacy networks
-
-### 🌐 **Network Types Supported**
-- 📶 **Mobile Data** (3G/4G/5G) - including roaming detection
-- 📡 **WiFi** - home, work, public hotspots
-- 🔗 **VPN** - all VPN types and providers
-- 🔄 **Tethering** - WiFi hotspot, USB, Bluetooth
-- 🧅 **Tor** - onion routing support
-- 🏠 **LAN** - local network access
+
+## Key Features
+
+
+### Granular Control
+- Per-app network rules: Allow or block individual apps
+- Network type filtering: Different rules for WiFi, mobile data, VPN, tethering
+- IPv4 & IPv6 support
+- Custom rule scripting for advanced users
+
+
+### User Experience
+- Clean, intuitive interface
+- Quick search and filtering
+- Bulk operations for multiple apps
+- Profile management for different rule sets
+
+
+### Monitoring & Logging
+- Real-time network monitoring
+- Detailed connection logs
+- Notification system for blocked attempts
+- Export/import rules for backup or sharing
+
+
+### Advanced Features
+- Boot protection: Apply rules before apps start
+- Startup delay management
+- Multi-user support
+- Tasker/Locale integration for automation
+- Password protection
+- Tor and VPN detection
+
+
+### Network Types Supported
+- Mobile Data (3G/4G/5G), including roaming detection
+- WiFi (home, work, public hotspots)
+- VPN (all types and providers)
+- Tethering (WiFi hotspot, USB, Bluetooth)
+- Tor (onion routing support)
+- LAN (local network access)
---
-## 📋 System Requirements
-### ✅ **Compatibility**
-- **Android versions**: 5.0 (API 21) to 14+ (actively maintained)
+## System Requirements
+
+
+### Compatibility
+- Android versions: 5.0 (API 21) to 14+ (actively maintained)
- Legacy support: Android 4.x (version 2.9.9), Android 2.x (version 1.3.4.1)
-- **Root access**: Required (Magisk, SuperSU, LineageOS su)
-- **Architectures**: ARM, ARM64, x86, x86_64, MIPS
-- **Storage**: ~15MB app + ~5MB for binaries
-
-### 🔧 **Root Methods Supported**
-- ✅ **Magisk** (recommended)
-- ✅ **LineageOS built-in su**
-- ✅ **SuperSU** (legacy)
-- ✅ **KingRoot** (not recommended)
-
-### 🚫 **Limitations**
-- **Requires root access** - No root = no functionality
-- **Not an antivirus** - Doesn't scan files for malware
-- **Not an ad-blocker** - Blocks network access, not ads within allowed connections
-- **VPN conflicts** - Some VPN apps may interfere with firewall rules
-- **System-level apps** - Some system processes may bypass rules if they have root access
+- Root access: Required (Magisk, SuperSU, LineageOS su)
+- Architectures: ARM, ARM64, x86, x86_64
+- Storage: ~15MB app + ~5MB for binaries
+
+
+### Root Methods Supported
+- Magisk (recommended)
+- LineageOS built-in su
+- SuperSU (legacy)
+- KingRoot (not recommended)
+
+
+### Limitations
+- Requires root access (no root = no functionality)
+- Not an antivirus (doesn't scan files for malware)
+- Not an ad-blocker (blocks network access, not ads within allowed connections)
+- VPN conflicts: Some VPN apps may interfere with firewall rules
+- System-level apps: Some system processes may bypass rules if they have root access
---
-## 🚀 Quick Start Guide
-### 1. **Pre-Installation**
+## Quick Start Guide
+
+
+### 1. Pre-Installation
```bash
# Verify root access
su -c "id"
# Should return: uid=0(root) gid=0(root)
```
-### 2. **Installation**
+### 2. Installation
- Install AFWall+ from your preferred source
- Grant root permission when prompted
-- Enable firewall in main screen
+- Enable the firewall on the main screen
-### 3. **Basic Configuration**
-1. **Enable the firewall** - Toggle the main switch
-2. **Configure apps** - Tap apps to allow WiFi (green) or mobile data (orange)
-3. **Apply rules** - Tap the apply button (firewall icon)
-4. **Test connectivity** - Verify apps work as expected
+### 3. Basic Configuration
+1. Enable the firewall (toggle the main switch)
+2. Configure apps (tap apps to allow WiFi or mobile data)
+3. Apply rules (tap the apply button)
+4. Test connectivity (verify apps work as expected)
-### 4. **Essential Settings**
-- **Boot startup delay**: Prevents rule conflicts during boot
-- **Notification settings**: Control alert behavior
-- **Log settings**: Enable if you want connection monitoring
+### 4. Essential Settings
+- Boot startup delay: Prevents rule conflicts during boot
+- Notification settings: Control alert behavior
+- Log settings: Enable if you want connection monitoring
---
-## 🔧 Advanced Configuration
-### 📝 **Custom Rules**
+## Advanced Configuration
+
+
+### Custom Rules
AFWall+ supports custom iptables rules for advanced users:
```bash
@@ -150,20 +204,23 @@ AFWall+ supports custom iptables rules for advanced users:
-A afwall -p tcp --dport 443 -j REJECT
```
-### 🔄 **Profiles**
+
+### Profiles
Create different rule sets for different scenarios:
-- **Home**: Relaxed rules for trusted network
-- **Work**: Restrictive rules for corporate network
-- **Public**: Maximum security for public WiFi
-- **Travel**: Balanced rules for mobile use
+- Home: Relaxed rules for trusted network
+- Work: Restrictive rules for corporate network
+- Public: Maximum security for public WiFi
+- Travel: Balanced rules for mobile use
+
-### 📊 **Logging Configuration**
-- **Packet logging**: Uses nflog for detailed connection tracking
-- **Log rotation**: Automatic cleanup of old logs
-- **Export options**: Save logs for external analysis
+### Logging Configuration
+- Packet logging: Uses nflog for detailed connection tracking
+- Log rotation: Automatic cleanup of old logs
+- Export options: Save logs for external analysis
---
+
## 🌍 Language Support
AFWall+ is available in **40+ languages** thanks to our community translators:
@@ -174,24 +231,29 @@ AFWall+ is available in **40+ languages** thanks to our community translators:
---
-## 🛠️ Development
-### 🏗️ **Building from Source**
+## Development
+
+
+### Building from Source
+
-#### **Prerequisites**
+#### Prerequisites
- Android SDK (API level 21+)
- Java 17+
- Git
- Android NDK (for native binaries)
-#### **Quick Build**
+
+#### Quick Build
```bash
git clone https://github.com/ukanth/afwall.git
cd afwall
./gradlew clean assembleDebug
```
-#### **Native Binaries**
+
+#### Native Binaries
To compile iptables, busybox, and other native components:
```bash
# Requires Android NDK
@@ -199,7 +261,8 @@ export NDK=/opt/android-ndk-r25
make -C external NDK=$NDK
```
-### 📁 **Project Structure**
+
+### Project Structure
```
afwall/
├── app/src/main/java/dev/ukanth/ufirewall/
@@ -215,7 +278,8 @@ afwall/
└── scripts/ # Build scripts
```
-### 🧪 **Testing**
+
+### Testing
```bash
# Run lint checks
./gradlew lint
@@ -229,47 +293,53 @@ afwall/
---
-## 🤝 Contributing
+
+## Contributing
We welcome contributions! Here's how you can help:
-### 🐛 **Bug Reports**
+
+### Bug Reports
- Check [existing issues](https://github.com/ukanth/afwall/issues) first
- Follow our [bug report guide](https://github.com/ukanth/afwall/wiki/HOWTO-Report-Bug)
- Include device info, Android version, and logs
-### 💡 **Feature Requests**
+
+### Feature Requests
- Open an issue with the "enhancement" label
- Describe the use case and expected behavior
- Consider if it fits AFWall+'s scope and philosophy
-### 👨💻 **Code Contributions**
+
+### Code Contributions
```bash
# Standard GitHub workflow
1. Fork the repository
2. Create a feature branch: git checkout -b feature-name
3. Make your changes and test thoroughly
-4. Submit a pull request with clear description
+4. Submit a pull request with a clear description
```
-### 🌐 **Translations**
+
+### Translations
- Join our [Crowdin project](http://crowdin.net/project/afwall)
- No technical knowledge required
- Help make AFWall+ accessible worldwide
---
-## 📞 Community & Support
-### 💬 **Discussion Forums**
+## Community & Support
+
+### Discussion Forums
- **XDA Thread**: [Official community discussion](http://forum.xda-developers.com/showthread.php?t=1957231)
- **GitHub Issues**: Technical problems and feature requests
- **Wiki**: [Comprehensive documentation](https://github.com/ukanth/afwall/wiki)
-### ❓ **Frequently Asked Questions**
+### Frequently Asked Questions
Before reporting issues, check our [FAQ](https://github.com/ukanth/afwall/wiki/FAQ) for common solutions.
-### 🆘 **Getting Help**
+### Getting Help
1. Check the FAQ and wiki
2. Search existing GitHub issues
3. Ask on XDA forums
@@ -277,40 +347,47 @@ Before reporting issues, check our [FAQ](https://github.com/ukanth/afwall/wiki/F
---
-## 📖 Technical Details
-### 🔧 **Architecture**
-AFWall+ uses a **layered architecture**:
+## Technical Details
+
+
+### Architecture
+AFWall+ uses a layered architecture:
1. **UI Layer**: Android activities and fragments for user interaction
2. **Service Layer**: Background services for rule application and monitoring
3. **Core Layer**: iptables rule generation and management
4. **System Layer**: Native binaries and root shell interface
-### 🏗️ **Key Components**
-- **BootRuleManager**: Robust boot-time rule application with race condition prevention
-- **InterfaceTracker**: Network interface monitoring and change detection
-- **Api.java**: Central iptables command generation and execution
-- **FirewallService**: Background service for continuous monitoring
-- **LogService**: Network packet logging and analysis
-### 📱 **Android Integration**
-- **Broadcast Receivers**: Monitor system events (boot, network changes, app installs)
-- **Content Providers**: Share configuration data securely
-- **Notification System**: User alerts for blocked connections
-- **Quick Settings Tile**: Fast firewall toggle (Android 7+)
+### Key Components
+- BootRuleManager: Robust boot-time rule application with race condition prevention
+- InterfaceTracker: Network interface monitoring and change detection
+- Api.java: Central iptables command generation and execution
+- FirewallService: Background service for continuous monitoring
+- LogService: Network packet logging and analysis
+
+
+### Android Integration
+- Broadcast Receivers: Monitor system events (boot, network changes, app installs)
+- Content Providers: Share configuration data securely
+- Notification System: User alerts for blocked connections
+- Quick Settings Tile: Fast firewall toggle (Android 7+)
---
-## 🏆 Acknowledgements
+
+## Acknowledgements
AFWall+ builds upon the work of many open-source projects and contributors:
-### 🌟 **Origins**
-- **Original concept**: Derived from [DroidWall](http://code.google.com/p/droidwall) by Rodrigo Rosauro
-- **Current maintainer**: [Umakanthan Chandran](https://github.com/ukanth)
-### 📚 **Libraries & Dependencies**
+### Origins
+- Original concept: Derived from [DroidWall](http://code.google.com/p/droidwall) by Rodrigo Rosauro
+- Current maintainer: [Umakanthan Chandran](https://github.com/ukanth)
+
+
+### Libraries & Dependencies
| Component | License | Purpose |
|-----------|---------|---------|
| [iptables](http://netfilter.org/projects/iptables/) | GPL v2 | Linux firewall framework |
@@ -321,12 +398,14 @@ AFWall+ builds upon the work of many open-source projects and contributors:
| [DBFlow](https://github.com/Raizlabs/DBFlow) | MIT | Database ORM |
| [PrettyTime](https://github.com/ocpsoft/prettytime) | Apache 2.0 | Human-readable timestamps |
-### 👥 **Contributors**
+
+### Contributors
Thanks to all contributors who have helped improve AFWall+ over the years!
---
-## 📄 License
+
+## License
AFWall+ is released under the **GNU General Public License v3.0**.
@@ -345,28 +424,11 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
```
-**Full license text**: [LICENSE](LICENSE) file or [gnu.org/licenses/gpl-3.0](https://www.gnu.org/licenses/gpl-3.0.html)
-
----
-
-## 💝 Support the Project
-
-AFWall+ is developed and maintained by volunteers in their free time. If you find it useful, consider supporting the project:
-
-### 💰 **Donations**
-- **PayPal**: [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6E4VZTULRB8GU)
-- **Google Play**: Purchase the [unlocker key](https://play.google.com/store/apps/details?id=dev.ukanth.ufirewall.donate) for additional features
-
-### 🌟 **Other Ways to Help**
-- ⭐ Star this repository
-- 🐛 Report bugs and test new features
-- 🌐 Contribute translations
-- 📝 Improve documentation
-- 💬 Help other users in forums
+**Full license text**: See the [LICENSE](LICENSE) file or [gnu.org/licenses/gpl-3.0](https://www.gnu.org/licenses/gpl-3.0.html)
---
Made with ❤️ for Android privacy and security
AFWall+ - Your Network, Your Rules
-
\ No newline at end of file
+
diff --git a/app/build.gradle b/app/build.gradle
index 4821e41c4..451778c17 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,16 +1,21 @@
apply plugin: 'com.android.application'
android {
+ namespace 'dev.ukanth.ufirewall'
+ compileSdk 36
defaultConfig {
- compileSdk 35
- targetSdk 35
- applicationId "dev.ukanth.ufirewall"
//applicationId "dev.ukanth.ufirewall.donate"
- minSdkVersion 21
- versionCode 20241025
- versionName "3.6.1"
+ applicationId "dev.ukanth.ufirewall"
+ minSdkVersion 23
+ targetSdk 36
+ versionCode 20260927
+ versionName "4.2.0"
buildConfigField 'boolean', 'DONATE', 'false'
+ vectorDrawables.useSupportLibrary = true
+ ndk {
+ abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
+ }
}
buildFeatures {
@@ -28,46 +33,63 @@ android {
}
}
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
lint {
abortOnError true
disable 'MissingTranslation'
}
- namespace 'dev.ukanth.ufirewall'
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
- compileSdk 35
- buildToolsVersion '34.0.0'
+
+ packagingOptions {
+ jniLibs {
+ useLegacyPackaging = true
+ }
+ }
+ configurations.all {
+ resolutionStrategy {
+ force 'androidx.core:core:1.15.0'
+ force 'androidx.activity:activity:1.9.3'
+ force 'androidx.annotation:annotation:1.9.1'
+ }
+ }
}
dependencies {
-
def libsuVersion = '6.0.0'
def dbFlowVersion = '4.2.4'
+ // the only root library: rule scripts, one-off root commands and the log watcher
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
- implementation "com.github.topjohnwu.libsu:service:${libsuVersion}"
- implementation "com.github.topjohnwu.libsu:nio:${libsuVersion}"
- implementation "eu.chainfire:libsuperuser:1.1.0"
implementation "com.github.ukanth:android-lockpattern:8.0.4"
implementation "com.afollestad.material-dialogs:core:0.9.6.0"
- implementation "androidx.appcompat:appcompat:1.7.0"
+ implementation "androidx.appcompat:appcompat:1.6.1"
implementation "com.google.android.material:material:1.12.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.3.2"
- implementation "androidx.annotation:annotation:1.9.0"
- implementation "androidx.core:core:1.13.1"
+ implementation "androidx.annotation:annotation:1.9.1"
+ implementation "androidx.core:core:1.15.0"
implementation "androidx.preference:preference:1.2.1"
implementation "androidx.legacy:legacy-support-v13:1.0.0"
+ implementation "androidx.activity:activity:1.9.3"
+
+ // DBFlow
annotationProcessor "com.github.Raizlabs.DBFlow:dbflow-processor:${dbFlowVersion}"
implementation "com.github.Raizlabs.DBFlow:dbflow-core:${dbFlowVersion}"
implementation "com.github.Raizlabs.DBFlow:dbflow:${dbFlowVersion}"
- implementation "io.reactivex.rxjava3:rxjava:3.1.9"
+
+ implementation "io.reactivex.rxjava3:rxjava:3.1.12"
implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final"
- implementation "dnsjava:dnsjava:3.6.2"
+ implementation "dnsjava:dnsjava:3.6.5"
+ testImplementation "junit:junit:4.13.2"
+ // real org.json for unit tests (the Android one is only stubs there)
+ testImplementation "org.json:json:20240303"
}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index bc15fdec2..58ba74c47 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -6,3 +6,16 @@
-dontobfuscate
-keep class dev.ukanth.ufirewall.** { *; }
-optimizations !code/allocation/variable
+
+# Android 16 specific proguard rules
+-keep class android.window.** { *; }
+-keep class androidx.activity.** { *; }
+-dontwarn android.window.**
+-dontwarn androidx.window.**
+
+# Edge-to-edge and window insets support
+-keep class androidx.core.view.WindowInsetsCompat** { *; }
+-keep class androidx.core.view.ViewCompat** { *; }
+
+# Notification channel compatibility
+-keep class androidx.core.app.NotificationChannelCompat** { *; }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f12256dfb..b808cb0e8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -49,12 +49,25 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+ android:exported="false"
+ android:theme="@style/Theme.Transparent" />
-
-
-
-
+ android:exported="false"
+ android:theme="@style/Theme.Transparent" />
+
+
+
@@ -192,13 +215,24 @@
+
+
+
+
+
+
+
+
+
+ android:exported="false" >
@@ -210,7 +244,8 @@
android:icon="@drawable/notification_quest"
android:exported="true"
android:label="@string/widget_label_status"
- android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
+ android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
+ tools:targetApi="24">
@@ -229,7 +264,6 @@
-
-
-
@@ -319,14 +351,17 @@
-
+ android:label="AFWallLogService" >
+
+
+
+
+ android:exported="false">
- * By disabling this all callbacks will be called from a thread other than
- * the main UI thread.
- */
- public static boolean handlerEnabled = true;
-
-
- /**
- * Setting this will change the default command timeout.
- *
- * The default is 20000ms
- */
- public static int defaultCommandTimeout = 20000;
-
- public enum LogLevel {
- VERBOSE,
- ERROR,
- DEBUG,
- WARN
- }
- // --------------------
- // # Public Methods #
- // --------------------
-
- /**
- * This will close all open shells.
- */
- public static void closeAllShells() throws IOException {
- Shell.closeAll();
- }
-
- /**
- * This will close the custom shell that you opened.
- */
- public static void closeCustomShell() throws IOException {
- Shell.closeCustomShell();
- }
-
- /**
- * This will close either the root shell or the standard shell depending on what you specify.
- *
- * @param root a boolean to specify whether to close the root shell or the standard shell.
- */
- public static void closeShell(boolean root) throws IOException {
- if (root) {
- Shell.closeRootShell();
- } else {
- Shell.closeShell();
- }
- }
-
- /**
- * Use this to check whether or not a file exists on the filesystem.
- *
- * @param file String that represent the file, including the full path to the
- * file and its name.
- * @return a boolean that will indicate whether or not the file exists.
- */
- public static boolean exists(final String file) {
- return exists(file, false);
- }
-
- /**
- * Use this to check whether or not a file OR directory exists on the filesystem.
- *
- * @param file String that represent the file OR the directory, including the full path to the
- * file and its name.
- * @param isDir boolean that represent whether or not we are looking for a directory
- * @return a boolean that will indicate whether or not the file exists.
- */
- public static boolean exists(final String file, boolean isDir) {
- final List result = new ArrayList();
-
- String cmdToExecute = "ls " + (isDir ? "-d " : " ");
-
- Command command = new Command(0, false, cmdToExecute + file) {
- @Override
- public void commandOutput(int id, String line) {
- RootShell.log(line);
- result.add(line);
-
- super.commandOutput(id, line);
- }
- };
-
- try {
- //Try without root...
- RootShell.getShell(false).add(command);
- commandWait(RootShell.getShell(false), command);
-
- } catch (Exception e) {
- RootShell.log("Exception: " + e);
- return false;
- }
-
- for (String line : result) {
- if (line.trim().equals(file)) {
- return true;
- }
- }
-
- result.clear();
-
- command = new Command(0, false, cmdToExecute + file) {
- @Override
- public void commandOutput(int id, String line) {
- RootShell.log(line);
- result.add(line);
-
- super.commandOutput(id, line);
- }
- };
-
- try {
- RootShell.getShell(true).add(command);
- commandWait(RootShell.getShell(true), command);
-
- } catch (Exception e) {
- RootShell.log("Exception: " + e);
- return false;
- }
-
- //Avoid concurrent modification...
- List final_result = new ArrayList(result);
-
- for (String line : final_result) {
- if (line.trim().equals(file)) {
- return true;
- }
- }
-
- return false;
-
- }
-
- /**
- * @param binaryName String that represent the binary to find.
- * @param singlePath boolean that represents whether to return a single path or multiple.
- *
- * @return List containing the locations the binary was found at.
- */
- public static List findBinary(String binaryName, boolean singlePath) {
- return findBinary(binaryName, null, singlePath);
- }
-
- /**
- * @param binaryName String that represent the binary to find.
- * @param searchPaths List which contains the paths to search for this binary in.
- * @param singlePath boolean that represents whether to return a single path or multiple.
- *
- * @return List containing the locations the binary was found at.
- */
- public static List findBinary(final String binaryName, List searchPaths, boolean singlePath) {
-
- final List foundPaths = new ArrayList();
-
- boolean found = false;
-
- if(searchPaths == null)
- {
- searchPaths = RootShell.getPath();
- }
-
- RootShell.log("Checking for " + binaryName);
-
- //Try to use stat first
- try {
- for (String path : searchPaths) {
-
- if(!path.endsWith("/"))
- {
- path += "/";
- }
-
- final String currentPath = path;
-
- Command cc = new Command(0, false, "stat " + path + binaryName) {
- @Override
- public void commandOutput(int id, String line) {
- if (line.contains("File: ") && line.contains(binaryName)) {
- foundPaths.add(currentPath);
-
- RootShell.log(binaryName + " was found here: " + currentPath);
- }
-
- RootShell.log(line);
-
- super.commandOutput(id, line);
- }
- };
-
- cc = RootShell.getShell(false).add(cc);
- commandWait(RootShell.getShell(false), cc);
-
- if(foundPaths.size() > 0 && singlePath) {
- break;
- }
- }
-
- found = !foundPaths.isEmpty();
-
- } catch (Exception e) {
- RootShell.log(binaryName + " was not found, more information MAY be available with Debugging on.");
- }
-
- if (!found) {
- RootShell.log("Trying second method");
-
- for (String path : searchPaths) {
-
- if(!path.endsWith("/"))
- {
- path += "/";
- }
-
- if (RootShell.exists(path + binaryName)) {
- RootShell.log(binaryName + " was found here: " + path);
- foundPaths.add(path);
-
- if(foundPaths.size() > 0 && singlePath) {
- break;
- }
-
- } else {
- RootShell.log(binaryName + " was NOT found here: " + path);
- }
- }
- }
-
- Collections.reverse(foundPaths);
-
- return foundPaths;
- }
-
- /**
- * This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param shellPath a String to Indicate the path to the shell that you want to open.
- * @param timeout an int to Indicate the length of time before giving up on opening a shell.
- * @throws TimeoutException
- * @throws com.stericson.RootShell.exceptions.RootDeniedException
- * @throws IOException
- */
- public static Shell getCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException
- {
- return RootShell.getCustomShell(shellPath, timeout);
- }
-
- /**
- * This will return the environment variable PATH
- *
- * @return List A List of Strings representing the environment variable $PATH
- */
- public static List getPath() {
- return Arrays.asList(System.getenv("PATH").split(":"));
- }
-
- /**
- * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell
- * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell.
- * @param shellContext the context to execute the shell with
- * @param retry a int to indicate how many times the ROOT shell should try to open with root priviliges...
- */
- public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException {
- if (root) {
- return Shell.startRootShell(timeout, shellContext, retry);
- } else {
- return Shell.startShell(timeout);
- }
- }
-
- /**
- * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell
- * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell.
- * @param shellContext the context to execute the shell with
- */
- public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException {
- return getShell(root, timeout, shellContext, 3);
- }
-
- /**
- * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell
- * @param shellContext the context to execute the shell with
- */
- public static Shell getShell(boolean root, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException {
- return getShell(root, 0, shellContext, 3);
- }
-
- /**
- * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell
- * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell.
- */
- public static Shell getShell(boolean root, int timeout) throws IOException, TimeoutException, RootDeniedException {
- return getShell(root, timeout, Shell.defaultContext, 3);
- }
-
- /**
- * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell
- */
- public static Shell getShell(boolean root) throws IOException, TimeoutException, RootDeniedException {
- return RootShell.getShell(root, 0);
- }
-
- /**
- * @return true if your app has been given root access.
- * @throws TimeoutException if this operation times out. (cannot determine if access is given)
- */
- public static boolean isAccessGiven() {
- return isAccessGiven(0,3);
- }
- public static boolean isAccessGiven(int timeout, int retry) {
- final Set ID = new HashSet();
- final int IAG = 158;
-
- try {
- RootShell.log("Checking for Root access");
-
- Command command = new Command(IAG, false, "id") {
- @Override
- public void commandOutput(int id, String line) {
- if (id == IAG) {
- ID.addAll(Arrays.asList(line.split(" ")));
- }
- super.commandOutput(id, line);
- }
- };
-
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- //parse the userid
- for (String userid : ID) {
- RootShell.log(userid);
-
- if (userid.toLowerCase().contains("uid=0")) {
- RootShell.log("Access Given");
- return true;
- }
- }
-
- return false;
- } catch (Exception e) {
- e.printStackTrace();
- return false;
- }
- }
-
- /**
- * @return true if BusyBox or Toybox was found.
- */
- public static boolean isBusyboxAvailable()
- {
- return (findBinary("busybox", true)).size() > 0;
- }
-
- /**
- * @return true if su was found.
- */
- public static boolean isRootAvailable() {
- return (findBinary("su", true)).size() > 0;
- }
-
- /**
- * This method allows you to output debug messages only when debugging is on. This will allow
- * you to add a debug option to your app, which by default can be left off for performance.
- * However, when you need debugging information, a simple switch can enable it and provide you
- * with detailed logging.
- *
- * This method handles whether or not to log the information you pass it depending whether or
- * not RootShell.debugMode is on. So you can use this and not have to worry about handling it
- * yourself.
- *
- * @param msg The message to output.
- */
- public static void log(String msg) {
- log(null, msg, LogLevel.DEBUG, null);
- }
-
- /**
- * This method allows you to output debug messages only when debugging is on. This will allow
- * you to add a debug option to your app, which by default can be left off for performance.
- * However, when you need debugging information, a simple switch can enable it and provide you
- * with detailed logging.
- *
- * This method handles whether or not to log the information you pass it depending whether or
- * not RootShell.debugMode is on. So you can use this and not have to worry about handling it
- * yourself.
- *
- * @param TAG Optional parameter to define the tag that the Log will use.
- * @param msg The message to output.
- */
- public static void log(String TAG, String msg) {
- log(TAG, msg, LogLevel.DEBUG, null);
- }
-
- /**
- * This method allows you to output debug messages only when debugging is on. This will allow
- * you to add a debug option to your app, which by default can be left off for performance.
- * However, when you need debugging information, a simple switch can enable it and provide you
- * with detailed logging.
- *
- * This method handles whether or not to log the information you pass it depending whether or
- * not RootShell.debugMode is on. So you can use this and not have to worry about handling it
- * yourself.
- *
- * @param msg The message to output.
- * @param type The type of log, 1 for verbose, 2 for error, 3 for debug, 4 for warn
- * @param e The exception that was thrown (Needed for errors)
- */
- public static void log(String msg, LogLevel type, Exception e) {
- log(null, msg, type, e);
- }
-
- /**
- * This method allows you to check whether logging is enabled.
- * Yes, it has a goofy name, but that's to keep it as short as possible.
- * After all writing logging calls should be painless.
- * This method exists to save Android going through the various Java layers
- * that are traversed any time a string is created (i.e. what you are logging)
- *
- * Example usage:
- * if(islog) {
- * StrinbBuilder sb = new StringBuilder();
- * // ...
- * // build string
- * // ...
- * log(sb.toString());
- * }
- *
- * @return true if logging is enabled
- */
- public static boolean islog() {
- return debugMode;
- }
-
- /**
- * This method allows you to output debug messages only when debugging is on. This will allow
- * you to add a debug option to your app, which by default can be left off for performance.
- * However, when you need debugging information, a simple switch can enable it and provide you
- * with detailed logging.
- *
- * This method handles whether or not to log the information you pass it depending whether or
- * not RootShell.debugMode is on. So you can use this and not have to worry about handling it
- * yourself.
- *
- * @param TAG Optional parameter to define the tag that the Log will use.
- * @param msg The message to output.
- * @param type The type of log, 1 for verbose, 2 for error, 3 for debug
- * @param e The exception that was thrown (Needed for errors)
- */
- public static void log(String TAG, String msg, LogLevel type, Exception e) {
- if (msg != null && !msg.equals("")) {
- if (debugMode) {
- if (TAG == null) {
- TAG = version;
- }
-
- switch (type) {
- case VERBOSE:
- Log.v(TAG, msg);
- break;
- case ERROR:
- Log.e(TAG, msg, e);
- break;
- case DEBUG:
- Log.d(TAG, msg);
- break;
- case WARN:
- Log.w(TAG, msg);
- break;
- }
- }
- }
- }
-
- // --------------------
- // # Public Methods #
- // --------------------
-
- private static void commandWait(Shell shell, Command cmd) throws Exception {
- while (!cmd.isFinished()) {
-
- RootShell.log(version, shell.getCommandQueuePositionString(cmd));
- RootShell.log(version, "Processed " + cmd.totalOutputProcessed + " of " + cmd.totalOutput + " output from command.");
-
- synchronized (cmd) {
- try {
- if (!cmd.isFinished()) {
- cmd.wait(2000);
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- if (!cmd.isExecuting() && !cmd.isFinished()) {
- if (!shell.isExecuting && !shell.isReading) {
- RootShell.log(version, "Waiting for a command to be executed in a shell that is not executing and not reading! \n\n Command: " + cmd.getCommand());
- Exception e = new Exception();
- e.setStackTrace(Thread.currentThread().getStackTrace());
- e.printStackTrace();
- } else if (shell.isExecuting && !shell.isReading) {
- RootShell.log(version, "Waiting for a command to be executed in a shell that is executing but not reading! \n\n Command: " + cmd.getCommand());
- Exception e = new Exception();
- e.setStackTrace(Thread.currentThread().getStackTrace());
- e.printStackTrace();
- } else {
- RootShell.log(version, "Waiting for a command to be executed in a shell that is not reading! \n\n Command: " + cmd.getCommand());
- Exception e = new Exception();
- e.setStackTrace(Thread.currentThread().getStackTrace());
- e.printStackTrace();
- }
- }
-
- }
- }
-}
diff --git a/app/src/main/java/com/stericson/rootshell/SanityCheckRootShell.java b/app/src/main/java/com/stericson/rootshell/SanityCheckRootShell.java
deleted file mode 100755
index f2a572acc..000000000
--- a/app/src/main/java/com/stericson/rootshell/SanityCheckRootShell.java
+++ /dev/null
@@ -1,415 +0,0 @@
-/*
- * This file is part of the RootShell Project: http://code.google.com/p/RootShell/
- *
- * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.rootshell;
-
-import android.app.Activity;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.os.StrictMode;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import com.stericson.rootshell.exceptions.RootDeniedException;
-import com.stericson.rootshell.execution.Command;
-import com.stericson.rootshell.execution.Shell;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.concurrent.TimeoutException;
-
-public class SanityCheckRootShell extends Activity
-{
- private ScrollView mScrollView;
- private TextView mTextView;
- private ProgressDialog mPDialog;
-
- @Override
- public void onCreate(Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
-
- StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
- .detectDiskReads()
- .detectDiskWrites()
- .detectNetwork() // or .detectAll() for all detectable problems
- .penaltyLog()
- .build());
- StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
- .detectLeakedSqlLiteObjects()
- .detectLeakedClosableObjects()
- .penaltyLog()
- .penaltyDeath()
- .build());
-
- RootShell.debugMode = true;
-
- mTextView = new TextView(this);
- mTextView.setText("");
- mScrollView = new ScrollView(this);
- mScrollView.addView(mTextView);
- setContentView(mScrollView);
-
- print("SanityCheckRootShell \n\n");
-
- if (RootShell.isRootAvailable())
- {
- print("Root found.\n");
- }
- else
- {
- print("Root not found");
- }
-
- try
- {
- RootShell.getShell(true);
- }
- catch (IOException e2)
- {
- // TODO Auto-generated catch block
- e2.printStackTrace();
- }
- catch (TimeoutException e)
- {
- print("[ TIMEOUT EXCEPTION! ]\n");
- e.printStackTrace();
- }
- catch (RootDeniedException e)
- {
- print("[ ROOT DENIED EXCEPTION! ]\n");
- e.printStackTrace();
- }
-
- try
- {
- if (!RootShell.isAccessGiven())
- {
- print("ERROR: No root access to this device.\n");
- return;
- }
- }
- catch (Exception e)
- {
- print("ERROR: could not determine root access to this device.\n");
- return;
- }
-
- // Display infinite progress bar
- mPDialog = new ProgressDialog(this);
- mPDialog.setCancelable(false);
- mPDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
-
- new SanityCheckThread(this, new TestHandler()).start();
- }
-
- protected void print(CharSequence text)
- {
- mTextView.append(text);
- mScrollView.post(new Runnable()
- {
- public void run()
- {
- mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
- }
- });
- }
-
- // Run our long-running tests in their separate thread so as to
- // not interfere with proper rendering.
- private class SanityCheckThread extends Thread
- {
- private final Handler mHandler;
-
- public SanityCheckThread(Context context, Handler handler)
- {
- mHandler = handler;
- }
-
- public void run()
- {
- visualUpdate(TestHandler.ACTION_SHOW, null);
-
- // First test: Install a binary file for future use
- // if it wasn't already installed.
- /*
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Installing binary if needed");
- if(false == RootShell.installBinary(mContext, R.raw.nes, "nes_binary")) {
- visualUpdate(TestHandler.ACTION_HIDE, "ERROR: Failed to install binary. Please see log file.");
- return;
- }
- */
-
- boolean result;
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getPath");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ getPath ]\n");
-
- try
- {
- List paths = RootShell.getPath();
-
- for (String path : paths)
- {
- visualUpdate(TestHandler.ACTION_DISPLAY, path + " k\n\n");
- }
-
- }
- catch (Exception e)
- {
- e.printStackTrace();
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing A ton of commands");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Ton of Commands ]\n");
-
- for (int i = 0; i < 100; i++)
- {
- RootShell.exists("/system/xbin/busybox");
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Find Binary");
- result = RootShell.isRootAvailable();
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Root ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
-
- result = RootShell.isBusyboxAvailable();
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Busybox ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing file exists");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Exists() ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, RootShell.exists("/system/sbin/[") + " k\n\n");
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Is Access Given");
- result = RootShell.isAccessGiven();
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking for Access to Root ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
-
-
- Shell shell;
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing output capture");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ busybox ash --help ]\n");
-
- try
- {
- shell = RootShell.getShell(true);
- Command cmd = new Command(
- 0,
- "busybox ash --help")
- {
-
- @Override
- public void commandOutput(int id, String line)
- {
- visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
- //super.commandOutput(id, line);
- }
- };
- shell.add(cmd);
-
- }
- catch (Exception e)
- {
- e.printStackTrace();
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - SYSTEM_APP");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - SYSTEM_APP ]\n");
-
- try
- {
- shell = RootShell.getShell(true, Shell.ShellContext.SYSTEM_APP);
- Command cmd = new Command(
- 0,
- "id")
- {
-
- @Override
- public void commandOutput(int id, String line)
- {
- visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
- super.commandOutput(id, line);
- }
- };
- shell.add(cmd);
-
- }
- catch (Exception e)
- {
- e.printStackTrace();
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - UNTRUSTED");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - UNTRUSTED ]\n");
-
- try
- {
- shell = RootShell.getShell(true, Shell.ShellContext.UNTRUSTED_APP);
- Command cmd = new Command(
- 0,
- "id")
- {
-
- @Override
- public void commandOutput(int id, String line)
- {
- visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
- super.commandOutput(id, line);
- }
- };
- shell.add(cmd);
-
- }
- catch (Exception e)
- {
- e.printStackTrace();
- }
-
- try
- {
- shell = RootShell.getShell(true);
-
- Command cmd = new Command(42, false, "echo done")
- {
-
- boolean _catch = false;
-
- @Override
- public void commandOutput(int id, String line)
- {
- if (_catch)
- {
- RootShell.log("CAUGHT!!!");
- }
-
- super.commandOutput(id, line);
-
- }
-
- @Override
- public void commandTerminated(int id, String reason)
- {
- synchronized (SanityCheckRootShell.this)
- {
-
- _catch = true;
- visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete.");
- visualUpdate(TestHandler.ACTION_HIDE, null);
-
- try
- {
- RootShell.closeAllShells();
- }
- catch (IOException e)
- {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
-
- }
- }
-
- @Override
- public void commandCompleted(int id, int exitCode)
- {
- synchronized (SanityCheckRootShell.this)
- {
- _catch = true;
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete.");
- visualUpdate(TestHandler.ACTION_HIDE, null);
-
- try
- {
- RootShell.closeAllShells();
- }
- catch (IOException e)
- {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
-
- }
- }
- };
-
- shell.add(cmd);
-
- }
- catch (Exception e)
- {
- e.printStackTrace();
- }
-
- }
-
- private void visualUpdate(int action, String text)
- {
- Message msg = mHandler.obtainMessage();
- Bundle bundle = new Bundle();
- bundle.putInt(TestHandler.ACTION, action);
- bundle.putString(TestHandler.TEXT, text);
- msg.setData(bundle);
- mHandler.sendMessage(msg);
- }
- }
-
- private class TestHandler extends Handler
- {
- static final public String ACTION = "action";
- static final public int ACTION_SHOW = 0x01;
- static final public int ACTION_HIDE = 0x02;
- static final public int ACTION_DISPLAY = 0x03;
- static final public int ACTION_PDISPLAY = 0x04;
- static final public String TEXT = "text";
-
- public void handleMessage(Message msg)
- {
- int action = msg.getData().getInt(ACTION);
- String text = msg.getData().getString(TEXT);
-
- switch (action)
- {
- case ACTION_SHOW:
- mPDialog.show();
- mPDialog.setMessage("Running Root Library Tests...");
- break;
- case ACTION_HIDE:
- if (null != text)
- { print(text); }
- mPDialog.hide();
- break;
- case ACTION_DISPLAY:
- print(text);
- break;
- case ACTION_PDISPLAY:
- mPDialog.setMessage(text);
- break;
- }
- }
- }
-}
diff --git a/app/src/main/java/com/stericson/rootshell/containers/RootClass.java b/app/src/main/java/com/stericson/rootshell/containers/RootClass.java
deleted file mode 100644
index 239812a49..000000000
--- a/app/src/main/java/com/stericson/rootshell/containers/RootClass.java
+++ /dev/null
@@ -1,328 +0,0 @@
-package com.stericson.rootshell.containers;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileFilter;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/* #ANNOTATIONS @SupportedAnnotationTypes("com.stericson.RootShell.containers.RootClass.Candidate") */
-/* #ANNOTATIONS @SupportedSourceVersion(SourceVersion.RELEASE_6) */
-public class RootClass /* #ANNOTATIONS extends AbstractProcessor */ {
-
- /* #ANNOTATIONS
- @Override
- public boolean process(Set extends TypeElement> typeElements, RoundEnvironment roundEnvironment) {
- processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "I was invoked!!!");
-
- return false;
- }
- */
-
- static String PATH_TO_DX = "/Users/Chris/Projects/android-sdk-macosx/build-tools/18.0.1/dx";
-
- enum READ_STATE {
- STARTING, FOUND_ANNOTATION
- }
-
- public RootClass(String[] args) throws ClassNotFoundException, NoSuchMethodException,
- IllegalAccessException, InvocationTargetException, InstantiationException {
-
- // Note: rather than calling System.load("/system/lib/libandroid_runtime.so");
- // which would leave a bunch of unresolved JNI references,
- // we are using the 'withFramework' class as a preloader.
- // So, yeah, russian dolls: withFramework > RootClass > actual method
-
- String className = args[0];
- RootArgs actualArgs = new RootArgs();
- actualArgs.args = new String[args.length - 1];
- System.arraycopy(args, 1, actualArgs.args, 0, args.length - 1);
- Class> classHandler = Class.forName(className);
- Constructor> classConstructor = classHandler.getConstructor(RootArgs.class);
- classConstructor.newInstance(actualArgs);
- }
-
- public @interface Candidate {
-
- }
-
- public static class RootArgs {
-
- public String[] args;
- }
-
- static void displayError(Exception e) {
- // Not using system.err to make it easier to capture from
- // calling library.
- System.out.println("##ERR##" + e.getMessage() + "##");
- e.printStackTrace();
- }
-
- // I reckon it would be better to investigate classes using getAttribute()
- // however this method allows the developer to simply select "Run" on RootClass
- // and immediately re-generate the necessary jar file.
- static public class AnnotationsFinder {
-
- private final String AVOIDDIRPATH = "stericson" + File.separator + "RootShell" + File.separator;
-
- private final List classFiles;
-
- public AnnotationsFinder() throws IOException {
- System.out.println("Discovering root class annotations...");
- classFiles = new ArrayList();
- lookup(new File("src"), classFiles);
- System.out.println("Done discovering annotations. Building jar file.");
- File builtPath = getBuiltPath();
- if (null != builtPath) {
- // Android! Y U no have com.google.common.base.Joiner class?
- String rc1 = "com" + File.separator
- + "stericson" + File.separator
- + "RootShell" + File.separator
- + "containers" + File.separator
- + "RootClass.class";
- String rc2 = "com" + File.separator
- + "stericson" + File.separator
- + "RootShell" + File.separator
- + "containers" + File.separator
- + "RootClass$RootArgs.class";
- String rc3 = "com" + File.separator
- + "stericson" + File.separator
- + "RootShell" + File.separator
- + "containers" + File.separator
- + "RootClass$AnnotationsFinder.class";
- String rc4 = "com" + File.separator
- + "stericson" + File.separator
- + "RootShell" + File.separator
- + "containers" + File.separator
- + "RootClass$AnnotationsFinder$1.class";
- String rc5 = "com" + File.separator
- + "stericson" + File.separator
- + "RootShell" + File.separator
- + "containers" + File.separator
- + "RootClass$AnnotationsFinder$2.class";
- String[] cmd;
- boolean onWindows = (-1 != System.getProperty("os.name").toLowerCase().indexOf("win"));
- if (onWindows) {
- StringBuilder sb = new StringBuilder(
- " " + rc1 + " " + rc2 + " " + rc3 + " " + rc4 + " " + rc5
- );
- for (File file : classFiles) {
- sb.append(" ").append(file.getPath());
- }
- cmd = new String[]{
- "cmd", "/C",
- "jar cvf" +
- " anbuild.jar" +
- sb.toString()
- };
- } else {
- ArrayList al = new ArrayList();
- al.add("jar");
- al.add("cf");
- al.add("anbuild.jar");
- al.add(rc1);
- al.add(rc2);
- al.add(rc3);
- al.add(rc4);
- al.add(rc5);
- for (File file : classFiles) {
- al.add(file.getPath());
- }
- cmd = al.toArray(new String[0]);
- }
- ProcessBuilder jarBuilder = new ProcessBuilder(cmd);
- jarBuilder.directory(builtPath);
- try {
- jarBuilder.start().waitFor();
- } catch (IOException e) {
- } catch (InterruptedException e) {
- }
-
- File rawFolder = new File("res" + File.separator + "raw");
- if (!rawFolder.exists()) {
- rawFolder.mkdirs();
- }
-
- System.out.println("Done building jar file. Creating dex file.");
- if (onWindows) {
- cmd = new String[]{
- "cmd", "/C",
- "dx --dex --output=res" + File.separator + "raw" + File.separator + "anbuild.dex "
- + builtPath + File.separator + "anbuild.jar"
- };
- } else {
- cmd = new String[]{
- getPathToDx(),
- "--dex",
- "--output=res" + File.separator + "raw" + File.separator + "anbuild.dex",
- builtPath + File.separator + "anbuild.jar"
- };
- }
- ProcessBuilder dexBuilder = new ProcessBuilder(cmd);
- try {
- dexBuilder.start().waitFor();
- } catch (IOException e) {
- } catch (InterruptedException e) {
- }
- }
- System.out.println("All done. ::: anbuild.dex should now be in your project's res" + File.separator + "raw" + File.separator + " folder :::");
- }
-
- protected void lookup(File path, List fileList) {
- String desourcedPath = path.toString().replace("src" + File.separator, "");
- File[] files = path.listFiles();
- for (File file : files) {
- if (file.isDirectory()) {
- if (-1 == file.getAbsolutePath().indexOf(AVOIDDIRPATH)) {
- lookup(file, fileList);
- }
- } else {
- if (file.getName().endsWith(".java")) {
- if (hasClassAnnotation(file)) {
- final String fileNamePrefix = file.getName().replace(".java", "");
- final File compiledPath = new File(getBuiltPath().toString() + File.separator + desourcedPath);
- File[] classAndInnerClassFiles = compiledPath.listFiles(new FilenameFilter() {
- @Override
- public boolean accept(File dir, String filename) {
- return filename.startsWith(fileNamePrefix);
- }
- });
- for (final File matchingFile : classAndInnerClassFiles) {
- fileList.add(new File(desourcedPath + File.separator + matchingFile.getName()));
- }
-
- }
- }
- }
- }
- }
-
- protected boolean hasClassAnnotation(File file) {
- READ_STATE readState = READ_STATE.STARTING;
- Pattern p = Pattern.compile(" class ([A-Za-z0-9_]+)");
- try {
- BufferedReader reader = new BufferedReader(new FileReader(file));
- String line;
- while (null != (line = reader.readLine())) {
- switch (readState) {
- case STARTING:
- if (-1 < line.indexOf("@RootClass.Candidate")) {
- readState = READ_STATE.FOUND_ANNOTATION;
- }
- break;
- case FOUND_ANNOTATION:
- Matcher m = p.matcher(line);
- if (m.find()) {
- System.out.println(" Found annotated class: " + m.group(0));
- return true;
- } else {
- System.err.println("Error: unmatched annotation in " +
- file.getAbsolutePath());
- readState = READ_STATE.STARTING;
- }
- break;
- }
- }
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- return false;
- }
-
- protected String getPathToDx() throws IOException {
- String androidHome = System.getenv("ANDROID_HOME");
- if (null == androidHome) {
- throw new IOException("Error: you need to set $ANDROID_HOME globally");
- }
- String dxPath = null;
- File[] files = new File(androidHome + File.separator + "build-tools").listFiles();
- int recentSdkVersion = 0;
- for (File file : files) {
-
- String fileName = null;
- if (file.getName().contains("-")) {
- String[] splitFileName = file.getName().split("-");
- if (splitFileName[1].contains("W")) {
- char[] fileNameChars = splitFileName[1].toCharArray();
- fileName = String.valueOf(fileNameChars[0]);
- } else {
- fileName = splitFileName[1];
- }
- } else {
- fileName = file.getName();
- }
-
- int sdkVersion;
-
- String[] sdkVersionBits = fileName.split("[.]");
- sdkVersion = Integer.parseInt(sdkVersionBits[0]) * 10000;
- if (sdkVersionBits.length > 1) {
- sdkVersion += Integer.parseInt(sdkVersionBits[1]) * 100;
- if (sdkVersionBits.length > 2) {
- sdkVersion += Integer.parseInt(sdkVersionBits[2]);
- }
- }
- if (sdkVersion > recentSdkVersion) {
- String tentativePath = file.getAbsolutePath() + File.separator + "dx";
- if (new File(tentativePath).exists()) {
- recentSdkVersion = sdkVersion;
- dxPath = tentativePath;
- }
- }
- }
- if (dxPath == null) {
- throw new IOException("Error: unable to find dx binary in $ANDROID_HOME");
- }
- return dxPath;
- }
-
- protected File getBuiltPath() {
- File foundPath = null;
-
- File ideaPath = new File("out" + File.separator + "production"); // IntelliJ
- if (ideaPath.isDirectory()) {
- File[] children = ideaPath.listFiles(new FileFilter() {
- @Override
- public boolean accept(File pathname) {
- return pathname.isDirectory();
- }
- });
- if (children.length > 0) {
- foundPath = new File(ideaPath.getAbsolutePath() + File.separator + children[0].getName());
- }
- }
- if (null == foundPath) {
- File eclipsePath = new File("bin" + File.separator + "classes"); // Eclipse IDE
- if (eclipsePath.isDirectory()) {
- foundPath = eclipsePath;
- }
- }
-
- return foundPath;
- }
-
-
- }
-
- public static void main(String[] args) {
- try {
- if (args.length == 0) {
- new AnnotationsFinder();
- } else {
- new RootClass(args);
- }
- } catch (Exception e) {
- displayError(e);
- }
- }
-}
diff --git a/app/src/main/java/com/stericson/rootshell/exceptions/RootDeniedException.java b/app/src/main/java/com/stericson/rootshell/exceptions/RootDeniedException.java
deleted file mode 100644
index 4ee384963..000000000
--- a/app/src/main/java/com/stericson/rootshell/exceptions/RootDeniedException.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * This file is part of the RootShell Project: https://github.com/Stericson/RootShell
- *
- * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.rootshell.exceptions;
-
-public class RootDeniedException extends Exception {
-
- private static final long serialVersionUID = -8713947214162841310L;
-
- public RootDeniedException(String error) {
- super(error);
- }
-}
diff --git a/app/src/main/java/com/stericson/rootshell/execution/Command.java b/app/src/main/java/com/stericson/rootshell/execution/Command.java
deleted file mode 100644
index f4b5ec90a..000000000
--- a/app/src/main/java/com/stericson/rootshell/execution/Command.java
+++ /dev/null
@@ -1,325 +0,0 @@
-/*
- * This file is part of the RootShell Project: http://code.google.com/p/RootShell/
- *
- * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.rootshell.execution;
-
-
-import android.content.Context;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-
-import com.stericson.rootshell.RootShell;
-
-import java.io.IOException;
-
-public class Command {
-
- //directly modified by JavaCommand
- protected boolean javaCommand = false;
- protected Context context = null;
-
- public int totalOutput = 0;
-
- public int totalOutputProcessed = 0;
-
- ExecutionMonitor executionMonitor = null;
-
- Handler mHandler = null;
-
- //Has this command already been used?
- protected boolean used = false;
-
- boolean executing = false;
-
- String[] command = {};
-
- boolean finished = false;
-
- boolean terminated = false;
-
- boolean handlerEnabled = true;
-
- int exitCode = -1;
-
- int id = 0;
-
- int timeout = RootShell.defaultCommandTimeout;
-
- /**
- * Constructor for executing a normal shell command
- *
- * @param id the id of the command being executed
- * @param command the command, or commands, to be executed.
- */
- public Command(int id, String... command) {
- this.command = command;
- this.id = id;
-
- createHandler(RootShell.handlerEnabled);
- }
-
- /**
- * Constructor for executing a normal shell command
- *
- * @param id the id of the command being executed
- * @param handlerEnabled when true the handler will be used to call the
- * callback methods if possible.
- * @param command the command, or commands, to be executed.
- */
- public Command(int id, boolean handlerEnabled, String... command) {
- this.command = command;
- this.id = id;
-
- createHandler(handlerEnabled);
- }
-
- /**
- * Constructor for executing a normal shell command
- *
- * @param id the id of the command being executed
- * @param timeout the time allowed before the shell will give up executing the command
- * and throw a TimeoutException.
- * @param command the command, or commands, to be executed.
- */
- public Command(int id, int timeout, String... command) {
- this.command = command;
- this.id = id;
- this.timeout = timeout;
-
- createHandler(RootShell.handlerEnabled);
- }
-
- //If you override this you MUST make a final call
- //to the super method. The super call should be the last line of this method.
- public void commandOutput(int id, String line) {
- RootShell.log("Command", "ID: " + id + ", " + line);
- totalOutputProcessed++;
- }
-
- public void commandTerminated(int id, String reason) {
- //pass
- }
-
- public void commandCompleted(int id, int exitcode) {
- //pass
- }
-
- protected final void commandFinished() {
- if (!terminated) {
- synchronized (this) {
- if (mHandler != null && handlerEnabled) {
- Message msg = mHandler.obtainMessage();
- Bundle bundle = new Bundle();
- bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_COMPLETED);
- msg.setData(bundle);
- mHandler.sendMessage(msg);
- } else {
- commandCompleted(id, exitCode);
- }
-
- RootShell.log("Command " + id + " finished.");
- finishCommand();
- }
- }
- }
-
- private void createHandler(boolean handlerEnabled) {
-
- this.handlerEnabled = handlerEnabled;
-
- if (Looper.myLooper() != null && handlerEnabled) {
- RootShell.log("CommandHandler created");
- mHandler = new CommandHandler();
- } else {
- RootShell.log("CommandHandler not created");
- }
- }
-
- public final void finish()
- {
- RootShell.log("Command finished at users request!");
- commandFinished();
- }
-
- protected final void finishCommand() {
- this.executing = false;
- this.finished = true;
- this.notifyAll();
- }
-
-
- public final String getCommand() {
- StringBuilder sb = new StringBuilder();
-
- for (int i = 0; i < command.length; i++) {
- if (i > 0) {
- sb.append('\n');
- }
-
- sb.append(command[i]);
- }
-
- return sb.toString();
- }
-
- public final boolean isExecuting() {
- return executing;
- }
-
- public final boolean isHandlerEnabled() {
- return handlerEnabled;
- }
-
- public final boolean isFinished() {
- return finished;
- }
-
- public final int getExitCode() {
- return this.exitCode;
- }
-
- protected final void setExitCode(int code) {
- synchronized (this) {
- exitCode = code;
- }
- }
-
- protected final void startExecution() {
- this.used = true;
- executionMonitor = new ExecutionMonitor(this);
- executionMonitor.setPriority(Thread.MIN_PRIORITY);
- executionMonitor.start();
- executing = true;
- }
-
- public final void terminate()
- {
- RootShell.log("Terminating command at users request!");
- terminated("Terminated at users request!");
- }
-
- protected final void terminate(String reason) {
- try {
- Shell.closeAll();
- RootShell.log("Terminating all shells.");
- terminated(reason);
- } catch (IOException e) {
- }
- }
-
- protected final void terminated(String reason) {
- synchronized (Command.this) {
-
- if (mHandler != null && handlerEnabled) {
- Message msg = mHandler.obtainMessage();
- Bundle bundle = new Bundle();
- bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_TERMINATED);
- bundle.putString(CommandHandler.TEXT, reason);
- msg.setData(bundle);
- mHandler.sendMessage(msg);
- } else {
- commandTerminated(id, reason);
- }
-
- RootShell.log("Command " + id + " did not finish because it was terminated. Termination reason: " + reason);
- setExitCode(-1);
- terminated = true;
- finishCommand();
- }
- }
-
- protected final void output(int id, String line) {
- totalOutput++;
-
- if (mHandler != null && handlerEnabled) {
- Message msg = mHandler.obtainMessage();
- Bundle bundle = new Bundle();
- bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_OUTPUT);
- bundle.putString(CommandHandler.TEXT, line);
- msg.setData(bundle);
- mHandler.sendMessage(msg);
- } else {
- commandOutput(id, line);
- }
- }
-
- private class ExecutionMonitor extends Thread {
-
- private final Command command;
-
- public ExecutionMonitor(Command command) {
- this.command = command;
- }
-
- public void run() {
-
- if(command.timeout > 0)
- {
- synchronized (command) {
- try {
- RootShell.log("Command " + command.id + " is waiting for: " + command.timeout);
- command.wait(command.timeout);
- } catch (InterruptedException e) {
- RootShell.log("Exception: " + e);
- }
-
- if (!command.isFinished()) {
- RootShell.log("Timeout Exception has occurred for command: " + command.id + ".");
- terminate("Timeout Exception");
- }
- }
- }
- }
- }
-
- private class CommandHandler extends Handler {
-
- static final public String ACTION = "action";
-
- static final public String TEXT = "text";
-
- static final public int COMMAND_OUTPUT = 0x01;
-
- static final public int COMMAND_COMPLETED = 0x02;
-
- static final public int COMMAND_TERMINATED = 0x03;
-
- public final void handleMessage(Message msg) {
- int action = msg.getData().getInt(ACTION);
- String text = msg.getData().getString(TEXT);
-
- switch (action) {
- case COMMAND_OUTPUT:
- commandOutput(id, text);
- break;
- case COMMAND_COMPLETED:
- commandCompleted(id, exitCode);
- break;
- case COMMAND_TERMINATED:
- commandTerminated(id, text);
- break;
- }
- }
- }
-}
diff --git a/app/src/main/java/com/stericson/rootshell/execution/JavaCommand.java b/app/src/main/java/com/stericson/rootshell/execution/JavaCommand.java
deleted file mode 100644
index 4614e21b4..000000000
--- a/app/src/main/java/com/stericson/rootshell/execution/JavaCommand.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.stericson.rootshell.execution;
-
-import android.content.Context;
-
-public class JavaCommand extends Command
-{
- /**
- * Constructor for executing Java commands rather than binaries
- *
- * @param context needed to execute java command.
- */
- public JavaCommand(int id, Context context, String... command) {
- super(id, command);
- this.context = context;
- this.javaCommand = true;
- }
-
- /**
- * Constructor for executing Java commands rather than binaries
- *
- * @param context needed to execute java command.
- */
- public JavaCommand(int id, boolean handlerEnabled, Context context, String... command) {
- super(id, handlerEnabled, command);
- this.context = context;
- this.javaCommand = true;
- }
-
- /**
- * Constructor for executing Java commands rather than binaries
- *
- * @param context needed to execute java command.
- */
- public JavaCommand(int id, int timeout, Context context, String... command) {
- super(id, timeout, command);
- this.context = context;
- this.javaCommand = true;
- }
-
-
- @Override
- public void commandOutput(int id, String line)
- {
- super.commandOutput(id, line);
- }
-
- @Override
- public void commandTerminated(int id, String reason)
- {
- // pass
- }
-
- @Override
- public void commandCompleted(int id, int exitCode)
- {
- // pass
- }
-}
diff --git a/app/src/main/java/com/stericson/rootshell/execution/Shell.java b/app/src/main/java/com/stericson/rootshell/execution/Shell.java
deleted file mode 100644
index f5a522cd8..000000000
--- a/app/src/main/java/com/stericson/rootshell/execution/Shell.java
+++ /dev/null
@@ -1,1029 +0,0 @@
-/*
- * This file is part of the RootShell Project: http://code.google.com/p/RootShell/
- *
- * Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-package com.stericson.rootshell.execution;
-
-
-import android.content.Context;
-
-import com.stericson.rootshell.RootShell;
-import com.stericson.rootshell.exceptions.RootDeniedException;
-
-import java.io.BufferedReader;
-import java.io.EOFException;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.Reader;
-import java.io.Writer;
-import java.lang.reflect.Field;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeoutException;
-
-public class Shell {
-
- public enum ShellType {
- NORMAL,
- ROOT,
- CUSTOM
- }
-
- //this is only used with root shells
- public enum ShellContext {
- NORMAL("normal"), //The normal context...
- SHELL("u:r:shell:s0"), //unprivileged shell (such as an adb shell)
- SYSTEM_SERVER("u:r:system_server:s0"), // system_server, u:r:system:s0 on some firmwares
- SYSTEM_APP("u:r:system_app:s0"), // System apps
- PLATFORM_APP("u:r:platform_app:s0"), // System apps
- UNTRUSTED_APP("u:r:untrusted_app:s0"), // Third-party apps
- RECOVERY("u:r:recovery:s0"); //Recovery
-
- private final String value;
-
- ShellContext(String value) {
- this.value = value;
- }
-
- public String getValue() {
- return this.value;
- }
-
- }
-
- //Statics -- visible to all
- private static final String token = "F*D^W@#FGF";
-
- private static Shell rootShell = null;
-
- private static Shell shell = null;
-
- private static Shell customShell = null;
-
- private static final String[] suVersion = new String[]{
- null, null
- };
-
- //the default context for root shells...
- public static ShellContext defaultContext = ShellContext.NORMAL;
-
- //per shell
- private int shellTimeout = 25000;
-
- private ShellType shellType = null;
-
- private ShellContext shellContext = ShellContext.NORMAL;
-
- private String error = "";
-
- private final Process proc;
-
- private final BufferedReader inputStream;
-
- private final BufferedReader errorStream;
-
- private final OutputStreamWriter outputStream;
-
- private final List commands = new ArrayList();
-
- //indicates whether or not to close the shell
- private boolean close = false;
-
- private Boolean isSELinuxEnforcing = null;
-
- public boolean isExecuting = false;
-
- public boolean isReading = false;
-
- public boolean isClosed = false;
-
- private final int maxCommands = 5000;
-
- private int read = 0;
-
- private int write = 0;
-
- private int totalExecuted = 0;
-
- private int totalRead = 0;
-
- private boolean isCleaning = false;
-
- private Shell(String cmd, ShellType shellType, ShellContext shellContext, int shellTimeout) throws IOException, TimeoutException, RootDeniedException {
-
- RootShell.log("Starting shell: " + cmd);
- RootShell.log("Context: " + shellContext.getValue());
- RootShell.log("Timeout: " + shellTimeout);
-
- this.shellType = shellType;
- this.shellTimeout = shellTimeout > 0 ? shellTimeout : this.shellTimeout;
- this.shellContext = shellContext;
-
- if (this.shellContext == ShellContext.NORMAL) {
- this.proc = Runtime.getRuntime().exec(cmd);
- } else {
- String display = getSuVersion(false);
- String internal = getSuVersion(true);
-
- //only done for root shell...
- //Right now only SUPERSU supports the --context switch
- if (isSELinuxEnforcing() &&
- (display != null) &&
- (internal != null) &&
- (display.endsWith("SUPERSU")) &&
- (Integer.valueOf(internal) >= 190)) {
- cmd += " --context " + this.shellContext.getValue();
- } else {
- RootShell.log("Su binary --context switch not supported!");
- RootShell.log("Su binary display version: " + display);
- RootShell.log("Su binary internal version: " + internal);
- RootShell.log("SELinuxEnforcing: " + isSELinuxEnforcing());
- }
-
- this.proc = Runtime.getRuntime().exec(cmd);
-
- }
-
- this.inputStream = new BufferedReader(new InputStreamReader(this.proc.getInputStream(), StandardCharsets.UTF_8));
- this.errorStream = new BufferedReader(new InputStreamReader(this.proc.getErrorStream(), StandardCharsets.UTF_8));
- this.outputStream = new OutputStreamWriter(this.proc.getOutputStream(), StandardCharsets.UTF_8);
-
- /**
- * Thread responsible for carrying out the requested operations
- */
- Worker worker = new Worker(this);
- worker.start();
-
- try {
- /**
- * The flow of execution will wait for the thread to die or wait until the
- * given timeout has expired.
- *
- * The result of the worker, which is determined by the exit code of the worker,
- * will tell us if the operation was completed successfully or it the operation
- * failed.
- */
- worker.join(this.shellTimeout);
-
- /**
- * The operation could not be completed before the timeout occurred.
- */
- if (worker.exit == -911) {
-
- try {
- this.proc.destroy();
- } catch (Exception e) {
- }
-
- closeQuietly(this.inputStream);
- closeQuietly(this.errorStream);
- closeQuietly(this.outputStream);
-
- throw new TimeoutException(this.error);
- }
- /**
- * Root access denied?
- */
- else if (worker.exit == -42) {
-
- try {
- this.proc.destroy();
- } catch (Exception e) {
- }
-
- closeQuietly(this.inputStream);
- closeQuietly(this.errorStream);
- closeQuietly(this.outputStream);
-
- throw new RootDeniedException("Root Access Denied");
- }
- /**
- * Normal exit
- */
- else {
- /**
- * The shell is open.
- *
- * Start two threads, one to handle the input and one to handle the output.
- *
- * input, and output are runnables that the threads execute.
- */
- Thread si = new Thread(this.input, "Shell Input");
- si.setPriority(Thread.NORM_PRIORITY);
- si.start();
-
- Thread so = new Thread(this.output, "Shell Output");
- so.setPriority(Thread.NORM_PRIORITY);
- so.start();
- }
- } catch (InterruptedException ex) {
- worker.interrupt();
- Thread.currentThread().interrupt();
- throw new TimeoutException();
- }
- }
-
-
- public Command add(Command command) throws IOException {
- if (this.close) {
- throw new IllegalStateException(
- "Unable to add commands to a closed shell");
- }
-
- if(command.used) {
- //The command has been used, don't re-use...
- throw new IllegalStateException(
- "This command has already been executed. (Don't re-use command instances.)");
- }
-
- while (this.isCleaning) {
- //Don't add commands while cleaning
- }
-
- this.commands.add(command);
-
- this.notifyThreads();
-
- return command;
- }
-
- public final void useCWD(Context context) throws IOException, TimeoutException, RootDeniedException {
- add(
- new Command(
- -1,
- false,
- "cd " + context.getApplicationInfo().dataDir)
- );
- }
-
- private void cleanCommands() {
- this.isCleaning = true;
- int toClean = Math.abs(this.maxCommands - (this.maxCommands / 4));
- RootShell.log("Cleaning up: " + toClean);
-
- this.commands.subList(0, toClean).clear();
-
- this.read = this.commands.size() - 1;
- this.write = this.commands.size() - 1;
- this.isCleaning = false;
- }
-
- private void closeQuietly(final Reader input) {
- try {
- if (input != null) {
- input.close();
- }
- } catch (Exception ignore) {
- }
- }
-
- private void closeQuietly(final Writer output) {
- try {
- if (output != null) {
- output.close();
- }
- } catch (Exception ignore) {
- }
- }
-
- public void close() throws IOException {
- RootShell.log("Request to close shell!");
-
- int count = 0;
- while (isExecuting) {
- RootShell.log("Waiting on shell to finish executing before closing...");
- count++;
-
- //fail safe
- if (count > 10000) {
- break;
- }
-
- }
-
- synchronized (this.commands) {
- /**
- * instruct the two threads monitoring input and output
- * of the shell to close.
- */
- this.close = true;
- this.notifyThreads();
- }
-
- RootShell.log("Shell Closed!");
-
- if (this == Shell.rootShell) {
- Shell.rootShell = null;
- } else if (this == Shell.shell) {
- Shell.shell = null;
- } else if (this == Shell.customShell) {
- Shell.customShell = null;
- }
- }
-
- public static void closeCustomShell() throws IOException {
- RootShell.log("Request to close custom shell!");
-
- if (Shell.customShell == null) {
- return;
- }
-
- Shell.customShell.close();
- }
-
- public static void closeRootShell() throws IOException {
- RootShell.log("Request to close root shell!");
-
- if (Shell.rootShell == null) {
- return;
- }
- Shell.rootShell.close();
- }
-
- public static void closeShell() throws IOException {
- RootShell.log("Request to close normal shell!");
-
- if (Shell.shell == null) {
- return;
- }
- Shell.shell.close();
- }
-
- public static void closeAll() throws IOException {
- RootShell.log("Request to close all shells!");
-
- Shell.closeShell();
- Shell.closeRootShell();
- Shell.closeCustomShell();
- }
-
- public int getCommandQueuePosition(Command cmd) {
- return this.commands.indexOf(cmd);
- }
-
- public String getCommandQueuePositionString(Command cmd) {
- return "Command is in position " + getCommandQueuePosition(cmd) + " currently executing command at position " + this.write + " and the number of commands is " + commands.size();
- }
-
- public static Shell getOpenShell() {
- if (Shell.customShell != null) {
- return Shell.customShell;
- } else if (Shell.rootShell != null) {
- return Shell.rootShell;
- } else {
- return Shell.shell;
- }
- }
-
- /**
- * From libsuperuser.
- *
- *
- * Detects the version of the su binary installed (if any), if supported
- * by the binary. Most binaries support two different version numbers,
- * the public version that is displayed to users, and an internal
- * version number that is used for version number comparisons. Returns
- * null if su not available or retrieving the version isn't supported.
- *
- *
- * Note that su binary version and GUI (APK) version can be completely
- * different.
- *
- *
- * This function caches its result to improve performance on multiple
- * calls
- *
- *
- * @param internal Request human-readable version or application
- * internal version
- * @return String containing the su version or null
- */
- private synchronized String getSuVersion(boolean internal) {
- int idx = internal ? 0 : 1;
- if (suVersion[idx] == null) {
- String version = null;
-
- // Replace libsuperuser:Shell.run with manual process execution
- Process process;
- try {
- process = Runtime.getRuntime().exec(internal ? "su -V" : "su -v", null);
- process.waitFor();
- } catch (IOException e) {
- e.printStackTrace();
- return null;
- } catch (InterruptedException e) {
- e.printStackTrace();
- return null;
- }
-
- // From libsuperuser:StreamGobbler
- List stdout = new ArrayList();
-
- BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
- try {
- String line = null;
- while ((line = reader.readLine()) != null) {
- stdout.add(line);
- }
- } catch (IOException e) {
- }
- // make sure our stream is closed and resources will be freed
- try {
- reader.close();
- } catch (IOException e) {
- }
-
- process.destroy();
-
- if (stdout != null) {
- for (String line : stdout) {
- if (!internal) {
- if (line.contains(".")) {
- version = line;
- break;
- }
- } else {
- try {
- if (Integer.parseInt(line) > 0) {
- version = line;
- break;
- }
- } catch (NumberFormatException e) {
- }
- }
- }
- }
-
- suVersion[idx] = version;
- }
- return suVersion[idx];
- }
-
- public static boolean isShellOpen() {
- return Shell.shell == null;
- }
-
- public static boolean isCustomShellOpen() {
- return Shell.customShell == null;
- }
-
- public static boolean isRootShellOpen() {
- return Shell.rootShell == null;
- }
-
- public static boolean isAnyShellOpen() {
- return Shell.shell != null || Shell.rootShell != null || Shell.customShell != null;
- }
-
- /**
- * From libsuperuser.
- *
- * Detect if SELinux is set to enforcing, caches result
- *
- * @return true if SELinux set to enforcing, or false in the case of
- * permissive or not present
- */
- public synchronized boolean isSELinuxEnforcing() {
- if (isSELinuxEnforcing == null) {
- Boolean enforcing = null;
-
- // First known firmware with SELinux built-in was a 4.2 (17)
- // leak
- if (android.os.Build.VERSION.SDK_INT >= 17) {
-
- // Detect enforcing through sysfs, not always present
- File f = new File("/sys/fs/selinux/enforce");
- if (f.exists()) {
- try {
- InputStream is = new FileInputStream("/sys/fs/selinux/enforce");
- try {
- enforcing = (is.read() == '1');
- } finally {
- is.close();
- }
- } catch (Exception e) {
- }
- }
-
- // 4.4+ builds are enforcing by default, take the gamble
- if (enforcing == null) {
- enforcing = (android.os.Build.VERSION.SDK_INT >= 19);
- }
- }
-
- if (enforcing == null) {
- enforcing = false;
- }
-
- isSELinuxEnforcing = enforcing;
- }
- return isSELinuxEnforcing;
- }
-
- /**
- * Runnable to write commands to the open shell.
- *
- * When writing commands we stay in a loop and wait for new
- * commands to added to "commands"
- *
- * The notification of a new command is handled by the method add in this class
- */
- private final Runnable input = new Runnable() {
- public void run() {
-
- try {
- while (true) {
-
- synchronized (commands) {
- /**
- * While loop is used in the case that notifyAll is called
- * and there are still no commands to be written, a rare
- * case but one that could happen.
- */
- while (!close && write >= commands.size()) {
- isExecuting = false;
- commands.wait();
- }
- }
-
- if (write >= maxCommands) {
-
- /**
- * wait for the read to catch up.
- */
- while (read != write) {
- RootShell.log("Waiting for read and write to catch up before cleanup.");
- }
- /**
- * Clean up the commands, stay neat.
- */
- cleanCommands();
- }
-
- /**
- * Write the new command
- *
- * We write the command followed by the token to indicate
- * the end of the command execution
- */
- if (write < commands.size()) {
- isExecuting = true;
- Command cmd = commands.get(write);
- cmd.startExecution();
- RootShell.log("Executing: " + cmd.getCommand() + " with context: " + shellContext);
-
- //write the command
- outputStream.write(cmd.getCommand());
- outputStream.flush();
-
- //write the token...
- String line = "\necho " + token + " " + totalExecuted + " $?\n";
- outputStream.write(line);
- outputStream.flush();
-
- write++;
- totalExecuted++;
- } else if (close) {
- /**
- * close the thread, the shell is closing.
- */
- isExecuting = false;
- outputStream.write("\nexit 0\n");
- outputStream.flush();
- RootShell.log("Closing shell");
- return;
- }
- }
- } catch (IOException | InterruptedException e) {
- RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e);
- }
- finally {
- write = 0;
- closeQuietly(outputStream);
- }
- }
- };
-
- protected void notifyThreads() {
- Thread t = new Thread() {
- public void run() {
- synchronized (commands) {
- commands.notifyAll();
- }
- }
- };
-
- t.start();
- }
-
- /**
- * Runnable to monitor the responses from the open shell.
- *
- * This include the output and error stream
- */
- private final Runnable output = new Runnable() {
- public void run() {
- try {
- Command command = null;
-
- //as long as there is something to read, we will keep reading.
- while (!close || inputStream.ready() || read < commands.size()) {
- isReading = false;
- String outputLine = inputStream.readLine();
- isReading = true;
-
- /**
- * If we receive EOF then the shell closed?
- */
- if (outputLine == null) {
- break;
- }
-
- if (command == null) {
- if (read >= commands.size()) {
- if (close) {
- break;
- }
-
- continue;
- }
-
- command = commands.get(read);
- }
-
- /**
- * trying to determine if all commands have been completed.
- *
- * if the token is present then the command has finished execution.
- */
- int pos = -1;
-
- pos = outputLine.indexOf(token);
-
- if (pos == -1) {
- /**
- * send the output for the implementer to process
- */
- command.output(command.id, outputLine);
- } else if (pos > 0) {
- /**
- * token is suffix of output, send output part to implementer
- */
- RootShell.log("Found token, line: " + outputLine);
- command.output(command.id, outputLine.substring(0, pos));
- }
-
- if (pos >= 0) {
- outputLine = outputLine.substring(pos);
- String[] fields = outputLine.split(" ");
-
- if (fields.length >= 2 && fields[1] != null) {
- int id = 0;
-
- try {
- id = Integer.parseInt(fields[1]);
- } catch (NumberFormatException e) {
- }
-
- int exitCode = -1;
-
- try {
- exitCode = Integer.parseInt(fields[2]);
- } catch (NumberFormatException e) {
- }
-
- if (id == totalRead) {
- processErrors(command);
-
-
- /**
- * wait for output to be processed...
- *
- */
- int iterations = 0;
- while (command.totalOutput > command.totalOutputProcessed) {
-
- if(iterations == 0)
- {
- iterations++;
- RootShell.log("Waiting for output to be processed. " + command.totalOutputProcessed + " Of " + command.totalOutput);
- }
-
- try {
-
- synchronized (this)
- {
- this.wait(2000);
- }
- } catch (Exception e) {
- RootShell.log(e.getMessage());
- }
- }
-
- RootShell.log("Read all output");
-
- command.setExitCode(exitCode);
- command.commandFinished();
-
- command = null;
-
- read++;
- totalRead++;
- continue;
- }
- }
- }
- }
-
- try {
- proc.waitFor();
- proc.destroy();
- } catch (Exception e) {
- }
-
- while (read < commands.size()) {
- if (command == null) {
- command = commands.get(read);
- }
-
- if(command.totalOutput < command.totalOutputProcessed)
- {
- command.terminated("All output not processed!");
- command.terminated("Did you forget the super.commandOutput call or are you waiting on the command object?");
- }
- else
- {
- command.terminated("Unexpected Termination.");
- }
-
- command = null;
- read++;
- }
-
- read = 0;
-
- } catch (IOException e) {
- RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e);
- } finally {
- closeQuietly(outputStream);
- closeQuietly(errorStream);
- closeQuietly(inputStream);
-
- RootShell.log("Shell destroyed");
- isClosed = true;
- isReading = false;
- }
- }
- };
-
- public void processErrors(Command command) {
- try {
- while (errorStream.ready() && command != null) {
- String line = errorStream.readLine();
-
- /**
- * If we recieve EOF then the shell closed?
- */
- if (line == null) {
- break;
- }
-
- /**
- * send the output for the implementer to process
- */
- command.output(command.id, line);
- }
- } catch (Exception e) {
- RootShell.log(e.getMessage(), RootShell.LogLevel.ERROR, e);
- }
- }
-
- public static Command runRootCommand(Command command) throws IOException, TimeoutException, RootDeniedException {
- return Shell.startRootShell().add(command);
- }
-
- public static Command runCommand(Command command) throws IOException, TimeoutException {
- return Shell.startShell().add(command);
- }
-
- public static Shell startRootShell() throws IOException, TimeoutException, RootDeniedException {
- return Shell.startRootShell(0, 3);
- }
-
- public static Shell startRootShell(int timeout) throws IOException, TimeoutException, RootDeniedException {
- return Shell.startRootShell(timeout, 3);
- }
-
- public static Shell startRootShell(int timeout, int retry) throws IOException, TimeoutException, RootDeniedException {
- return Shell.startRootShell(timeout, Shell.defaultContext, retry);
- }
-
- public static Shell startRootShell(int timeout, ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException {
- // keep prompting the user until they accept for x amount of times...
- int retries = 0;
-
- if (Shell.rootShell == null) {
-
- RootShell.log("Starting Root Shell!");
- String cmd = "su";
- while (Shell.rootShell == null) {
- try {
- RootShell.log("Trying to open Root Shell, attempt #" + retries);
- Shell.rootShell = new Shell(cmd, ShellType.ROOT, shellContext, timeout);
- } catch (IOException e) {
- if (retries++ >= retry) {
- RootShell.log("IOException, could not start shell");
- throw e;
- }
- } catch (RootDeniedException e) {
- if (retries++ >= retry) {
- RootShell.log("RootDeniedException, could not start shell");
- throw e;
- }
- } catch (TimeoutException e) {
- if (retries++ >= retry) {
- RootShell.log("TimeoutException, could not start shell");
- throw e;
- }
- }
- }
- } else if (Shell.rootShell.shellContext != shellContext) {
- try {
- RootShell.log("Context is different than open shell, switching context... " + Shell.rootShell.shellContext + " VS " + shellContext);
- Shell.rootShell.switchRootShellContext(shellContext);
- } catch (IOException e) {
- if (retries++ >= retry) {
- RootShell.log("IOException, could not switch context!");
- throw e;
- }
- } catch (RootDeniedException e) {
- if (retries++ >= retry) {
- RootShell.log("RootDeniedException, could not switch context!");
- throw e;
- }
- } catch (TimeoutException e) {
- if (retries++ >= retry) {
- RootShell.log("TimeoutException, could not switch context!");
- throw e;
- }
- }
- } else {
- RootShell.log("Using Existing Root Shell!");
- }
-
- return Shell.rootShell;
- }
-
- public static Shell startCustomShell(String shellPath) throws IOException, TimeoutException, RootDeniedException {
- return Shell.startCustomShell(shellPath, 0);
- }
-
- public static Shell startCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException {
-
- if (Shell.customShell == null) {
- RootShell.log("Starting Custom Shell!");
- Shell.customShell = new Shell(shellPath, ShellType.CUSTOM, ShellContext.NORMAL, timeout);
- } else {
- RootShell.log("Using Existing Custom Shell!");
- }
-
- return Shell.customShell;
- }
-
- public static Shell startShell() throws IOException, TimeoutException {
- return Shell.startShell(0);
- }
-
- public static Shell startShell(int timeout) throws IOException, TimeoutException {
-
- try {
- if (Shell.shell == null) {
- RootShell.log("Starting Shell!");
- Shell.shell = new Shell("/system/bin/sh", ShellType.NORMAL, ShellContext.NORMAL, timeout);
- } else {
- RootShell.log("Using Existing Shell!");
- }
- return Shell.shell;
- } catch (RootDeniedException e) {
- //Root Denied should never be thrown.
- throw new IOException();
- }
- }
-
- public Shell switchRootShellContext(ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException {
- if (this.shellType == ShellType.ROOT) {
- try {
- Shell.closeRootShell();
- } catch (Exception e) {
- RootShell.log("Problem closing shell while trying to switch context...");
- }
-
- //create new root shell with new context...
-
- return Shell.startRootShell(this.shellTimeout, shellContext, 3);
- } else {
- //can only switch context on a root shell...
- RootShell.log("Can only switch context on a root shell!");
- return this;
- }
- }
-
- protected static class Worker extends Thread {
-
- public int exit = -911;
-
- public Shell shell;
-
- private Worker(Shell shell) {
- this.shell = shell;
- }
-
- public void run() {
-
- /**
- * Trying to open the shell.
- *
- * We echo "Started" and we look for it in the output.
- *
- * If we find the output then the shell is open and we return.
- *
- * If we do not find it then we determine the error and report
- * it by setting the value of the variable exit
- */
- try {
- shell.outputStream.write("echo Started\n");
- shell.outputStream.flush();
-
- while (true) {
- String line = shell.inputStream.readLine();
-
- if (line == null) {
- throw new EOFException();
- } else if ("".equals(line)) {
- continue;
- } else if ("Started".equals(line)) {
- this.exit = 1;
- setShellOom();
- break;
- }
-
- shell.error = "unknown error occurred.";
- }
- } catch (IOException e) {
- exit = -42;
- if (e.getMessage() != null) {
- shell.error = e.getMessage();
- } else {
- shell.error = "RootAccess denied?.";
- }
- }
-
- }
-
- /*
- * setOom for shell processes (sh and su if root shell) and discard outputs
- * Negative values make the process LESS likely to be killed in an OOM situation
- * Positive values make the process MORE likely to be killed in an OOM situation
- */
- private void setShellOom() {
- try {
- Class> processClass = shell.proc.getClass();
- Field field;
- try {
- field = processClass.getDeclaredField("pid");
- } catch (NoSuchFieldException e) {
- field = processClass.getDeclaredField("id");
- }
- field.setAccessible(true);
- int pid = (Integer) field.get(shell.proc);
- shell.outputStream.write("(echo -17 > /proc/" + pid + "/oom_adj) &> /dev/null\n");
- shell.outputStream.write("(echo -17 > /proc/$$/oom_adj) &> /dev/null\n");
- shell.outputStream.flush();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
-}
diff --git a/app/src/main/java/com/stericson/roottools/Constants.java b/app/src/main/java/com/stericson/roottools/Constants.java
deleted file mode 100644
index f15ee7edc..000000000
--- a/app/src/main/java/com/stericson/roottools/Constants.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.stericson.roottools;
-
-public class Constants
-{
- public static final String TAG = "RootTools v4.4";
- public static final int FPS = 1;
- public static final int BBA = 3;
- public static final int BBV = 4;
- public static final int GI = 5;
- public static final int GS = 6;
- public static final int GSYM = 7;
- public static final int GET_MOUNTS = 8;
- public static final int GET_SYMLINKS = 9;
-
-}
diff --git a/app/src/main/java/com/stericson/roottools/RootTools.java b/app/src/main/java/com/stericson/roottools/RootTools.java
deleted file mode 100644
index 6b80873b5..000000000
--- a/app/src/main/java/com/stericson/roottools/RootTools.java
+++ /dev/null
@@ -1,848 +0,0 @@
-/*
- * This file is part of the RootTools Project: http://code.google.com/p/RootTools/
- *
- * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.roottools;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-import com.stericson.rootshell.RootShell;
-import com.stericson.rootshell.exceptions.RootDeniedException;
-import com.stericson.rootshell.execution.Command;
-import com.stericson.rootshell.execution.Shell;
-import com.stericson.roottools.containers.Mount;
-import com.stericson.roottools.containers.Permissions;
-import com.stericson.roottools.containers.Symlink;
-import com.stericson.roottools.internal.Remounter;
-import com.stericson.roottools.internal.RootToolsInternalMethods;
-import com.stericson.roottools.internal.Runner;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.TimeoutException;
-
-public final class RootTools {
-
- /**
- * This class is the gateway to every functionality within the RootTools library.The developer
- * should only have access to this class and this class only.This means that this class should
- * be the only one to be public.The rest of the classes within this library must not have the
- * public modifier.
- *
- * All methods and Variables that the developer may need to have access to should be here.
- *
- * If a method, or a specific functionality, requires a fair amount of code, or work to be done,
- * then that functionality should probably be moved to its own class and the call to it done
- * here.For examples of this being done, look at the remount functionality.
- */
-
- private static RootToolsInternalMethods rim = null;
-
- public static void setRim(RootToolsInternalMethods rim) {
- RootTools.rim = rim;
- }
-
- private static final RootToolsInternalMethods getInternals() {
- if (rim == null) {
- RootToolsInternalMethods.getInstance();
- return rim;
- } else {
- return rim;
- }
- }
-
- // --------------------
- // # Public Variables #
- // --------------------
-
- public static boolean debugMode = false;
- public static String utilPath;
-
- /**
- * Setting this to false will disable the handler that is used
- * by default for the 3 callback methods for Command.
- *
- * By disabling this all callbacks will be called from a thread other than
- * the main UI thread.
- */
- public static boolean handlerEnabled = true;
-
-
- /**
- * Setting this will change the default command timeout.
- *
- * The default is 20000ms
- */
- public static int default_Command_Timeout = 20000;
-
-
- // ---------------------------
- // # Public Variable Getters #
- // ---------------------------
-
- // ------------------
- // # Public Methods #
- // ------------------
-
- /**
- * This will check a given binary, determine if it exists and determine that it has either the
- * permissions 755, 775, or 777.
- *
- * @param util Name of the utility to check.
- * @return boolean to indicate whether the binary is installed and has appropriate permissions.
- */
- public static boolean checkUtil(String util) {
-
- return getInternals().checkUtil(util);
- }
-
- /**
- * This will close all open shells.
- *
- * @throws IOException
- */
- public static void closeAllShells() throws IOException {
- RootShell.closeAllShells();
- }
-
- /**
- * This will close the custom shell that you opened.
- *
- * @throws IOException
- */
- public static void closeCustomShell() throws IOException {
- RootShell.closeCustomShell();
- }
-
- /**
- * This will close either the root shell or the standard shell depending on what you specify.
- *
- * @param root a boolean to specify whether to close the root shell or the standard shell.
- * @throws IOException
- */
- public static void closeShell(boolean root) throws IOException {
- RootShell.closeShell(root);
- }
-
- /**
- * Copys a file to a destination. Because cp is not available on all android devices, we have a
- * fallback on the cat command
- *
- * @param source example: /data/data/org.adaway/files/hosts
- * @param destination example: /system/etc/hosts
- * @param remountAsRw remounts the destination as read/write before writing to it
- * @param preserveFileAttributes tries to copy file attributes from source to destination, if only cat is available
- * only permissions are preserved
- * @return true if it was successfully copied
- */
- public static boolean copyFile(String source, String destination, boolean remountAsRw,
- boolean preserveFileAttributes) {
- return getInternals().copyFile(source, destination, remountAsRw, preserveFileAttributes);
- }
-
- /**
- * Deletes a file or directory
- *
- * @param target example: /data/data/org.adaway/files/hosts
- * @param remountAsRw remounts the destination as read/write before writing to it
- * @return true if it was successfully deleted
- */
- public static boolean deleteFileOrDirectory(String target, boolean remountAsRw) {
- return getInternals().deleteFileOrDirectory(target, remountAsRw);
- }
-
- /**
- * Use this to check whether or not a file exists on the filesystem.
- *
- * @param file String that represent the file, including the full path to the
- * file and its name.
- * @return a boolean that will indicate whether or not the file exists.
- */
- public static boolean exists(final String file) {
- return exists(file, false);
- }
-
- /**
- * Use this to check whether or not a file OR directory exists on the filesystem.
- *
- * @param file String that represent the file OR the directory, including the full path to the
- * file and its name.
- * @param isDir boolean that represent whether or not we are looking for a directory
- * @return a boolean that will indicate whether or not the file exists.
- */
- public static boolean exists(final String file, boolean isDir) {
- return RootShell.exists(file, isDir);
- }
-
- /**
- * This will try and fix a given binary. (This is for Busybox applets or Toolbox applets) By
- * "fix", I mean it will try and symlink the binary from either toolbox or Busybox and fix the
- * permissions if the permissions are not correct.
- *
- * @param util Name of the utility to fix.
- * @param utilPath path to the toolbox that provides ln, rm, and chmod. This can be a blank string, a
- * path to a binary that will provide these, or you can use
- * RootTools.getWorkingToolbox()
- */
- public static void fixUtil(String util, String utilPath) {
- getInternals().fixUtil(util, utilPath);
- }
-
- /**
- * This will check an array of binaries, determine if they exist and determine that it has
- * either the permissions 755, 775, or 777. If an applet is not setup correctly it will try and
- * fix it. (This is for Busybox applets or Toolbox applets)
- *
- * @param utils Name of the utility to check.
- * @return boolean to indicate whether the operation completed. Note that this is not indicative
- * of whether the problem was fixed, just that the method did not encounter any
- * exceptions.
- * @throws Exception if the operation cannot be completed.
- */
- public static boolean fixUtils(String[] utils) throws Exception {
- return getInternals().fixUtils(utils);
- }
-
- /**
- * @param binaryName String that represent the binary to find.
- * @param singlePath boolean that represents whether to return a single path or multiple.
- *
- * @return List containing the paths the binary was found at.
- */
- public static List findBinary(String binaryName, boolean singlePath) {
- return RootShell.findBinary(binaryName, singlePath);
- }
-
- /**
- * @param path String that represents the path to the Busybox binary you want to retrieve the version of.
- * @return BusyBox version is found, "" if not found.
- */
- public static String getBusyBoxVersion(String path) {
- return getInternals().getBusyBoxVersion(path);
- }
-
- /**
- * @return BusyBox version is found, "" if not found.
- */
- public static String getBusyBoxVersion() {
- return RootTools.getBusyBoxVersion("");
- }
-
- /**
- * This will return an List of Strings. Each string represents an applet available from BusyBox.
- *
- *
- * @return null If we cannot return the list of applets.
- */
- public static List getBusyBoxApplets() throws Exception {
- return RootTools.getBusyBoxApplets("");
- }
-
- /**
- * This will return an List of Strings. Each string represents an applet available from BusyBox.
- *
- *
- * @param path Path to the busybox binary that you want the list of applets from.
- * @return null If we cannot return the list of applets.
- */
- public static List getBusyBoxApplets(String path) throws Exception {
- return getInternals().getBusyBoxApplets(path);
- }
-
- /**
- * This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param shellPath a String to Indicate the path to the shell that you want to open.
- * @param timeout an int to Indicate the length of time before giving up on opening a shell.
- * @throws TimeoutException
- * @throws com.stericson.RootShell.exceptions.RootDeniedException
- * @throws IOException
- */
- public static Shell getCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException {
- return RootShell.getCustomShell(shellPath, timeout);
- }
-
- /**
- * This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param shellPath a String to Indicate the path to the shell that you want to open.
- * @throws TimeoutException
- * @throws com.stericson.RootShell.exceptions.RootDeniedException
- * @throws IOException
- */
- public static Shell getCustomShell(String shellPath) throws IOException, TimeoutException, RootDeniedException {
- return RootTools.getCustomShell(shellPath, 10000);
- }
-
- /**
- * @param file String that represent the file, including the full path to the file and its name.
- * @return An instance of the class permissions from which you can get the permissions of the
- * file or if the file could not be found or permissions couldn't be determined then
- * permissions will be null.
- */
- public static Permissions getFilePermissionsSymlinks(String file) {
- return getInternals().getFilePermissionsSymlinks(file);
- }
-
- /**
- * This method will return the inode number of a file. This method is dependent on having a version of
- * ls that supports the -i parameter.
- *
- * @param file path to the file that you wish to return the inode number
- * @return String The inode number for this file or "" if the inode number could not be found.
- */
- public static String getInode(String file) {
- return getInternals().getInode(file);
- }
-
- /**
- * This will return an ArrayList of the class Mount. The class mount contains the following
- * property's: device mountPoint type flags
- *
- * These will provide you with any information you need to work with the mount points.
- *
- * @return ArrayList an ArrayList of the class Mount.
- * @throws Exception if we cannot return the mount points.
- */
- public static ArrayList getMounts() throws Exception {
- return getInternals().getMounts();
- }
-
- /**
- * This will tell you how the specified mount is mounted. rw, ro, etc...
- *
- *
- * @param path The mount you want to check
- * @return String What the mount is mounted as.
- * @throws Exception if we cannot determine how the mount is mounted.
- */
- public static String getMountedAs(String path) throws Exception {
- return getInternals().getMountedAs(path);
- }
-
- /**
- * This will return the environment variable PATH
- *
- * @return List A List of Strings representing the environment variable $PATH
- */
- public static List getPath() {
- return Arrays.asList(System.getenv("PATH").split(":"));
- }
-
- /**
- * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell
- * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell.
- * @param shellContext the context to execute the shell with
- * @param retry a int to indicate how many times the ROOT shell should try to open with root priviliges...
- * @throws TimeoutException
- * @throws com.stericson.RootShell.exceptions.RootDeniedException
- * @throws IOException
- */
- public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException {
- return RootShell.getShell(root, timeout, shellContext, retry);
- }
-
- /**
- * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell
- * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell.
- * @param shellContext the context to execute the shell with
- * @throws TimeoutException
- * @throws com.stericson.RootShell.exceptions.RootDeniedException
- * @throws IOException
- */
- public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException {
- return getShell(root, timeout, shellContext, 3);
- }
-
- /**
- * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell
- * @param shellContext the context to execute the shell with
- * @throws TimeoutException
- * @throws com.stericson.RootShell.exceptions.RootDeniedException
- * @throws IOException
- */
- public static Shell getShell(boolean root, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException {
- return getShell(root, 0, shellContext, 3);
- }
-
- /**
- * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell
- * @param timeout an int to Indicate the length of time to wait before giving up on opening a shell.
- * @throws TimeoutException
- * @throws com.stericson.RootShell.exceptions.RootDeniedException
- * @throws IOException
- */
- public static Shell getShell(boolean root, int timeout) throws IOException, TimeoutException, RootDeniedException {
- return getShell(root, timeout, Shell.defaultContext, 3);
- }
-
- /**
- * This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
- * and for closing the shell when you are done using it.
- *
- * @param root a boolean to Indicate whether or not you want to open a root shell or a standard shell
- * @throws TimeoutException
- * @throws com.stericson.RootShell.exceptions.RootDeniedException
- * @throws IOException
- */
- public static Shell getShell(boolean root) throws IOException, TimeoutException, RootDeniedException {
- return RootTools.getShell(root, 0);
- }
-
- /**
- * Get the space for a desired partition.
- *
- * @param path The partition to find the space for.
- * @return the amount if space found within the desired partition. If the space was not found
- * then the value is -1
- * @throws TimeoutException
- */
- public static long getSpace(String path) {
- return getInternals().getSpace(path);
- }
-
- /**
- * This will return a String that represent the symlink for a specified file.
- *
- *
- * @param file path to the file to get the Symlink for. (must have absolute path)
- * @return String a String that represent the symlink for a specified file or an
- * empty string if no symlink exists.
- */
- public static String getSymlink(String file) {
- return getInternals().getSymlink(file);
- }
-
- /**
- * This will return an ArrayList of the class Symlink. The class Symlink contains the following
- * property's: path SymplinkPath
- *
- * These will provide you with any Symlinks in the given path.
- *
- * @param path path to search for Symlinks.
- * @return ArrayList an ArrayList of the class Symlink.
- * @throws Exception if we cannot return the Symlinks.
- */
- public static ArrayList getSymlinks(String path) throws Exception {
- return getInternals().getSymlinks(path);
- }
-
- /**
- * This will return to you a string to be used in your shell commands which will represent the
- * valid working toolbox with correct permissions. For instance, if Busybox is available it will
- * return "busybox", if busybox is not available but toolbox is then it will return "toolbox"
- *
- * @return String that indicates the available toolbox to use for accessing applets.
- */
- public static String getWorkingToolbox() {
- return getInternals().getWorkingToolbox();
- }
-
- /**
- * Checks if there is enough Space on SDCard
- *
- * @param updateSize size to Check (long)
- * @return true if the Update will fit on SDCard, false if not enough
- * space on SDCard. Will also return false, if the SDCard is not mounted as
- * read/write
- */
- public static boolean hasEnoughSpaceOnSdCard(long updateSize) {
- return getInternals().hasEnoughSpaceOnSdCard(updateSize);
- }
-
- /**
- * Checks whether the toolbox or busybox binary contains a specific util
- *
- * @param util
- * @param box Should contain "toolbox" or "busybox"
- * @return true if it contains this util
- */
- public static boolean hasUtil(final String util, final String box) {
- //TODO Convert this to use the new shell.
- return getInternals().hasUtil(util, box);
- }
-
- /**
- * This method can be used to unpack a binary from the raw resources folder and store it in
- * /data/data/app.package/files/ This is typically useful if you provide your own C- or
- * C++-based binary. This binary can then be executed using sendShell() and its full path.
- *
- * @param context the current activity's Context
- * @param sourceId resource id; typically R.raw.id
- * @param destName destination file name; appended to /data/data/app.package/files/
- * @param mode chmod value for this file
- * @return a boolean which indicates whether or not we were able to create the new
- * file.
- */
- public static boolean installBinary(Context context, int sourceId, String destName, String mode) {
- return getInternals().installBinary(context, sourceId, destName, mode);
- }
-
- /**
- * This method can be used to unpack a binary from the raw resources folder and store it in
- * /data/data/app.package/files/ This is typically useful if you provide your own C- or
- * C++-based binary. This binary can then be executed using sendShell() and its full path.
- *
- * @param context the current activity's Context
- * @param sourceId resource id; typically R.raw.id
- * @param binaryName destination file name; appended to /data/data/app.package/files/
- * @return a boolean which indicates whether or not we were able to create the new
- * file.
- */
- public static boolean installBinary(Context context, int sourceId, String binaryName) {
- return installBinary(context, sourceId, binaryName, "700");
- }
-
- /**
- * This method checks whether a binary is installed.
- *
- * @param context the current activity's Context
- * @param binaryName binary file name; appended to /data/data/app.package/files/
- * @return a boolean which indicates whether or not
- * the binary already exists.
- */
- public static boolean hasBinary(Context context, String binaryName) {
- return getInternals().isBinaryAvailable(context, binaryName);
- }
-
- /**
- * This will let you know if an applet is available from BusyBox
- *
- *
- * @param applet The applet to check for.
- * @param path Path to the busybox binary that you want to check. (do not include binary name)
- * @return true if applet is available, false otherwise.
- */
- public static boolean isAppletAvailable(String applet, String path) {
- return getInternals().isAppletAvailable(applet, path);
- }
-
- /**
- * This will let you know if an applet is available from BusyBox
- *
- *
- * @param applet The applet to check for.
- * @return true if applet is available, false otherwise.
- */
- public static boolean isAppletAvailable(String applet) {
- return RootTools.isAppletAvailable(applet, "");
- }
- /**
- * @return true if your app has been given root access.
- * @throws TimeoutException if this operation times out. (cannot determine if access is given)
- */
- public static boolean isAccessGiven() {
- return RootShell.isAccessGiven();
- }
-
- /**
- * Control how many time of retries should request
- *
- * @param timeout The timeout
- * @param retries The number of retries
- *
- * @return true if your app has been given root access.
- * @throws TimeoutException if this operation times out. (cannot determine if access is given)
- */
- public static boolean isAccessGiven(int timeout, int retries) {
- return RootShell.isAccessGiven(timeout, retries);
- }
-
- /**
- * @return true if BusyBox was found.
- */
- public static boolean isBusyboxAvailable() {
- return RootShell.isBusyboxAvailable();
- }
-
- public static boolean isNativeToolsReady(int nativeToolsId, Context context) {
- return getInternals().isNativeToolsReady(nativeToolsId, context);
- }
-
- /**
- * This method can be used to to check if a process is running
- *
- * @param processName name of process to check
- * @return true if process was found
- * @throws TimeoutException (Could not determine if the process is running)
- */
- public static boolean isProcessRunning(final String processName) {
- //TODO convert to new shell
- return getInternals().isProcessRunning(processName);
- }
-
- /**
- * @return true if su was found.
- */
- public static boolean isRootAvailable() {
- return RootShell.isRootAvailable();
- }
-
- /**
- * This method can be used to kill a running process
- *
- * @param processName name of process to kill
- * @return true if process was found and killed successfully
- */
- public static boolean killProcess(final String processName) {
- //TODO convert to new shell
- return getInternals().killProcess(processName);
- }
-
- /**
- * This will launch the Android market looking for BusyBox
- *
- * @param activity pass in your Activity
- */
- public static void offerBusyBox(Activity activity) {
- getInternals().offerBusyBox(activity);
- }
-
- /**
- * This will launch the Android market looking for BusyBox, but will return the intent fired and
- * starts the activity with startActivityForResult
- *
- * @param activity pass in your Activity
- * @param requestCode pass in the request code
- * @return intent fired
- */
- public static Intent offerBusyBox(Activity activity, int requestCode) {
- return getInternals().offerBusyBox(activity, requestCode);
- }
-
- /**
- * This will launch the Android market looking for SuperUser
- *
- * @param activity pass in your Activity
- */
- public static void offerSuperUser(Activity activity) {
- getInternals().offerSuperUser(activity);
- }
-
- /**
- * This will launch the Android market looking for SuperUser, but will return the intent fired
- * and starts the activity with startActivityForResult
- *
- * @param activity pass in your Activity
- * @param requestCode pass in the request code
- * @return intent fired
- */
- public static Intent offerSuperUser(Activity activity, int requestCode) {
- return getInternals().offerSuperUser(activity, requestCode);
- }
-
- /**
- * This will take a path, which can contain the file name as well, and attempt to remount the
- * underlying partition.
- *
- * For example, passing in the following string:
- * "/system/bin/some/directory/that/really/would/never/exist" will result in /system ultimately
- * being remounted. However, keep in mind that the longer the path you supply, the more work
- * this has to do, and the slower it will run.
- *
- * @param file file path
- * @param mountType mount type: pass in RO (Read only) or RW (Read Write)
- * @return a boolean which indicates whether or not the partition has been
- * remounted as specified.
- */
- public static boolean remount(String file, String mountType) {
- // Recieved a request, get an instance of Remounter
- Remounter remounter = new Remounter();
- // send the request.
- return (remounter.remount(file, mountType));
- }
-
- public static boolean remount(String file, String mountType, String customPath) {
- // Recieved a request, get an instance of Remounter
- Remounter remounter = new Remounter(customPath);
- // send the request.
- return (remounter.remount(file, mountType));
- }
-
- /**
- * This restarts only Android OS without rebooting the whole device. This does NOT work on all
- * devices. This is done by killing the main init process named zygote. Zygote is restarted
- * automatically by Android after killing it.
- *
- * @throws TimeoutException
- */
- /* public static void restartAndroid() {
- RootTools.log("Restart Android");
- killProcess("zygote");
- }*/
-
- /**
- * Executes binary in a separated process. Before using this method, the binary has to be
- * installed in /data/data/app.package/files/ using the installBinary method.
- *
- * @param context the current activity's Context
- * @param binaryName name of installed binary
- * @param parameter parameter to append to binary like "-vxf"
- */
- public static void runBinary(Context context, String binaryName, String parameter) {
- Runner runner = new Runner(context, binaryName, parameter);
- runner.start();
- }
-
- /**
- * Executes a given command with root access or without depending on the value of the boolean passed.
- * This will also start a root shell or a standard shell without you having to open it specifically.
- *
- * You will still need to close the shell after you are done using the shell.
- *
- * @param shell The shell to execute the command on, this can be a root shell or a standard shell.
- * @param command The command to execute in the shell
- *
- * @throws IOException
- */
- public static void runShellCommand(Shell shell, Command command) throws IOException {
- shell.add(command);
- }
-
- /**
- * This method allows you to output debug messages only when debugging is on. This will allow
- * you to add a debug option to your app, which by default can be left off for performance.
- * However, when you need debugging information, a simple switch can enable it and provide you
- * with detailed logging.
- *
- * This method handles whether or not to log the information you pass it depending whether or
- * not RootTools.debugMode is on. So you can use this and not have to worry about handling it
- * yourself.
- *
- * @param msg The message to output.
- */
- public static void log(String msg) {
- log(null, msg, 3, null);
- }
-
- /**
- * This method allows you to output debug messages only when debugging is on. This will allow
- * you to add a debug option to your app, which by default can be left off for performance.
- * However, when you need debugging information, a simple switch can enable it and provide you
- * with detailed logging.
- *
- * This method handles whether or not to log the information you pass it depending whether or
- * not RootTools.debugMode is on. So you can use this and not have to worry about handling it
- * yourself.
- *
- * @param TAG Optional parameter to define the tag that the Log will use.
- * @param msg The message to output.
- */
- public static void log(String TAG, String msg) {
- log(TAG, msg, 3, null);
- }
-
- /**
- * This method allows you to output debug messages only when debugging is on. This will allow
- * you to add a debug option to your app, which by default can be left off for performance.
- * However, when you need debugging information, a simple switch can enable it and provide you
- * with detailed logging.
- *
- * This method handles whether or not to log the information you pass it depending whether or
- * not RootTools.debugMode is on. So you can use this and not have to worry about handling it
- * yourself.
- *
- * @param msg The message to output.
- * @param type The type of log, 1 for verbose, 2 for error, 3 for debug
- * @param e The exception that was thrown (Needed for errors)
- */
- public static void log(String msg, int type, Exception e) {
- log(null, msg, type, e);
- }
-
- /**
- * This method allows you to check whether logging is enabled.
- * Yes, it has a goofy name, but that's to keep it as short as possible.
- * After all writing logging calls should be painless.
- * This method exists to save Android going through the various Java layers
- * that are traversed any time a string is created (i.e. what you are logging)
- *
- * Example usage:
- * if(islog) {
- * StrinbBuilder sb = new StringBuilder();
- * // ...
- * // build string
- * // ...
- * log(sb.toString());
- * }
- *
- * @return true if logging is enabled
- */
- public static boolean islog() {
- return debugMode;
- }
-
- /**
- * This method allows you to output debug messages only when debugging is on. This will allow
- * you to add a debug option to your app, which by default can be left off for performance.
- * However, when you need debugging information, a simple switch can enable it and provide you
- * with detailed logging.
- *
- * This method handles whether or not to log the information you pass it depending whether or
- * not RootTools.debugMode is on. So you can use this and not have to worry about handling it
- * yourself.
- *
- * @param TAG Optional parameter to define the tag that the Log will use.
- * @param msg The message to output.
- * @param type The type of log, 1 for verbose, 2 for error, 3 for debug
- * @param e The exception that was thrown (Needed for errors)
- */
- public static void log(String TAG, String msg, int type, Exception e) {
- if (msg != null && !msg.equals("")) {
- if (debugMode) {
- if (TAG == null) {
- TAG = Constants.TAG;
- }
-
- switch (type) {
- case 1:
- Log.v(TAG, msg);
- break;
- case 2:
- Log.e(TAG, msg, e);
- break;
- case 3:
- Log.d(TAG, msg);
- break;
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/com/stericson/roottools/SanityCheckRootTools.java b/app/src/main/java/com/stericson/roottools/SanityCheckRootTools.java
deleted file mode 100644
index 9d36748d7..000000000
--- a/app/src/main/java/com/stericson/roottools/SanityCheckRootTools.java
+++ /dev/null
@@ -1,459 +0,0 @@
-/*
- * This file is part of the RootTools Project: http://code.google.com/p/RootTools/
- *
- * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.roottools;
-
-import android.app.Activity;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
-import android.os.StrictMode;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import com.stericson.rootshell.exceptions.RootDeniedException;
-import com.stericson.rootshell.execution.Command;
-import com.stericson.rootshell.execution.Shell;
-import com.stericson.roottools.containers.Permissions;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.concurrent.TimeoutException;
-
-public class SanityCheckRootTools extends Activity {
- private ScrollView mScrollView;
- private TextView mTextView;
- private ProgressDialog mPDialog;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
- .detectDiskReads()
- .detectDiskWrites()
- .detectNetwork() // or .detectAll() for all detectable problems
- .penaltyLog()
- .build());
- StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
- .detectLeakedSqlLiteObjects()
- .detectLeakedClosableObjects()
- .penaltyLog()
- .penaltyDeath()
- .build());
-
- RootTools.debugMode = true;
-
- mTextView = new TextView(this);
- mTextView.setText("");
- mScrollView = new ScrollView(this);
- mScrollView.addView(mTextView);
- setContentView(mScrollView);
-
- print("SanityCheckRootTools \n\n");
-
- if (RootTools.isRootAvailable()) {
- print("Root found.\n");
- } else {
- print("Root not found");
- }
-
- try {
- Shell.startRootShell();
- } catch (IOException e2) {
- // TODO Auto-generated catch block
- e2.printStackTrace();
- } catch (TimeoutException e) {
- print("[ TIMEOUT EXCEPTION! ]\n");
- e.printStackTrace();
- } catch (RootDeniedException e) {
- print("[ ROOT DENIED EXCEPTION! ]\n");
- e.printStackTrace();
- }
-
- try {
- if (!RootTools.isAccessGiven()) {
- print("ERROR: No root access to this device.\n");
- return;
- }
- } catch (Exception e) {
- print("ERROR: could not determine root access to this device.\n");
- return;
- }
-
- // Display infinite progress bar
- mPDialog = new ProgressDialog(this);
- mPDialog.setCancelable(false);
- mPDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
-
- new SanityCheckThread(this, new TestHandler()).start();
- }
-
- protected void print(CharSequence text) {
- mTextView.append(text);
- mScrollView.post(new Runnable() {
- public void run() {
- mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
- }
- });
- }
-
- // Run our long-running tests in their separate thread so as to
- // not interfere with proper rendering.
- private class SanityCheckThread extends Thread {
- private final Handler mHandler;
-
- public SanityCheckThread(Context context, Handler handler) {
- mHandler = handler;
- }
-
- public void run() {
- visualUpdate(TestHandler.ACTION_SHOW, null);
-
- // First test: Install a binary file for future use
- // if it wasn't already installed.
- /*
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Installing binary if needed");
- if(false == RootTools.installBinary(mContext, R.raw.nes, "nes_binary")) {
- visualUpdate(TestHandler.ACTION_HIDE, "ERROR: Failed to install binary. Please see log file.");
- return;
- }
- */
-
- boolean result;
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getPath");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ getPath ]\n");
-
- try {
- List paths = RootTools.getPath();
-
- for (String path : paths) {
- visualUpdate(TestHandler.ACTION_DISPLAY, path + " k\n\n");
- }
-
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing A ton of commands");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Ton of Commands ]\n");
-
- for (int i = 0; i < 100; i++) {
- RootTools.exists("/system/xbin/busybox");
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Find Binary");
- result = RootTools.isRootAvailable();
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Root ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing file exists");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Exists() ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.exists("/system/sbin/[") + " k\n\n");
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Is Access Given");
- result = RootTools.isAccessGiven();
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking for Access to Root ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Remount");
- result = RootTools.remount("/system", "rw");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Remounting System as RW ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing CheckUtil");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking busybox is setup ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.checkUtil("busybox") + " k\n\n");
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getBusyBoxVersion");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking busybox version ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.getBusyBoxVersion("/system/xbin/") + " k\n\n");
-
- try {
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing fixUtils");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Utils ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.fixUtils(new String[]{"ls", "rm", "ln", "dd", "chmod", "mount"}) + " k\n\n");
- } catch (Exception e2) {
- // TODO Auto-generated catch block
- e2.printStackTrace();
- }
-
- try {
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getSymlink");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking [[ for symlink ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.getSymlink("/system/bin/[[") + " k\n\n");
- } catch (Exception e2) {
- // TODO Auto-generated catch block
- e2.printStackTrace();
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getInode");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Inodes ]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.getInode("/system/bin/busybox") + " k\n\n");
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing GetBusyBoxapplets");
- try {
-
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Getting all available Busybox applets ]\n");
- for (String applet : RootTools.getBusyBoxApplets("/data/data/stericson.busybox/files/bb/busybox")) {
- visualUpdate(TestHandler.ACTION_DISPLAY, applet + " k\n\n");
- }
-
- } catch (Exception e1) {
- // TODO Auto-generated catch block
- e1.printStackTrace();
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing GetBusyBox version in a special directory!");
- try {
-
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Testing GetBusyBox version in a special directory! ]\n");
- String v = RootTools.getBusyBoxVersion("/data/data/stericson.busybox/files/bb/");
-
- visualUpdate(TestHandler.ACTION_DISPLAY, v + " k\n\n");
-
- } catch (Exception e1) {
- // TODO Auto-generated catch block
- e1.printStackTrace();
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getFilePermissionsSymlinks");
- Permissions permissions = RootTools.getFilePermissionsSymlinks("/system/xbin/busybox");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking busybox permissions and symlink ]\n");
-
- if (permissions != null) {
- visualUpdate(TestHandler.ACTION_DISPLAY, "Symlink: " + permissions.getSymlink() + " k\n\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, "Group Permissions: " + permissions.getGroupPermissions() + " k\n\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, "Owner Permissions: " + permissions.getOtherPermissions() + " k\n\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, "Permissions: " + permissions.getPermissions() + " k\n\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, "Type: " + permissions.getType() + " k\n\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, "User Permissions: " + permissions.getUserPermissions() + " k\n\n");
- } else {
- visualUpdate(TestHandler.ACTION_DISPLAY, "Permissions == null k\n\n");
- }
-
- Shell shell;
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing output capture");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ busybox ash --help ]\n");
-
- try {
- shell = RootTools.getShell(true);
- Command cmd = new Command(
- 0,
- "busybox ash --help") {
-
- @Override
- public void commandOutput(int id, String line) {
- visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
- super.commandOutput(id, line);
- }
- };
- shell.add(cmd);
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "getevent - /dev/input/event0");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ getevent - /dev/input/event0 ]\n");
-
- cmd = new Command(0, 0, "getevent /dev/input/event0") {
- @Override
- public void commandOutput(int id, String line) {
- visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
- super.commandOutput(id, line);
- }
-
- };
- shell.add(cmd);
-
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - SYSTEM_APP");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - SYSTEM_APP ]\n");
-
- try {
- shell = RootTools.getShell(true, Shell.ShellContext.SYSTEM_APP);
- Command cmd = new Command(
- 0,
- "id") {
-
- @Override
- public void commandOutput(int id, String line) {
- visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
- super.commandOutput(id, line);
- }
- };
- shell.add(cmd);
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing PM");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Testing pm list packages -d ]\n");
-
- cmd = new Command(
- 0,
- "sh /system/bin/pm list packages -d") {
-
- @Override
- public void commandOutput(int id, String line) {
- visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
- super.commandOutput(id, line);
- }
- };
- shell.add(cmd);
-
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - UNTRUSTED");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - UNTRUSTED ]\n");
-
- try {
- shell = RootTools.getShell(true, Shell.ShellContext.UNTRUSTED_APP);
- Command cmd = new Command(
- 0,
- "id") {
-
- @Override
- public void commandOutput(int id, String line) {
- visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
- super.commandOutput(id, line);
- }
- };
- shell.add(cmd);
-
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing df");
- long spaceValue = RootTools.getSpace("/data");
- visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking /data partition size]\n");
- visualUpdate(TestHandler.ACTION_DISPLAY, spaceValue + "k\n\n");
-
- try {
- shell = RootTools.getShell(true);
-
- Command cmd = new Command(42, false, "echo done") {
-
- boolean _catch = false;
-
- @Override
- public void commandOutput(int id, String line) {
- if (_catch) {
- RootTools.log("CAUGHT!!!");
- }
-
- super.commandOutput(id, line);
-
- }
-
- @Override
- public void commandTerminated(int id, String reason) {
- synchronized (SanityCheckRootTools.this) {
-
- _catch = true;
- visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete.");
- visualUpdate(TestHandler.ACTION_HIDE, null);
-
- try {
- RootTools.closeAllShells();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
-
- }
- }
-
- @Override
- public void commandCompleted(int id, int exitCode) {
- synchronized (SanityCheckRootTools.this) {
- _catch = true;
-
- visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete.");
- visualUpdate(TestHandler.ACTION_HIDE, null);
-
- try {
- RootTools.closeAllShells();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
-
- }
- }
- };
-
- shell.add(cmd);
-
- } catch (Exception e) {
- e.printStackTrace();
- }
-
- }
-
- private void visualUpdate(int action, String text) {
- Message msg = mHandler.obtainMessage();
- Bundle bundle = new Bundle();
- bundle.putInt(TestHandler.ACTION, action);
- bundle.putString(TestHandler.TEXT, text);
- msg.setData(bundle);
- mHandler.sendMessage(msg);
- }
- }
-
- private class TestHandler extends Handler {
- static final public String ACTION = "action";
- static final public int ACTION_SHOW = 0x01;
- static final public int ACTION_HIDE = 0x02;
- static final public int ACTION_DISPLAY = 0x03;
- static final public int ACTION_PDISPLAY = 0x04;
- static final public String TEXT = "text";
-
- public void handleMessage(Message msg) {
- int action = msg.getData().getInt(ACTION);
- String text = msg.getData().getString(TEXT);
-
- switch (action) {
- case ACTION_SHOW:
- mPDialog.show();
- mPDialog.setMessage("Running Root Library Tests...");
- break;
- case ACTION_HIDE:
- if (null != text) {
- print(text);
- }
- mPDialog.hide();
- break;
- case ACTION_DISPLAY:
- print(text);
- break;
- case ACTION_PDISPLAY:
- mPDialog.setMessage(text);
- break;
- }
- }
- }
-}
diff --git a/app/src/main/java/com/stericson/roottools/containers/Mount.java b/app/src/main/java/com/stericson/roottools/containers/Mount.java
deleted file mode 100644
index ce455f2d4..000000000
--- a/app/src/main/java/com/stericson/roottools/containers/Mount.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * This file is part of the RootTools Project: http://code.google.com/p/RootTools/
- *
- * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.roottools.containers;
-
-import java.io.File;
-import java.util.Arrays;
-import java.util.LinkedHashSet;
-import java.util.Set;
-
-public class Mount
-{
- final File mDevice;
- final File mMountPoint;
- final String mType;
- final Set mFlags;
-
- public Mount(File device, File path, String type, String flagsStr)
- {
- mDevice = device;
- mMountPoint = path;
- mType = type;
- mFlags = new LinkedHashSet(Arrays.asList(flagsStr.split(",")));
- }
-
- public File getDevice()
- {
- return mDevice;
- }
-
- public File getMountPoint()
- {
- return mMountPoint;
- }
-
- public String getType()
- {
- return mType;
- }
-
- public Set getFlags()
- {
- return mFlags;
- }
-
- @Override
- public String toString()
- {
- return String.format("%s on %s type %s %s", mDevice, mMountPoint, mType, mFlags);
- }
-}
diff --git a/app/src/main/java/com/stericson/roottools/containers/Permissions.java b/app/src/main/java/com/stericson/roottools/containers/Permissions.java
deleted file mode 100644
index 51aae5f0d..000000000
--- a/app/src/main/java/com/stericson/roottools/containers/Permissions.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * This file is part of the RootTools Project: http://code.google.com/p/RootTools/
- *
- * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.roottools.containers;
-
-public class Permissions
-{
- String type;
- String user;
- String group;
- String other;
- String symlink;
- int permissions;
-
- public String getSymlink()
- {
- return this.symlink;
- }
-
- public String getType()
- {
- return type;
- }
-
- public int getPermissions()
- {
- return this.permissions;
- }
-
- public String getUserPermissions()
- {
- return this.user;
- }
-
- public String getGroupPermissions()
- {
- return this.group;
- }
-
- public String getOtherPermissions()
- {
- return this.other;
- }
-
- public void setSymlink(String symlink)
- {
- this.symlink = symlink;
- }
-
- public void setType(String type)
- {
- this.type = type;
- }
-
- public void setPermissions(int permissions)
- {
- this.permissions = permissions;
- }
-
- public void setUserPermissions(String user)
- {
- this.user = user;
- }
-
- public void setGroupPermissions(String group)
- {
- this.group = group;
- }
-
- public void setOtherPermissions(String other)
- {
- this.other = other;
- }
-
- public String getUser()
- {
- return user;
- }
-
- public void setUser(String user)
- {
- this.user = user;
- }
-
- public String getGroup()
- {
- return group;
- }
-
- public void setGroup(String group)
- {
- this.group = group;
- }
-
- public String getOther()
- {
- return other;
- }
-
- public void setOther(String other)
- {
- this.other = other;
- }
-
-
-}
diff --git a/app/src/main/java/com/stericson/roottools/containers/Symlink.java b/app/src/main/java/com/stericson/roottools/containers/Symlink.java
deleted file mode 100644
index b811b1a59..000000000
--- a/app/src/main/java/com/stericson/roottools/containers/Symlink.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * This file is part of the RootTools Project: http://code.google.com/p/RootTools/
- *
- * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.roottools.containers;
-
-import java.io.File;
-
-public class Symlink
-{
- protected final File file;
- protected final File symlinkPath;
-
- public Symlink(File file, File path)
- {
- this.file = file;
- symlinkPath = path;
- }
-
- public File getFile()
- {
- return this.file;
- }
-
- public File getSymlinkPath()
- {
- return symlinkPath;
- }
-}
diff --git a/app/src/main/java/com/stericson/roottools/internal/Installer.java b/app/src/main/java/com/stericson/roottools/internal/Installer.java
deleted file mode 100644
index 54d57ae70..000000000
--- a/app/src/main/java/com/stericson/roottools/internal/Installer.java
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- * This file is part of the RootTools Project: http://code.google.com/p/RootTools/
- *
- * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.roottools.internal;
-
-import android.content.Context;
-import android.util.Log;
-
-import com.stericson.rootshell.execution.Command;
-import com.stericson.rootshell.execution.Shell;
-import com.stericson.roottools.RootTools;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.channels.Channels;
-import java.nio.channels.FileChannel;
-import java.nio.channels.ReadableByteChannel;
-import java.security.DigestInputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-class Installer
-{
-
- //-------------
- //# Installer #
- //-------------
-
- static final String LOG_TAG = "RootTools::Installer";
-
- static final String BOGUS_FILE_NAME = "bogus";
-
- Context context;
- String filesPath;
-
- public Installer(Context context)
- throws IOException
- {
-
- this.context = context;
- this.filesPath = context.getFilesDir().getCanonicalPath();
- }
-
- /**
- * This method can be used to unpack a binary from the raw resources folder and store it in
- * /data/data/app.package/files/
- * This is typically useful if you provide your own C- or C++-based binary.
- * This binary can then be executed using sendShell() and its full path.
- *
- * @param sourceId resource id; typically R.raw.id
- * @param destName destination file name; appended to /data/data/app.package/files/
- * @param mode chmod value for this file
- * @return a boolean which indicates whether or not we were
- * able to create the new file.
- */
- protected boolean installBinary(int sourceId, String destName, String mode)
- {
- File mf = new File(filesPath + File.separator + destName);
- if (!mf.exists() ||
- !getFileSignature(mf).equals(
- getStreamSignature(
- context.getResources().openRawResource(sourceId))
- ))
- {
- Log.e(LOG_TAG, "Installing a new version of binary: " + destName);
- // First, does our files/ directory even exist?
- // We cannot wait for android to lazily create it as we will soon
- // need it.
- try
- {
- FileInputStream fis = context.openFileInput(BOGUS_FILE_NAME);
- fis.close();
- }
- catch (FileNotFoundException e)
- {
- FileOutputStream fos = null;
- try
- {
- fos = context.openFileOutput("bogus", Context.MODE_PRIVATE);
- fos.write("justcreatedfilesdirectory".getBytes());
- }
- catch (Exception ex)
- {
- if (RootTools.debugMode)
- {
- Log.e(LOG_TAG, ex.toString());
- }
- return false;
- }
- finally
- {
- if (null != fos)
- {
- try
- {
- fos.close();
- context.deleteFile(BOGUS_FILE_NAME);
- }
- catch (IOException e1)
- {
- }
- }
- }
- }
- catch (IOException ex)
- {
- if (RootTools.debugMode)
- {
- Log.e(LOG_TAG, ex.toString());
- }
- return false;
- }
-
- // Only now can we start creating our actual file
- InputStream iss = context.getResources().openRawResource(sourceId);
- ReadableByteChannel rfc = Channels.newChannel(iss);
- FileOutputStream oss = null;
- try
- {
- oss = new FileOutputStream(mf);
- FileChannel ofc = oss.getChannel();
- long pos = 0;
- try
- {
- long size = iss.available();
- while ((pos += ofc.transferFrom(rfc, pos, size - pos)) < size)
- {
- }
- }
- catch (IOException ex)
- {
- if (RootTools.debugMode)
- {
- Log.e(LOG_TAG, ex.toString());
- }
- return false;
- }
- }
- catch (FileNotFoundException ex)
- {
- if (RootTools.debugMode)
- {
- Log.e(LOG_TAG, ex.toString());
- }
- return false;
- }
- finally
- {
- if (oss != null)
- {
- try
- {
- oss.flush();
- oss.getFD().sync();
- oss.close();
- }
- catch (Exception e)
- {
- }
- }
- }
- try
- {
- iss.close();
- }
- catch (IOException ex)
- {
- if (RootTools.debugMode)
- {
- Log.e(LOG_TAG, ex.toString());
- }
- return false;
- }
-
- try
- {
- Command command = new Command(0, false, "chmod " + mode + " " + filesPath + File.separator + destName);
- Shell.startRootShell().add(command);
- commandWait(command);
-
- }
- catch (Exception e)
- {
- }
- }
- return true;
- }
-
- protected boolean isBinaryInstalled(String destName)
- {
- boolean installed = false;
- File mf = new File(filesPath + File.separator + destName);
- if (mf.exists())
- {
- installed = true;
- // TODO: pass mode as argument and check it matches
- }
- return installed;
- }
-
- protected String getFileSignature(File f)
- {
- String signature = "";
- try
- {
- signature = getStreamSignature(new FileInputStream(f));
- }
- catch (FileNotFoundException ex)
- {
- Log.e(LOG_TAG, ex.toString());
- }
- return signature;
- }
-
- /*
- * Note: this method will close any string passed to it
- */
- protected String getStreamSignature(InputStream is)
- {
- String signature = "";
- try
- {
- MessageDigest md = MessageDigest.getInstance("MD5");
- DigestInputStream dis = new DigestInputStream(is, md);
- byte[] buffer = new byte[4096];
- while (-1 != dis.read(buffer))
- {
- }
- byte[] digest = md.digest();
- StringBuffer sb = new StringBuffer();
-
- for (int i = 0; i < digest.length; i++)
- {
- sb.append(Integer.toHexString(digest[i] & 0xFF));
- }
-
- signature = sb.toString();
- }
- catch (IOException ex)
- {
- Log.e(LOG_TAG, ex.toString());
- }
- catch (NoSuchAlgorithmException ex)
- {
- Log.e(LOG_TAG, ex.toString());
- }
- finally
- {
- try
- {
- is.close();
- }
- catch (IOException e)
- {
- }
- }
- return signature;
- }
-
- private void commandWait(Command cmd)
- {
- synchronized (cmd)
- {
- try
- {
- if (!cmd.isFinished())
- {
- cmd.wait(2000);
- }
- }
- catch (InterruptedException ex)
- {
- Log.e(LOG_TAG, ex.toString());
- }
- }
- }
-}
diff --git a/app/src/main/java/com/stericson/roottools/internal/InternalVariables.java b/app/src/main/java/com/stericson/roottools/internal/InternalVariables.java
deleted file mode 100644
index 6b52e20bd..000000000
--- a/app/src/main/java/com/stericson/roottools/internal/InternalVariables.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * This file is part of the RootTools Project: http://code.google.com/p/RootTools/
- *
- * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.roottools.internal;
-
-import com.stericson.roottools.containers.Mount;
-import com.stericson.roottools.containers.Permissions;
-import com.stericson.roottools.containers.Symlink;
-
-import java.util.ArrayList;
-import java.util.regex.Pattern;
-
-public class InternalVariables
-{
-
- // ----------------------
- // # Internal Variables #
- // ----------------------
-
-
- protected static boolean nativeToolsReady = false;
- protected static boolean found = false;
- protected static boolean processRunning = false;
-
- protected static String[] space;
- protected static String getSpaceFor;
- protected static String busyboxVersion;
- protected static String pid_list = "";
- protected static ArrayList mounts;
- protected static ArrayList symlinks;
- protected static String inode = "";
- protected static Permissions permissions;
-
- // regex to get pid out of ps line, example:
- // root 2611 0.0 0.0 19408 2104 pts/2 S 13:41 0:00 bash
- protected static final String PS_REGEX = "^\\S+\\s+([0-9]+).*$";
- protected static Pattern psPattern;
-
- static
- {
- psPattern = Pattern.compile(PS_REGEX);
- }
-}
diff --git a/app/src/main/java/com/stericson/roottools/internal/Remounter.java b/app/src/main/java/com/stericson/roottools/internal/Remounter.java
deleted file mode 100644
index f054962a8..000000000
--- a/app/src/main/java/com/stericson/roottools/internal/Remounter.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * This file is part of the RootTools Project: http://code.google.com/p/RootTools/
- *
- * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.roottools.internal;
-
-import com.stericson.rootshell.execution.Command;
-import com.stericson.rootshell.execution.Shell;
-import com.stericson.roottools.Constants;
-import com.stericson.roottools.RootTools;
-import com.stericson.roottools.containers.Mount;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-
-import dev.ukanth.ufirewall.Api;
-import dev.ukanth.ufirewall.log.Log;
-
-
-public class Remounter
-{
-
- private String customPath;
-
- public Remounter() {
- }
-
- public Remounter(String path) {
- this.customPath = path;
- }
- //-------------
- //# Remounter #
- //-------------
-
- /**
- * This will take a path, which can contain the file name as well,
- * and attempt to remount the underlying partition.
- *
- * For example, passing in the following string:
- * "/system/bin/some/directory/that/really/would/never/exist"
- * will result in /system ultimately being remounted.
- * However, keep in mind that the longer the path you supply, the more work this has to do,
- * and the slower it will run.
- *
- * @param file file path
- * @param mountType mount type: pass in RO (Read only) or RW (Read Write)
- * @return a boolean which indicates whether or not the partition
- * has been remounted as specified.
- */
- public boolean remount(String file, String mountType)
- {
- //if the path has a trailing slash get rid of it.
- if (file.endsWith("/") && !file.equals("/"))
- {
- file = file.substring(0, file.lastIndexOf("/"));
- }
- //Make sure that what we are trying to remount is in the mount list.
- boolean foundMount = false;
- while (!foundMount)
- {
- try
- {
- for (Mount mount : RootTools.getMounts())
- {
- RootTools.log(mount.getMountPoint().toString());
-
- if (file.equals(mount.getMountPoint().toString()))
- {
- foundMount = true;
- break;
- }
- }
- }
- catch (Exception e)
- {
- if (RootTools.debugMode)
- {
- Log.d(Api.TAG, e.getMessage(), e);
- }
- return false;
- }
- if (!foundMount)
- {
- try
- {
- file = (new File(file).getParent());
- }
- catch (Exception e)
- {
- Log.e(Api.TAG, e.getMessage(), e);
- return false;
- }
- }
- }
-
- Mount mountPoint = findMountPointRecursive(file);
-
- if (mountPoint != null)
- {
-
- RootTools.log(Constants.TAG, "Remounting " + mountPoint.getMountPoint().getAbsolutePath() + " as " + mountType.toLowerCase());
- final boolean isMountMode = mountPoint.getFlags().contains(mountType.toLowerCase());
-
- if (!isMountMode)
- {
- //grab an instance of the internal class
- try
- {
- Command command = new Command(0,
- true,
- "busybox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(),
- "toolbox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(),
- "toybox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(),
- "mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(),
- "mount -o remount," + mountType.toLowerCase() + " " + file,
- "/system/bin/toolbox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(),
- "/system/bin/toybox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath()
- );
- Shell.startRootShell().add(command);
- commandWait(command);
-
- if(customPath != null) {
- command = new Command(0,
- true,
- customPath + " mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath());
- Shell.startRootShell().add(command);
- commandWait(command);
- }
- }
- catch (Exception e)
- {
- }
-
- mountPoint = findMountPointRecursive(file);
- }
-
- if (mountPoint != null)
- {
- RootTools.log(Constants.TAG, mountPoint.getFlags() + " AND " + mountType.toLowerCase());
- if (mountPoint.getFlags().contains(mountType.toLowerCase()))
- {
- RootTools.log(mountPoint.getFlags().toString());
- return true;
- }
- else
- {
- RootTools.log(mountPoint.getFlags().toString());
- return false;
- }
- }
- else
- {
- RootTools.log("mount is null, file was: " + file + " mountType was: " + mountType);
- }
- }
- else
- {
- RootTools.log("mount is null, file was: " + file + " mountType was: " + mountType);
- }
-
- return false;
- }
-
- private Mount findMountPointRecursive(String file)
- {
- try
- {
- ArrayList mounts = RootTools.getMounts();
-
- for (File path = new File(file); path != null; )
- {
- for (Mount mount : mounts)
- {
- if (mount.getMountPoint().equals(path))
- {
- return mount;
- }
- }
- }
-
- return null;
-
- }
- catch (IOException e)
- {
- if (RootTools.debugMode)
- {
- e.printStackTrace();
- }
- }
- catch (Exception e)
- {
- if (RootTools.debugMode)
- {
- e.printStackTrace();
- }
- }
-
- return null;
- }
-
- private void commandWait(Command cmd)
- {
- synchronized (cmd)
- {
- try
- {
- if (!cmd.isFinished())
- {
- cmd.wait(2000);
- }
- }
- catch (InterruptedException e)
- {
- e.printStackTrace();
- }
- }
- }
-}
diff --git a/app/src/main/java/com/stericson/roottools/internal/RootToolsInternalMethods.java b/app/src/main/java/com/stericson/roottools/internal/RootToolsInternalMethods.java
deleted file mode 100644
index f5b825778..000000000
--- a/app/src/main/java/com/stericson/roottools/internal/RootToolsInternalMethods.java
+++ /dev/null
@@ -1,1342 +0,0 @@
-/*
- * This file is part of the RootTools Project: http://code.google.com/p/RootTools/
- *
- * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.roottools.internal;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Environment;
-import android.os.StatFs;
-import android.util.Log;
-
-import com.stericson.rootshell.RootShell;
-import com.stericson.rootshell.execution.Command;
-import com.stericson.rootshell.execution.Shell;
-import com.stericson.roottools.Constants;
-import com.stericson.roottools.RootTools;
-import com.stericson.roottools.containers.Mount;
-import com.stericson.roottools.containers.Permissions;
-import com.stericson.roottools.containers.Symlink;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Locale;
-import java.util.concurrent.TimeoutException;
-import java.util.regex.Matcher;
-
-public final class RootToolsInternalMethods {
-
- // --------------------
- // # Internal methods #
- // --------------------
-
- protected RootToolsInternalMethods() {
- }
-
- public static void getInstance() {
- //this will allow RootTools to be the only one to get an instance of this class.
- RootTools.setRim(new RootToolsInternalMethods());
- }
-
- public Permissions getPermissions(String line) {
-
- String[] lineArray = line.split(" ");
- String rawPermissions = lineArray[0];
-
- if (rawPermissions.length() == 10
- && (rawPermissions.charAt(0) == '-'
- || rawPermissions.charAt(0) == 'd' || rawPermissions
- .charAt(0) == 'l')
- && (rawPermissions.charAt(1) == '-' || rawPermissions.charAt(1) == 'r')
- && (rawPermissions.charAt(2) == '-' || rawPermissions.charAt(2) == 'w')) {
- RootTools.log(rawPermissions);
-
- Permissions permissions = new Permissions();
-
- permissions.setType(rawPermissions.substring(0, 1));
-
- RootTools.log(permissions.getType());
-
- permissions.setUserPermissions(rawPermissions.substring(1, 4));
-
- RootTools.log(permissions.getUserPermissions());
-
- permissions.setGroupPermissions(rawPermissions.substring(4, 7));
-
- RootTools.log(permissions.getGroupPermissions());
-
- permissions.setOtherPermissions(rawPermissions.substring(7, 10));
-
- RootTools.log(permissions.getOtherPermissions());
-
- StringBuilder finalPermissions = new StringBuilder();
- finalPermissions.append(parseSpecialPermissions(rawPermissions));
- finalPermissions.append(parsePermissions(permissions.getUserPermissions()));
- finalPermissions.append(parsePermissions(permissions.getGroupPermissions()));
- finalPermissions.append(parsePermissions(permissions.getOtherPermissions()));
-
- permissions.setPermissions(Integer.parseInt(finalPermissions.toString()));
-
- return permissions;
- }
-
- return null;
- }
-
- public int parsePermissions(String permission) {
- permission = permission.toLowerCase(Locale.US);
- int tmp;
- if (permission.charAt(0) == 'r') {
- tmp = 4;
- } else {
- tmp = 0;
- }
-
- RootTools.log("permission " + tmp);
- RootTools.log("character " + permission.charAt(0));
-
- if (permission.charAt(1) == 'w') {
- tmp += 2;
- } else {
- tmp += 0;
- }
-
- RootTools.log("permission " + tmp);
- RootTools.log("character " + permission.charAt(1));
-
- if (permission.charAt(2) == 'x' || permission.charAt(2) == 's'
- || permission.charAt(2) == 't') {
- tmp += 1;
- } else {
- tmp += 0;
- }
-
- RootTools.log("permission " + tmp);
- RootTools.log("character " + permission.charAt(2));
-
- return tmp;
- }
-
- public int parseSpecialPermissions(String permission) {
- int tmp = 0;
- if (permission.charAt(2) == 's') {
- tmp += 4;
- }
-
- if (permission.charAt(5) == 's') {
- tmp += 2;
- }
-
- if (permission.charAt(8) == 't') {
- tmp += 1;
- }
-
- RootTools.log("special permissions " + tmp);
-
- return tmp;
- }
-
- /**
- * Copys a file to a destination. Because cp is not available on all android devices, we have a
- * fallback on the cat command
- *
- * @param source example: /data/data/org.adaway/files/hosts
- * @param destination example: /system/etc/hosts
- * @param remountAsRw remounts the destination as read/write before writing to it
- * @param preserveFileAttributes tries to copy file attributes from source to destination, if only cat is available
- * only permissions are preserved
- * @return true if it was successfully copied
- */
- public boolean copyFile(String source, String destination, boolean remountAsRw,
- boolean preserveFileAttributes) {
-
- Command command = null;
- boolean result = true;
-
- try {
- // mount destination as rw before writing to it
- if (remountAsRw) {
- RootTools.remount(destination, "RW");
- }
-
- // if cp is available and has appropriate permissions
- if (checkUtil("cp")) {
- RootTools.log("cp command is available!");
-
- if (preserveFileAttributes) {
- command = new Command(0, false, "cp -fp " + source + " " + destination);
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- //ensure that the file was copied, an exitcode of zero means success
- result = command.getExitCode() == 0;
-
- } else {
- command = new Command(0, false, "cp -f " + source + " " + destination);
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- //ensure that the file was copied, an exitcode of zero means success
- result = command.getExitCode() == 0;
-
- }
- } else {
- if (checkUtil("busybox") && hasUtil("cp", "busybox")) {
- RootTools.log("busybox cp command is available!");
-
- if (preserveFileAttributes) {
- command = new Command(0, false, "busybox cp -fp " + source + " " + destination);
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- } else {
- command = new Command(0, false, "busybox cp -f " + source + " " + destination);
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- }
- } else { // if cp is not available use cat
- // if cat is available and has appropriate permissions
- if (checkUtil("cat")) {
- RootTools.log("cp is not available, use cat!");
-
- int filePermission = -1;
- if (preserveFileAttributes) {
- // get permissions of source before overwriting
- Permissions permissions = getFilePermissionsSymlinks(source);
- filePermission = permissions.getPermissions();
- }
-
- // copy with cat
- command = new Command(0, false, "cat " + source + " > " + destination);
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- if (preserveFileAttributes) {
- // set premissions of source to destination
- command = new Command(0, false, "chmod " + filePermission + " " + destination);
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
- }
- } else {
- result = false;
- }
- }
- }
-
- // mount destination back to ro
- if (remountAsRw) {
- RootTools.remount(destination, "RO");
- }
- } catch (Exception e) {
- e.printStackTrace();
- result = false;
- }
-
- if (command != null) {
- //ensure that the file was copied, an exitcode of zero means success
- result = command.getExitCode() == 0;
- }
-
- return result;
- }
-
- /**
- * This will check a given binary, determine if it exists and determine that
- * it has either the permissions 755, 775, or 777.
- *
- * @param util Name of the utility to check.
- * @return boolean to indicate whether the binary is installed and has
- * appropriate permissions.
- */
- public boolean checkUtil(String util) {
- List foundPaths = RootShell.findBinary(util, true);
- if (foundPaths.size() > 0) {
-
- for (String path : foundPaths) {
- Permissions permissions = RootTools
- .getFilePermissionsSymlinks(path + "/" + util);
-
- if (permissions != null) {
- String permission;
-
- if (Integer.toString(permissions.getPermissions()).length() > 3) {
- permission = Integer.toString(permissions.getPermissions()).substring(1);
- } else {
- permission = Integer.toString(permissions.getPermissions());
- }
-
- if (permission.equals("755") || permission.equals("777")
- || permission.equals("775")) {
- RootTools.utilPath = path + "/" + util;
- return true;
- }
- }
- }
- }
-
- return false;
-
- }
-
- /**
- * Deletes a file or directory
- *
- * @param target example: /data/data/org.adaway/files/hosts
- * @param remountAsRw remounts the destination as read/write before writing to it
- * @return true if it was successfully deleted
- */
- public boolean deleteFileOrDirectory(String target, boolean remountAsRw) {
- boolean result = true;
-
- try {
- // mount destination as rw before writing to it
- if (remountAsRw) {
- RootTools.remount(target, "RW");
- }
-
- if (hasUtil("rm", "toolbox")) {
- RootTools.log("rm command is available!");
-
- Command command = new Command(0, false, "rm -r " + target);
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- if (command.getExitCode() != 0) {
- RootTools.log("target not exist or unable to delete file");
- result = false;
- }
- } else {
- if (checkUtil("busybox") && hasUtil("rm", "busybox")) {
- RootTools.log("busybox rm command is available!");
-
- Command command = new Command(0, false, "busybox rm -rf " + target);
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- if (command.getExitCode() != 0) {
- RootTools.log("target not exist or unable to delete file");
- result = false;
- }
- }
- }
-
- // mount destination back to ro
- if (remountAsRw) {
- RootTools.remount(target, "RO");
- }
- } catch (Exception e) {
- e.printStackTrace();
- result = false;
- }
-
- return result;
- }
-
- /**
- * This will try and fix a given binary. (This is for Busybox applets or Toolbox applets) By
- * "fix", I mean it will try and symlink the binary from either toolbox or Busybox and fix the
- * permissions if the permissions are not correct.
- *
- * @param util Name of the utility to fix.
- * @param utilPath path to the toolbox that provides ln, rm, and chmod. This can be a blank string, a
- * path to a binary that will provide these, or you can use
- * RootTools.getWorkingToolbox()
- */
- public void fixUtil(String util, String utilPath) {
- try {
- RootTools.remount("/system", "rw");
-
- List foundPaths = RootShell.findBinary(util, true);
-
- if (foundPaths.size() > 0) {
- for (String path : foundPaths) {
- Command command = new Command(0, false, utilPath + " rm " + path + "/" + util);
- RootShell.getShell(true).add(command);
- commandWait(RootShell.getShell(true), command);
-
- }
-
- Command command = new Command(0, false, utilPath + " ln -s " + utilPath + " /system/bin/" + util, utilPath + " chmod 0755 /system/bin/" + util);
- RootShell.getShell(true).add(command);
- commandWait(RootShell.getShell(true), command);
-
- }
-
- RootTools.remount("/system", "ro");
- } catch (Exception e) {
- }
- }
-
- /**
- * This will check an array of binaries, determine if they exist and determine that it has
- * either the permissions 755, 775, or 777. If an applet is not setup correctly it will try and
- * fix it. (This is for Busybox applets or Toolbox applets)
- *
- * @param utils Name of the utility to check.
- * @return boolean to indicate whether the operation completed. Note that this is not indicative
- * of whether the problem was fixed, just that the method did not encounter any
- * exceptions.
- * @throws Exception if the operation cannot be completed.
- */
- public boolean fixUtils(String[] utils) throws Exception {
-
- for (String util : utils) {
- if (!checkUtil(util)) {
- if (checkUtil("busybox")) {
- if (hasUtil(util, "busybox")) {
- fixUtil(util, RootTools.utilPath);
- }
- } else {
- if (checkUtil("toolbox")) {
- if (hasUtil(util, "toolbox")) {
- fixUtil(util, RootTools.utilPath);
- }
- } else {
- return false;
- }
- }
- }
- }
-
- return true;
- }
-
- /**
- * This will return an List of Strings. Each string represents an applet available from BusyBox.
- *
- *
- * @param path Path to the busybox binary that you want the list of applets from.
- * @return null If we cannot return the list of applets.
- */
- public List getBusyBoxApplets(String path) throws Exception {
-
- if (path != null && !path.endsWith("/") && !path.equals("")) {
- path += "/";
- } else if (path == null) {
- //Don't know what the user wants to do...what am I pshycic?
- throw new Exception("Path is null, please specifiy a path");
- }
-
- final List results = new ArrayList();
-
- Command command = new Command(Constants.BBA, false, path + "busybox --list") {
- @Override
- public void commandOutput(int id, String line) {
- if (id == Constants.BBA) {
- if (!line.trim().equals("") && !line.trim().contains("not found") && !line.trim().contains("file busy")) {
- results.add(line);
- }
- }
-
- super.commandOutput(id, line);
- }
- };
-
- //try without root first...
- RootShell.getShell(false).add(command);
- commandWait(RootShell.getShell(false), command);
-
- if (results.size() <= 0) {
- //try with root...
-
- command = new Command(Constants.BBA, false, path + "busybox --list") {
- @Override
- public void commandOutput(int id, String line) {
- if (id == Constants.BBA) {
- if (!line.trim().equals("") && !line.trim().contains("not found") && !line.trim().contains("file busy")) {
- results.add(line);
- }
- }
-
- super.commandOutput(id, line);
- }
- };
-
- RootShell.getShell(true).add(command);
- commandWait(RootShell.getShell(true), command);
- }
-
- return results;
- }
-
- /**
- * @return BusyBox version if found, "" if not found.
- */
- public String getBusyBoxVersion(String path) {
-
- final StringBuilder version = new StringBuilder();
-
- if (!path.equals("") && !path.endsWith("/")) {
- path += "/";
- }
-
- try {
- Command command = new Command(Constants.BBV, false, path + "busybox") {
- @Override
- public void commandOutput(int id, String line) {
- line = line.trim();
-
- boolean foundVersion = false;
-
- if (id == Constants.BBV) {
- RootTools.log("Version Output: " + line);
-
- String[] temp = line.split(" ");
-
- if (temp.length > 1 && temp[1].contains("v1.") && !foundVersion) {
- foundVersion = true;
- version.append(temp[1]);
- RootTools.log("Found Version: " + version.toString());
- }
- }
-
- super.commandOutput(id, line);
- }
- };
-
- //try without root first
- RootTools.log("Getting BusyBox Version without root");
- Shell shell = RootTools.getShell(false);
- shell.add(command);
- commandWait(shell, command);
-
- if (version.length() <= 0) {
-
- command = new Command(Constants.BBV, false, path + "busybox") {
- @Override
- public void commandOutput(int id, String line) {
- line = line.trim();
-
- boolean foundVersion = false;
-
- if (id == Constants.BBV) {
- RootTools.log("Version Output: " + line);
-
- String[] temp = line.split(" ");
-
- if (temp.length > 1 && temp[1].contains("v1.") && !foundVersion) {
- foundVersion = true;
- version.append(temp[1]);
- RootTools.log("Found Version: " + version.toString());
- }
- }
-
- super.commandOutput(id, line);
- }
- };
-
- RootTools.log("Getting BusyBox Version with root");
- Shell rootShell = RootTools.getShell(true);
- //Now look for it...
- rootShell.add(command);
- commandWait(rootShell, command);
- }
-
- } catch (Exception e) {
- RootTools.log("BusyBox was not found, more information MAY be available with Debugging on.");
- return "";
- }
-
- RootTools.log("Returning found version: " + version.toString());
- return version.toString();
- }
-
- /**
- * @return long Size, converted to kilobytes (from xxx or xxxm or xxxk etc.)
- */
- public long getConvertedSpace(String spaceStr) {
- try {
- double multiplier = 1.0;
- char c;
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < spaceStr.length(); i++) {
- c = spaceStr.charAt(i);
- if (!Character.isDigit(c) && c != '.') {
- if (c == 'm' || c == 'M') {
- multiplier = 1024.0;
- } else if (c == 'g' || c == 'G') {
- multiplier = 1024.0 * 1024.0;
- }
- break;
- }
- sb.append(spaceStr.charAt(i));
- }
- return (long) Math.ceil(Double.valueOf(sb.toString()) * multiplier);
- } catch (Exception e) {
- return -1;
- }
- }
-
- /**
- * This method will return the inode number of a file. This method is dependent on having a version of
- * ls that supports the -i parameter.
- *
- * @param file path to the file that you wish to return the inode number
- * @return String The inode number for this file or "" if the inode number could not be found.
- */
- public String getInode(String file) {
- try {
- Command command = new Command(Constants.GI, false, "/data/local/ls -i " + file) {
-
- @Override
- public void commandOutput(int id, String line) {
- if (id == Constants.GI) {
- if (!line.trim().equals("") && Character.isDigit(line.trim().substring(0, 1).toCharArray()[0])) {
- InternalVariables.inode = line.trim().split(" ")[0];
- }
- }
-
- super.commandOutput(id, line);
- }
- };
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- return InternalVariables.inode;
- } catch (Exception ignore) {
- return "";
- }
- }
-
- public boolean isNativeToolsReady(int nativeToolsId, Context context) {
- RootTools.log("Preparing Native Tools");
- InternalVariables.nativeToolsReady = false;
-
- Installer installer;
- try {
- installer = new Installer(context);
- } catch (IOException ex) {
- if (RootTools.debugMode) {
- ex.printStackTrace();
- }
- return false;
- }
-
- if (installer.isBinaryInstalled("nativetools")) {
- InternalVariables.nativeToolsReady = true;
- } else {
- InternalVariables.nativeToolsReady = installer.installBinary(nativeToolsId,
- "nativetools", "700");
- }
- return InternalVariables.nativeToolsReady;
- }
-
- /**
- * @param file String that represent the file, including the full path to the
- * file and its name.
- * @return An instance of the class permissions from which you can get the
- * permissions of the file or if the file could not be found or
- * permissions couldn't be determined then permissions will be null.
- */
- public Permissions getFilePermissionsSymlinks(String file) {
- RootTools.log("Checking permissions for " + file);
- if (RootTools.exists(file)) {
- RootTools.log(file + " was found.");
- try {
-
- Command command = new Command(
- Constants.FPS, false, "ls -l " + file,
- "busybox ls -l " + file,
- "/system/bin/failsafe/toolbox ls -l " + file,
- "toolbox ls -l " + file) {
- @Override
- public void commandOutput(int id, String line) {
- if (id == Constants.FPS) {
- String symlink_final = "";
-
- String[] lineArray = line.split(" ");
- if (lineArray[0].length() != 10) {
- super.commandOutput(id, line);
- return;
- }
-
- RootTools.log("Line " + line);
-
- try {
- String[] symlink = line.split(" ");
- if (symlink[symlink.length - 2].equals("->")) {
- RootTools.log("Symlink found.");
- symlink_final = symlink[symlink.length - 1];
- }
- } catch (Exception e) {
- }
-
- try {
- InternalVariables.permissions = getPermissions(line);
- if (InternalVariables.permissions != null) {
- InternalVariables.permissions.setSymlink(symlink_final);
- }
- } catch (Exception e) {
- RootTools.log(e.getMessage());
- }
- }
-
- super.commandOutput(id, line);
- }
- };
- RootShell.getShell(true).add(command);
- commandWait(RootShell.getShell(true), command);
-
- return InternalVariables.permissions;
-
- } catch (Exception e) {
- RootTools.log(e.getMessage());
- return null;
- }
- }
-
- return null;
- }
-
- /**
- * This will return an ArrayList of the class Mount. The class mount contains the following
- * property's: device mountPoint type flags
- *
- * These will provide you with any information you need to work with the mount points.
- *
- * @return ArrayList an ArrayList of the class Mount.
- * @throws Exception if we cannot return the mount points.
- */
- public ArrayList getMounts() throws Exception {
-
- InternalVariables.mounts = new ArrayList<>();
-
- if(null == InternalVariables.mounts || InternalVariables.mounts.isEmpty()) {
- Shell shell = RootTools.getShell(true);
-
- Command cmd = new Command(Constants.GET_MOUNTS,
- false,
- "cat /proc/mounts") {
-
- @Override
- public void commandOutput(int id, String line) {
- if (id == Constants.GET_MOUNTS) {
- RootTools.log(line);
-
- String[] fields = line.split(" ");
- InternalVariables.mounts.add(new Mount(new File(fields[0]), // device
- new File(fields[1]), // mountPoint
- fields[2], // fstype
- fields[3] // flags
- ));
- }
-
- super.commandOutput(id, line);
- }
- };
- shell.add(cmd);
- this.commandWait(shell, cmd);
- }
-
- return InternalVariables.mounts;
- }
-
- /**
- * This will tell you how the specified mount is mounted. rw, ro, etc...
- *
- *
- * @param path mount you want to check
- * @return String What the mount is mounted as.
- * @throws Exception if we cannot determine how the mount is mounted.
- */
- public String getMountedAs(String path) throws Exception {
- InternalVariables.mounts = getMounts();
- String mp;
- if (InternalVariables.mounts != null) {
- for (Mount mount : InternalVariables.mounts) {
-
- mp = mount.getMountPoint().getAbsolutePath();
-
- if (mp.equals("/")) {
- if (path.equals("/")) {
- return (String) mount.getFlags().toArray()[0];
- } else {
- continue;
- }
- }
-
- if (path.equals(mp) || path.startsWith(mp + "/")) {
- RootTools.log((String) mount.getFlags().toArray()[0]);
- return (String) mount.getFlags().toArray()[0];
- }
- }
-
- throw new Exception();
- } else {
- throw new Exception();
- }
- }
-
- /**
- * Get the space for a desired partition.
- *
- * @param path The partition to find the space for.
- * @return the amount if space found within the desired partition. If the space was not found
- * then the value is -1
- * @throws TimeoutException
- */
- public long getSpace(String path) {
- InternalVariables.getSpaceFor = path;
- boolean found = false;
- RootTools.log("Looking for Space");
- try {
- final Command command = new Command(Constants.GS, false, "df " + path) {
-
- @Override
- public void commandOutput(int id, String line) {
- if (id == Constants.GS) {
- if (line.contains(InternalVariables.getSpaceFor.trim())) {
- InternalVariables.space = line.split(" ");
- }
- }
-
- super.commandOutput(id, line);
- }
- };
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- } catch (Exception e) {
- }
-
- if (InternalVariables.space != null) {
- RootTools.log("First Method");
-
- for (String spaceSearch : InternalVariables.space) {
-
- RootTools.log(spaceSearch);
-
- if (found) {
- return getConvertedSpace(spaceSearch);
- } else if (spaceSearch.equals("used,")) {
- found = true;
- }
- }
-
- // Try this way
- int count = 0, targetCount = 3;
-
- RootTools.log("Second Method");
-
- if (InternalVariables.space[0].length() <= 5) {
- targetCount = 2;
- }
-
- for (String spaceSearch : InternalVariables.space) {
-
- RootTools.log(spaceSearch);
- if (spaceSearch.length() > 0) {
- RootTools.log(spaceSearch + ("Valid"));
- if (count == targetCount) {
- return getConvertedSpace(spaceSearch);
- }
- count++;
- }
- }
- }
- RootTools.log("Returning -1, space could not be determined.");
- return -1;
- }
-
- /**
- * This will return a String that represent the symlink for a specified file.
- *
- *
- * @param file file to get the Symlink for. (must have absolute path)
- * @return String a String that represent the symlink for a specified file or an
- * empty string if no symlink exists.
- */
- public String getSymlink(String file) {
- RootTools.log("Looking for Symlink for " + file);
-
- try {
- final List results = new ArrayList();
-
- Command command = new Command(Constants.GSYM, false, "ls -l " + file) {
-
- @Override
- public void commandOutput(int id, String line) {
- if (id == Constants.GSYM) {
- if (!line.trim().equals("")) {
- results.add(line);
- }
- }
-
- super.commandOutput(id, line);
- }
- };
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- String[] symlink = results.get(0).split(" ");
- if (symlink.length > 2 && symlink[symlink.length - 2].equals("->")) {
- RootTools.log("Symlink found.");
-
- String final_symlink;
-
- if (!symlink[symlink.length - 1].equals("") && !symlink[symlink.length - 1].contains("/")) {
- //We assume that we need to get the path for this symlink as it is probably not absolute.
- List paths = RootShell.findBinary(symlink[symlink.length - 1], true);
- if (paths.size() > 0) {
- //We return the first found location.
- final_symlink = paths.get(0) + symlink[symlink.length - 1];
- } else {
- //we couldnt find a path, return the symlink by itself.
- final_symlink = symlink[symlink.length - 1];
- }
- } else {
- final_symlink = symlink[symlink.length - 1];
- }
-
- return final_symlink;
- }
- } catch (Exception e) {
- if (RootTools.debugMode) {
- e.printStackTrace();
- }
- }
-
- RootTools.log("Symlink not found");
- return "";
- }
-
- /**
- * This will return an ArrayList of the class Symlink. The class Symlink contains the following
- * property's: path SymplinkPath
- *
- * These will provide you with any Symlinks in the given path.
- *
- * @param path path to search for Symlinks.
- * @return ArrayList an ArrayList of the class Symlink.
- * @throws Exception if we cannot return the Symlinks.
- */
- public ArrayList getSymlinks(String path) throws Exception {
-
- // this command needs find
- if (!checkUtil("find")) {
- throw new Exception();
- }
-
- InternalVariables.symlinks = new ArrayList<>();
-
- Command command = new Command(0, false, "find " + path + " -type l -exec ls -l {} \\;") {
- @Override
- public void commandOutput(int id, String line) {
- if (id == Constants.GET_SYMLINKS) {
- RootTools.log(line);
-
- String[] fields = line.split(" ");
- InternalVariables.symlinks.add(new Symlink(new File(fields[fields.length - 3]), // file
- new File(fields[fields.length - 1]) // SymlinkPath
- ));
-
- }
-
- super.commandOutput(id, line);
- }
- };
- Shell.startRootShell().add(command);
- commandWait(Shell.startRootShell(), command);
-
- if (InternalVariables.symlinks != null) {
- return InternalVariables.symlinks;
- } else {
- throw new Exception();
- }
- }
-
- /**
- * This will return to you a string to be used in your shell commands which will represent the
- * valid working toolbox with correct permissions. For instance, if Busybox is available it will
- * return "busybox", if busybox is not available but toolbox is then it will return "toolbox"
- *
- * @return String that indicates the available toolbox to use for accessing applets.
- */
- public String getWorkingToolbox() {
- if (RootTools.checkUtil("busybox")) {
- return "busybox";
- } else if (RootTools.checkUtil("toolbox")) {
- return "toolbox";
- } else {
- return "";
- }
- }
-
- /**
- * Checks if there is enough Space on SDCard
- *
- * @param updateSize size to Check (long)
- * @return true if the Update will fit on SDCard, false if not enough
- * space on SDCard. Will also return false, if the SDCard is not mounted as
- * read/write
- */
- @SuppressWarnings("deprecation")
- public boolean hasEnoughSpaceOnSdCard(long updateSize) {
- RootTools.log("Checking SDcard size and that it is mounted as RW");
- String status = Environment.getExternalStorageState();
- if (!status.equals(Environment.MEDIA_MOUNTED)) {
- return false;
- }
- File path = Environment.getExternalStorageDirectory();
- StatFs stat = new StatFs(path.getPath());
- long blockSize = 0;
- long availableBlocks = 0;
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
- blockSize = stat.getBlockSize();
- availableBlocks = stat.getAvailableBlocks();
- } else {
- blockSize = stat.getBlockSizeLong();
- availableBlocks = stat.getAvailableBlocksLong();
- }
- return (updateSize < availableBlocks * blockSize);
- }
-
- /**
- * Checks whether the toolbox or busybox binary contains a specific util
- *
- * @param util
- * @param box Should contain "toolbox" or "busybox"
- * @return true if it contains this util
- */
- public boolean hasUtil(final String util, final String box) {
-
- InternalVariables.found = false;
-
- // only for busybox and toolbox
- if (!(box.endsWith("toolbox") || box.endsWith("busybox"))) {
- return false;
- }
-
- try {
-
- Command command = new Command(0, false, box.endsWith("toolbox") ? box + " " + util : box + " --list") {
-
- @Override
- public void commandOutput(int id, String line) {
- if (box.endsWith("toolbox")) {
- if (!line.contains("no such tool")) {
- InternalVariables.found = true;
- }
- } else if (box.endsWith("busybox")) {
- // go through all lines of busybox --list
- if (line.contains(util)) {
- RootTools.log("Found util!");
- InternalVariables.found = true;
- }
- }
-
- super.commandOutput(id, line);
- }
- };
- RootTools.getShell(true).add(command);
- commandWait(RootTools.getShell(true), command);
-
- if (InternalVariables.found) {
- RootTools.log("Box contains " + util + " util!");
- return true;
- } else {
- RootTools.log("Box does not contain " + util + " util!");
- return false;
- }
- } catch (Exception e) {
- RootTools.log(e.getMessage());
- return false;
- }
- }
-
- /**
- * This method can be used to unpack a binary from the raw resources folder and store it in
- * /data/data/app.package/files/ This is typically useful if you provide your own C- or
- * C++-based binary. This binary can then be executed using sendShell() and its full path.
- *
- * @param context the current activity's Context
- * @param sourceId resource id; typically R.raw.id
- * @param destName destination file name; appended to /data/data/app.package/files/
- * @param mode chmod value for this file
- * @return a boolean which indicates whether or not we were able to create the new
- * file.
- */
- public boolean installBinary(Context context, int sourceId, String destName, String mode) {
- Installer installer;
-
- try {
- installer = new Installer(context);
- } catch (IOException ex) {
- if (RootTools.debugMode) {
- ex.printStackTrace();
- }
- return false;
- }
-
- return (installer.installBinary(sourceId, destName, mode));
- }
-
- /**
- * This method checks whether a binary is installed.
- *
- * @param context the current activity's Context
- * @param binaryName binary file name; appended to /data/data/app.package/files/
- * @return a boolean which indicates whether or not
- * the binary already exists.
- */
- public boolean isBinaryAvailable(Context context, String binaryName) {
- Installer installer;
-
- try {
- installer = new Installer(context);
- } catch (IOException ex) {
- if (RootTools.debugMode) {
- ex.printStackTrace();
- }
- return false;
- }
-
- return (installer.isBinaryInstalled(binaryName));
- }
-
- /**
- * This will let you know if an applet is available from BusyBox
- *
- *
- * @param applet The applet to check for.
- * @return true if applet is available, false otherwise.
- */
- public boolean isAppletAvailable(String applet, String binaryPath) {
- try {
- for (String aplet : getBusyBoxApplets(binaryPath)) {
- if (aplet.equals(applet)) {
- return true;
- }
- }
- return false;
- } catch (Exception e) {
- RootTools.log(e.toString());
- return false;
- }
- }
-
- /**
- * This method can be used to to check if a process is running
- *
- * @param processName name of process to check
- * @return true if process was found
- * @throws TimeoutException (Could not determine if the process is running)
- */
- public boolean isProcessRunning(final String processName) {
-
- RootTools.log("Checks if process is running: " + processName);
-
- InternalVariables.processRunning = false;
-
- try {
- Command command = new Command(0, false, "ps") {
- @Override
- public void commandOutput(int id, String line) {
- if (line.contains(processName)) {
- InternalVariables.processRunning = true;
- }
-
- super.commandOutput(id, line);
- }
- };
- RootTools.getShell(true).add(command);
- commandWait(RootTools.getShell(true), command);
-
- } catch (Exception e) {
- RootTools.log(e.getMessage());
- }
-
- return InternalVariables.processRunning;
- }
-
- /**
- * This method can be used to kill a running process
- *
- * @param processName name of process to kill
- * @return true if process was found and killed successfully
- */
- public boolean killProcess(final String processName) {
- RootTools.log("Killing process " + processName);
-
- InternalVariables.pid_list = "";
-
- //Assume that the process is running
- InternalVariables.processRunning = true;
-
- try {
-
- Command command = new Command(0, false, "ps") {
- @Override
- public void commandOutput(int id, String line) {
- if (line.contains(processName)) {
- Matcher psMatcher = InternalVariables.psPattern.matcher(line);
-
- try {
- if (psMatcher.find()) {
- String pid = psMatcher.group(1);
-
- InternalVariables.pid_list += " " + pid;
- InternalVariables.pid_list = InternalVariables.pid_list.trim();
-
- RootTools.log("Found pid: " + pid);
- } else {
- RootTools.log("Matching in ps command failed!");
- }
- } catch (Exception e) {
- RootTools.log("Error with regex!");
- e.printStackTrace();
- }
- }
-
- super.commandOutput(id, line);
- }
- };
- RootTools.getShell(true).add(command);
- commandWait(RootTools.getShell(true), command);
-
- // get all pids in one string, created in process method
- String pids = InternalVariables.pid_list;
-
- // kill processes
- if (!pids.equals("")) {
- try {
- // example: kill -9 1234 1222 5343
- command = new Command(0, false, "kill -9 " + pids);
- RootTools.getShell(true).add(command);
- commandWait(RootTools.getShell(true), command);
-
- return true;
- } catch (Exception e) {
- RootTools.log(e.getMessage());
- }
- } else {
- //no pids match, must be dead
- return true;
- }
- } catch (Exception e) {
- RootTools.log(e.getMessage());
- }
-
- return false;
- }
-
- /**
- * This will launch the Android market looking for BusyBox
- *
- * @param activity pass in your Activity
- */
- public void offerBusyBox(Activity activity) {
- RootTools.log("Launching Market for BusyBox");
- Intent i = new Intent(Intent.ACTION_VIEW,
- Uri.parse("market://details?id=stericson.busybox"));
- activity.startActivity(i);
- }
-
- /**
- * This will launch the Android market looking for BusyBox, but will return the intent fired and
- * starts the activity with startActivityForResult
- *
- * @param activity pass in your Activity
- * @param requestCode pass in the request code
- * @return intent fired
- */
- public Intent offerBusyBox(Activity activity, int requestCode) {
- RootTools.log("Launching Market for BusyBox");
- Intent i = new Intent(Intent.ACTION_VIEW,
- Uri.parse("market://details?id=stericson.busybox"));
- activity.startActivityForResult(i, requestCode);
- return i;
- }
-
- /**
- * This will launch the Play Store looking for SuperUser
- *
- * @param activity pass in your Activity
- */
- public void offerSuperUser(Activity activity) {
- RootTools.log("Launching Play Store for SuperSU");
- Intent i = new Intent(Intent.ACTION_VIEW,
- Uri.parse("market://details?id=eu.chainfire.supersu"));
- activity.startActivity(i);
- }
-
- /**
- * This will launch the Play Store looking for SuperSU, but will return the intent fired
- * and starts the activity with startActivityForResult
- *
- * @param activity pass in your Activity
- * @param requestCode pass in the request code
- * @return intent fired
- */
- public Intent offerSuperUser(Activity activity, int requestCode) {
- RootTools.log("Launching Play Store for SuperSU");
- Intent i = new Intent(Intent.ACTION_VIEW,
- Uri.parse("market://details?id=eu.chainfire.supersu"));
- activity.startActivityForResult(i, requestCode);
- return i;
- }
-
- private void commandWait(Shell shell, Command cmd) throws Exception {
-
- while (!cmd.isFinished()) {
-
- RootTools.log(Constants.TAG, shell.getCommandQueuePositionString(cmd));
- RootTools.log(Constants.TAG, "Processed " + cmd.totalOutputProcessed + " of " + cmd.totalOutput + " output from command.");
-
- synchronized (cmd) {
- try {
- if (!cmd.isFinished()) {
- cmd.wait(2000);
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- if (!cmd.isExecuting() && !cmd.isFinished()) {
- if (!shell.isExecuting && !shell.isReading) {
- Log.e(Constants.TAG, "Waiting for a command to be executed in a shell that is not executing and not reading! \n\n Command: " + cmd.getCommand());
- Exception e = new Exception();
- e.setStackTrace(Thread.currentThread().getStackTrace());
- e.printStackTrace();
- } else if (shell.isExecuting && !shell.isReading) {
- Log.e(Constants.TAG, "Waiting for a command to be executed in a shell that is executing but not reading! \n\n Command: " + cmd.getCommand());
- Exception e = new Exception();
- e.setStackTrace(Thread.currentThread().getStackTrace());
- e.printStackTrace();
- } else {
- Log.e(Constants.TAG, "Waiting for a command to be executed in a shell that is not reading! \n\n Command: " + cmd.getCommand());
- Exception e = new Exception();
- e.setStackTrace(Thread.currentThread().getStackTrace());
- e.printStackTrace();
- }
- }
-
- }
- }
-}
diff --git a/app/src/main/java/com/stericson/roottools/internal/Runner.java b/app/src/main/java/com/stericson/roottools/internal/Runner.java
deleted file mode 100644
index 4221fca24..000000000
--- a/app/src/main/java/com/stericson/roottools/internal/Runner.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * This file is part of the RootTools Project: http://code.google.com/p/RootTools/
- *
- * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
- *
- * This code is dual-licensed under the terms of the Apache License Version 2.0 and
- * the terms of the General Public License (GPL) Version 2.
- * You may use this code according to either of these licenses as is most appropriate
- * for your project on a case-by-case basis.
- *
- * The terms of each license can be found in the root directory of this project's repository as well as at:
- *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * * http://www.gnu.org/licenses/gpl-2.0.txt
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under these Licenses is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See each License for the specific language governing permissions and
- * limitations under that License.
- */
-
-package com.stericson.roottools.internal;
-
-import android.content.Context;
-import android.util.Log;
-
-import com.stericson.rootshell.execution.Command;
-import com.stericson.rootshell.execution.Shell;
-import com.stericson.roottools.RootTools;
-
-import java.io.IOException;
-
-public class Runner extends Thread
-{
-
- private static final String LOG_TAG = "RootTools::Runner";
-
- Context context;
- String binaryName;
- String parameter;
-
- public Runner(Context context, String binaryName, String parameter)
- {
- this.context = context;
- this.binaryName = binaryName;
- this.parameter = parameter;
- }
-
- public void run()
- {
- String privateFilesPath = null;
- try
- {
- privateFilesPath = context.getFilesDir().getCanonicalPath();
- }
- catch (IOException e)
- {
- if (RootTools.debugMode)
- {
- Log.e(LOG_TAG, "Problem occured while trying to locate private files directory!");
- }
- e.printStackTrace();
- }
- if (privateFilesPath != null)
- {
- try
- {
- Command command = new Command(0, false, privateFilesPath + "/" + binaryName + " " + parameter);
- Shell.startRootShell().add(command);
- commandWait(command);
-
- }
- catch (Exception e)
- {
- }
- }
- }
-
- private void commandWait(Command cmd)
- {
- synchronized (cmd)
- {
- try
- {
- if (!cmd.isFinished())
- {
- cmd.wait(2000);
- }
- }
- catch (InterruptedException e)
- {
- e.printStackTrace();
- }
- }
- }
-
-}
diff --git a/app/src/main/java/dev/ukanth/ufirewall/Api.java b/app/src/main/java/dev/ukanth/ufirewall/Api.java
index d274ee4da..69fceca82 100644
--- a/app/src/main/java/dev/ukanth/ufirewall/Api.java
+++ b/app/src/main/java/dev/ukanth/ufirewall/Api.java
@@ -68,6 +68,7 @@
import android.util.Base64;
import android.util.SparseArray;
import android.widget.Toast;
+import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
@@ -78,12 +79,12 @@
import com.raizlabs.android.dbflow.sql.language.Delete;
import com.raizlabs.android.dbflow.sql.language.SQLite;
import com.raizlabs.android.dbflow.sql.language.Select;
-import com.stericson.roottools.RootTools;
import com.topjohnwu.superuser.Shell;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import org.json.JSONTokener;
import java.io.BufferedReader;
import java.io.File;
@@ -105,10 +106,12 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.TreeSet;
import java.util.StringTokenizer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -127,14 +130,26 @@
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.log.LogData;
import dev.ukanth.ufirewall.log.LogData_Table;
-import dev.ukanth.ufirewall.preferences.DefaultConnectionPref;
-import dev.ukanth.ufirewall.preferences.DefaultConnectionPref_Table;
import dev.ukanth.ufirewall.profiles.ProfileData;
import dev.ukanth.ufirewall.profiles.ProfileHelper;
import dev.ukanth.ufirewall.service.FirewallService;
import dev.ukanth.ufirewall.service.RootCommand;
+import dev.ukanth.ufirewall.service.RootShellService;
+import dev.ukanth.ufirewall.customrules.CustomRule;
+import dev.ukanth.ufirewall.customrules.CustomRule_Table;
+import dev.ukanth.ufirewall.util.ApkInfo;
+import dev.ukanth.ufirewall.util.AppRuleHelper;
+import dev.ukanth.ufirewall.util.BackupHelper;
import dev.ukanth.ufirewall.util.G;
+import dev.ukanth.ufirewall.util.IptablesRestorePlanner;
+import dev.ukanth.ufirewall.util.IptablesVersion;
import dev.ukanth.ufirewall.util.JsonHelper;
+import dev.ukanth.ufirewall.util.NetworkChangeDebouncer;
+import dev.ukanth.ufirewall.util.Notifications;
+import dev.ukanth.ufirewall.util.RootFiles;
+import dev.ukanth.ufirewall.util.SystemUids;
+import dev.ukanth.ufirewall.util.UidListParser;
+import dev.ukanth.ufirewall.util.UidResolver;
import dev.ukanth.ufirewall.widget.StatusWidget;
/**
@@ -192,6 +207,8 @@ public final class Api {
public static final String SCRIPT_EXTRA = "dev.ukanth.ufirewall.intent.extra.SCRIPT";
public static final String SCRIPT2_EXTRA = "dev.ukanth.ufirewall.intent.extra.SCRIPT2";
public static final int ERROR_NOTIFICATION_ID = 9;
+ public static final int CUSTOM_SCRIPT_WARNING_NOTIFICATION_ID = 10;
+ public static final int LOG_WATCHER_FAILED_NOTIFICATION_ID = 11;
private static final int WIFI_EXPORT = 0;
private static final int DATA_EXPORT = 1;
private static final int ROAM_EXPORT = 2;
@@ -208,10 +225,21 @@ public final class Api {
private static final int IPTABLES_TRY_AGAIN = 4;
private static final String[] dynChains = {"-3g-postcustom", "-3g-fork", "-wifi-postcustom", "-wifi-fork"};
private static final String[] natChains = {"", "-tor-check", "-tor-filter"};
- private static final String[] staticChains = {"", "-input", "-3g", "-wifi", "-reject", "-vpn", "-3g-tether", "-3g-home", "-3g-roam", "-wifi-tether", "-wifi-wan", "-wifi-lan", "-usb-tether", "-tor", "-tor-reject", "-tether"};
+ private static final String[] staticChains = {"", "-input", "-3g", "-wifi", "-reject", "-vpn", "-3g-tether", "-3g-home", "-3g-roam", "-wifi-tether", "-wifi-wan", "-wifi-lan", "-usb-tether", "-tor", "-tor-reject", "-tether", "-3g-home-reject", "-3g-roam-reject", "-wifi-wan-reject", "-wifi-lan-reject", "-vpn-reject", "-tether-reject"};
+ // LAN-selected apps also need discovery destinations such as mDNS, SSDP, and broadcast.
+ private static final String[] LOCAL_RESERVED_IPV4_RANGES = {"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "224.0.0.0/4", "255.255.255.255/32"};
+ private static final String[] LOCAL_RESERVED_IPV6_RANGES = {"fc00::/7", "fe80::/10", "ff00::/8"};
private static volatile boolean globalStatus = false;
private static final Object GLOBAL_STATUS_LOCK = new Object();
+
+ /**
+ * Check if rules are currently being applied
+ * @return true if rules application is in progress
+ */
+ public static boolean isRulesBeingApplied() {
+ return globalStatus;
+ }
public static List getListOfUids() {
return listOfUids;
@@ -220,10 +248,10 @@ public static List getListOfUids() {
private static List listOfUids = new ArrayList<>();
- private static Map uidToApplicationInfoMap = null;
- private static final Pattern dual_pattern = Pattern.compile("package:(.*) uid:(.*)", Pattern.MULTILINE);
+ // "package: uid:"; without --user the uids of every user follow, comma-separated
+ // ("uid:10152,1010152,1110152"): take the first one (the main user's)
/**
* @brief Special user/group IDs that aren't associated with
@@ -272,6 +300,15 @@ public static List getListOfUids() {
private static Map specialApps = null;
private static volatile boolean rulesUpToDate = false;
private static final Object RULES_LOCK = new Object();
+ /**
+ * @return true while a full rule apply runs
+ */
+ public static boolean isApplyInProgress() {
+ synchronized (GLOBAL_STATUS_LOCK) {
+ return globalStatus;
+ }
+ }
+
public static void setRulesUpToDate(boolean rulesUpToDate) {
synchronized (RULES_LOCK) {
Api.rulesUpToDate = rulesUpToDate;
@@ -329,32 +366,110 @@ public static void toast(final Context ctx, final CharSequence msgText, final in
}
public static String getBinaryPath(Context ctx, boolean setv6) {
- boolean builtin;
String ip_path = G.ip_path();
-
- if (ip_path.equals("system")) {
- builtin = false;
- } else if(ip_path.equals("builtin")) {
- builtin = true;
- } else{
- builtin = false;
+ String binaryName = setv6 ? "ip6tables" : "iptables";
+
+ // If built-in binaries have previously failed with exit 126, prefer system binaries
+ if (G.isBuiltinIptablesFailed() && !ip_path.equals("builtin")) {
+ Log.i(TAG, "Built-in iptables previously failed, preferring system binary for " + binaryName);
+ String systemBinaryPath = findSystemBinary(binaryName);
+ if (systemBinaryPath != null) {
+ if (Api.bbPath == null) {
+ Api.bbPath = getBusyBoxPath(ctx, true);
+ }
+ return systemBinaryPath;
+ }
+ Log.w(TAG, "System binary " + binaryName + " not found despite previous built-in failure");
}
-
- String dir = "";
- if (builtin) {
- dir = ctx.getDir("bin", 0).getAbsolutePath() + "/";
+
+ // First priority: check system binary if preference is "system" or "auto"
+ if (ip_path.equals("system") || ip_path.equals("auto")) {
+ String systemBinaryPath = findSystemBinary(binaryName);
+ if (systemBinaryPath != null) {
+ if (Api.bbPath == null) {
+ Api.bbPath = getBusyBoxPath(ctx, true);
+ }
+ return systemBinaryPath;
+ }
+
+ // If system binary not found and preference is "system", log warning
+ if (ip_path.equals("system")) {
+ Log.w(TAG, "System binary " + binaryName + " not found, falling back to built-in");
+ }
}
-
- String ipPath = dir + (setv6 ? "ip6tables" : "iptables" );
-
- /*if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
- dir = ctx.getDir("bin", 0).getAbsolutePath() + "/";
- ipPath = dir + "run_pie " + dir + (setv6 ? "ip6tables" : "iptables");
- }*/
+
+ // Second priority: use built-in binary
+ // Check if built-in binary exists for current architecture
+ String builtinDir = ctx.getDir("bin", 0).getAbsolutePath() + "/";
+ String builtinPath = builtinDir + binaryName;
+
+ File builtinFile = new File(builtinPath);
+ if (builtinFile.exists() && builtinFile.canExecute()) {
+ if (Api.bbPath == null) {
+ Api.bbPath = getBusyBoxPath(ctx, true);
+ }
+ return builtinPath;
+ }
+
+ // Fallback: try to install built-in binaries if they don't exist
+ Log.w(TAG, "Built-in binary " + binaryName + " not found, attempting to install binaries");
+ if (assertBinaries(ctx, false)) {
+ if (Api.bbPath == null) {
+ Api.bbPath = getBusyBoxPath(ctx, true);
+ }
+ return builtinPath;
+ }
+
+ // Last resort: return the path even if binary doesn't exist (will likely fail at runtime)
+ Log.e(TAG, "No working " + binaryName + " binary found, returning built-in path anyway");
if (Api.bbPath == null) {
Api.bbPath = getBusyBoxPath(ctx, true);
}
- return ipPath;
+ return builtinPath;
+ }
+
+ /**
+ * Find system binary by checking common system paths
+ *
+ * @param binaryName the name of the binary to find
+ * @return full path to the binary if found, null otherwise
+ */
+ public static String findSystemBinary(String binaryName) {
+ // Common paths where system iptables/ip6tables binaries are located
+ String[] systemPaths = {
+ "/system/bin/" + binaryName,
+ "/system/xbin/" + binaryName,
+ "/vendor/bin/" + binaryName,
+ "/sbin/" + binaryName,
+ "/usr/bin/" + binaryName,
+ "/bin/" + binaryName
+ };
+
+ for (String path : systemPaths) {
+ File binaryFile = new File(path);
+ if (binaryFile.exists() && binaryFile.canExecute()) {
+ Log.i(TAG, "Found system binary: " + path);
+ return path;
+ }
+ }
+
+ // Also try using 'which' command if available
+ try {
+ Shell.Result result = Shell.cmd("which " + binaryName).exec();
+ if (result.isSuccess() && !result.getOut().isEmpty()) {
+ String whichPath = result.getOut().get(0).trim();
+ File whichFile = new File(whichPath);
+ if (whichFile.exists() && whichFile.canExecute()) {
+ Log.i(TAG, "Found system binary via 'which': " + whichPath);
+ return whichPath;
+ }
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "Unable to use 'which' command to find " + binaryName + ": " + e.getMessage());
+ }
+
+ Log.d(TAG, "System binary " + binaryName + " not found in any standard location");
+ return null;
}
/**
@@ -365,45 +480,94 @@ public static String getBinaryPath(Context ctx, boolean setv6) {
* @return
*/
public static String getBusyBoxPath(Context ctx, boolean considerSystem) {
-
- if (G.bb_path().equals("system") && considerSystem) {
- return "busybox ";
+ String bb_path = G.bb_path();
+
+ // First priority: check system busybox if preference is "system" or "auto" and considerSystem is true
+ if (considerSystem && (bb_path.equals("system") || bb_path.equals("auto"))) {
+ String systemBusybox = findSystemBinary("busybox");
+ if (systemBusybox != null) {
+ return systemBusybox + " ";
+ }
+
+ // If system busybox not found and preference is "system", log warning and fall back
+ if (bb_path.equals("system")) {
+ Log.w(TAG, "System busybox not found, falling back to built-in");
+ }
+ }
+
+ // Second priority: use built-in busybox
+ String dir = ctx.getDir("bin", 0).getAbsolutePath();
+ String builtinPath = dir + "/busybox";
+
+ File builtinFile = new File(builtinPath);
+ if (builtinFile.exists() && builtinFile.canExecute()) {
+ return builtinPath + " ";
+ }
+
+ // Fallback: return built-in path even if it doesn't exist yet (may be installed later)
+ if (!builtinFile.exists()) {
+ Log.w(TAG, "Built-in busybox not found at " + builtinPath + ", returning path anyway");
} else {
- String dir = ctx.getDir("bin", 0).getAbsolutePath();
- return dir + "/busybox ";
+ Log.w(TAG, "Built-in busybox exists but not executable at " + builtinPath + ", permissions: " +
+ (builtinFile.canRead() ? "R" : "-") +
+ (builtinFile.canWrite() ? "W" : "-") +
+ (builtinFile.canExecute() ? "X" : "-"));
}
+ return builtinPath + " ";
}
/**
- * Get NFLog Path
+ * Get NFLog Path - Enhanced version with fallback support
*
- * @param ctx
- * @returnC
+ * @param ctx Context
+ * @return path to best available nflog binary
*/
public static String getNflogPath(Context ctx) {
String dir = ctx.getDir("bin", 0).getAbsolutePath();
- String nflogPath = dir + "/nflog";
+ String originalPath = dir + "/nflog";
+ File originalFile = new File(originalPath);
- // Check if nflog binary exists and is executable
- File nflogFile = new File(nflogPath);
- if (!nflogFile.exists()) {
- Log.w(TAG, "NFLOG binary not found at: " + nflogPath);
+ if (!originalFile.exists()) {
+ Log.w(TAG, "No NFLOG binary found at: " + originalPath);
return null;
}
- if (!nflogFile.canExecute()) {
- Log.w(TAG, "NFLOG binary not executable at: " + nflogPath);
+ if (!originalFile.canExecute()) {
+ Log.w(TAG, "NFLOG binary not executable at: " + originalPath);
// Try to make it executable
try {
- nflogFile.setExecutable(true);
+ originalFile.setExecutable(true);
+ if (!originalFile.canExecute()) {
+ Log.e(TAG, "Failed to make nflog executable");
+ return null;
+ }
} catch (Exception e) {
Log.e(TAG, "Failed to make nflog executable: " + e.getMessage());
return null;
}
}
- return nflogPath + " ";
+ Log.i(TAG, "Using original NFLOG binary");
+ return originalPath;
+ }
+
+ /**
+ * Get enhanced NFLOG command with optimized parameters
+ *
+ * @param ctx Context
+ * @param queueNum NFLOG queue number
+ * @return complete command string with optimizations
+ */
+ public static String getEnhancedNflogCommand(Context ctx, int queueNum) {
+ String nflogPath = getNflogPath(ctx);
+ if (nflogPath == null) {
+ return null;
+ }
+
+ // Use standard nflog command with queue number
+ return nflogPath + " " + queueNum;
}
+
/**
* Copies a raw resource file, given its ID to the given location
@@ -429,7 +593,45 @@ private static void copyRawFile(Context ctx, int resid, File file, String mode)
is.close();
// Change the permissions
- Runtime.getRuntime().exec("chmod " + mode + " " + abspath).waitFor();
+ executeSecureCommand(new String[]{"chmod", mode, abspath});
+ }
+
+ /**
+ * Execute system commands securely using ProcessBuilder to prevent command injection
+ *
+ * @param command Array of command and arguments (prevents shell interpretation)
+ * @throws IOException if command execution fails
+ * @throws InterruptedException if command is interrupted
+ */
+ private static void executeSecureCommand(String[] command) throws IOException, InterruptedException {
+ if (command == null || command.length == 0) {
+ throw new IllegalArgumentException("Command cannot be null or empty");
+ }
+
+ // Validate command and arguments don't contain dangerous characters
+ for (String arg : command) {
+ if (arg == null || arg.contains("\n") || arg.contains("\r") ||
+ arg.contains(";") || arg.contains("&") || arg.contains("|") ||
+ arg.contains("`") || arg.contains("$")) {
+ Log.w(TAG, "Rejecting command with potentially dangerous characters: " + java.util.Arrays.toString(command));
+ throw new SecurityException("Command contains illegal characters");
+ }
+ }
+
+ ProcessBuilder pb = new ProcessBuilder(command);
+ pb.environment().clear(); // Clear environment to prevent injection via env vars
+ Process process = pb.start();
+ int exitCode = process.waitFor();
+
+ if (exitCode != 0) {
+ // For chmod commands, permission denied is expected on Android - don't fail the installation
+ if (command.length > 0 && "chmod".equals(command[0])) {
+ Log.w(TAG, "chmod command failed (expected on Android without root): exit code " + exitCode + " for " + java.util.Arrays.toString(command));
+ return; // Don't throw exception for chmod failures
+ }
+ Log.w(TAG, "Command failed with exit code " + exitCode + ": " + java.util.Arrays.toString(command));
+ throw new IOException("Command execution failed with exit code: " + exitCode);
+ }
}
/**
@@ -448,6 +650,45 @@ private static void addRuleForUsers(List listCommands, String[] users, S
}
}
+ // null until probed; the owner "--socket-exists" and conntrack matches depend on the kernel
+ private static volatile Boolean ownerlessRuleSupported;
+
+ /**
+ * @return true if the kernel supports the rule for socket-less packets of established
+ * connections (tested once per app run in a scratch chain, IPv4 and IPv6)
+ */
+ static boolean ownerlessEstablishedSupported() {
+ Boolean supported = ownerlessRuleSupported;
+ if (supported != null) {
+ return supported;
+ }
+ boolean ok = probeOwnerlessRule(false) && (!G.enableIPv6() || probeOwnerlessRule(true));
+ ownerlessRuleSupported = ok;
+ Log.i(TAG, "Rule for closing packets of allowed connections: " + (ok ? "supported" : "not supported by this kernel"));
+ return ok;
+ }
+
+ private static boolean probeOwnerlessRule(boolean ipv6) {
+ try {
+ Context c = G.getContext();
+ String bin = getBinaryPath(c, ipv6);
+ if (bin == null) {
+ return false;
+ }
+ String chain = "afwall-probe";
+ Shell.Result result = Shell.cmd(
+ bin + " -N " + chain + " 2>/dev/null",
+ bin + " -A " + chain + " -m owner ! --socket-exists -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN"
+ + " && echo AFWALL_PROBE_OK",
+ bin + " -F " + chain + " 2>/dev/null",
+ bin + " -X " + chain + " 2>/dev/null").exec();
+ return result.getOut().contains("AFWALL_PROBE_OK");
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to test iptables support: " + e.getMessage());
+ return false;
+ }
+ }
+
private static void addRulesForUidlist(List cmds, List uids, String chain, boolean whitelist) {
String action = whitelist ? " -j RETURN" : " -j " + chain + "-reject";
@@ -490,66 +731,129 @@ private static void addRulesForUidlist(List cmds, List uids, St
//cmds.add("-A " + chain + " -p tcp --dport 443" + " -j ACCEPT");
}
+ if (ownerlessEstablishedSupported()) {
+ // Packets without a socket that belong to a connection already allowed: the
+ // closing ACK/FIN/RST sent after the app closed its socket. Unless the kernel entry
+ // was allowed these were rejected (and logged as "unknown"), leaving the peer to
+ // retransmit. New connections without a socket (kernel VPNs such as WireGuard,
+ // IPsec) still follow the kernel entry: they are not ESTABLISHED yet.
+ cmds.add("-A " + chain + " -m owner ! --socket-exists -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN");
+ }
+
boolean kernel_checked = uids.contains(SPECIAL_UID_KERNEL);
+
if (whitelist) {
if (kernel_checked) {
// reject any other UIDs, but allow the kernel through
- cmds.add("-A " + chain + " -m owner --uid-owner 0:999999999 -j " + chain + "-reject");
+ // Use fallback rule if owner module is not available
+ if (G.hasOwnerModule()) {
+ Log.d(TAG, "Adding whitelist kernel rule with owner module for chain " + chain);
+ cmds.add("-A " + chain + " -m owner --uid-owner 0:999999999 -j " + chain + "-reject");
+ } else {
+ Log.w(TAG, "Owner module not available, using fallback rule for chain " + chain);
+ cmds.add("-A " + chain + " -j " + chain + "-reject");
+ }
} else {
// kernel is blocked so reject everything
- cmds.add("-A " + chain + " -j " + chain + "-reject");
+ String rejectRule = "-A " + chain + " -j " + chain + "-reject";
+ cmds.add(rejectRule);
}
} else {
if (kernel_checked) {
// allow any other UIDs, but block the kernel
- cmds.add("-A " + chain + " -m owner --uid-owner 0:999999999 -j RETURN");
- cmds.add("-A " + chain + " -j " + chain + "-reject");
+ if (G.hasOwnerModule()) {
+ cmds.add("-A " + chain + " -m owner --uid-owner 0:999999999 -j RETURN");
+ cmds.add("-A " + chain + " -j " + chain + "-reject");
+ } else {
+ Log.w(TAG, "Owner module not available, using fallback rule for chain " + chain);
+ cmds.add("-A " + chain + " -j " + chain + "-reject");
+ }
}
}
- //add 1052 for LAN
- if(G.enableLAN()) {
- cmds.add("-A " + "afwall-wifi-lan" + " -m owner --uid-owner 1052 -j RETURN");
+ // add 1052 for LAN; "afwall" under multi-user, so take the prefix of this chain
+ String chainPrefix = chain.contains("-") ? chain.substring(0, chain.indexOf('-')) : chain;
+ if(G.enableLAN() && G.hasOwnerModule()) {
+ cmds.add("-A " + chainPrefix + "-wifi-lan" + " -m owner --uid-owner 1052 -j RETURN");
}
- cmds.add("-A " + "afwall-wifi-wan" + " -m owner --uid-owner 1052 -j RETURN");
+ if (G.hasOwnerModule()) {
+ cmds.add("-A " + chainPrefix + "-wifi-wan" + " -m owner --uid-owner 1052 -j RETURN");
+ }
}
}
+ private static final String[] INDIVIDUAL_REJECT_CHAIN_SUFFIXES = {"-3g-home-reject", "-3g-roam-reject",
+ "-wifi-wan-reject", "-wifi-lan-reject", "-vpn-reject", "-tether-reject"};
+ /** every chain {@link #addRejectRules} appends to */
+ private static final String[] REJECT_CHAIN_SUFFIXES = {"-reject", "-3g-home-reject", "-3g-roam-reject",
+ "-wifi-wan-reject", "-wifi-lan-reject", "-vpn-reject", "-tether-reject"};
+
private static void addRejectRules(List cmds, String chainName) {
// set up reject chain to log or not log
// this can be changed dynamically through the Firewall Logs activity
- if (G.enableLogService()) {
- if (G.logTarget().trim().equals("LOG")) {
- //cmds.add("-A " + chainName + " -m limit --limit 1000/min -j LOG --log-prefix \"{AFL-ALLOW}\" --log-level 4 --log-uid");
- cmds.add("-A " + chainName + "-reject" + " -m limit --limit 1000/min -j LOG --log-prefix \"{AFL}\" --log-level 4 --log-uid --log-tcp-options --log-ip-options");
- } else if (G.logTarget().trim().equals("NFLOG")) {
- //cmds.add("-A " + chainName + " -j NFLOG --nflog-prefix \"{AFL-ALLOW}\" --nflog-group 40");
- cmds.add("-A " + chainName + "-reject" + " -j NFLOG --nflog-prefix \"{AFL}\" --nflog-group 40");
- }
+ addLogRuleForRejectChain(cmds, chainName + "-reject");
+ String rejectRule = "-A " + chainName + "-reject" + " -j REJECT";
+ Log.d(TAG, "Adding final REJECT rule: " + rejectRule);
+ cmds.add(rejectRule);
+
+ // Also populate individual reject chains that are used by whitelist mode
+ for (String suffix : INDIVIDUAL_REJECT_CHAIN_SUFFIXES) {
+ String individualRejectChain = chainName + suffix;
+ Log.d(TAG, "Populating individual reject chain: " + individualRejectChain);
+ addLogRuleForRejectChain(cmds, individualRejectChain);
+ String individualRejectRule = "-A " + individualRejectChain + " -j REJECT";
+ Log.d(TAG, "Adding REJECT to individual reject chain: " + individualRejectRule);
+ cmds.add(individualRejectRule);
+ }
+ }
+
+ private static void addLogRuleForRejectChain(List cmds, String rejectChain) {
+ if (!G.enableLogService()) {
+ return;
+ }
+ String logTarget = G.logTarget().trim();
+ if (logTarget.equals("LOG")) {
+ // Whitelist mode uses per-interface reject chains, so LOG must be
+ // added anywhere packets can be rejected, not only the shared chain.
+ String logRule = "-A " + rejectChain + " -m limit --limit 1000/min -j LOG --log-prefix \"{AFL}\" --log-level 4 --log-uid --log-tcp-options --log-ip-options";
+ Log.d(TAG, "Adding LOG rule to reject chain: " + logRule);
+ cmds.add(logRule);
+ } else if (logTarget.equals("NFLOG")) {
+ String nflogRule = "-A " + rejectChain + " -j NFLOG --nflog-prefix \"{AFL}\" --nflog-group 40";
+ Log.d(TAG, "Adding NFLOG rule to reject chain: " + nflogRule);
+ cmds.add(nflogRule);
}
- cmds.add("-A " + chainName + "-reject" + " -j REJECT");
}
private static void addTorRules(List cmds, List uids, Boolean whitelist, Boolean ipv6, String chainName) {
+ Integer socks_port = 9050;
+ Integer http_port = 8118;
+ Integer dns_port = 5400;
+ Integer tcp_port = 9040;
+
+ Log.i(TAG, "Adding Tor redirect rules before interface filters");
+ // Tor selection is an outbound owner match; jumping from INPUT breaks on several iptables backends.
+
for (Integer uid : uids) {
if (uid != null && uid >= 0) {
- if (G.enableInbound() || ipv6) {
+ if (ipv6) {
cmds.add("-A " + chainName + "-tor-reject -m owner --uid-owner " + uid + " -j " + chainName + "-reject");
}
if (!ipv6) {
cmds.add("-t nat -A " + chainName + "-tor-check -m owner --uid-owner " + uid + " -j " + chainName + "-tor-filter");
+ // Tor rules run before interface chains so redirected traffic is not rejected as plain Wi-Fi/mobile.
+ cmds.add("-A " + chainName + "-tor -m owner --uid-owner " + uid + " -d 127.0.0.1 -p tcp --dport " + socks_port + " -j ACCEPT");
+ cmds.add("-A " + chainName + "-tor -m owner --uid-owner " + uid + " -d 127.0.0.1 -p tcp --dport " + http_port + " -j ACCEPT");
+ cmds.add("-A " + chainName + "-tor -m owner --uid-owner " + uid + " -d 127.0.0.1 -p tcp --dport " + tcp_port + " -j ACCEPT");
+ cmds.add("-A " + chainName + "-tor -m owner --uid-owner " + uid + " -d 127.0.0.1 -p udp --dport " + dns_port + " -j ACCEPT");
}
}
}
if (ipv6) {
cmds.add("-A " + chainName + " -j " + chainName + "-tor-reject");
} else {
- Integer socks_port = 9050;
- Integer http_port = 8118;
- Integer dns_port = 5400;
- Integer tcp_port = 9040;
cmds.add("-t nat -A " + chainName + "-tor-filter -d 127.0.0.1 -p tcp --dport " + socks_port + " -j RETURN");
cmds.add("-t nat -A " + chainName + "-tor-filter -d 127.0.0.1 -p tcp --dport " + http_port + " -j RETURN");
cmds.add("-t nat -A " + chainName + "-tor-filter -p udp --dport 53 -j REDIRECT --to-ports " + dns_port);
@@ -559,47 +863,148 @@ private static void addTorRules(List cmds, List uids, Boolean w
cmds.add("-A " + chainName + "-tor -m mark --mark 0x500 -j " + chainName + "-reject");
cmds.add("-A " + chainName + " -j " + chainName + "-tor");
}
- if (G.enableInbound()) {
- cmds.add("-A " + chainName + "-input -j " + chainName + "-tor-reject");
- }
}
private static String sanitizeRule(String rule) {
- // Remove potentially dangerous characters and commands
- if (rule.contains("&&") || rule.contains("||") || rule.contains(";") ||
- rule.contains("|") || rule.contains("`") || rule.contains("$") ||
- rule.contains("rm ") || rule.contains("dd ") || rule.contains("chmod ") ||
- rule.contains("chown ") || rule.contains("su ") || rule.contains("sudo ")) {
- Log.w(TAG, "Rejecting potentially dangerous custom rule: " + rule);
+ String trimmed = rule.trim();
+
+ // Check for dangerous command chaining/substitution
+ if (trimmed.contains("&&") || trimmed.contains("||") || trimmed.contains(";") ||
+ trimmed.contains("|") || trimmed.contains("`")) {
+ Log.w(TAG, "Rejecting potentially dangerous custom rule (command chaining): " + rule);
+ return null;
+ }
+
+ // Check for dangerous commands
+ if (trimmed.contains("rm ") || trimmed.contains("dd ") ||
+ trimmed.contains("chmod ") || trimmed.contains("chown ") ||
+ trimmed.contains("su ") || trimmed.contains("sudo ")) {
+ Log.w(TAG, "Rejecting potentially dangerous custom rule (system modification): " + rule);
+ return null;
+ }
+
+ // Allow $ only for whitelisted variables
+ if (trimmed.contains("$")) {
+ // Check if it's using allowed variables
+ String tempRule = trimmed;
+ tempRule = tempRule.replace("$IPTABLES", "");
+ tempRule = tempRule.replace("$IP6TABLES", "");
+ tempRule = tempRule.replace("$BUSYBOX", "");
+ tempRule = tempRule.replace("$IPV6", "");
+
+ if (tempRule.contains("$")) {
+ Log.w(TAG, "Rejecting custom rule with non-whitelisted variables: " + rule);
+ return null;
+ }
+ }
+
+ // Reject file sourcing (dot-source) - potential command injection vector
+ if (trimmed.startsWith(". ") || trimmed.startsWith("source ")) {
+ Log.w(TAG, "Rejecting file sourcing in custom rule (security risk): " + rule);
return null;
}
- // Only allow basic iptables/ip6tables commands
- if (!rule.startsWith("iptables ") && !rule.startsWith("ip6tables ") &&
- !rule.startsWith("-A ") && !rule.startsWith("-I ") &&
- !rule.startsWith("-D ") && !rule.startsWith("-F ") &&
- !rule.startsWith("-P ") && !rule.startsWith("-N ")) {
+ // Allow basic iptables commands (keep existing check)
+ if (!trimmed.startsWith("iptables ") && !trimmed.startsWith("ip6tables ") &&
+ !trimmed.startsWith("$IPTABLES ") && !trimmed.startsWith("$IP6TABLES ") &&
+ !trimmed.startsWith("-A ") && !trimmed.startsWith("-I ") &&
+ !trimmed.startsWith("-D ") && !trimmed.startsWith("-F ") &&
+ !trimmed.startsWith("-P ") && !trimmed.startsWith("-N ")) {
Log.w(TAG, "Rejecting non-iptables rule: " + rule);
return null;
}
- return rule;
+ return trimmed;
+ }
+
+ public static String validateCustomRuleForStorage(String rule) {
+ if (rule == null) {
+ return null;
+ }
+ return sanitizeRule(rule);
}
private static void addCustomRules(String prefName, List cmds) {
+ addCustomRules(prefName, cmds, false);
+ }
+
+ private static void addCustomRules(String prefName, List cmds, boolean ipv6) {
+ addCustomRules(prefName, cmds, ipv6, true);
+ }
+
+ /**
+ * @param includeDatabaseRules add the per-app (direct) rules too. They are appended to the main
+ * chain, which only a full apply rebuilds, so a partial apply must
+ * not add them again (each network change used to add a copy).
+ */
+ private static void addCustomRules(String prefName, List cmds, boolean ipv6,
+ boolean includeDatabaseRules) {
String customRulesStr = G.pPrefs.getString(prefName, "");
- if (customRulesStr.isEmpty()) return;
+ if (!customRulesStr.isEmpty()) {
+ String[] customRules = customRulesStr.split("[\\r\\n]+");
+ for (String rule : customRules) {
+ if (rule.matches(".*\\S.*")) {
+ // Sanitize the rule to prevent command injection
+ String sanitizedRule = sanitizeRule(rule.trim());
+ if (sanitizedRule != null && !sanitizedRule.isEmpty()) {
+ cmds.add("#LITERAL# " + sanitizedRule);
+ }
+ }
+ }
+ }
+
+ if (includeDatabaseRules && PREF_CUSTOMSCRIPT.equals(prefName)) {
+ addDatabaseCustomRules(cmds, ipv6);
+ }
+ }
- String[] customRules = customRulesStr.split("[\\r\\n]+");
- for (String rule : customRules) {
- if (rule.matches(".*\\S.*")) {
- // Sanitize the rule to prevent command injection
- String sanitizedRule = sanitizeRule(rule.trim());
- if (sanitizedRule != null && !sanitizedRule.isEmpty()) {
- cmds.add("#LITERAL# " + sanitizedRule);
+ /**
+ * The direct (per-app) rules of the current profile. Built from the rule itself for the main
+ * chain of this user and the table's address family: IPv6 only with IPv6 support on, and a
+ * rule with a destination only in the table of its family.
+ */
+ private static void addDatabaseCustomRules(List cmds, boolean ipv6) {
+ if (!G.enableCustomRules() || (ipv6 && !G.enableIPv6())) {
+ return;
+ }
+ String chain = getThreadSafeChainName();
+ try {
+ List customRules = SQLite.select()
+ .from(CustomRule.class)
+ .where(CustomRule_Table.active.eq(true))
+ .queryList();
+
+ for (CustomRule customRule : customRules) {
+ if (!AppRuleHelper.belongsToCurrentProfile(customRule)) {
+ continue;
+ }
+ String rule = AppRuleHelper.buildRule(AppRuleHelper.parseRuleName(customRule.getName()), chain, ipv6);
+ if (rule != null && rule.matches(".*\\S.*")) {
+ String sanitizedRule = sanitizeRule(rule.trim());
+ if (sanitizedRule != null && !sanitizedRule.isEmpty()) {
+ cmds.add(sanitizedRule);
+ }
}
}
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to load database custom rules", e);
+ }
+ }
+
+ private static Set getLanDestinationRanges(InterfaceDetails cfg, boolean ipv6) {
+ LinkedHashSet ranges = new LinkedHashSet<>();
+ if (ipv6) {
+ if (cfg != null) {
+ ranges.addAll(cfg.lanMaskV6);
+ }
+ ranges.addAll(Arrays.asList(LOCAL_RESERVED_IPV6_RANGES));
+ } else {
+ if (cfg != null) {
+ ranges.addAll(cfg.lanMaskV4);
+ }
+ ranges.addAll(Arrays.asList(LOCAL_RESERVED_IPV4_RANGES));
}
+ return ranges;
}
/**
@@ -612,6 +1017,11 @@ private static void addCustomRules(String prefName, List cmds) {
* @param cmds command list
*/
private static void addInterfaceRouting(Context ctx, List cmds, boolean ipv6, String chainName) {
+ addInterfaceRouting(ctx, cmds, ipv6, chainName, null);
+ }
+
+ private static void addInterfaceRouting(Context ctx, List cmds, boolean ipv6, String chainName,
+ List lanList) {
try {
//force only for v4
final InterfaceDetails cfg = InterfaceTracker.getCurrentCfg(ctx, !ipv6);
@@ -631,7 +1041,7 @@ private static void addInterfaceRouting(Context ctx, List cmds, boolean
} else {
cmds.add("-A " + chainName + "-wifi-postcustom -j " + chainName + "-wifi-fork");
}
-
+
if (cfg.isUsbTethered) {
cmds.add("-A " + chainName + "-3g-postcustom -j " + chainName + "-usb-tether");
} else {
@@ -643,26 +1053,24 @@ private static void addInterfaceRouting(Context ctx, List cmds, boolean
}
if (G.enableLAN() && !cfg.isWifiTethered) {
- if (ipv6) {
- if (!cfg.lanMaskV6.equals("")) {
- cmds.add("-A " + chainName + "-wifi-fork -d " + cfg.lanMaskV6 + " -j " + chainName + "-wifi-lan");
- cmds.add("-A " + chainName + "-wifi-fork '!' -d " + cfg.lanMaskV6 + " -j " + chainName + "-wifi-wan");
- } else {
- Log.i(TAG, "no ipv6 found: " + G.enableIPv6() + "," + cfg.lanMaskV6);
- }
- } else {
- if (!cfg.lanMaskV4.equals("")) {
- cmds.add("-A " + chainName + "-wifi-fork -d " + cfg.lanMaskV4 + " -j " + chainName + "-wifi-lan");
- cmds.add("-A " + chainName + "-wifi-fork '!' -d " + cfg.lanMaskV4 + " -j " + chainName + "-wifi-wan");
- } else {
- Log.i(TAG, "no ipv4 found:" + G.enableIPv6() + "," + cfg.lanMaskV4);
- }
+ // Support multiple LAN subnets (Issue #1362) plus reserved local/discovery ranges.
+ // Subnet-specific rules are added first, then a catch-all routes remaining traffic to WAN.
+ Set lanRanges = getLanDestinationRanges(cfg, ipv6);
+ if (lanRanges.isEmpty()) {
+ Log.i(TAG, "no LAN ranges found: " + G.enableIPv6() + "," + (ipv6 ? cfg.lanMaskV6 : cfg.lanMaskV4));
+ }
+ for (String subnet : lanRanges) {
+ cmds.add("-A " + chainName + "-wifi-fork -d " + subnet + " -g " + chainName + "-wifi-lan");
}
- if (cfg.lanMaskV4.equals("") && cfg.lanMaskV6.equals("")) {
- Log.i(TAG, "No ipaddress found for LAN");
- // lets find one more time
- //atleast allow internet - don't block completely
- cmds.add("-A " + chainName + "-wifi-fork -j " + chainName + "-wifi-wan");
+ // Catch-all: route everything not matching a LAN subnet to WAN
+ cmds.add("-A " + chainName + "-wifi-fork -j " + chainName + "-wifi-wan");
+
+ // Rebuild the LAN chain's per-UID rules so fastApply is self-healing.
+ // If lanList is null this path is skipped (e.g., during a full apply that already
+ // rebuilds staticChains separately).
+ if (lanList != null) {
+ cmds.add("#NOCHK# -F " + chainName + "-wifi-lan");
+ addRulesForUidlist(cmds, lanList, chainName + "-wifi-lan", whitelist);
}
} else {
cmds.add("-A " + chainName + "-wifi-fork -j " + chainName + "-wifi-wan");
@@ -674,32 +1082,49 @@ private static void addInterfaceRouting(Context ctx, List cmds, boolean
cmds.add("-A " + chainName + "-3g-fork -j " + chainName + "-3g-home");
}
-
} catch (Exception e) {
- Log.i(TAG, "Exception while applying shortRules " + e.getMessage());
+ // The dynamic chains were already flushed above; running the rest of the script would
+ // leave them empty. Let the caller fail the apply instead.
+ throw new IllegalStateException("Unable to build interface routing rules", e);
}
+ }
+ /**
+ * Add or update the LAN chain rule for a single UID. Used for incremental per-uid updates
+ * so LAN-selected apps retain RFC1918/multicast access without a full rule rebuild.
+ */
+ static void addLanReservedUidDelta(List cmds, int uid, String chainName, boolean whitelist) {
+ if (uid < 0) return;
+ String action = whitelist ? " -j RETURN" : " -j " + chainName + "-wifi-lan-reject";
+ // Remove any existing rule for this UID before re-adding (idempotent).
+ cmds.add("#NOCHK# -D " + chainName + "-wifi-lan -m owner --uid-owner " + uid + action);
+ cmds.add("-A " + chainName + "-wifi-lan -m owner --uid-owner " + uid + action);
}
public static String getSpecialAppName(int uid) {
+ // First, try special apps (AFWall+ specific entries)
List packageInfoData = getSpecialData();
for (PackageInfoData infoData : packageInfoData) {
if (infoData.uid == uid) {
return infoData.names.get(0);
}
}
- return ctx.getString(R.string.unknown_item);
+
+ // If not found in special apps, use comprehensive UID resolver
+ return UidResolver.resolveUid(ctx, uid);
}
private static void applyShortRules(Context ctx, List cmds, boolean ipv6) {
Log.i(TAG, "Setting OUTPUT chain to DROP");
cmds.add("-P OUTPUT DROP");
- /*FIXME: Adding custom rules might increase the time */
Log.i(TAG, "Applying custom rules");
- addCustomRules(Api.PREF_CUSTOMSCRIPT, cmds);
+ addCustomRules(Api.PREF_CUSTOMSCRIPT, cmds, ipv6, false);
String chainName = getThreadSafeChainName();
- addInterfaceRouting(ctx, cmds, ipv6, chainName);
+ // Pass the current LAN UID list so fastApply also rebuilds the -wifi-lan chain,
+ // keeping LAN access self-healing across network-change routing refreshes.
+ List lanList = getDataSet().lanList;
+ addInterfaceRouting(ctx, cmds, ipv6, chainName, lanList);
Log.i(TAG, "Setting OUTPUT chain to ACCEPT");
cmds.add("-P OUTPUT ACCEPT");
}
@@ -713,11 +1138,17 @@ private static void applyShortRules(Context ctx, List cmds, boolean ipv6
*/
private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet ruleDataSet, final boolean showErrors,
List out, boolean ipv6) {
- return applyIptablesRulesImpl(ctx, ruleDataSet, showErrors, out, ipv6, null);
+ return applyIptablesRulesImpl(ctx, ruleDataSet, showErrors, out, ipv6, null, null);
}
-
+
+ /**
+ * @param out receives the shell commands to run one by one
+ * @param rawOut if not null, receives the same commands before they are turned into shell
+ * commands (input for {@link IptablesRestorePlanner})
+ */
private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet ruleDataSet, final boolean showErrors,
- List out, boolean ipv6, String threadSafeChainName) {
+ List out, boolean ipv6, String threadSafeChainName,
+ List rawOut) {
if (ctx == null) {
return false;
}
@@ -753,13 +1184,14 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul
cmds.add("-P OUTPUT DROP");
// Create and flush all chains first to ensure they exist
+ // Use NOCHK to avoid errors if chain already exists, then flush to ensure clean state
for (String s : staticChains) {
cmds.add("#NOCHK# -N " + chainName + s);
- cmds.add("-F " + chainName + s);
+ cmds.add("#NOCHK# -F " + chainName + s);
}
for (String s : dynChains) {
cmds.add("#NOCHK# -N " + chainName + s);
- cmds.add("-F " + chainName + s);
+ cmds.add("#NOCHK# -F " + chainName + s);
}
@@ -782,11 +1214,22 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul
}
// custom rules in afwall-{3g,wifi,reject} supersede everything else
- addCustomRules(Api.PREF_CUSTOMSCRIPT, cmds);
+ addCustomRules(Api.PREF_CUSTOMSCRIPT, cmds, ipv6);
+
+ // Loopback is self-device traffic, not LAN or WAN. Keep it out of
+ // the LAN split chains so local app services continue to work when
+ // LAN control is enabled in either firewall mode.
+ cmds.add("-A " + chainName + " -o lo -j RETURN");
+ if (G.enableInbound()) {
+ cmds.add("-A " + chainName + "-input -i lo -j RETURN");
+ }
cmds.add("-A " + chainName + "-3g -j " + chainName + "-3g-postcustom");
cmds.add("-A " + chainName + "-wifi -j " + chainName + "-wifi-postcustom");
addRejectRules(cmds, chainName);
+ if (ipv6) {
+ addIpv6ControlTrafficRules(cmds, chainName);
+ }
if (G.enableInbound()) {
// we don't have any rules in the INPUT chain prohibiting inbound traffic, but
@@ -795,6 +1238,12 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul
cmds.add("-A " + chainName + "-input -m state --state ESTABLISHED -j RETURN");
}
+ // Tor must redirect before interface chains so redirected traffic is not
+ // rejected as plain Wi-Fi/mobile before reaching the local Orbot ports.
+ if (G.enableTor()) {
+ addTorRules(cmds, ruleDataSet.torList, whitelist, ipv6, chainName);
+ }
+
addInterfaceRouting(ctx, cmds, ipv6, chainName);
// send wifi, 3G, VPN packets to the appropriate dynamic chain based on interface
@@ -819,6 +1268,13 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul
}
}
+ /*if (G.enableLAN()) {
+ // Allow all Android system UIDs (0-9999) on loopback unconditionally
+ cmds.add("-A " + chainName + " -o lo -m owner --uid-owner 0:9999 -j RETURN");
+ // Route remaining loopback traffic through the LAN chain for per-app control
+ cmds.add("-A " + chainName + " -o lo -j " + chainName + "-wifi-lan");
+ }*/
+
for (final String itf : ITFS_WIFI) {
cmds.add("#NOCHK# -A " + chainName + " -o " + itf + " -j " + chainName + "-wifi");
}
@@ -836,6 +1292,7 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul
if (containsUidOrAny(ruleDataSet.wifiList, SPECIAL_UID_TETHER)) {
// DHCP replies to client
addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-wifi-tether", "-p udp --sport=67 --dport=68" + action);
+ addTetherDhcpReplyRule(cmds, chainName + "-wifi-tether", action);
// DNS replies to client
addRuleForUsers(cmds, users_dns, "-A " + chainName + "-wifi-tether", "-p udp --sport=53" + action);
addRuleForUsers(cmds, users_dns, "-A " + chainName + "-wifi-tether", "-p tcp --sport=53" + action);
@@ -846,13 +1303,15 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul
if (containsUidOrAny(ruleDataSet.wifiList, SPECIAL_UID_TETHER) || containsUidOrAny(ruleDataSet.tetherList, SPECIAL_UID_TETHER)) {
// DHCP replies to USB tethered client
addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-usb-tether", "-p udp --sport=67 --dport=68" + action);
- // DNS replies to USB tethered client
+ addTetherDhcpReplyRule(cmds, chainName + "-usb-tether", action);
+ // DNS replies to USB tethered client
addRuleForUsers(cmds, users_dns, "-A " + chainName + "-usb-tether", "-p udp --sport=53" + action);
addRuleForUsers(cmds, users_dns, "-A " + chainName + "-usb-tether", "-p tcp --sport=53" + action);
}
if (containsUidOrAny(ruleDataSet.tetherList, SPECIAL_UID_TETHER)) {
// DHCP replies to client
addRuleForUsers(cmds, users_dhcp, "-A " + chainName + "-tether", "-p udp --sport=67 --dport=68" + action);
+ addTetherDhcpReplyRule(cmds, chainName + "-tether", action);
// DNS replies to client
addRuleForUsers(cmds, users_dns, "-A " + chainName + "-tether", "-p udp --sport=53" + action);
addRuleForUsers(cmds, users_dns, "-A " + chainName + "-tether", "-p tcp --sport=53" + action);
@@ -878,7 +1337,7 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul
// on the LAN - use specific DNS servers instead of opening to all LAN hosts
if (whitelist) {
// Add rules for specific DNS servers instead of all LAN hosts
- addDnsServerRules(cmds, cfg, chainName + "-wifi-lan", false);
+ addDnsServerRules(cmds, cfg, chainName + "-wifi-lan", ipv6);
// Fallback: if no specific DNS servers found, use the old broad rule
if (cfg.dnsServersV4.isEmpty() && cfg.dnsServersV6.isEmpty()) {
@@ -909,16 +1368,19 @@ private static boolean applyIptablesRulesImpl(final Context ctx, RuleDataSet rul
addRulesForUidlist(cmds, ruleDataSet.lanList, chainName + "-wifi-lan", whitelist);
addRulesForUidlist(cmds, ruleDataSet.vpnList, chainName + "-vpn", whitelist);
addRulesForUidlist(cmds, ruleDataSet.tetherList, chainName + "-tether", whitelist);
- if (G.enableTor()) {
- addTorRules(cmds, ruleDataSet.torList, whitelist, ipv6, chainName);
- }
+
cmds.add("-P OUTPUT ACCEPT");
} catch (Exception e) {
- Log.e(e.getClass().getName(), e.getMessage(), e);
+ // Never run a partially built rule set: it would flush the chains, set OUTPUT to DROP
+ // and stop before the per-app rules, while being reported as a success.
+ Log.e(TAG, "Unable to build " + (ipv6 ? "IPv6" : "IPv4") + " rules", e);
+ return false;
}
+ if (rawOut != null) {
+ rawOut.addAll(cmds);
+ }
iptablesCommands(cmds, out, ipv6);
- Log.i(TAG, "Total # of rules for " + (ipv6 ? "v6": "v4") + " " + cmds.size());
return true;
}
@@ -943,8 +1405,10 @@ private static void iptablesCommands(List in, List out, boolean
String ipPath = getBinaryPath(G.ctx, ipv6);
String waitTime = "";
- if(G.ip_path().equals("system") && G.addDelay()) {
- waitTime = " -w 1";
+ if(G.ip_path().equals("system")) {
+ // Always use wait flag with system iptables to prevent lock contention
+ // (in the form this iptables version supports)
+ waitTime = IptablesVersion.waitOption(iptablesVersion(ipPath));
}
boolean firstLit = true;
for (String s : in) {
@@ -959,7 +1423,9 @@ private static void iptablesCommands(List in, List out, boolean
+ "export IPV6=" + (ipv6 ? "1" : "0") + "; "
+ "true");
}
- out.add(s.replaceFirst("^#LITERAL# ", ""));
+ // custom script line: a failure is reported, but no longer aborts the whole apply
+ // (e.g. an IPv4-only line in the IPv6 pass, #1493)
+ out.add("#WARN# " + s.replaceFirst("^#LITERAL# ", ""));
} else if (s.matches("#NOCHK# .*")) {
out.add(s.replaceFirst("^#NOCHK# ", "#NOCHK# " + ipPath + " "));
} else {
@@ -1004,143 +1470,430 @@ public static void waitAndTerminate(ExecutorService executorService) {
}
}
- public static void applySavedIptablesRules(Context ctx, boolean showErrors, RootCommand callback) {
- synchronized (GLOBAL_STATUS_LOCK) {
- if(!globalStatus) {
- Log.i(TAG, "Using applySavedIptablesRules");
- globalStatus = true;
-
+ private static void completeRootCommandFailure(Context ctx, RootCommand callback, String command, Throwable throwable) {
+ if (callback == null || callback.done) {
+ return;
+ }
+ callback.lastCommand = command;
+ if (throwable != null && throwable.getMessage() != null) {
+ callback.lastCommandResult = new StringBuilder(throwable.getMessage());
+ }
+ deliverRootCommandResult(ctx, callback, 1, true);
+ }
+
+ /**
+ * Complete a RootCommand that was not itself run on the root shell: record the result,
+ * invoke its callback and show its toast, the same way the shell does for submitted commands.
+ */
+ private static void deliverRootCommandResult(Context ctx, RootCommand target, int exitCode, boolean showToast) {
+ target.exitCode = exitCode;
+ target.done = true;
+ try {
+ if (target.cb != null) {
+ target.cb.cbFunc(target);
+ }
+ } catch (Throwable t) {
+ Log.e(TAG, "RootCommand callback failed: " + android.util.Log.getStackTraceString(t));
+ }
+ if (showToast && ctx != null) {
+ int toast = exitCode == 0 ? target.successToast : target.failureToast;
+ if (toast != RootShellService.NO_TOAST) {
+ sendToastBroadcast(ctx.getApplicationContext(), ctx.getString(toast));
+ }
+ }
+ }
+
+ // Apply requests that arrive while an apply is running are coalesced into a single full apply
+ // that starts as soon as the current one finishes (it reads the latest saved rules). Each
+ // request's callback receives that apply's result. Guarded by GLOBAL_STATUS_LOCK.
+ private static final List pendingApplyCallbacks = new ArrayList<>();
+ // purgeGeneration at the time each pending request was queued (same order as above)
+ private static final List pendingApplyGenerations = new ArrayList<>();
+ private static boolean pendingApplyShowErrors;
+ // Incremented by purgeIptables(). A queued apply requested before a later purge (e.g. the
+ // firewall was disabled meanwhile) is dropped instead of re-adding rules after the purge.
+ private static long purgeGeneration;
+ private static final java.util.concurrent.atomic.AtomicInteger disablesInProgress =
+ new java.util.concurrent.atomic.AtomicInteger();
+ // Incremented on every connectivity/tether broadcast, including ones ignored because the
+ // firewall is still being enabled, so an apply built on a stale network config is detected.
+ private static final java.util.concurrent.atomic.AtomicLong networkChangeSeq =
+ new java.util.concurrent.atomic.AtomicLong();
+
+ public static void noteNetworkChange() {
+ networkChangeSeq.incrementAndGet();
+ }
+
+ // Wrap the caller-supplied callback so that globalStatus and the up-to-date flag are only
+ // reset once the entire (IPv4 + IPv6) command sequence has actually finished.
+ private static RootCommand wrapApplyCompletionCallback(Context ctx, RootCommand callback) {
+ final Context appCtx = ctx != null ? ctx.getApplicationContext() : G.getContext();
+ // the rules are built right after this from the current network config
+ final long networkSeqAtBuild = networkChangeSeq.get();
+ final RootCommand completionCallback = callback == null ? new RootCommand() : callback;
+ final RootCommand.Callback originalCallback = completionCallback.cb;
+ completionCallback.setCallback(new RootCommand.Callback() {
+ @Override
+ public void cbFunc(RootCommand state) {
+ // before the callbacks: they may post the error notification, which shows the details
+ Notifications.recordApplyResult(state);
try {
- RuleDataSet dataSet = getDataSet();
- List ipv4cmds = new ArrayList<>();
- List ipv6cmds = new ArrayList<>();
-
- // Create thread-safe chain name for this execution
- final String chainName = getThreadSafeChainName();
-
- // Apply IPv4 rules first (sequentially)
- try {
- Log.i(TAG, "Applying IPv4 rules");
- applyIptablesRulesImpl(ctx, dataSet, showErrors, ipv4cmds, false, chainName);
- applySavedIp4tablesRules(ctx, ipv4cmds, callback);
- Log.i(TAG, "Successfully applied IPv4 rules");
- } catch (Exception e) {
- Log.e(TAG, "Error applying IPv4 rules", e);
- throw new RuntimeException(e);
+ if (originalCallback != null) {
+ originalCallback.cbFunc(state);
}
-
- // Apply IPv6 rules second (sequentially after IPv4)
- if (G.enableIPv6()) {
- try {
- Log.i(TAG, "Applying IPv6 rules");
- applyIptablesRulesImpl(ctx, dataSet, showErrors, ipv6cmds, true, chainName);
- applySavedIp6tablesRules(ctx, ipv6cmds, new RootCommand());
- Log.i(TAG, "Successfully applied IPv6 rules");
- } catch (Exception e) {
- Log.e(TAG, "Error applying IPv6 rules", e);
- throw new RuntimeException(e);
- }
+ } finally {
+ onApplyFinished(appCtx, state.exitCode == 0, networkSeqAtBuild);
+ Notifications.onApplyFinished(appCtx, state.exitCode == 0);
+ if (!state.warnings.isEmpty() && appCtx != null) {
+ customScriptWarningNotification(appCtx, new ArrayList<>(state.warnings));
}
-
- Log.i(TAG, "Successfully applied all firewall rules");
+ }
+ }
+ });
+ return completionCallback;
+ }
- } catch (Exception e) {
- Log.e(TAG, "Error applying rules", e);
- } finally {
- globalStatus = false;
- setRulesUpToDate(true);
+ private static void onApplyFinished(Context ctx, boolean success, long networkSeqAtBuild) {
+ final List pending = new ArrayList<>();
+ final List overtaken = new ArrayList<>();
+ final Context pendingCtx;
+ final boolean pendingShowErrors;
+ synchronized (GLOBAL_STATUS_LOCK) {
+ globalStatus = false;
+ setRulesUpToDate(success);
+ for (int i = 0; i < pendingApplyCallbacks.size(); i++) {
+ if (pendingApplyGenerations.get(i) < purgeGeneration) {
+ overtaken.add(pendingApplyCallbacks.get(i));
+ } else {
+ pending.add(pendingApplyCallbacks.get(i));
}
- } else {
- Log.i(TAG, "ignore applySavedIptablesRules as existing thread running");
}
+ pendingApplyCallbacks.clear();
+ pendingApplyGenerations.clear();
+ pendingCtx = ctx != null ? ctx : G.getContext();
+ pendingShowErrors = pendingApplyShowErrors;
+ pendingApplyShowErrors = false;
+ }
+
+ if (!success && ctx != null) {
+ // A failed script can stop right after "-P OUTPUT DROP" and leave the device without
+ // any network. Put the chain policies back to what the user configured.
+ Log.w(TAG, "Rule apply failed; restoring configured default chain policies");
+ applyDefaultChains(ctx, new RootCommand());
+ }
+
+ if (success && pending.isEmpty() && ctx != null && networkChangeSeq.get() != networkSeqAtBuild) {
+ // The network changed while these rules were being applied (e.g. while enabling, when
+ // the change is ignored); re-check the interface routing once things have settled.
+ Log.i(TAG, "Network changed during rule apply; re-checking interface rules");
+ NetworkChangeDebouncer.scheduleNetworkChange(ctx, InterfaceTracker.CONNECTIVITY_CHANGE);
+ }
+
+ if (!overtaken.isEmpty()) {
+ Log.i(TAG, "Dropping " + overtaken.size() + " queued apply request(s) superseded by a purge");
+ for (RootCommand request : overtaken) {
+ deliverRootCommandResult(pendingCtx, request, 0, false);
+ }
+ }
+ if (pending.isEmpty()) {
+ return;
}
+ Log.i(TAG, "Running " + pending.size() + " apply request(s) queued during the previous apply");
+ applySavedIptablesRules(pendingCtx, pendingShowErrors, new RootCommand()
+ .setCallback(new RootCommand.Callback() {
+ @Override
+ public void cbFunc(RootCommand state) {
+ for (RootCommand request : pending) {
+ request.lastCommand = state.lastCommand;
+ request.lastCommandResult = state.lastCommandResult;
+ deliverRootCommandResult(pendingCtx, request, state.exitCode, true);
+ }
+ }
+ }));
}
+ private static RootCommand newApplyPart(RootCommand parent, List cmds, boolean ipv6) {
+ RootCommand part = new RootCommand()
+ .setRetryExitCode(IPTABLES_TRY_AGAIN)
+ .setReopenShell(parent.reopenShell);
+ part.res = parent.res;
+ part.isv6 = ipv6;
+ part.setCommmands(cmds);
+ return part;
+ }
- private static RuleDataSet getDataSet() {
- initSpecial();
+ // "iptables --version" output per binary path
+ private static final Map iptablesVersions = new java.util.concurrent.ConcurrentHashMap<>();
- final String savedPkg_wifi_uid = G.pPrefs.getString(PREF_WIFI_PKG_UIDS, "");
- final String savedPkg_3g_uid = G.pPrefs.getString(PREF_3G_PKG_UIDS, "");
- final String savedPkg_roam_uid = G.pPrefs.getString(PREF_ROAMING_PKG_UIDS, "");
- final String savedPkg_vpn_uid = G.pPrefs.getString(PREF_VPN_PKG_UIDS, "");
- final String savedPkg_tether_uid = G.pPrefs.getString(PREF_TETHER_PKG_UIDS, "");
- final String savedPkg_lan_uid = G.pPrefs.getString(PREF_LAN_PKG_UIDS, "");
- final String savedPkg_tor_uid = G.pPrefs.getString(PREF_TOR_PKG_UIDS, "");
+ /**
+ * @return "iptables --version" output of the binary (cached), or null if it can't be run
+ */
+ static String iptablesVersion(String binPath) {
+ if (binPath == null) {
+ return null;
+ }
+ String cached = iptablesVersions.get(binPath);
+ if (cached != null) {
+ return cached.isEmpty() ? null : cached;
+ }
+ String version = "";
+ try {
+ Process process = new ProcessBuilder(binPath, "--version").redirectErrorStream(true).start();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ String line = reader.readLine();
+ if (line != null) {
+ version = line.trim();
+ }
+ }
+ process.waitFor();
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to read the version of " + binPath + ": " + e.getMessage());
+ }
+ Log.i(TAG, binPath + " version: " + (version.isEmpty() ? "unknown" : version));
+ iptablesVersions.put(binPath, version);
+ return version.isEmpty() ? null : version;
+ }
- return new RuleDataSet(getListFromPref(savedPkg_wifi_uid),
- getListFromPref(savedPkg_3g_uid),
- getListFromPref(savedPkg_roam_uid),
- getListFromPref(savedPkg_vpn_uid),
- getListFromPref(savedPkg_tether_uid),
- getListFromPref(savedPkg_lan_uid),
- getListFromPref(savedPkg_tor_uid));
+ private static final String RESTORE_SUFFIX = "-restore";
+ // iptables-restore failed for this family (0 = IPv4, 1 = IPv6) in this process; use the
+ // per-command script from now on.
+ private static final boolean[] restoreUnavailable = new boolean[2];
+ /**
+ * Commands that load a full rule set with one iptables-restore (see
+ * {@link IptablesRestorePlanner}) followed by the few built-in chain commands, or null when the
+ * per-command script has to be used.
+ */
+ private static List buildRestoreCommands(Context ctx, List rawCmds, boolean ipv6) {
+ if (restoreUnavailable[ipv6 ? 1 : 0]) {
+ return null;
+ }
+ IptablesRestorePlanner.Plan plan = IptablesRestorePlanner.plan(rawCmds);
+ if (plan == null) {
+ Log.i(TAG, "Rules can't be loaded with iptables-restore (e.g. custom script); applying one by one");
+ return null;
+ }
+ String restoreBin = getRestoreBinary(ctx, ipv6);
+ if (restoreBin == null) {
+ restoreUnavailable[ipv6 ? 1 : 0] = true;
+ Log.i(TAG, "No iptables-restore available for " + (ipv6 ? "IPv6" : "IPv4") + "; applying rules one by one");
+ return null;
+ }
+ File file = new File(ctx.getFilesDir(), ipv6 ? "rules6.restore" : "rules4.restore");
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ fos.write(plan.restoreInput.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to write " + file, e);
+ return null;
+ }
+ Log.i(TAG, "Loading " + (ipv6 ? "IPv6" : "IPv4") + " rules with " + restoreBin);
+ List out = new ArrayList<>();
+ String ipPath = getBinaryPath(ctx, ipv6);
+ out.add(restoreBin + IptablesVersion.restoreWaitOption(iptablesVersion(ipPath))
+ + " --noflush " + file.getAbsolutePath());
+ iptablesCommands(plan.postCommands, out, ipv6);
+ return out;
}
/**
- * Purge and re-add all saved rules (not in-memory ones).
- * This is much faster than just calling "applyIptablesRules", since it don't need to read installed applications.
- *
- * @param ctx application context (mandatory)
- * @param callback If non-null, use a callback instead of blocking the current thread
+ * @return iptables-restore / ip6tables-restore matching the configured iptables binary, or null
*/
- public static boolean applySavedIp4tablesRules(Context ctx, List cmds, RootCommand callback) {
- if (ctx == null) {
- return false;
+ private static String getRestoreBinary(Context ctx, boolean ipv6) {
+ String ipPath = getBinaryPath(ctx, ipv6);
+ if (ipPath == null || !ipPath.startsWith("/")) {
+ return null;
}
- try {
- Log.i(TAG, "Using applySaved4IptablesRules");
- callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, cmds);
- return true;
- } catch (Exception e) {
- Log.d(TAG, "Exception while applying rules: " + e.getMessage());
- applyDefaultChains(ctx, callback);
- return false;
+ File restore = new File(ipPath + RESTORE_SUFFIX);
+ String builtinDir = ctx.getDir("bin", 0).getAbsolutePath() + "/";
+ if (ipPath.startsWith(builtinDir) && !restore.exists()) {
+ // The bundled binary is a multi-call xtables binary that runs as iptables-restore when
+ // invoked under that name.
+ try {
+ android.system.Os.symlink(new File(ipPath).getName(), restore.getAbsolutePath());
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to create " + restore + ": " + e.getMessage());
+ return null;
+ }
}
+ return restore.exists() ? restore.getAbsolutePath() : null;
}
+ /**
+ * Run the IPv4/IPv6 parts of an apply back to back and report one combined result to
+ * {@code parent}. If a part fails, the remaining parts are skipped. A part that was loaded
+ * with iptables-restore and whose restore failed is re-run with its per-command fallback.
+ */
+ private static void runApplyParts(final Context ctx, final RootCommand parent, final List parts,
+ final List> fallbacks) {
+ final int[] remaining = {parts.size()};
+ final RootCommand[] failed = {null};
+ for (int i = 0; i < parts.size(); i++) {
+ final int index = i;
+ parts.get(i).setCallback(new RootCommand.Callback() {
+ @Override
+ public void cbFunc(RootCommand state) {
+ // parts run one after another on the root shell thread, so no locking needed
+ List fallback = fallbacks.get(index);
+ if (state.exitCode != 0 && fallback != null && state.lastCommand != null
+ && state.lastCommand.contains(RESTORE_SUFFIX + " ")) {
+ // The restore itself failed; each table commits atomically, so nothing half
+ // done was left behind. Don't try it again this session for this family.
+ restoreUnavailable[state.isv6 ? 1 : 0] = true;
+ Log.w(TAG, "iptables-restore failed (exit " + state.exitCode + "): "
+ + state.lastCommandResult + "; applying " + (state.isv6 ? "IPv6" : "IPv4")
+ + " rules one by one");
+ fallbacks.set(index, null);
+ RootCommand retry = newApplyPart(parent, fallback, state.isv6);
+ retry.setCallback(this);
+ parts.set(index, retry);
+ RootCommand.runAll(ctx, java.util.Collections.singletonList(retry));
+ return;
+ }
+ parent.warnings.addAll(state.warnings);
+ if (state.exitCode != 0 && failed[0] == null) {
+ failed[0] = state;
+ for (int j = index + 1; j < parts.size(); j++) {
+ parts.get(j).getCommmands().clear();
+ }
+ }
+ if (--remaining[0] == 0) {
+ RootCommand result = failed[0] != null ? failed[0] : state;
+ parent.lastCommand = result.lastCommand;
+ parent.lastCommandResult = result.lastCommandResult;
+ deliverRootCommandResult(ctx, parent, result.exitCode, true);
+ }
+ }
+ });
+ }
+ RootCommand.runAll(ctx, parts);
+ }
- public static boolean applySavedIp6tablesRules(Context ctx, List cmds, RootCommand callback) {
- if (ctx == null) {
- return false;
+ public static void applySavedIptablesRules(Context ctx, boolean showErrors, RootCommand callback) {
+ synchronized (GLOBAL_STATUS_LOCK) {
+ if (globalStatus) {
+ // Don't report "busy" as a failure: callers treat failures as a broken firewall.
+ Log.i(TAG, "Apply already in progress; queued to run after it finishes");
+ pendingApplyCallbacks.add(callback != null ? callback : new RootCommand());
+ pendingApplyGenerations.add(purgeGeneration);
+ pendingApplyShowErrors |= showErrors;
+ return;
+ }
+ globalStatus = true;
}
+ // status notification: "applying rules"
+ Notifications.onApplyStarted(ctx);
+
+ final RootCommand completionCallback = wrapApplyCompletionCallback(ctx, callback);
try {
- Log.i(TAG, "Using applySavedIp6tablesRules");
- callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, cmds,true);
- return true;
+ Log.i(TAG, "Starting full firewall rules apply");
+ RuleDataSet dataSet = getDataSet();
+ // Create thread-safe chain name for this execution
+ final String chainName = getThreadSafeChainName();
+
+ // Build both families up front and queue them together: IPv6 still runs after IPv4,
+ // but nothing else (e.g. a purge) can run in between.
+ // Each family is loaded with one iptables-restore when possible (atomic, and far faster
+ // than one full table rewrite per command); the per-command script is the fallback.
+ List parts = new ArrayList<>();
+ List> fallbacks = new ArrayList<>();
+ List ipv4cmds = new ArrayList<>();
+ List ipv4raw = new ArrayList<>();
+ if (!applyIptablesRulesImpl(ctx, dataSet, showErrors, ipv4cmds, false, chainName, ipv4raw)) {
+ completeRootCommandFailure(ctx, completionCallback, "build IPv4 rules", null);
+ return;
+ }
+ List ipv4restore = buildRestoreCommands(ctx, ipv4raw, false);
+ parts.add(newApplyPart(completionCallback, ipv4restore != null ? ipv4restore : ipv4cmds, false));
+ fallbacks.add(ipv4restore != null ? ipv4cmds : null);
+ if (G.enableIPv6()) {
+ List ipv6cmds = new ArrayList<>();
+ List ipv6raw = new ArrayList<>();
+ if (!applyIptablesRulesImpl(ctx, dataSet, showErrors, ipv6cmds, true, chainName, ipv6raw)) {
+ completeRootCommandFailure(ctx, completionCallback, "build IPv6 rules", null);
+ return;
+ }
+ List ipv6restore = buildRestoreCommands(ctx, ipv6raw, true);
+ parts.add(newApplyPart(completionCallback, ipv6restore != null ? ipv6restore : ipv6cmds, true));
+ fallbacks.add(ipv6restore != null ? ipv6cmds : null);
+ }
+ runApplyParts(ctx, completionCallback, parts, fallbacks);
+ Log.i(TAG, "Submitted firewall rule command sequence");
} catch (Exception e) {
- Log.d(TAG, "Exception while applying rules: " + e.getMessage());
- applyDefaultChains(ctx, callback);
- return false;
+ Log.e(TAG, "Error applying rules", e);
+ completeRootCommandFailure(ctx, completionCallback, "applySavedIptablesRules", e);
}
}
+ private static RuleDataSet getDataSet() {
+ initSpecial();
+
+ final String savedPkg_wifi_uid = G.pPrefs.getString(PREF_WIFI_PKG_UIDS, "");
+ final String savedPkg_3g_uid = G.pPrefs.getString(PREF_3G_PKG_UIDS, "");
+ final String savedPkg_roam_uid = G.pPrefs.getString(PREF_ROAMING_PKG_UIDS, "");
+ final String savedPkg_vpn_uid = G.pPrefs.getString(PREF_VPN_PKG_UIDS, "");
+ final String savedPkg_tether_uid = G.pPrefs.getString(PREF_TETHER_PKG_UIDS, "");
+ final String savedPkg_lan_uid = G.pPrefs.getString(PREF_LAN_PKG_UIDS, "");
+ final String savedPkg_tor_uid = G.pPrefs.getString(PREF_TOR_PKG_UIDS, "");
+
+
+ List wifiList = getListFromPref(savedPkg_wifi_uid);
+ List dataList = getListFromPref(savedPkg_3g_uid);
+
+
+ // Warn if no applications are configured - this means no blocking will occur
+ if (wifiList.isEmpty() && dataList.isEmpty()) {
+ Log.w(TAG, "WARNING: No applications configured for firewall rules - firewall will not block any traffic!");
+ Log.w(TAG, "Please configure applications in AFWall+ main screen and apply rules.");
+ }
+
+ return new RuleDataSet(wifiList,
+ dataList,
+ getListFromPref(savedPkg_roam_uid),
+ getListFromPref(savedPkg_vpn_uid),
+ getListFromPref(savedPkg_tether_uid),
+ getListFromPref(savedPkg_lan_uid),
+ getListFromPref(savedPkg_tor_uid));
+
+ }
+
+ /**
+ * Re-apply only the interface routing rules after a network change. Falls back to a full apply
+ * when the rules are not known to be up to date, or when another apply is in progress (the
+ * queued full apply then picks up the new network state).
+ */
public static boolean fastApply(Context ctx, RootCommand callback) {
+ boolean fullApply;
+ synchronized (GLOBAL_STATUS_LOCK) {
+ fullApply = globalStatus || !getRulesUpToDate();
+ if (!fullApply) {
+ globalStatus = true;
+ }
+ }
+ if (fullApply) {
+ Log.i(TAG, "Using full Apply");
+ applySavedIptablesRules(ctx, true, callback);
+ return true;
+ }
+
+ final RootCommand completionCallback = wrapApplyCompletionCallback(ctx, callback);
try {
- if (!getRulesUpToDate()) {
- Log.i(TAG, "Using full Apply");
- applySavedIptablesRules(ctx, true, callback);
- } else {
- Log.i(TAG, "Using fastApply");
- List out = new ArrayList();
- List cmds;
- cmds = new ArrayList();
- applyShortRules(ctx, cmds, false);
- iptablesCommands(cmds, out, false);
- if (G.enableIPv6()) {
- cmds = new ArrayList();
- applyShortRules(ctx, cmds, true);
- iptablesCommands(cmds, out, true);
- }
- callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out);
+ Log.i(TAG, "Using fastApply");
+ List out = new ArrayList();
+ List cmds = new ArrayList();
+ applyShortRules(ctx, cmds, false);
+ iptablesCommands(cmds, out, false);
+ if (G.enableIPv6()) {
+ cmds = new ArrayList();
+ applyShortRules(ctx, cmds, true);
+ iptablesCommands(cmds, out, true);
}
+ // up-to-date flag is set from the real result by the completion callback
+ completionCallback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out);
} catch (Exception e) {
- Log.d(TAG, "Exception while applying rules: " + e.getMessage());
- applyDefaultChains(ctx, callback);
+ Log.e(TAG, "Exception in fastApply: " + e.getMessage(), e);
+ completeRootCommandFailure(ctx, completionCallback, "fastApply", e);
}
- setRulesUpToDate(true);
return true;
}
@@ -1214,24 +1967,37 @@ public static RuleDataSet generateRules(Context ctx, List apps,
}
}
- String wifi = android.text.TextUtils.join("|", newpkg_wifi);
- String data = android.text.TextUtils.join("|", newpkg_3g);
- String roam = android.text.TextUtils.join("|", newpkg_roam);
- String vpn = android.text.TextUtils.join("|", newpkg_vpn);
- String tether = android.text.TextUtils.join("|", newpkg_tether);
- String lan = android.text.TextUtils.join("|", newpkg_lan);
- String tor = android.text.TextUtils.join("|", newpkg_tor);
// save the new list of UIDs
if (store) {
+ // Only the apps in the list change; UIDs not shown there (other profiles with dual
+ // apps off, apps without INTERNET with "show all apps" off, ...) keep their rules,
+ // and so do the lists of connection types that are switched off. Rebuilding the
+ // lists from the shown apps alone deleted all of those.
+ Set shown = new HashSet<>();
+ for (PackageInfoData app : apps) {
+ if (app != null) {
+ shown.add(app.uid);
+ }
+ }
SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
Editor edit = prefs.edit();
- edit.putString(PREF_WIFI_PKG_UIDS, wifi);
- edit.putString(PREF_3G_PKG_UIDS, data);
- edit.putString(PREF_ROAMING_PKG_UIDS, roam);
- edit.putString(PREF_VPN_PKG_UIDS, vpn);
- edit.putString(PREF_TETHER_PKG_UIDS, tether);
- edit.putString(PREF_LAN_PKG_UIDS, lan);
- edit.putString(PREF_TOR_PKG_UIDS, tor);
+ edit.putString(PREF_WIFI_PKG_UIDS, UidListParser.merge(prefs.getString(PREF_WIFI_PKG_UIDS, ""), shown, newpkg_wifi));
+ edit.putString(PREF_3G_PKG_UIDS, UidListParser.merge(prefs.getString(PREF_3G_PKG_UIDS, ""), shown, newpkg_3g));
+ if (G.enableRoam()) {
+ edit.putString(PREF_ROAMING_PKG_UIDS, UidListParser.merge(prefs.getString(PREF_ROAMING_PKG_UIDS, ""), shown, newpkg_roam));
+ }
+ if (G.enableVPN()) {
+ edit.putString(PREF_VPN_PKG_UIDS, UidListParser.merge(prefs.getString(PREF_VPN_PKG_UIDS, ""), shown, newpkg_vpn));
+ }
+ if (G.enableTether()) {
+ edit.putString(PREF_TETHER_PKG_UIDS, UidListParser.merge(prefs.getString(PREF_TETHER_PKG_UIDS, ""), shown, newpkg_tether));
+ }
+ if (G.enableLAN()) {
+ edit.putString(PREF_LAN_PKG_UIDS, UidListParser.merge(prefs.getString(PREF_LAN_PKG_UIDS, ""), shown, newpkg_lan));
+ }
+ if (G.enableTor()) {
+ edit.putString(PREF_TOR_PKG_UIDS, UidListParser.merge(prefs.getString(PREF_TOR_PKG_UIDS, ""), shown, newpkg_tor));
+ }
edit.apply();
} else {
dataSet = new RuleDataSet(new ArrayList<>(newpkg_wifi),
@@ -1256,21 +2022,59 @@ public static RuleDataSet generateRules(Context ctx, List apps,
* @return true if the rules were purged
*/
public static void purgeIptables(Context ctx, boolean showErrors, RootCommand callback) {
+ synchronized (GLOBAL_STATUS_LOCK) {
+ purgeGeneration++;
+ }
+ // Until the purge's callback has updated the enabled flag, automatic applies (boot,
+ // network change) must not re-add the rules based on the old "enabled" state.
+ final RootCommand purgeCallback = callback != null ? callback : new RootCommand();
+ final RootCommand.Callback originalCallback = purgeCallback.cb;
+ disablesInProgress.incrementAndGet();
+ purgeCallback.setCallback(new RootCommand.Callback() {
+ @Override
+ public void cbFunc(RootCommand state) {
+ try {
+ if (originalCallback != null) {
+ originalCallback.cbFunc(state);
+ }
+ } finally {
+ disablesInProgress.decrementAndGet();
+ }
+ }
+ });
+ try {
+ submitPurge(ctx, purgeCallback);
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to build purge commands", e);
+ completeRootCommandFailure(ctx, purgeCallback, "purgeIptables", e);
+ }
+ }
+
+ /**
+ * @return true while a purge (firewall disable) has been requested but not finished yet
+ */
+ public static boolean isDisableInProgress() {
+ return disablesInProgress.get() > 0;
+ }
+
+ private static void submitPurge(Context ctx, RootCommand callback) {
String chainName = getThreadSafeChainName();
List cmds = new ArrayList<>();
List cmdsv4 = new ArrayList<>();
List out = new ArrayList<>();
+ // The chains don't exist if the rules were never applied (or after a reboot before the
+ // first apply); a missing chain must not make the purge fail.
for (String s : staticChains) {
- cmds.add("-F " + chainName + s);
+ cmds.add("#NOCHK# -F " + chainName + s);
}
for (String s : dynChains) {
- cmds.add("-F " + chainName + s);
+ cmds.add("#NOCHK# -F " + chainName + s);
}
if (G.enableTor()) {
for (String s : natChains) {
- cmdsv4.add("-t nat -F " + chainName + s);
+ cmdsv4.add("#NOCHK# -t nat -F " + chainName + s);
}
cmdsv4.add("#NOCHK# -t nat -D OUTPUT -j " + chainName);
} else {
@@ -1284,13 +2088,54 @@ public static void purgeIptables(Context ctx, boolean showErrors, RootCommand ca
//cmds.add("-D OUTPUT -j " + chainName);
if (G.enableInbound()) {
- cmds.add("-D INPUT -j " + chainName + "-input");
+ cmds.add("#NOCHK# -D INPUT -j " + chainName + "-input");
}
addCustomRules(Api.PREF_CUSTOMSCRIPT2, cmds);
-
+
+ // Execute the purge commands and call the callback
+ Log.i(TAG, "Executing purge commands for IPv4");
+ cmds.addAll(cmdsv4);
+ iptablesCommands(cmds, out, false);
+
+ if (G.enableIPv6()) {
+ Log.i(TAG, "Executing purge commands for IPv6");
+ List cmdsv6 = new ArrayList<>();
+ for (String s : staticChains) {
+ cmdsv6.add("#NOCHK# -F " + chainName + s);
+ }
+ for (String s : dynChains) {
+ cmdsv6.add("#NOCHK# -F " + chainName + s);
+ }
+ cmdsv6.add("#NOCHK# -D OUTPUT -j " + chainName);
+ cmdsv6.add("-P OUTPUT ACCEPT");
+ if (G.enableInbound()) {
+ cmdsv6.add("#NOCHK# -D INPUT -j " + chainName + "-input");
+ }
+ iptablesCommands(cmdsv6, out, true);
+ }
+
+ Log.i(TAG, "Submitted purge commands");
+ callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out);
}
+ private static void addTetherDhcpReplyRule(List cmds, String chain, String action) {
+ // dnsmasq can run under device-specific app UIDs, so the special tethering entry
+ // must allow DHCP replies by port instead of relying only on a fixed UID list.
+ cmds.add("-A " + chain + " -p udp --sport=67 --dport=68" + action);
+ }
+
+ private static void addIpv6ControlTrafficRules(List cmds, String chainName) {
+ // IPv6 connectivity depends on router and neighbor discovery before app UID rules match.
+ String[] icmpv6Types = {"133", "134", "135", "136"};
+ for (String type : icmpv6Types) {
+ cmds.add("-A " + chainName + " -p ipv6-icmp --icmpv6-type " + type + " -j RETURN");
+ if (G.enableInbound()) {
+ cmds.add("-A " + chainName + "-input -p ipv6-icmp --icmpv6-type " + type + " -j RETURN");
+ }
+ }
+ }
+
/**
* Add DNS-specific iptables rules for identified DNS servers instead of broad LAN access
*/
@@ -1390,8 +2235,12 @@ public static void updateLogRules(Context ctx, RootCommand callback) {
}
String chainName = getThreadSafeChainName();
List cmds = new ArrayList();
- cmds.add("#NOCHK# -N " + chainName + "-reject");
- cmds.add("-F " + chainName + "-reject");
+ // addRejectRules() fills all reject chains, so all of them have to be flushed first;
+ // flushing only the main one piled up NFLOG/REJECT pairs on every log setting change.
+ for (String suffix : REJECT_CHAIN_SUFFIXES) {
+ cmds.add("#NOCHK# -N " + chainName + suffix);
+ cmds.add("-F " + chainName + suffix);
+ }
addRejectRules(cmds, chainName);
apply46(ctx, cmds, callback);
}
@@ -1437,60 +2286,51 @@ public static List fetchLogs() {
* @param callback Callback for completion status
*/
public static void runIfconfig(Context ctx, RootCommand callback) {
- // Android 16+ fallback: try system ifconfig first, then busybox
- if (Build.VERSION.SDK_INT >= 35) { // Android 16+
- callback.run(ctx, "ifconfig -a || " + getBusyBoxPath(ctx, true) + " ifconfig -a");
- } else {
- callback.run(ctx, getBusyBoxPath(ctx, true) + " ifconfig -a");
- }
+ // Try system ifconfig first, then busybox for all versions
+ callback.run(ctx, "ifconfig -a || " + getBusyBoxPath(ctx, true) + " ifconfig -a");
}
public static void runNetworkInterface(Context ctx, RootCommand callback) {
- // Android 16+ fallback: try multiple methods for network interface detection
- if (Build.VERSION.SDK_INT >= 35) { // Android 16+
- // First try Android API method as fallback
- try {
- StringBuilder result = new StringBuilder();
- java.util.Enumeration interfaces = java.net.NetworkInterface.getNetworkInterfaces();
- while (interfaces.hasMoreElements()) {
- java.net.NetworkInterface networkInterface = interfaces.nextElement();
- result.append(networkInterface.getName()).append("\n");
- }
- if (result.length() > 0) {
- // Create a mock RootCommand with API results
- RootCommand apiResult = new RootCommand();
- apiResult.res = result;
- apiResult.exitCode = 0;
- apiResult.done = true;
- if (callback.cb != null) {
- callback.cb.cbFunc(apiResult);
- }
- return;
+ // Try Android API method first for all versions
+ try {
+ StringBuilder result = new StringBuilder();
+ java.util.Enumeration interfaces = java.net.NetworkInterface.getNetworkInterfaces();
+ while (interfaces.hasMoreElements()) {
+ java.net.NetworkInterface networkInterface = interfaces.nextElement();
+ result.append(networkInterface.getName()).append("\n");
+ }
+ if (result.length() > 0) {
+ // Create a mock RootCommand with API results
+ RootCommand apiResult = new RootCommand();
+ apiResult.res = result;
+ apiResult.exitCode = 0;
+ apiResult.done = true;
+ if (callback.cb != null) {
+ callback.cb.cbFunc(apiResult);
}
- } catch (Exception e) {
- Log.d(TAG, "Android API network interface detection failed: " + e.getMessage());
+ return;
}
-
- // Fallback to shell commands
- String cmd = "ls /sys/class/net 2>/dev/null || " +
- getBusyBoxPath(ctx, true) + " ls /sys/class/net 2>/dev/null || " +
- "ip link show 2>/dev/null";
- callback.run(ctx, cmd);
- } else {
- callback.run(ctx, getBusyBoxPath(ctx, true) + " ls /sys/class/net");
+ } catch (Exception e) {
+ Log.d(TAG, "Android API network interface detection failed: " + e.getMessage());
}
+
+ // Fallback to shell commands with multiple options
+ String cmd = "ls /sys/class/net 2>/dev/null || " +
+ getBusyBoxPath(ctx, true) + " ls /sys/class/net 2>/dev/null || " +
+ "ip link show 2>/dev/null";
+ callback.run(ctx, cmd);
}
public static void fixFolderPermissionsAsync(Context mContext) {
AsyncTask.execute(() -> {
try {
- mContext.getFilesDir().setExecutable(true, false);
- mContext.getFilesDir().setReadable(true, false);
+ mContext.getFilesDir().setExecutable(true, true);
+ mContext.getFilesDir().setReadable(true, true);
File sharedPrefsFolder = new File(mContext.getFilesDir().getAbsolutePath()
+ "/../shared_prefs");
- sharedPrefsFolder.setExecutable(true, false);
- sharedPrefsFolder.setReadable(true, false);
+ sharedPrefsFolder.setExecutable(true, true);
+ sharedPrefsFolder.setReadable(true, true);
} catch (Exception e) {
Log.e(Api.TAG, e.getMessage(), e);
}
@@ -1508,6 +2348,12 @@ public static List getApps(Context ctx, GetAppList appList) {
// return cached instance
return applications;
}
+ // system UIDs that use the network get entries (root; rate limited)
+ if (android.os.Looper.myLooper() == android.os.Looper.getMainLooper()) {
+ new Thread(() -> SystemUids.discover(ctx), "AFWall-SystemUids").start();
+ } else {
+ SystemUids.discover(ctx);
+ }
SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
@@ -1567,6 +2413,15 @@ public static List getApps(Context ctx, GetAppList appList) {
}
}
}
+ // getUserProfiles() can leave out profiles (e.g. a Private Space), so also take the
+ // profiles root reports (#1485, #1482)
+ Map profileTypes = G.supportDual() ? getProfileTypes() : new HashMap<>();
+ for (Map.Entry profile : profileTypes.entrySet()) {
+ if (profile.getKey() > 0 && profile.getValue().startsWith("profile.")
+ && !listOfUids.contains(profile.getKey())) {
+ listOfUids.add(profile.getKey());
+ }
+ }
//use pm list packages -f -U --user 10
int pkgManagerFlags = PackageManager.GET_META_DATA;
// it's useless to iterate over uninstalled packages if we don't support multi-profile apps
@@ -1574,7 +2429,14 @@ public static List getApps(Context ctx, GetAppList appList) {
pkgManagerFlags |= PackageManager.GET_UNINSTALLED_PACKAGES;
}
PackageManager pkgmanager = ctx.getPackageManager();
+ // Load the app list purely through PackageManager. The previous root-shell
+ // supplementation ("pm list packages -U" + per-package dumpsys INTERNET checks)
+ // made every scan run dozens of shell round-trips, which dominated load time.
List installed = pkgmanager.getInstalledApplications(pkgManagerFlags);
+ if (appList != null) {
+ appList.doMaxProgress(installed.size());
+ }
+
SparseArray syncMap = new SparseArray<>();
Editor edit = cachePrefs.edit();
boolean changed = false;
@@ -1589,8 +2451,12 @@ public static List getApps(Context ctx, GetAppList appList) {
SparseArray multiUserAppsMap = new SparseArray<>();
HashMap packagesForUser = new HashMap<>();
+ HashMap profileMarkers = new HashMap<>();
+ HashMap internetPermissionCache = new HashMap<>();
if(G.supportDual()) {
- packagesForUser = getPackagesForUser(listOfUids);
+ packagesForUser = getPackagesForUser(listOfUids);
+ profileMarkers = getUserProfileMarkers(listOfUids, profileTypes);
+ lastProfileMarkers = profileMarkers;
}
for (int i = 0; i < installed.size(); i++) {
@@ -1613,7 +2479,12 @@ public static List getApps(Context ctx, GetAppList appList) {
name = prefs.getString(cachekey, "");
if (name.length() == 0 || isRecentlyInstalled(apinfo.packageName)) {
// get label and put on cache
- name = pkgmanager.getApplicationLabel(apinfo).toString();
+ try {
+ name = pkgmanager.getApplicationLabel(apinfo).toString();
+ } catch (Exception e) {
+ // For apps invisible to PackageManager, use package name as label
+ name = apinfo.packageName;
+ }
edit.putString(cachekey, name);
changed = true;
firstseen = true;
@@ -1621,7 +2492,21 @@ public static List getApps(Context ctx, GetAppList appList) {
if (app == null) {
app = new PackageInfoData();
app.uid = apinfo.uid;
- app.installTime = new File(apinfo.sourceDir).lastModified();
+
+ // Handle null sourceDir to prevent NullPointerException
+ if (apinfo.sourceDir != null) {
+ app.installTime = new File(apinfo.sourceDir).lastModified();
+ } else {
+ // Try to get install time from PackageInfo as fallback
+ try {
+ PackageInfo pkgInfo = pkgmanager.getPackageInfo(apinfo.packageName, 0);
+ app.installTime = pkgInfo.firstInstallTime;
+ } catch (PackageManager.NameNotFoundException e) {
+ // Shell-discovered apps invisible to PackageManager — use 0
+ app.installTime = 0;
+ }
+ }
+
app.names = new ArrayList();
app.names.add(name);
app.appinfo = apinfo;
@@ -1663,11 +2548,20 @@ public static List getApps(Context ctx, GetAppList appList) {
app.selected_tor = true;
}
if (G.supportDual()) {
- checkPartOfMultiUser(apinfo, name, listOfUids, packagesForUser, multiUserAppsMap);
+ checkPartOfMultiUser(apinfo, name, listOfUids, packagesForUser, profileMarkers, multiUserAppsMap);
}
}
+ // Headless system apps (CaptivePortalLogin, eSIM/euicc, sync adapters, ...) are invisible
+ // to PackageManager without QUERY_ALL_PACKAGES, so they were missing from the list while
+ // their traffic was blocked (#1476, #1490, #1499). Find them with one root call.
+ addPackagesHiddenFromPackageManager(pkgmanager, installed, syncMap, selected_wifi, selected_3g,
+ selected_roam, selected_vpn, selected_tether, selected_lan, selected_tor);
+
if (G.supportDual()) {
+ addProfileOnlyPackages(pkgmanager, packagesForUser, profileMarkers, syncMap,
+ selected_wifi, selected_3g, selected_roam, selected_vpn,
+ selected_tether, selected_lan, selected_tor, internetPermissionCache);
//run through multi user map
for (int i = 0; i < multiUserAppsMap.size(); i++) {
app = multiUserAppsMap.valueAt(i);
@@ -1746,7 +2640,12 @@ public static List getApps(Context ctx, GetAppList appList) {
} catch (Exception e) {
Log.i(TAG, "Exception in getting app list", e);
}
- return new ArrayList<>();
+ // Never leave the cache null after a run, otherwise UI callers that route a
+ // cold cache to the async loader could loop on a persistent scan failure.
+ if (applications == null) {
+ applications = Collections.synchronizedList(new ArrayList());
+ }
+ return applications;
}
/* public boolean isSuPackage(PackageManager pm, String suPackage) {
@@ -1762,7 +2661,26 @@ public static List getApps(Context ctx, GetAppList appList) {
return found;
}*/
+ /**
+ * @return the special entries plus the system UIDs found to use the network (see SystemUids)
+ */
public static List getSpecialData() {
+ List specialData = getFixedSpecialData();
+ for (Map.Entry e : SystemUids.known().entrySet()) {
+ specialData.add(new PackageInfoData(e.getKey(),
+ "(" + e.getValue() + ") - " + ctx.getString(R.string.system_uid_item),
+ SYSTEM_UID_PKG_PREFIX + e.getKey()));
+ }
+ return specialData;
+ }
+
+ // package name of a discovered system UID's entry; the UID is the same on devices of a vendor
+ public static final String SYSTEM_UID_PKG_PREFIX = "dev.afwall.special.uid";
+
+ /**
+ * @return the fixed special entries (any, kernel, tethering, NTP, mDNS, system accounts)
+ */
+ public static List getFixedSpecialData() {
List specialData = new ArrayList<>();
specialData.add(new PackageInfoData(SPECIAL_UID_ANY, ctx.getString(R.string.all_item), "dev.afwall.special.any"));
specialData.add(new PackageInfoData(SPECIAL_UID_KERNEL, ctx.getString(R.string.kernel_item), "dev.afwall.special.kernel"));
@@ -1789,30 +2707,30 @@ public static List getSpecialData() {
return specialData;
}
- private static void checkPartOfMultiUser(ApplicationInfo apinfo, String name, List uid1, HashMap pkgs, SparseArray syncMap) {
+ private static void checkPartOfMultiUser(ApplicationInfo apinfo, String name, List uid1,
+ HashMap pkgs,
+ HashMap profileMarkers,
+ SparseArray syncMap) {
try {
for (Integer integer : uid1) {
- int appUid = Integer.parseInt(integer + "" + apinfo.uid + "");
- try{
- //String[] pkgs = pkgmanager.getPackagesForUid(appUid);
+ int appUid = UidResolver.createMultiUserUid(integer, UidResolver.getAppId(apinfo.uid));
+ try {
if (packagesExistForUserUid(pkgs, appUid)) {
PackageInfoData app = new PackageInfoData();
app.uid = appUid;
- app.installTime = new File(apinfo.sourceDir).lastModified();
+ app.installTime = getInstallTime(null, apinfo, apinfo.packageName);
app.names = new ArrayList();
- app.names.add(name + "(M)");
+ app.names.add(name + getProfileMarker(profileMarkers, integer));
app.appinfo = apinfo;
if (app.appinfo != null && (app.appinfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
- //user app
app.appType = 1;
} else {
- //system app
app.appType = 0;
}
app.pkgName = apinfo.packageName;
syncMap.put(appUid, app);
}
- }catch (Exception e) {
+ } catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
}
@@ -1821,30 +2739,370 @@ private static void checkPartOfMultiUser(ApplicationInfo apinfo, String name, Li
}
}
- private static boolean packagesExistForUserUid(HashMap pkgs, int appUid) {
- if(pkgs.containsKey(appUid)){
- return true;
+ /**
+ * Add the main user's packages that PackageManager doesn't show us (package visibility), as
+ * listed by root "pm list packages -f -U". They can't be queried through PackageManager; their
+ * label and icon are read from their APK (else they are shown by package name as system apps). Like the visible apps, the ones without INTERNET
+ * are left out unless "show all apps" is enabled.
+ */
+ private static void addPackagesHiddenFromPackageManager(PackageManager pkgmanager,
+ List visible,
+ SparseArray syncMap,
+ List selectedWifi, List selected3g,
+ List selectedRoam, List selectedVpn,
+ List selectedTether, List selectedLan,
+ List selectedTor) {
+ Set visiblePackages = new HashSet<>();
+ for (ApplicationInfo info : visible) {
+ visiblePackages.add(info.packageName);
+ }
+ List out;
+ try {
+ Shell.Result result = Shell.cmd("pm list packages -f -U").exec();
+ if (!result.isSuccess()) {
+ return;
+ }
+ out = result.getOut();
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to list packages hidden from PackageManager: " + e.getMessage());
+ return;
}
- return false;
+ // null = unknown (then nothing is filtered out)
+ Set withInternet = showAllApps() ? null : packagesHoldingInternet();
+ // updated system apps (WebView, ...) have their APK in /data too: ask which are third-party
+ Set thirdParty = new HashSet<>();
+ try {
+ for (String line : Shell.cmd("pm list packages -3").exec().getOut()) {
+ if (line.startsWith("package:")) {
+ thirdParty.add(line.substring("package:".length()).trim());
+ }
+ }
+ } catch (Exception ignored) {
+ }
+ int added = 0;
+ for (String line : out) {
+ ApkInfo.Line parsed = ApkInfo.parse(line);
+ if (parsed == null) {
+ continue;
+ }
+ String pkg = parsed.packageName;
+ if (withInternet != null && !withInternet.contains(pkg)) {
+ continue;
+ }
+ int uid = parsed.uid;
+ // only app UIDs; shared system UIDs (1000, 1073, ...) are listed as special entries
+ if (visiblePackages.contains(pkg) || uid % 100000 < android.os.Process.FIRST_APPLICATION_UID) {
+ continue;
+ }
+ PackageInfoData app = syncMap.get(uid);
+ if (app != null) {
+ if (!app.names.contains(pkg)) {
+ app.names.add(pkg); // another package sharing the UID
+ }
+ continue;
+ }
+ app = new PackageInfoData();
+ app.uid = uid;
+ app.pkgName = pkg;
+ app.names = new ArrayList<>();
+ app.names.add(pkg);
+ app.appType = 0; // system
+ ApplicationInfo apk = ApkInfo.load(pkgmanager, parsed.apkPath, uid);
+ if (apk != null) {
+ app.appinfo = apk;
+ String label = getApplicationLabel(pkgmanager, apk, pkg);
+ if (label != null && !label.trim().isEmpty()) {
+ app.names.set(0, label);
+ }
+ if (thirdParty.contains(pkg)) {
+ apk.flags &= ~ApplicationInfo.FLAG_SYSTEM;
+ app.appType = 1;
+ } else {
+ apk.flags |= ApplicationInfo.FLAG_SYSTEM;
+ }
+ }
+ app.selected_wifi = Collections.binarySearch(selectedWifi, uid) >= 0;
+ app.selected_3g = Collections.binarySearch(selected3g, uid) >= 0;
+ app.selected_roam = G.enableRoam() && Collections.binarySearch(selectedRoam, uid) >= 0;
+ app.selected_vpn = G.enableVPN() && Collections.binarySearch(selectedVpn, uid) >= 0;
+ app.selected_tether = G.enableTether() && Collections.binarySearch(selectedTether, uid) >= 0;
+ app.selected_lan = G.enableLAN() && Collections.binarySearch(selectedLan, uid) >= 0;
+ app.selected_tor = G.enableTor() && Collections.binarySearch(selectedTor, uid) >= 0;
+ syncMap.put(uid, app);
+ added++;
+ }
+ if (added > 0) {
+ Log.i(TAG, "Added " + added + " app(s) hidden from PackageManager");
+ }
+ }
+
+ /**
+ * @return packages granted INTERNET, from one root "dumpsys package packages" pass (checking
+ * each package separately made list loading far too slow), or null if it can't be determined
+ */
+ private static Set packagesHoldingInternet() {
+ try {
+ Shell.Result result = Shell.cmd("dumpsys package packages | grep -E '^ Package \\[|android.permission.INTERNET: granted=true'").exec();
+ if (!result.isSuccess() && result.getOut().isEmpty()) {
+ return null;
+ }
+ Pattern packageLine = Pattern.compile("^ Package \\[([^\\]]+)\\]");
+ Set packages = new HashSet<>();
+ String current = null;
+ for (String line : result.getOut()) {
+ Matcher m = packageLine.matcher(line);
+ if (m.find()) {
+ current = m.group(1);
+ } else if (current != null && line.contains("android.permission.INTERNET: granted=true")) {
+ packages.add(current);
+ }
+ }
+ return packages.isEmpty() ? null : packages;
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to read INTERNET permission grants: " + e.getMessage());
+ return null;
+ }
+ }
+
+ private static boolean packagesExistForUserUid(HashMap pkgs, int appUid) {
+ return pkgs != null && pkgs.containsKey(appUid);
}
+ /**
+ * APK of the packages of other users (work profile, Private Space, ...), from
+ * {@link #getPackagesForUser}: the label and icon of an app PackageManager doesn't show us.
+ */
+ private static final Map apkPaths = new java.util.concurrent.ConcurrentHashMap<>();
+
public static HashMap getPackagesForUser(List userProfile) {
- HashMap listApps = new HashMap<>();
- for(Integer integer: userProfile) {
- Shell.Result result = Shell.cmd("pm list packages -U --user " + integer).exec();
- List out = result.getOut();
- Matcher matcher;
- for (String item : out) {
- matcher = dual_pattern.matcher(item);
- if (matcher.find() && matcher.groupCount() > 0) {
- String packageName = matcher.group(1);
- String packageId = matcher.group(2);
- Log.i(TAG, packageId + " " + packageName);
- listApps.put(Integer.parseInt(packageId), packageName);
+ HashMap listApps = new HashMap<>();
+ for (Integer integer : userProfile) {
+ try {
+ Shell.Result result = Shell.cmd("pm list packages -f -U --user " + integer).exec();
+ List out = result.getOut();
+ int userPackageCount = 0;
+ for (String item : out) {
+ ApkInfo.Line parsed = ApkInfo.parse(item);
+ if (parsed != null) {
+ listApps.put(parsed.uid, parsed.packageName);
+ if (parsed.apkPath != null) {
+ apkPaths.put(parsed.packageName, parsed.apkPath);
+ }
+ userPackageCount++;
+ }
+ }
+ Log.i(TAG, "Discovered " + userPackageCount + " package(s) for user " + integer);
+ } catch (java.util.concurrent.RejectedExecutionException e) {
+ Log.w(TAG, "Package listing rejected for user " + integer + ": " + e.getMessage());
+ break;
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to list packages for user " + integer + ": " + e.getMessage());
+ }
+ }
+ return listApps;
+ }
+
+ private static void addProfileOnlyPackages(PackageManager pkgmanager,
+ HashMap packagesForUser,
+ HashMap profileMarkers,
+ SparseArray syncMap,
+ List selectedWifi,
+ List selected3g,
+ List selectedRoam,
+ List selectedVpn,
+ List selectedTether,
+ List selectedLan,
+ List selectedTor,
+ HashMap internetPermissionCache) {
+ if (packagesForUser == null || packagesForUser.isEmpty()) {
+ return;
+ }
+ int addedPackages = 0;
+ for (Map.Entry entry : packagesForUser.entrySet()) {
+ int uid = entry.getKey();
+ String packageName = entry.getValue();
+ // Already discovered via PackageManager in the main scan — skip.
+ if (syncMap.get(uid) != null || packageName == null || packageName.trim().isEmpty()) {
+ continue;
+ }
+ if (!showAllApps() && !hasInternetPermission(pkgmanager, packageName, internetPermissionCache)) {
+ continue;
+ }
+ ApplicationInfo apinfo = getApplicationInfoForPackage(pkgmanager, packageName, uid);
+ PackageInfoData app = new PackageInfoData();
+ app.uid = uid;
+ app.installTime = getInstallTime(pkgmanager, apinfo, packageName);
+ app.names = new ArrayList();
+ app.names.add(getApplicationLabel(pkgmanager, apinfo, packageName)
+ + getProfileMarker(profileMarkers, UidResolver.getUserId(uid)));
+ app.appinfo = apinfo;
+ app.appType = (apinfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0 ? 1 : 0;
+ app.pkgName = packageName;
+ // Apply selection state using the same sorted-list binary-search pattern as the main scan.
+ if (Collections.binarySearch(selectedWifi, uid) >= 0) app.selected_wifi = true;
+ if (Collections.binarySearch(selected3g, uid) >= 0) app.selected_3g = true;
+ if (G.enableRoam() && Collections.binarySearch(selectedRoam, uid) >= 0) app.selected_roam = true;
+ if (G.enableVPN() && Collections.binarySearch(selectedVpn, uid) >= 0) app.selected_vpn = true;
+ if (G.enableTether() && Collections.binarySearch(selectedTether, uid) >= 0) app.selected_tether = true;
+ if (G.enableLAN() && Collections.binarySearch(selectedLan, uid) >= 0) app.selected_lan = true;
+ if (G.enableTor() && Collections.binarySearch(selectedTor, uid) >= 0) app.selected_tor = true;
+ syncMap.put(uid, app);
+ addedPackages++;
+ }
+ if (addedPackages > 0) {
+ Log.i(TAG, "Added " + addedPackages + " profile-only package(s) to app list");
+ }
+ }
+
+ private static ApplicationInfo getApplicationInfoForPackage(PackageManager pkgmanager,
+ String packageName, int uid) {
+ try {
+ ApplicationInfo apinfo = pkgmanager.getApplicationInfo(packageName,
+ PackageManager.GET_META_DATA | PackageManager.GET_UNINSTALLED_PACKAGES);
+ apinfo.uid = uid;
+ return apinfo;
+ } catch (Exception ignored) {
+ ApplicationInfo apk = ApkInfo.load(pkgmanager, apkPaths.get(packageName), uid);
+ if (apk != null) {
+ return apk;
+ }
+ ApplicationInfo apinfo = new ApplicationInfo();
+ apinfo.packageName = packageName;
+ apinfo.uid = uid;
+ // Profile-only packages can be invisible to PackageManager — keep a minimal
+ // entry so firewall rules can still target the pm-reported UID.
+ apinfo.flags = ApplicationInfo.FLAG_INSTALLED;
+ return apinfo;
+ }
+ }
+
+ private static boolean hasInternetPermission(PackageManager pkgmanager, String packageName,
+ HashMap internetPermissionCache) {
+ try {
+ if (PackageManager.PERMISSION_GRANTED == pkgmanager.checkPermission(
+ Manifest.permission.INTERNET, packageName)) {
+ if (internetPermissionCache != null) internetPermissionCache.put(packageName, true);
+ return true;
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "PackageManager permission check failed for " + packageName + ": " + e.getMessage());
+ }
+ return hasInternetPermissionViaShell(packageName, internetPermissionCache);
+ }
+
+ private static String getApplicationLabel(PackageManager pkgmanager,
+ ApplicationInfo apinfo, String packageName) {
+ try {
+ return pkgmanager.getApplicationLabel(apinfo).toString();
+ } catch (Exception ignored) {
+ return packageName;
+ }
+ }
+
+ private static long getInstallTime(PackageManager pkgmanager, ApplicationInfo apinfo,
+ String packageName) {
+ if (apinfo != null && apinfo.sourceDir != null) {
+ return new File(apinfo.sourceDir).lastModified();
+ }
+ if (pkgmanager != null) {
+ try {
+ return pkgmanager.getPackageInfo(packageName, 0).firstInstallTime;
+ } catch (Exception ignored) {
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * @return user id -> user type (e.g. "profile.MANAGED", "profile.PRIVATE", "full.SYSTEM") as
+ * reported by root "cmd user list -v"; empty if unavailable (older Android)
+ */
+ public static Map getProfileTypes() {
+ Map types = new HashMap<>();
+ try {
+ Shell.Result result = Shell.cmd("cmd user list -v").exec();
+ Pattern userPattern = Pattern.compile("id=(\\d+),.*?type=([\\w.]+)");
+ for (String line : result.getOut()) {
+ Matcher m = userPattern.matcher(line);
+ if (m.find()) {
+ types.put(Integer.parseInt(m.group(1)), m.group(2));
}
}
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to list user types: " + e.getMessage());
+ }
+ return types;
+ }
+
+ // markers of the last app list: "(W)" work profile, "(P)" Private Space, "(M)" other users
+ private static volatile Map lastProfileMarkers = new HashMap<>();
+ private static volatile boolean profileMarkersLookedUp;
+
+ /**
+ * @return the marker of the user of {@code uid} as the app list shows it ("(W)", "(P)", "(M)")
+ */
+ public static String profileMarker(int uid) {
+ int userId = uid / 100000;
+ String marker = lastProfileMarkers.get(userId);
+ if (marker == null && userId > 0 && !profileMarkersLookedUp
+ && Looper.myLooper() != Looper.getMainLooper()) {
+ // dual apps off: the list didn't look up the profiles (root; not on the main thread)
+ profileMarkersLookedUp = true;
+ try {
+ lastProfileMarkers = getUserProfileMarkers(getListOfUids(), getProfileTypes());
+ marker = lastProfileMarkers.get(userId);
+ } catch (Exception ignored) {
+ }
}
- return listApps.size() > 0 ? listApps : null;
+ return marker != null ? marker : "(M)";
+ }
+
+ private static HashMap getUserProfileMarkers(List userProfile,
+ Map profileTypes) {
+ HashMap profileMarkers = new HashMap<>();
+ for (Integer userId : userProfile) {
+ // the user type is reliable; profile names are localized or chosen by apps (Shelter)
+ String type = profileTypes.get(userId);
+ String marker = markerForProfileType(type);
+ profileMarkers.put(userId, marker != null ? marker : "(M)");
+ }
+ try {
+ Shell.Result result = Shell.cmd("pm list users").exec();
+ Pattern userInfoPattern = Pattern.compile("UserInfo\\{(\\d+):([^:}]*)");
+ for (String line : result.getOut()) {
+ Matcher matcher = userInfoPattern.matcher(line);
+ if (matcher.find()) {
+ int userId = Integer.parseInt(matcher.group(1));
+ if (profileMarkers.containsKey(userId) && markerForProfileType(profileTypes.get(userId)) == null) {
+ profileMarkers.put(userId, markerForProfileName(matcher.group(2)));
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to label user profiles: " + e.getMessage());
+ }
+ return profileMarkers;
+ }
+
+ /** @return marker for a known profile type, or null */
+ private static String markerForProfileType(String type) {
+ if ("profile.MANAGED".equals(type)) return "(W)";
+ if ("profile.PRIVATE".equals(type)) return "(P)";
+ return null;
+ }
+
+ private static String markerForProfileName(String profileName) {
+ String name = profileName == null ? "" : profileName.toLowerCase(Locale.US);
+ if (name.contains("work")) return "(W)";
+ if (name.contains("private")) return "(P)";
+ return "(M)";
+ }
+
+ private static String getProfileMarker(HashMap profileMarkers, int userId) {
+ if (profileMarkers != null && profileMarkers.containsKey(userId)) {
+ return profileMarkers.get(userId);
+ }
+ return "(M)";
}
private static boolean isRecentlyInstalled(String packageName) {
@@ -1857,17 +3115,8 @@ private static boolean isRecentlyInstalled(String packageName) {
}
private static List getListFromPref(String savedPkg_uid) {
- StringTokenizer tok = new StringTokenizer(savedPkg_uid, "|");
- List listUids = new ArrayList<>();
- while (tok.hasMoreTokens()) {
- String uid = tok.nextToken();
- if (!uid.equals("")) {
- listUids.add(Integer.parseInt(uid));
- }
- }
- // Sort the array to allow using "Arrays.binarySearch" later
- Collections.sort(listUids);
- return listUids;
+ // Sorted to allow using "Arrays.binarySearch" later
+ return UidListParser.parse(savedPkg_uid);
}
/*public static boolean isAppAllowed(Context context, ApplicationInfo applicationInfo, SharedPreferences sharedPreferences, SharedPreferences pPrefs) {
@@ -1968,13 +3217,28 @@ public static int runScriptAsRoot(Context ctx, List script, StringBuilde
}
try {
- returnCode = new RunCommand().execute(script, res, ctx).get();
+ RunCommand runCommand = new RunCommand();
+ returnCode = runCommand.execute(script, res, ctx).get();
} catch (RejectedExecutionException r) {
- Log.e(TAG, "runScript failed: " + r.getLocalizedMessage());
+ Log.w(TAG, "Shell execution rejected, likely due to app shutdown: " + r.getLocalizedMessage());
+ returnCode = -1;
} catch (InterruptedException e) {
- Log.e(TAG, "Caught InterruptedException");
+ Log.w(TAG, "Shell execution was interrupted: " + e.getLocalizedMessage());
+ Thread.currentThread().interrupt(); // Restore interrupted status
+ returnCode = -1;
+ } catch (java.util.concurrent.ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof java.io.InterruptedIOException) {
+ Log.w(TAG, "Shell execution interrupted (IO): " + cause.getMessage());
+ } else if (cause instanceof java.util.concurrent.RejectedExecutionException) {
+ Log.w(TAG, "Shell execution rejected in wrapped exception: " + cause.getMessage());
+ } else {
+ Log.e(TAG, "Shell execution failed with ExecutionException: " + e.getLocalizedMessage());
+ }
+ returnCode = -1;
} catch (Exception e) {
- Log.e(TAG, "runScript failed: " + e.getLocalizedMessage());
+ Log.e(TAG, "Unexpected error during shell execution: " + e.getLocalizedMessage());
+ returnCode = -1;
}
return returnCode;
@@ -1982,73 +3246,116 @@ public static int runScriptAsRoot(Context ctx, List script, StringBuilde
private static boolean installBinary(Context ctx, int resId, String filename) {
try {
- File f = new File(ctx.getDir("bin", 0), filename);
+ File binDir = ctx.getDir("bin", 0);
+ File f = new File(binDir, filename);
+
+ Log.d(TAG, "Installing binary: " + filename + " to " + f.getAbsolutePath());
+
if (f.exists()) {
- f.delete();
+ Log.d(TAG, "Removing existing binary: " + filename);
+ if (!f.delete()) {
+ Log.w(TAG, "Failed to delete existing binary: " + filename);
+ }
}
+
copyRawFile(ctx, resId, f, "0755");
+
+ // Verify the binary was installed correctly
+ if (!f.exists()) {
+ Log.e(TAG, "Binary installation failed - file does not exist: " + filename);
+ return false;
+ }
+
+ if (!f.canExecute()) {
+ Log.w(TAG, "Binary installed but not executable: " + filename);
+ // Try to fix permissions manually
+ try {
+ f.setExecutable(true, false);
+ Log.d(TAG, "Fixed permissions for: " + filename);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to fix permissions for: " + filename + " - " + e.getMessage());
+ }
+ }
+
+ Log.d(TAG, "Successfully installed binary: " + filename +
+ " (size: " + f.length() + " bytes, executable: " + f.canExecute() + ")");
return true;
+
} catch (Exception e) {
- Log.e(TAG, "installBinary failed: " + e.getLocalizedMessage());
+ Log.e(TAG, "installBinary failed for " + filename + ": " + e.getClass().getSimpleName() +
+ " - " + e.getLocalizedMessage(), e);
return false;
}
}
- private static boolean installBinariesX86() {
+ /**
+ * Install binary if the resource exists, using reflection to check for resource availability
+ * @param ctx Context
+ * @param resourceName Name of the resource (e.g., "busybox_arm64")
+ * @param filename Target filename
+ * @return true if installed successfully or resource doesn't exist, false on installation error
+ */
+ private static boolean installBinaryIfExists(Context ctx, String resourceName, String filename) {
+ try {
+ // Use reflection to check if the resource exists
+ Class> rawClass = R.raw.class;
+ java.lang.reflect.Field field = rawClass.getDeclaredField(resourceName);
+ int resId = field.getInt(null);
+
+ // Resource exists, try to install it
+ return installBinary(ctx, resId, filename);
+ } catch (NoSuchFieldException e) {
+ // Resource doesn't exist - this is expected when binaries are not yet added
+ Log.d(TAG, "Resource " + resourceName + " not found - this is expected if binary is not yet available");
+ return false;
+ } catch (Exception e) {
+ Log.e(TAG, "Error checking/installing binary " + resourceName + ": " + e.getMessage());
+ return false;
+ }
+ }
+
+ private static boolean installBinariesX86(Context ctx) {
if (!installBinary(ctx, R.raw.busybox_x86, "busybox")) return false;
if (!installBinary(ctx, R.raw.iptables_x86, "iptables")) return false;
if (!installBinary(ctx, R.raw.ip6tables_x86, "ip6tables")) return false;
if (!installBinary(ctx, R.raw.nflog_x86, "nflog")) return false;
- //if (!installBinary(ctx, R.raw.run_pie_x86, "run_pie")) return false;
+
+
return true;
}
- private static boolean installBinariesMips() {
- if (!installBinary(ctx, R.raw.busybox_mips, "busybox")) return false;
- if (!installBinary(ctx, R.raw.iptables_mips, "iptables")) return false;
- if (!installBinary(ctx, R.raw.ip6tables_mips, "ip6tables")) return false;
- if (!installBinary(ctx, R.raw.nflog_mips, "nflog")) return false;
- //if (!installBinary(ctx, R.raw.run_pie_mips, "run_pie")) return false;
- return true;
- }
- private static boolean installBinariesArm64() {
- // ARM64 devices use system binaries for iptables/busybox, only install nflog
- try {
- if (!installBinary(ctx, R.raw.nflog_arm64, "nflog")) {
- Log.e(TAG, "Failed to install ARM64 nflog binary");
- return false;
- }
- Log.i(TAG, "Successfully installed ARM64 binaries");
- return true;
- } catch (Exception e) {
- Log.e(TAG, "Error installing ARM64 binaries: " + e.getMessage());
- return false;
- }
+ private static boolean installBinariesArm64(Context ctx) {
+ if (!installBinary(ctx, R.raw.busybox_arm64, "busybox")) return false;
+ if (!installBinary(ctx, R.raw.iptables_arm64, "iptables")) return false;
+ if (!installBinary(ctx, R.raw.ip6tables_arm64, "ip6tables")) return false;
+ if (!installBinary(ctx, R.raw.nflog_arm64, "nflog")) return false;
+
+
+ return true;
}
- private static boolean installBinariesArm() {
+ private static boolean installBinariesArm(Context ctx) {
if (!installBinary(ctx, R.raw.busybox_arm, "busybox")) return false;
if (!installBinary(ctx, R.raw.iptables_arm, "iptables")) return false;
if (!installBinary(ctx, R.raw.ip6tables_arm, "ip6tables")) return false;
if (!installBinary(ctx, R.raw.nflog_arm, "nflog")) return false;
- //if (!installBinary(ctx, R.raw.run_pie_arm, "run_pie")) return false;
+
+
return true;
}
- private static boolean installBinariesForAbi(String abi) {
+ private static boolean installBinariesForAbi(Context ctx, String abi) {
if (abi.startsWith("x86")) {
- return installBinariesX86();
- } else if (abi.startsWith("mips")) {
- return installBinariesMips();
+ return installBinariesX86(ctx);
} else if (abi.startsWith("arm64")) {
- return installBinariesArm64();
+ return installBinariesArm64(ctx);
} else {
- return installBinariesArm();
+ return installBinariesArm(ctx);
}
}
- private static int getPackageVersion() {
+ private static int getPackageVersion(Context ctx) {
try {
return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionCode;
} catch (NameNotFoundException e) {
@@ -2065,6 +3372,9 @@ private static String getAbi() {
}
}
+ // Static lock object for synchronizing binary installation
+ private static final Object BINARY_INSTALL_LOCK = new Object();
+
/**
* Asserts that the binary files are installed in the cache directory.
*
@@ -2073,19 +3383,30 @@ private static String getAbi() {
* @return false if the binary files could not be installed
*/
public static boolean assertBinaries(Context ctx, boolean showErrors) {
-
- int currentVer = getPackageVersion();
-
- if (G.appVersion() == currentVer) {
- // The version hasn't changed: Use the previously installed binaries.
- return true;
+ synchronized (BINARY_INSTALL_LOCK) {
+ Log.d(TAG, "assertBinaries() called - Entry point");
+
+ int currentVer = getPackageVersion(ctx);
+ boolean wasAlreadyInstalled = (G.appVersion() == currentVer);
+ Log.d(TAG, "assertBinaries() - currentVer=" + currentVer + ", storedVer=" + G.appVersion() + ", wasAlreadyInstalled=" + wasAlreadyInstalled);
+
+ if (wasAlreadyInstalled) {
+ // The version hasn't changed: Check if binaries are still functional
+ Log.d(TAG, "assertBinaries() - Verifying existing binaries...");
+ if (verifyBinaries(ctx)) {
+ Log.d(TAG, "assertBinaries() - Verification passed, returning true (no reinstall needed)");
+ return true;
+ } else {
+ Log.w(TAG, "Binaries verification failed, forcing reinstallation");
+ }
}
String abi = getAbi();
- Log.d(TAG, "Installing binaries for " + abi + "...");
+ Log.d(TAG, "Installing binaries for " + abi + " (currentVer=" + currentVer +
+ ", storedVer=" + G.appVersion() + ", wasAlreadyInstalled=" + wasAlreadyInstalled + ")...");
- if (!installBinariesForAbi(abi))
+ if (!installBinariesForAbi(ctx, abi))
{
Log.e(TAG, "Installation of the binaries for " + abi + " failed!");
toast(ctx, ctx.getString(R.string.error_binary), Toast.LENGTH_LONG);
@@ -2101,11 +3422,119 @@ public static boolean assertBinaries(Context ctx, boolean showErrors) {
}
Log.d(TAG, "Installed binaries for " + abi + ".");
- toast(ctx, ctx.getString(R.string.toast_bin_installed), Toast.LENGTH_SHORT);
+
+ // Only show toast for actual new installations (not verification failures)
+ if (!wasAlreadyInstalled) {
+ Log.d(TAG, "New installation completed - showing toast");
+ toast(ctx, ctx.getString(R.string.toast_bin_installed), Toast.LENGTH_SHORT);
+ } else {
+ Log.d(TAG, "Binaries reinstalled (wasAlreadyInstalled=true) - no toast shown");
+ }
G.appVersion(currentVer); // This indicates that the installation of the binaries for this version was successful.
return true;
+ } // End synchronized block
+ }
+
+ /**
+ * Force reinstallation of binaries regardless of version
+ *
+ * @param ctx Context
+ * @param showErrors indicates if errors should be alerted
+ * @return true if installation successful
+ */
+ public static boolean forceReinstallBinaries(Context ctx, boolean showErrors) {
+ Log.i(TAG, "Forcing binary reinstallation...");
+
+ // Clear the version to force reinstallation
+ G.appVersion(-1);
+
+ return assertBinaries(ctx, showErrors);
+ }
+
+ /**
+ * Verify that installed binaries are functional
+ *
+ * @param ctx Context
+ * @return true if binaries are functional, false if they need reinstallation
+ */
+ private static boolean verifyBinaries(Context ctx) {
+ Log.d(TAG, "verifyBinaries() called - Starting verification");
+ String dir = ctx.getDir("bin", 0).getAbsolutePath();
+ Log.d(TAG, "verifyBinaries() - Binary directory: " + dir);
+
+ // Check if busybox exists and is executable
+ File busybox = new File(dir, "busybox");
+ boolean exists = busybox.exists();
+ boolean canExecute = busybox.canExecute();
+ boolean canRead = busybox.canRead();
+ long size = busybox.length();
+ Log.d(TAG, "verifyBinaries() - Checking busybox: exists=" + exists + ", canExecute=" + canExecute + ", canRead=" + canRead + ", size=" + size + " bytes");
+ if (!exists || !canExecute) {
+ Log.w(TAG, "Busybox binary missing or not executable");
+ return false;
+ }
+
+ // Test busybox functionality by running a simple command
+ // Note: On modern Android, binaries in app private directories may not be executable
+ // from the app context, but they will work when executed with root privileges
+ try {
+ Log.d(TAG, "verifyBinaries() - Testing busybox functionality with 'echo test'");
+ ProcessBuilder pb = new ProcessBuilder(busybox.getAbsolutePath(), "echo", "test");
+ pb.environment().clear();
+ Process process = pb.start();
+ int exitCode = process.waitFor();
+ Log.d(TAG, "verifyBinaries() - Busybox test exitCode: " + exitCode);
+
+ if (exitCode != 0) {
+ Log.w(TAG, "Busybox test command failed with exit code: " + exitCode);
+ return false;
+ }
+
+ // Read and verify output
+ java.util.Scanner scanner = new java.util.Scanner(process.getInputStream());
+ if (scanner.hasNextLine()) {
+ String output = scanner.nextLine().trim();
+ Log.d(TAG, "verifyBinaries() - Busybox test output: '" + output + "'");
+ scanner.close();
+ if (!"test".equals(output)) {
+ Log.w(TAG, "Busybox test output unexpected: " + output);
+ return false;
+ }
+ } else {
+ scanner.close();
+ Log.w(TAG, "Busybox test produced no output");
+ return false;
+ }
+
+ } catch (Exception e) {
+ String errorMsg = e.getMessage();
+ if (errorMsg != null && (errorMsg.contains("Permission denied") || errorMsg.contains("error=13"))) {
+ Log.w(TAG, "Busybox execution test failed due to Android security restrictions (expected behavior)");
+ Log.w(TAG, "Binary will be available for root execution. Skipping direct execution test.");
+ // Don't fail verification for permission denied - the binary will work with root
+ // Just log the issue and continue with other checks
+ } else {
+ Log.w(TAG, "Busybox verification failed: " + errorMsg);
+ return false;
+ }
+ }
+
+ // Check other critical binaries exist
+ Log.d(TAG, "verifyBinaries() - Checking other required binaries");
+ String[] requiredBinaries = {"iptables", "ip6tables"};
+ for (String binary : requiredBinaries) {
+ File binaryFile = new File(dir, binary);
+ Log.d(TAG, "verifyBinaries() - Checking " + binary + ": exists=" + binaryFile.exists() + ", canExecute=" + binaryFile.canExecute());
+ if (!binaryFile.exists() || !binaryFile.canExecute()) {
+ Log.w(TAG, "Required binary missing or not executable: " + binary);
+ return false;
+ }
+ }
+
+ Log.d(TAG, "Binary verification successful - All checks passed");
+ return true;
}
/**
@@ -2140,135 +3569,65 @@ public static void setEnabled(Context ctx, boolean enabled, boolean showErrors)
return;
}
- //addNotification();
- Intent myService = new Intent(ctx, FirewallService.class);
- ctx.stopService(myService);
- ctx.startService(myService);
+ if (FirewallService.isInstanceRunning()) {
+ // the notification is rebuilt from the new enabled state
+ FirewallService.refreshNotification();
+ } else {
+ // may be refused from the background; MainActivity starts it again when opened
+ FirewallService.ensureRunning(ctx);
+ }
/* notify */
Intent message = new Intent(ctx, StatusWidget.class);
message.setAction(STATUS_CHANGED_MSG);
message.putExtra(Api.STATUS_EXTRA, enabled);
ctx.sendBroadcast(message);
- }
-
-
- public static void errorNotification(Context ctx) {
-
- String NOTIFICATION_CHANNEL_ID = "firewall.error";
- String channelName = ctx.getString(R.string.firewall_error_notify);
-
- NotificationManager manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
- manager.cancel(ERROR_NOTIFICATION_ID);
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_DEFAULT);
- notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
- if (G.getNotificationPriority() == 0) {
- notificationChannel.setImportance(NotificationManager.IMPORTANCE_DEFAULT);
- }
- notificationChannel.setSound(null, null);
- notificationChannel.setShowBadge(false);
- notificationChannel.enableLights(false);
- notificationChannel.enableVibration(false);
- manager.createNotificationChannel(notificationChannel);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ dev.ukanth.ufirewall.service.ToggleTileService.requestRefresh(ctx);
}
-
-
- Intent appIntent = new Intent(ctx, MainActivity.class);
- appIntent.setAction(Intent.ACTION_MAIN);
- appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
- appIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
- // Artificial stack so that navigating backward leads back to the Home screen
- TaskStackBuilder stackBuilder = TaskStackBuilder.create(ctx)
- .addParentStack(MainActivity.class)
- .addNextIntent(new Intent(ctx, MainActivity.class));
-
- PendingIntent notifyPendingIntent = PendingIntent.getActivity(ctx, 0, appIntent, PendingIntent.FLAG_IMMUTABLE);
- NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID);
- notificationBuilder.setContentIntent(notifyPendingIntent);
-
- Notification notification = notificationBuilder.setOngoing(false)
- .setCategory(NotificationCompat.CATEGORY_ERROR)
- .setVisibility(NotificationCompat.VISIBILITY_SECRET)
- .setContentTitle(ctx.getString(R.string.error_notification_title))
- .setContentText(ctx.getString(R.string.error_notification_text))
- .setTicker(ctx.getString(R.string.error_notification_ticker))
- .setSmallIcon(R.drawable.notification_warn)
- .setAutoCancel(true)
- .setContentIntent(notifyPendingIntent)
- .build();
-
- manager.notify(ERROR_NOTIFICATION_ID, notification);
}
- public static void updateNotification(boolean status, Context ctx) {
-
- String NOTIFICATION_CHANNEL_ID = "firewall.service";
- String channelName = ctx.getString(R.string.firewall_service);
-
- NotificationManager manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
- manager.cancel(NOTIFICATION_ID);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_LOW);
- notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
- if (G.getNotificationPriority() == 0) {
- notificationChannel.setImportance(NotificationManager.IMPORTANCE_DEFAULT);
- }
- notificationChannel.setSound(null, null);
- notificationChannel.setShowBadge(false);
- notificationChannel.enableLights(false);
- notificationChannel.enableVibration(false);
- manager.createNotificationChannel(notificationChannel);
- }
-
- Intent appIntent = new Intent(ctx, MainActivity.class);
- appIntent.setAction(Intent.ACTION_MAIN);
- appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
- appIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
- int icon = status ? R.drawable.notification : R.drawable.notification_error;
- String notificationText = status ? getNotificationText(ctx) : ctx.getString(R.string.inactive);
+ /**
+ * Custom script lines no longer abort an apply when they fail; report them instead.
+ */
+ public static void customScriptWarningNotification(Context ctx, List failures) {
+ for (String failure : failures) {
+ Log.w(TAG, "Custom script line failed: " + failure);
+ }
+ String text = ctx.getResources().getQuantityString(R.plurals.custom_script_warning_text,
+ failures.size(), failures.size(), failures.get(0));
+ showNotification(ctx, CUSTOM_SCRIPT_WARNING_NOTIFICATION_ID,
+ ctx.getString(R.string.custom_script_warning_title), text);
+ }
- PendingIntent notifyPendingIntent = PendingIntent.getActivity(ctx, 0, appIntent, PendingIntent.FLAG_IMMUTABLE);
- NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID);
- notificationBuilder.setContentIntent(notifyPendingIntent);
+ /**
+ * An error-channel notification that opens the app.
+ */
+ public static void showNotification(Context ctx, int id, String title, String text) {
+ Notifications.show(ctx, id, title, text, text, null);
+ }
- Notification notification = notificationBuilder.setOngoing(true)
- .setContentTitle(ctx.getString(R.string.app_name))
- .setTicker(ctx.getString(R.string.app_name))
- .setSound(null)
- .setPriority(NotificationCompat.PRIORITY_LOW)
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
- .setVisibility(NotificationCompat.VISIBILITY_SECRET)
- .setContentText(notificationText)
- .setSmallIcon(icon)
- .build();
+ /**
+ * An error-channel notification that opens {@code target}.
+ */
+ public static void showNotification(Context ctx, int id, String title, String text, Intent target) {
+ Notifications.show(ctx, id, title, text, text, target);
+ }
- notification.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_FOREGROUND_SERVICE | Notification.FLAG_NO_CLEAR;
- manager.notify(NOTIFICATION_ID, notification);
+ /**
+ * "Error applying firewall rules", with the failing command of the last apply. Cleared by the
+ * next successful apply.
+ */
+ public static void errorNotification(Context ctx) {
+ Notifications.showApplyError(ctx);
}
- private static String getNotificationText(Context ctx) {
- if (G.enableMultiProfile()) {
- String storedProfile = G.storedProfile();
- switch (storedProfile) {
- case "AFWallPrefs":
- return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("default", ctx.getString(R.string.defaultProfile)) + ")";
- case "AFWallProfile1":
- return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("profile1", ctx.getString(R.string.profile1)) + ")";
- case "AFWallProfile2":
- return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("profile2", ctx.getString(R.string.profile2)) + ")";
- case "AFWallProfile3":
- return ctx.getString(R.string.active) + " (" + G.gPrefs.getString("profile3", ctx.getString(R.string.profile3)) + ")";
- default:
- return ctx.getString(R.string.active) + " (" + storedProfile + ")";
- }
- } else {
- return ctx.getString(R.string.active);
- }
+ /**
+ * Redraw the status notification (owned by FirewallService).
+ */
+ public static void updateNotification(boolean status, Context ctx) {
+ Notifications.refreshStatus(ctx);
}
@@ -2372,28 +3731,6 @@ public static PackageInfo getPackageDetails(Context ctx, HashMap installedApplications = packageManager.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES);
- uidToApplicationInfoMap = new HashMap<>();
- for (ApplicationInfo applicationInfo : installedApplications) {
- if (!uidToApplicationInfoMap.containsKey(applicationInfo.uid)) {
- uidToApplicationInfoMap.put(applicationInfo.uid, applicationInfo);
- }
- }
- }
-
- ApplicationInfo applicationInfo = uidToApplicationInfoMap.get(appUid);
- if (applicationInfo != null) {
- PackageManager packageManager = context.getPackageManager();
- return applicationInfo.loadIcon(packageManager); // The application icon.
- } else {
- return context.getDrawable(R.drawable.ic_unknown); // The default icon.
- }
- }
-
/**
* Called when an application in removed (un-installed) from the system.
* This will look for that application in the selected list and update the persisted values if necessary
@@ -2434,11 +3771,19 @@ public static void applicationRemoved(Context ctx, int pkgRemoved, RootCommand c
}
}
+ // direct (per-app) rules are keyed by UID; drop them so a new app reusing the UID
+ // does not inherit them
+ if (AppRuleHelper.deleteRulesForUidInAllProfiles(pkgRemoved) > 0) {
+ isRuleChanged = true;
+ }
+
if (isRuleChanged) {
editor.apply();
- if (isEnabled(ctx)) {
- applySavedIptablesRules(ctx, false, new RootCommand());
- }
+ }
+ if (isRuleChanged && isEnabled(ctx)) {
+ applySavedIptablesRules(ctx, false, callback);
+ } else if (callback != null) {
+ deliverRootCommandResult(ctx, callback, 0, false);
}
}
@@ -2495,6 +3840,159 @@ public static void exportAllPreferencesToFileConfirm(final Context ctx) {
}
}
+ public static void exportRulesToFileWithPicker(final Context ctx) {
+ showExportFileDialog(ctx, false);
+ }
+
+ public static void exportAllPreferencesToFileWithPicker(final Context ctx) {
+ showExportFileDialog(ctx, true);
+ }
+
+ private static void showExportFileDialog(final Context ctx, final boolean exportAll) {
+ try {
+ File defaultPath;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ File extDir = ctx.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
+ if (extDir != null) {
+ extDir.mkdirs();
+ defaultPath = extDir;
+ } else {
+ defaultPath = new File(ctx.getExternalFilesDir(null), "/");
+ }
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ defaultPath = new File(ctx.getExternalFilesDir(null), "/");
+ } else {
+ defaultPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/afwall/");
+ defaultPath.mkdirs();
+ }
+
+ dev.ukanth.ufirewall.util.FileDialog fileDialog = new dev.ukanth.ufirewall.util.FileDialog((Activity) ctx, defaultPath, true);
+ fileDialog.setSelectDirectoryOption(true);
+ fileDialog.addDirectoryListener(directory -> {
+ String fileName = "afwall-backup" + (exportAll ? "-all" : "") + "-" +
+ new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".json";
+ File fullPath = new File(directory, fileName);
+
+ boolean success;
+ if (exportAll) {
+ success = exportAllToFile(ctx, fullPath);
+ } else {
+ success = exportRulesToFile(ctx, fullPath);
+ }
+
+ if (success) {
+ Api.toast(ctx, ctx.getString(R.string.export_rules_success) + " " + fullPath.getAbsolutePath());
+ } else {
+ Api.toast(ctx, ctx.getString(R.string.export_rules_fail));
+ }
+ });
+ fileDialog.showDialog();
+ } catch (Exception e) {
+ // Fallback to original method if file dialog fails
+ if (exportAll) {
+ exportAllPreferencesToFileConfirm(ctx);
+ } else {
+ exportRulesToFileConfirm(ctx);
+ }
+ }
+ }
+
+ private static boolean exportRulesToFile(Context ctx, File file) {
+ boolean res = false;
+ try (FileOutputStream fOut = new FileOutputStream(file);
+ OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut)) {
+
+ JSONObject obj = new JSONObject(getCurrentRulesAsMap(ctx));
+ JSONArray jArray = new JSONArray("[" + obj.toString() + "]");
+ JSONObject exportObject = new JSONObject();
+ exportObject.put("rules", jArray);
+ String mode = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST);
+ exportObject.put("mode", mode);
+ // portable rules; older versions ignore this key and read "rules"
+ exportObject.put(BackupHelper.V2_KEY, BackupHelper.exportRules(ctx, PREFS_NAME));
+
+ myOutWriter.write(exportObject.toString());
+ myOutWriter.flush(); // Ensure data is written
+ res = true;
+ Log.i(TAG, "Successfully exported rules to: " + file.getAbsolutePath());
+ } catch (Exception e) {
+ Log.e(TAG, "Error exporting rules to file: " + file.getAbsolutePath(), e);
+ }
+ return res;
+ }
+
+ /**
+ * @return rules of every profile, keyed by profile name ("AFWallPrefs" for the default profile)
+ */
+ private static JSONObject getAllProfileRules(Context ctx) throws JSONException {
+ JSONObject profileObject = new JSONObject();
+ profileObject.put(DEFAULT_PREFS_NAME, new JSONObject(getRulesForProfile(ctx, DEFAULT_PREFS_NAME)));
+ for (ProfileData profile : ProfileHelper.getProfiles()) {
+ // rules are stored under the identifier, which differs from the name (e.g. spaces removed)
+ profileObject.put(profile.getName(), new JSONObject(getRulesForProfile(ctx, profile.getIdentifier())));
+ }
+ return profileObject;
+ }
+
+ /**
+ * Full backup. The v1 sections keep their layout so older versions can still import the file;
+ * "v2" holds the complete, portable data and is what this version imports.
+ */
+ private static JSONObject buildFullExport(Context ctx) throws JSONException {
+ JSONObject exportObject = new JSONObject();
+ Map prefOverrides = new HashMap<>();
+ if (G.enableMultiProfile()) {
+ exportObject.put("_profiles", getAllProfileRules(ctx));
+ // versions that still use the old profile model read these instead: keys are the
+ // preference file names, custom profiles are listed in "plusprofiles"
+ JSONObject legacyProfiles = new JSONObject();
+ JSONObject legacyAdditional = new JSONObject();
+ List additional = new ArrayList<>();
+ legacyProfiles.put(DEFAULT_PREFS_NAME, new JSONObject(getRulesForProfile(ctx, DEFAULT_PREFS_NAME)));
+ for (ProfileData profile : ProfileHelper.getProfiles()) {
+ String identifier = profile.getIdentifier();
+ JSONObject rules = new JSONObject(getRulesForProfile(ctx, identifier));
+ if (identifier.matches("AFWallProfile[123]")) {
+ legacyProfiles.put(identifier, rules);
+ prefOverrides.put("profile" + identifier.charAt(identifier.length() - 1), profile.getName());
+ } else {
+ legacyAdditional.put(identifier, rules);
+ additional.add(identifier);
+ }
+ }
+ exportObject.put("profiles", legacyProfiles);
+ exportObject.put("additional_profiles", legacyAdditional);
+ prefOverrides.put("plusprofiles", TextUtils.join(",", additional));
+ } else {
+ exportObject.put("default", new JSONObject(getCurrentRulesAsMap(ctx)));
+ }
+
+ exportObject.put("prefs", BackupHelper.exportV1Prefs(G.gPrefs, prefOverrides));
+ // profile-specific preferences (mode, custom scripts, ...) of the active profile
+ if (G.pPrefs != null) {
+ exportObject.put("profilePrefs", BackupHelper.exportV1Prefs(G.pPrefs));
+ exportObject.put("mode", G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST));
+ }
+ exportObject.put(BackupHelper.V2_KEY, BackupHelper.exportFull(ctx));
+ return exportObject;
+ }
+
+ private static boolean exportAllToFile(Context ctx, File file) {
+ boolean res = false;
+ try (FileOutputStream fOut = new FileOutputStream(file);
+ OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut)) {
+
+ JSONObject exportObject = buildFullExport(ctx);
+ myOutWriter.write(exportObject.toString());
+ myOutWriter.flush(); // Ensure data is written
+ res = true;
+ Log.i(TAG, "Successfully exported all preferences to: " + file.getAbsolutePath());
+ } catch (Exception e) {
+ Log.e(TAG, "Error exporting all preferences to file: " + file.getAbsolutePath(), e);
+ }
+ return res;
+ }
+
private static void updateExportPackage(Map exportMap, String packageName, boolean isChecked, int identifier) throws JSONException {
if (!isChecked) {
return;
@@ -2513,13 +4011,9 @@ private static void updateExportPackage(Map exportMap, Strin
}
private static void updatePackage(Context ctx, String savedPkg_uid, Map exportMap, int identifier) throws JSONException {
- StringTokenizer tok = new StringTokenizer(savedPkg_uid, "|");
- while (tok.hasMoreTokens()) {
- String uid = tok.nextToken();
- if (!uid.isEmpty()) {
- String packageName = ctx.getPackageManager().getNameForUid(Integer.parseInt(uid));
- updateExportPackage(exportMap, packageName, /*is_checked=*/ true, identifier);
- }
+ for (int uid : UidListParser.parse(savedPkg_uid)) {
+ String packageName = ctx.getPackageManager().getNameForUid(uid);
+ updateExportPackage(exportMap, packageName, /*is_checked=*/ true, identifier);
}
}
@@ -2548,57 +4042,23 @@ public static boolean exportAll(Context ctx, final String fileName) {
boolean res = false;
try {
File file;
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ // Android 11+ (API 30+): Use scoped storage
+ file = new File(ctx.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), fileName);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // Android 10 (API 29): Use app-specific directory
+ file = new File(ctx.getExternalFilesDir(null), fileName);
+ } else {
+ // Android 9 and below: Use legacy external storage
File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "afwall");
dir.mkdirs();
- file = new File(dir, fileName);
- } else {
- file = new File(ctx.getExternalFilesDir(null), fileName);
+ file = new File(dir, fileName);
}
try (FileOutputStream fOut = new FileOutputStream(file);
OutputStreamWriter myOutWriter = new OutputStreamWriter(fOut)) {
- JSONObject exportObject = new JSONObject();
- if (G.enableMultiProfile()) {
- if (!G.isProfileMigrated()) {
- JSONObject profileObject = new JSONObject();
- for (String profile : G.profiles) {
- profileObject.put(profile, new JSONObject(getRulesForProfile(ctx, profile)));
- }
- exportObject.put("profiles", profileObject);
-
- JSONObject addProfileObject = new JSONObject();
- for (String profile : G.getAdditionalProfiles()) {
- addProfileObject.put(profile, new JSONObject(getRulesForProfile(ctx, profile)));
- }
- exportObject.put("additional_profiles", addProfileObject);
- } else {
- JSONObject profileObject = new JSONObject();
- String profileName = "AFWallPrefs";
- profileObject.put(profileName, new JSONObject(getRulesForProfile(ctx, profileName)));
-
- List profileDataList = ProfileHelper.getProfiles();
- for (ProfileData profile : profileDataList) {
- profileName = profile.getName();
- if (profile.getIdentifier().startsWith("AFWallProfile")) {
- profileName = profile.getIdentifier();
- }
- profileObject.put(profile.getName(), new JSONObject(getRulesForProfile(ctx, profileName)));
- }
- exportObject.put("_profiles", profileObject);
- }
- } else {
- JSONObject obj = new JSONObject(getCurrentRulesAsMap(ctx));
- exportObject.put("default", obj);
- }
-
- exportObject.put("prefs", getAllAppPreferences(ctx, G.gPrefs));
-
- String mode = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST);
- exportObject.put("mode", mode);
-
- myOutWriter.append(exportObject.toString());
+ myOutWriter.append(buildFullExport(ctx).toString());
res = true;
}
@@ -2623,27 +4083,21 @@ private static Map getRulesForProfile(Context ctx, String pr
return exportMap;
}
- private static JSONArray getAllAppPreferences(Context ctx, SharedPreferences gPrefs) throws JSONException {
- Map keys = gPrefs.getAll();
- JSONArray arr = new JSONArray();
- for (Map.Entry entry : keys.entrySet()) {
- JSONObject obj = new JSONObject();
- obj.put(entry.getKey(), entry.getValue().toString());
- arr.put(obj);
- }
- return arr;
- }
-
public static boolean exportRules(Context ctx, final String fileName) {
boolean res = false;
File file;
- if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ){
- File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/afwall/" );
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ // Android 11+ (API 30+): Use scoped storage
+ file = new File(ctx.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), fileName);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // Android 10 (API 29): Use app-specific directory
+ file = new File(ctx.getExternalFilesDir(null), fileName);
+ } else {
+ // Android 9 and below: Use legacy external storage
+ File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/afwall/");
dir.mkdirs();
file = new File(dir, fileName);
- } else{
- file = new File(ctx.getExternalFilesDir(null) + "/" + fileName) ;
}
try {
@@ -2660,6 +4114,8 @@ public static boolean exportRules(Context ctx, final String fileName) {
String mode = G.pPrefs.getString(Api.PREF_MODE, Api.MODE_WHITELIST);
exportObject.put("mode", mode);
+ // portable rules; older versions ignore this key and read "rules"
+ exportObject.put(BackupHelper.V2_KEY, BackupHelper.exportRules(ctx, PREFS_NAME));
myOutWriter.append(exportObject.toString());
res = true;
@@ -2683,29 +4139,19 @@ private static boolean importRulesRoot(Context ctx, File file, StringBuilder ms
boolean returnVal = false;
BufferedReader br = null;
try {
- com.topjohnwu.superuser.Shell.Result result = com.topjohnwu.superuser.Shell.cmd("cat " + file.getAbsolutePath()).exec();
+ // Use shell-safe quoting to prevent path injection
+ String safePath = "'" + file.getAbsolutePath().replace("'", "'\\''" ) + "'";
+ com.topjohnwu.superuser.Shell.Result result = com.topjohnwu.superuser.Shell.cmd("cat " + safePath).exec();
List out = result.getOut();
String data = TextUtils.join("", out);
-
- try {
- //old export format
- JSONArray array = new JSONArray(data);
- updateRulesFromJson(ctx, (JSONObject) array.get(0), PREFS_NAME);
- } catch (JSONException e) {
- //new exported format
- JSONObject jsonObject = new JSONObject(data);
- //save mode
- if(jsonObject.get("mode") != null) {
- G.pPrefs.edit().putString(PREF_MODE, jsonObject.getString("mode")).apply();
- }
- JSONArray array = (JSONArray) jsonObject.get("rules");
- updateRulesFromJson(ctx, (JSONObject) array.get(0), PREFS_NAME);
- }
+ importRulesData(ctx, data, msg);
returnVal = true;
+ } catch (java.util.concurrent.RejectedExecutionException e) {
+ Log.w(TAG, "Import rules file read rejected: " + e.getMessage());
} catch (JSONException e) {
- Log.e(TAG, e.getLocalizedMessage());
+ Log.e(TAG, "JSON parsing error during import: " + e.getLocalizedMessage());
} catch (Exception e) {
- Log.e(TAG, e.getLocalizedMessage());
+ Log.e(TAG, "Failed to import rules from file: " + e.getLocalizedMessage());
} finally {
if (br != null) {
try {
@@ -2727,17 +4173,12 @@ private static boolean importRules(Context ctx, File file, StringBuilder msg) {
text.append(line);
}
String data = text.toString();
- JSONObject jsonObject = new JSONObject(data);
- if (jsonObject.has("mode")) {
- G.pPrefs.edit().putString(PREF_MODE, jsonObject.getString("mode")).apply();
- }
- JSONArray array = jsonObject.optJSONArray("rules");
- if (array != null) {
- updateRulesFromJson(ctx, (JSONObject) array.get(0), PREFS_NAME);
- } else {
- updateRulesFromJson(ctx, jsonObject, PREFS_NAME);
+ if (data.trim().isEmpty()) {
+ msg.append("Import file contains no data");
+ return false;
}
-
+
+ importRulesData(ctx, data, msg);
returnVal = true;
} catch (FileNotFoundException e) {
if (e.getMessage().contains("EACCES")) {
@@ -2753,6 +4194,77 @@ private static boolean importRules(Context ctx, File file, StringBuilder msg) {
}
+ /**
+ * Import a "rules only" backup into the current profile. Handles every format: the oldest
+ * (a bare array), v1 ({"rules": [...], "mode"}) and v2 (a "v2" section next to the v1 keys).
+ */
+ private static void importRulesData(Context ctx, String data, StringBuilder msg) throws JSONException {
+ Object parsed = new JSONTokener(data).nextValue();
+ if (parsed instanceof JSONArray) {
+ updateRulesFromJson(ctx, ((JSONArray) parsed).getJSONObject(0), PREFS_NAME);
+ return;
+ }
+ if (!(parsed instanceof JSONObject)) {
+ throw new JSONException("Not an AFWall+ rules backup");
+ }
+ JSONObject jsonObject = (JSONObject) parsed;
+ JSONObject v2 = jsonObject.optJSONObject(BackupHelper.V2_KEY);
+ if (v2 != null && v2.optJSONArray("rules") != null) {
+ reportSkipped(ctx, BackupHelper.importRules(ctx, v2, PREFS_NAME), msg);
+ return;
+ }
+ if (jsonObject.has("mode")) {
+ G.pPrefs.edit().putString(PREF_MODE, jsonObject.getString("mode")).apply();
+ }
+ JSONArray array = jsonObject.optJSONArray("rules");
+ if (array != null) {
+ updateRulesFromJson(ctx, (JSONObject) array.get(0), PREFS_NAME);
+ } else {
+ updateRulesFromJson(ctx, jsonObject, PREFS_NAME);
+ }
+ }
+
+ private static void reportSkipped(Context ctx, BackupHelper.ImportStats stats, StringBuilder msg) {
+ int skipped = stats.skippedApps();
+ if (skipped > 0) {
+ msg.append(ctx.getResources().getQuantityString(R.plurals.import_skipped_apps, skipped, skipped));
+ }
+ }
+
+ /**
+ * UID on this device of a v1 rule key: a package name, a special entry
+ * ("dev.afwall.special..."), or "sharedUserId:uid" as written for shared UIDs. v1 has no
+ * Android user, so entries map to the main user.
+ *
+ * @return null if it doesn't exist on this device
+ */
+ private static Integer resolveV1RuleUid(String key, BackupHelper.DeviceUidMapper mapper) {
+ if (key.startsWith(SYSTEM_UID_PKG_PREFIX)) {
+ try {
+ return Integer.parseInt(key.substring(SYSTEM_UID_PKG_PREFIX.length()));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+ if (key.startsWith("dev.afwall.special")) {
+ return mapper.specialUid(key);
+ }
+ int sep = key.lastIndexOf(':');
+ if (sep > 0) {
+ String name = key.substring(0, sep);
+ try {
+ int srcUid = Integer.parseInt(key.substring(sep + 1));
+ if (srcUid >= 0 && srcUid % 100000 < android.os.Process.FIRST_APPLICATION_UID) {
+ return srcUid % 100000; // system UIDs are the same on every device
+ }
+ } catch (NumberFormatException ignored) {
+ }
+ Integer appId = mapper.appIdForSharedUser(name);
+ return appId != null ? appId : mapper.appIdForPackage(name);
+ }
+ return mapper.appIdForPackage(key);
+ }
+
private static void updateRulesFromJson(Context ctx, JSONObject object, String preferenceName) throws JSONException {
final StringBuilder[] uidBuilders = new StringBuilder[7];
uidBuilders[WIFI_EXPORT] = new StringBuilder();
@@ -2764,34 +4276,30 @@ private static void updateRulesFromJson(Context ctx, JSONObject object, String p
uidBuilders[TOR_EXPORT] = new StringBuilder();
Map json = JsonHelper.toMap(object);
- final PackageManager pm = ctx.getPackageManager();
+ BackupHelper.DeviceUidMapper mapper = new BackupHelper.DeviceUidMapper(ctx);
for (Map.Entry entry : json.entrySet()) {
- String pkgName = entry.getKey();
- if (pkgName.contains(":")) {
- pkgName = pkgName.split(":")[0];
+ Integer uid = resolveV1RuleUid(entry.getKey(), mapper);
+ if (uid == null) {
+ continue; // not on this device
}
-
JSONObject jsonObj = (JSONObject) JsonHelper.toJSON(entry.getValue());
Iterator> keys = jsonObj.keys();
while (keys.hasNext()) {
- String key = (String) keys.next();
- int exportType = Integer.parseInt(key);
+ int exportType;
+ try {
+ exportType = Integer.parseInt((String) keys.next());
+ } catch (NumberFormatException e) {
+ continue;
+ }
+ if (exportType < 0 || exportType >= uidBuilders.length) {
+ continue;
+ }
StringBuilder uidBuilder = uidBuilders[exportType];
-
if (uidBuilder.length() != 0) {
uidBuilder.append('|');
}
-
- if (pkgName.startsWith("dev.afwall.special")) {
- uidBuilder.append(specialApps.get(pkgName));
- } else {
- try {
- uidBuilder.append(pm.getApplicationInfo(pkgName, 0).uid);
- } catch (NameNotFoundException e) {
- // Handle exception if needed
- }
- }
+ uidBuilder.append(uid);
}
}
@@ -2808,28 +4316,39 @@ private static void updateRulesFromJson(Context ctx, JSONObject object, String p
edit.apply();
}
- private static boolean shouldIgnoreKey(String key) {
- String[] ignore = {"appVersion", "fixLeak", "enableLogService", "sort", "storedProfile", "hasRoot", "logChains", "kingDetect", "fingerprintEnabled"};
- return Arrays.asList(ignore).contains(key);
- }
-
- private static boolean isIntType(String key) {
- String[] intType = {"logPingTime", "customDelay", "patternMax", "widgetX", "widgetY", "notification_priority"};
- return Arrays.asList(intType).contains(key);
+ /**
+ * Import a "_profiles" section: keys are profile names. Profiles that don't exist yet are
+ * created.
+ */
+ private static void importProfilesByName(Context ctx, JSONObject profileObject) throws JSONException {
+ Iterator keys = profileObject.keys();
+ while (keys.hasNext()) {
+ String name = keys.next();
+ String identifier = DEFAULT_PREFS_NAME.equals(name) ? name : ProfileHelper.ensureProfileNamed(name);
+ importProfileRules(ctx, profileObject, name, identifier);
+ }
}
- private static void importProfiles(Context ctx, JSONObject profileObject) throws JSONException {
+ /**
+ * Import a "profiles" / "additional_profiles" section of the old profile model: keys are the
+ * preference file names (AFWallPrefs, AFWallProfile1-3, or the custom profile name).
+ */
+ private static void importLegacyProfiles(Context ctx, JSONObject profileObject) throws JSONException {
Iterator keys = profileObject.keys();
while (keys.hasNext()) {
- String key = keys.next();
- try {
- JSONObject obj = profileObject.getJSONObject(key);
- updateRulesFromJson(ctx, obj, key);
- } catch (JSONException e) {
- if (e.getMessage().contains("No value")) {
- // continue;
- }
+ String identifier = keys.next();
+ if (!DEFAULT_PREFS_NAME.equals(identifier)) {
+ ProfileHelper.ensureProfile(ctx, identifier);
}
+ importProfileRules(ctx, profileObject, identifier, identifier);
+ }
+ }
+
+ private static void importProfileRules(Context ctx, JSONObject profileObject, String key, String identifier) {
+ try {
+ updateRulesFromJson(ctx, profileObject.getJSONObject(key), identifier);
+ } catch (JSONException e) {
+ Log.w(TAG, "Skipping profile " + key + " on import: " + e.getMessage());
}
}
private static boolean importAll(Context ctx, File file, StringBuilder msg) {
@@ -2842,52 +4361,45 @@ private static boolean importAll(Context ctx, File file, StringBuilder msg) {
text.append(line);
}
String data = text.toString();
+ if (data.trim().isEmpty()) {
+ msg.append("Import file contains no data");
+ return false;
+ }
+
JSONObject object = new JSONObject(data);
- // Allow/deny rule
- if (object.has("mode")) {
- G.pPrefs.edit().putString(PREF_MODE, object.getString("mode")).apply();
+ // backups from this version on: the complete, portable section
+ JSONObject v2 = object.optJSONObject(BackupHelper.V2_KEY);
+ if (v2 != null && v2.optJSONArray("profiles") != null) {
+ reportSkipped(ctx, BackupHelper.importFull(ctx, v2), msg);
+ return true;
}
- JSONArray prefArray = object.getJSONArray("prefs");
- for (int i = 0; i < prefArray.length(); i++) {
- JSONObject prefObj = prefArray.getJSONObject(i);
- Iterator keys = prefObj.keys();
+ // Basic validation of expected JSON structure
+ if (!object.has("prefs") && !object.has("profiles") && !object.has("_profiles") && !object.has("default")) {
+ msg.append("Import file does not contain valid AFWall+ data");
+ Log.w(TAG, "Invalid import file structure - missing expected keys");
+ return false;
+ }
- while (keys.hasNext()) {
- String key = keys.next();
- String value = prefObj.getString(key);
- if (shouldIgnoreKey(key)) {
- continue;
- }
- if (value.equals("true") || value.equals("false")) {
- G.gPrefs.edit().putBoolean(key, Boolean.parseBoolean(value));
- } else {
- try {
- if (key.equals("multiUserId")) {
- G.gPrefs.edit().putLong(key, Long.parseLong(value));
- } else if (isIntType(key)) {
- G.gPrefs.edit().putString(key, value);
- } else {
- int intValue = Integer.parseInt(value);
- G.gPrefs.edit().putInt(key, intValue);
- }
- } catch (NumberFormatException e) {
- G.gPrefs.edit().putString(key, value);
- }
- }
- }
+ // Allow/deny rule
+ if (object.has("mode")) {
+ G.pPrefs.edit().putString(PREF_MODE, object.getString("mode")).apply();
}
- if (G.enableMultiProfile()) {
- if (G.isProfileMigrated()) {
- JSONObject profileObject = object.getJSONObject("_profiles");
- importProfiles(ctx, profileObject);
- } else {
- JSONObject profileObject = object.getJSONObject("profiles");
- importProfiles(ctx, profileObject);
- JSONObject customProfileObject = object.getJSONObject("additional_profiles");
- importProfiles(ctx, customProfileObject);
+ // v1 stored every preference as a string; write them with the types they are read with
+ BackupHelper.importV1Prefs(object.optJSONArray("prefs"), G.gPrefs);
+ // profile-specific preferences (mode, custom scripts, ...) of the exported profile
+ BackupHelper.importV1Prefs(object.optJSONArray("profilePrefs"), G.pPrefs, true);
+
+ // pick the rules section by what the file contains, not by the current settings
+ if (object.has("_profiles")) {
+ importProfilesByName(ctx, object.getJSONObject("_profiles"));
+ } else if (object.has("profiles")) {
+ // backup from the old profile model: keys are the preference file names
+ importLegacyProfiles(ctx, object.getJSONObject("profiles"));
+ if (object.has("additional_profiles")) {
+ importLegacyProfiles(ctx, object.getJSONObject("additional_profiles"));
}
} else {
JSONObject defaultRules = object.getJSONObject("default");
@@ -2896,8 +4408,8 @@ private static boolean importAll(Context ctx, File file, StringBuilder msg) {
returnVal = true;
} catch (FileNotFoundException e) {
msg.append(ctx.getString(R.string.import_rules_missing));
- } catch (IOException | JSONException e) {
- Log.e(TAG, e.getLocalizedMessage());
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to import " + file, e);
}
return returnVal;
@@ -2907,11 +4419,32 @@ public static boolean loadSharedPreferencesFromFile(Context ctx, StringBuilder b
boolean res = false;
File file = new File(fileName);
if (file.exists()) {
+ // Basic file validation
+ if (file.length() == 0) {
+ builder.append("Import file is empty");
+ Log.w(TAG, "Import file is empty: " + fileName);
+ return false;
+ }
+ if (file.length() > 50 * 1024 * 1024) { // 50MB limit
+ builder.append("Import file is too large (>50MB)");
+ Log.w(TAG, "Import file is too large: " + fileName + " (" + file.length() + " bytes)");
+ return false;
+ }
+
+ Log.i(TAG, "Importing from file: " + fileName + " (loadAll: " + loadAll + ")");
if (loadAll) {
res = importAll(ctx, file, builder);
} else {
res = importRules(ctx, file, builder);
}
+ if (res) {
+ // settings such as multi-profile or the active profile's file may have changed
+ G.reloadPrefs();
+ applications = null;
+ }
+ } else {
+ builder.append("Import file does not exist: " + fileName);
+ Log.w(TAG, "Import file does not exist: " + fileName);
}
return res;
}
@@ -2938,8 +4471,58 @@ public static void showInstalledAppDetails(Context context, String packageName)
public static boolean isNetfilterSupported() {
boolean netfiler_exists = new File("/proc/net/netfilter").exists();
- Shell.Result result = Shell.cmd("cat /proc/net/ip_tables_targets").exec();
- return netfiler_exists && result.isSuccess();
+ try {
+ Shell.Result result = Shell.cmd("cat /proc/net/ip_tables_targets").exec();
+ return netfiler_exists && result.isSuccess();
+ } catch (java.util.concurrent.RejectedExecutionException e) {
+ Log.w(TAG, "Netfilter check rejected: " + e.getMessage());
+ return false;
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to check netfilter support: " + e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Check if a package has android.permission.INTERNET via shell.
+ * Used for packages invisible to PackageManager due to package visibility restrictions.
+ */
+ private static boolean hasInternetPermissionViaShell(String packageName) {
+ return hasInternetPermissionViaShell(packageName, null);
+ }
+
+ private static boolean hasInternetPermissionViaShell(String packageName,
+ HashMap internetPermissionCache) {
+ if (internetPermissionCache != null && internetPermissionCache.containsKey(packageName)) {
+ return internetPermissionCache.get(packageName);
+ }
+ boolean hasPermission = false;
+ try {
+ // Fetch full dumpsys output and search in-process (avoids a second shell fork for grep).
+ Shell.Result result = Shell.cmd("dumpsys package " + packageName).exec();
+ if (result.isSuccess()) {
+ for (String line : result.getOut()) {
+ if (line.contains("android.permission.INTERNET")) {
+ hasPermission = true;
+ break;
+ }
+ }
+ } else {
+ Log.w(TAG, "dumpsys package failed while checking INTERNET permission for " + packageName);
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to check INTERNET permission for " + packageName + ": " + e.getMessage());
+ }
+ if (internetPermissionCache != null) internetPermissionCache.put(packageName, hasPermission);
+ return hasPermission;
+ }
+
+ /**
+ * @return special entry name ("dev.afwall.special...") -> UID on this device
+ */
+ public static Map getSpecialAppUids() {
+ initSpecial();
+ return new HashMap<>(specialApps);
}
private static void initSpecial() {
@@ -2965,7 +4548,7 @@ public static void updateLanguage(Context context, String lang) {
Resources res = context.getResources();
Configuration conf = res.getConfiguration();
conf.locale = defaultLocale;
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.createConfigurationContext(conf);
} else {
context.getResources().updateConfiguration(conf, context.getResources().getDisplayMetrics());
@@ -2979,7 +4562,7 @@ public static void updateLanguage(Context context, String lang) {
Resources res = context.getResources();
Configuration conf = res.getConfiguration();
conf.locale = locale;
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.createConfigurationContext(conf);
} else {
context.getResources().updateConfiguration(conf, context.getResources().getDisplayMetrics());
@@ -3034,12 +4617,15 @@ public static String loadData(final Context context,
}
/**
- * Encrypt the password
+ * Encrypt the password - DEPRECATED: Use SecureCrypto.encryptSecure() for new code
+ * This method is kept for backward compatibility only
*
* @param key
* @param data
* @return
+ * @deprecated Use SecureCrypto.encryptSecure() instead for better security
*/
+ @Deprecated
public static String hideCrypt(String key, String data) {
if (key == null || data == null)
return null;
@@ -3060,12 +4646,15 @@ public static String hideCrypt(String key, String data) {
}
/**
- * Decrypt the password
+ * Decrypt the password - DEPRECATED: Use SecureCrypto.decryptSecure() for new code
+ * This method is kept for backward compatibility only
*
* @param key
* @param data
* @return
+ * @deprecated Use SecureCrypto.decryptSecure() instead for better security
*/
+ @Deprecated
public static String unhideCrypt(String key, String data) {
if (key == null || data == null)
return null;
@@ -3139,24 +4728,59 @@ public static int getConnectivityStatus(Context context) {
* @param ctx
*/
public static void applyDefaultChains(Context ctx, RootCommand callback) {
+ List v4 = new ArrayList<>();
+ v4.add(G.ipv4Input() ? "-P INPUT ACCEPT" : "-P INPUT DROP");
+ v4.add(G.ipv4Fwd() ? "-P FORWARD ACCEPT" : "-P FORWARD DROP");
+ v4.add(G.ipv4Output() ? "-P OUTPUT ACCEPT" : "-P OUTPUT DROP");
+
+ // One script and one submission: submitting the same RootCommand twice (as this used to do
+ // for IPv4 and IPv6) replaces the first command list before it runs, so the IPv4 policies
+ // were never set, and the callback fired twice.
+ List out = new ArrayList<>();
+ iptablesCommands(v4, out, false);
+ if (G.controlIPv6()) {
+ iptablesCommands(defaultChainsv6Commands(), out, true);
+ } else if (G.enableIPv6() || G.fixLeak()) {
+ //related to #511, disable ipv6 but use startup leak.
+ iptablesCommands(v4, out, true);
+ }
+ callback.setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out);
+ }
+
+ /**
+ * The fix leak boot script sets INPUT/OUTPUT/FORWARD to DROP (IPv4 and IPv6) until AFWall+
+ * applies its rules. When the firewall is disabled or inactive at boot no rules are applied,
+ * so the policies have to be opened again explicitly, or the device stays offline.
+ */
+ public static void liftBootLeakProtection(Context ctx) {
+ if (G.initPath() == null) {
+ return; // no fix leak script configured
+ }
List cmds = new ArrayList<>();
- cmds.add(G.ipv4Input() ? "-P INPUT ACCEPT" : "-P INPUT DROP");
- cmds.add(G.ipv4Fwd() ? "-P FORWARD ACCEPT" : "-P FORWARD DROP");
- cmds.add(G.ipv4Output() ? "-P OUTPUT ACCEPT" : "-P OUTPUT DROP");
- applyQuick(ctx, cmds, callback);
- applyDefaultChainsv6(ctx, callback);
+ cmds.add("-P INPUT ACCEPT");
+ cmds.add("-P OUTPUT ACCEPT");
+ cmds.add("-P FORWARD ACCEPT");
+ List out = new ArrayList<>();
+ iptablesCommands(cmds, out, false);
+ iptablesCommands(cmds, out, true);
+ Log.i(TAG, "Firewall not active at boot; opening chain policies set by the fix leak script");
+ new RootCommand().setRetryExitCode(IPTABLES_TRY_AGAIN).run(ctx, out);
}
public static void applyDefaultChainsv6(Context ctx, RootCommand callback) {
if (G.controlIPv6()) {
- List cmds = new ArrayList<>();
- cmds.add(G.ipv6Input() ? "-P INPUT ACCEPT" : "-P INPUT DROP");
- cmds.add(G.ipv6Fwd() ? "-P FORWARD ACCEPT" : "-P FORWARD DROP");
- cmds.add(G.ipv6Output() ? "-P OUTPUT ACCEPT" : "-P OUTPUT DROP");
- applyIPv6Quick(ctx, cmds, callback);
+ applyIPv6Quick(ctx, defaultChainsv6Commands(), callback);
}
}
+ private static List defaultChainsv6Commands() {
+ List