• Working linux/macOS apkanalyzer example port to Windows for WhatsApp update debugging

    From Maria Sophia@mariasophia@comprehension.com to alt.os.linux,alt.comp.os.windows-11,alt.msdos.batch on Thu Oct 22 17:51:43 2026
    From Newsgroup: alt.comp.os.windows-11

    Working linux/macOS apkanalyzer example port to Windows for WhatsApp update debugging

    So that everyone can benefit, just moments ago I ported the macOS/Linux Android SDK apkanalyzer bash script to a Windows batch implementation.

    Newsgroups: comp.mobile.android
    Subject: A quick look at the WhatsApp Meta Services Framework server-side 7 day update
    Date: Sat, 17 Oct 2026 12:43:55 -0400
    Message-ID: <11b08kc$20fv$1@nnrp.usenet.blueworldhosting.com>

    Included are the before:after macOS/Linux:Windows scripts for your use.

    BACKGROUND:
    A week ago WhatsApp triggered a 7-day warning to update where I decided to debug (in gory detail) the update-process mechanism on the Android ng.

    I haven't seen the phone in a week (as the sd card is being replaced),
    but it's displayed on my monitor two feet tall, so I had full access to it.

    Note that my 2021 Android Galaxy A32-5G USB port is broken so everything
    is done over Wi-Fi where I wrote scripts to get around the security restrictions Google forced into Android 11+ to require hands-on access.
    adbconnect.bat (previously posted)
    adbconnecttls.bat (previously posted)

    Since I could connect to Android over Wi-Fi without dealing with Google's security random-port-assignment friction, I got lazy and used the phone
    all week without even knowing where it is (it's probably in a drawer).

    The battery is 5Ah (which is impressive for a 2021 phone) so it basically
    lasts forever before I have to put it back on a 2-hour fast charger.

    Each day, for the past week, when I used WhatsApp on the mirror image
    on my desktop to watch videos of the great grandkids, WhatsApp would
    warn me in a daily countdown that I had 7 days, 6 days, etc. to update.

    What was surprising (to me) was the WhatsApp APK came from the Google Play Store repository (via Aurora) and yet the intent to update from WhatsApp
    shows the APK actually coming from a /1 folder on WhatsApp servers instead.
    curl.exe -L "https://www.whatsapp.com/android/1/WhatsApp.apk"

    Interestingly, when the expiry triggered today, the intent *changed*, which debugging showed is now coming from a /current folder on WhatsApp servers.
    curl.exe -L "https://www.whatsapp.com/current/WhatsApp.apk"

    By pulling the existing WhatsApp base.apk to the Windows desktop, we were
    able to confirm the /1 & /current folder APKs were identical, and we also confirmed exactly what version changes occurred compared to the base.apk.
    adb shell pm path com.whatsapp
    adb pull "/data/app/~~Q0AH2roDwIp1Sab6n2Zk7w==/com.whatsapp-SuTfaV-YTnPJBe_GGlwL0w==/base.apk"

    First we confirmed the two cryptographic signatures were identical between
    the Google Play Store repository version & the WhatsApp website version.
    apksigner.bat verify --print-certs {base,current}.apk

    Two distinct signing blocks appear because modern Android apps use
    multiple signing schemes (in this case, v2/v3/v4 & v1/JAR signing).

    For Android 13+
    DN: CN=WhatsApp LLC, O=WhatsApp LLC, L=Menlo Park, ST=California, C=US
    SHA-256 Digest: fb920d381bee1b2093f27dc8f13d994da629dc91887d0529b35c9a2dc4f4a6c2
    For older Android versions
    DN: CN=Brian Acton, OU=Engineering, O=WhatsApp Inc., L=Santa Clara, ST=California, C=US
    SHA-256 Digest: 3987d043d10aefaf5a8710b3671418fe57e0e19b653c9df82558feb5ffce5d44

    From that we concluded that we could safely swap out the current WhatsApp
    apk over the existing base WhatsApp apk, without losing data, using adb.
    adb install -r WhatsApp.apk

    But first we wanted to see what the version and SDK changes would be.
    aapt2.exe dump badging {base,current}.apk | findstr /C:"package:"
    Which told us how the version code & the version name would increment.

    To further debug, we wanted to use apkanalyzer, but my apkanalyzer came
    from the legacy Android SDK Tools package, whose launcher was a Unix shell script. In 2017, which is when my SDK tools were installed, Google did not provide a Windows .bat launcher alongside that particular copy and I didn't know yet, that was the reason there was only a Linux/macOS script.
    <https://developer.android.com/tools/apkanalyzer>

    It was basically a packaging oversight on the Google side for SDK releases.
    type C:\app\editor\android\sdk\tools\source.properties
    Pkg.UserSrc=false
    Pkg.Revision=26.1.1
    Platform.MinPlatformToolsRev=20
    Pkg.Dependencies=emulator
    Pkg.Path=tools
    Pkg.Desc=Android SDK Tools

    Since everyone reading this is ware that sometimes you only learn the right way of doing something by doing it the hard way by mistake, what happened
    next is I converted that 2017 developer history into the modern era of
    OpenJDK 17 & forced that Linux/macOS script to work on Windows.

    Linux/macOS:Windows before:after scripts below are therefore instructive.

    The main hurdle arose when attempting to run that ported apkanalyzer tool
    on Windows to inspect the APK metrics. Because I was using a legacy 2017
    SDK script running on modern OpenJDK 17, it broke at first, due to missing Java XML/JAXB modules. This required manually hunting down, downloading,
    and mapping a nested chain of dependency libraries (jaxb-runtime, jaxb-api, istack-commons-runtime, and jakarta.activation-api) directly into the
    custom batch file's classpath.

    However, after a bit of debugging, the Linux/macOS-to-Windows environment successfully stabilized such that the APK analysis completed seamlessly.

    Although a final storage constraint on the target device (brought on by a missing SD card) paused the ultimate installation step, the core
    engineering successfully built a robust Windows command-line apkanalyzer toolkit to analyze and troubleshoot over Wi-Fi or USB from the desktop.
    apkanalyzer.bat apk file-size base.apk
    adb shell df -h /data

    To that end, below are two files, the first of which is the working Google macOS/Linux apkanalyzer ported to Windows & the second is the original.

    gvim C:\app\editor\android\sdk\tools\bin\apkanalyzer.bat
    ===== < below is the working Windows apkanalyzer.bat script > =====
    :: --------------------------------------------------------------------
    :: apkanalyzer.bat
    :: Conversion of Android sdk Linux/macOS apkanalyzer script to Windows
    :: --------------------------------------------------------------------
    :: v2p0 20261022 Made APP_HOME portable using dynamic-script-path resolution
    :: v1p9 20261022 Shortened CLASSPATH using wildcard and multiline execution
    :: v1p8 20261022 Split CLASSPATH to overcome 1K-characters/line limits
    :: v1p7 20261022 Added and tested example help command syntax above
    :: v1p6 20261022 Added and tested example command syntax above
    :: v1p5 20261022 Added jakarta.activation-api.jar to Windows and classpath
    :: v1p4 20261022 Added istack-commons-runtime.jar to Windows and classpath
    :: v1p3 20261022 Added jaxb-api-2.3.1.jar to Windows and classpath
    :: v1p2 20261022 Added jaxb-runtime-2.3.5.jar to Windows and classpath
    :: v1p1 20261022 Added variables to set custom paths to dependencies
    :: v1p0 20261022 Convert C:\app\editor\android\sdk\tools\bin\apkanalyzer
    :: --------------------------------------------------------------------
    :: Tested on the base apk installed 20261022 using the commands below:
    :: C:\> curl.exe -L "https://www.whatsapp.com/android/current/WhatsApp.apk"
    :: C:\> adb -r WhatsApp.apk
    :: C:\> adb shell pm path com.whatsapp
    :: C:\> adb pull "... that path ... app-SuTfaV-YTnPJBe_GGlwL0w==/base.apk"
    :: --------------------------------------------------------------------
    :: To use:
    :: C:\> apkanalyzer.bat manifest application-id base.apk
    :: C:\> apkanalyzer.bat manifest version-code base.apk
    :: C:\> apkanalyzer.bat manifest version-name base.apk
    :: C:\> apkanalyzer.bat manifest min-sdk base.apk
    :: C:\> apkanalyzer.bat manifest target-sdk base.apk
    :: C:\> apkanalyzer.bat manifest permissions base.apk
    :: C:\> apkanalyzer.bat manifest debuggable base.apk
    :: C:\> apkanalyzer.bat apk summary base.apk
    :: C:\> apkanalyzer.bat apk file-size base.apk
    :: C:\> apkanalyzer.bat apk download-size base.apk
    :: C:\> apkanalyzer.bat files list base.apk
    :: C:\> apkanalyzer.bat manifest print base.apk
    :: C:\> apkanalyzer.bat dex list base.apk
    :: C:\> apkanalyzer.bat dex references base.apk
    :: C:\> apkanalyzer.bat apk features base.apk
    :: C:\> apkanalyzer.bat files cat --file "AndroidManifest.xml" base.apk
    :: C:\> apkanalyzer.bat dex packages base.apk
    :: C:\> apkanalyzer.bat resources packages base.apk
    :: C:\> apkanalyzer.bat resources names --config default --type string base.apk
    :: C:\> apkanalyzer.bat files list base.apk | findstr /i "\.xml"
    :: C:\> apkanalyzer.bat resources xml --file "res/xml/splits0.xml" base.apk
    :: C:\> apkanalyzer.bat --help
    :: C:\> apkanalyzer.bat apk --help
    :: C:\> apkanalyzer.bat dex --help
    :: C:\> apkanalyzer.bat resources --help
    :: --------------------------------------------------------------------
    @ECHO OFF
    setlocal

    REM Dynamically resolve Android SDK Tools Root Path from script location (\tools\bin\ -> \tools\)
    for %%I in ("%~dp0..") do set "APP_HOME=%%~fI"

    REM Custom OpenJDK 17 Path (Adjust if your Java path changes)
    set "JAVA_EXE=C:\Program Files\Microsoft\jdk-17.0.18.8-hotspot\bin\java.exe"

    REM All JARs in the SDK lib directory via wildcard
    set "CLASSPATH=%APP_HOME%\lib\*"

    "%JAVA_EXE%" "-Dcom.android.sdklib.toolsdir=%APP_HOME%" ^
    -classpath "%CLASSPATH%" ^
    com.android.tools.apk.analyzer.ApkAnalyzerCli %*

    endlocal
    :: end of apkanalyzer.bat

    ===== < above is the working Windows apkanalyzer.bat script > =====

    gvim C:\app\editor\android\sdk\tools\bin\apkanalyzer
    ===== < below is the original Linux/macOS apkanalyzer.bat script > =====
    #!/usr/bin/env sh

    ##############################################################################
    ##
    ## apkanalyzer start up script for UN*X
    ##
    ##############################################################################

    # Attempt to set APP_HOME
    # Resolve links: $0 may be a link
    PRG="$0"
    # Need this for relative symlinks.
    while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
    PRG="$link"
    else
    PRG=`dirname "$PRG"`"/$link"
    fi
    done
    SAVED="`pwd`"
    cd "`dirname \"$PRG\"`/.." >/dev/null
    APP_HOME="`pwd -P`"
    cd "$SAVED" >/dev/null

    APP_NAME="apkanalyzer"
    APP_BASE_NAME=`basename "$0"`

    # Add default JVM options here. You can also use JAVA_OPTS and APKANALYZER_OPTS to pass JVM options to this script.
    DEFAULT_JVM_OPTS='"-Dcom.android.sdklib.toolsdir=$APP_HOME"'

    # Use the maximum available, or set MAX_FD != -1 to use that value.
    MAX_FD="maximum"

    warn () {
    echo "$*"
    }

    die () {
    echo
    echo "$*"
    echo
    exit 1
    }

    # OS specific support (must be 'true' or 'false').
    cygwin=false
    msys=false
    darwin=false
    nonstop=false
    case "`uname`" in
    CYGWIN* )
    cygwin=true
    ;;
    Darwin* )
    darwin=true
    ;;
    MINGW* )
    msys=true
    ;;
    NONSTOP* )
    nonstop=true
    ;;
    esac

    CLASSPATH=$APP_HOME/lib/dvlib-26.0.0-dev.jar:$APP_HOME/lib/util-2.2.1.jar:$APP_HOME/lib/jimfs-1.1.jar:$APP_HOME/lib/annotations-13.0.jar:$APP_HOME/lib/ddmlib-26.0.0-dev.jar: \
    $APP_HOME/lib/repository-26.0.0-dev.jar:$APP_HOME/lib/sdk-common-26.0.0-dev.jar:$APP_HOME/lib/kotlin-stdlib-1.1.3-2.jar:$APP_HOME/lib/protobuf-java-3.0.0.jar: \
    $APP_HOME/lib/apkanalyzer-cli.jar:$APP_HOME/lib/gson-2.3.jar:$APP_HOME/lib/httpcore-4.2.5.jar:$APP_HOME/lib/dexlib2-2.2.1.jar:$APP_HOME/lib/commons-compress-1.12.jar: \
    $APP_HOME/lib/generator.jar:$APP_HOME/lib/error_prone_annotations-2.0.18.jar:$APP_HOME/lib/commons-codec-1.6.jar:$APP_HOME/lib/kxml2-2.3.0.jar:$APP_HOME/lib/httpmime-4.1.jar: \
    $APP_HOME/lib/annotations-12.0.jar:$APP_HOME/lib/bcpkix-jdk15on-1.56.jar:$APP_HOME/lib/jsr305-3.0.0.jar:$APP_HOME/lib/explainer.jar:$APP_HOME/lib/builder-model-3.0.0-dev.jar: \
    $APP_HOME/lib/baksmali-2.2.1.jar:$APP_HOME/lib/j2objc-annotations-1.1.jar:$APP_HOME/lib/layoutlib-api-26.0.0-dev.jar:$APP_HOME/lib/jcommander-1.64.jar: \
    $APP_HOME/lib/commons-logging-1.1.1.jar:$APP_HOME/lib/annotations-26.0.0-dev.jar:$APP_HOME/lib/builder-test-api-3.0.0-dev.jar:$APP_HOME/lib/animal-sniffer-annotations-1.14.jar: \
    $APP_HOME/lib/bcprov-jdk15on-1.56.jar:$APP_HOME/lib/httpclient-4.2.6.jar:$APP_HOME/lib/common-26.0.0-dev.jar:$APP_HOME/lib/jopt-simple-4.9.jar:$APP_HOME/lib/sdklib-26.0.0-dev.jar: \
    $APP_HOME/lib/apkanalyzer.jar:$APP_HOME/lib/shared.jar:$APP_HOME/lib/binary-resources.jar:$APP_HOME/lib/guava-22.0.jar

    # Determine the Java command to use to start the JVM.
    if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
    # IBM's JDK on AIX uses strange locations for the executables
    JAVACMD="$JAVA_HOME/jre/sh/java"
    else
    JAVACMD="$JAVA_HOME/bin/java"
    fi
    if [ ! -x "$JAVACMD" ] ; then
    die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

    Please set the JAVA_HOME variable in your environment to match the
    location of your Java installation."
    fi
    else
    JAVACMD="java"
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

    Please set the JAVA_HOME variable in your environment to match the
    location of your Java installation."
    fi

    # Increase the maximum file descriptors if we can.
    if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
    if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
    MAX_FD="$MAX_FD_LIMIT"
    fi
    ulimit -n $MAX_FD
    if [ $? -ne 0 ] ; then
    warn "Could not set maximum file descriptor limit: $MAX_FD"
    fi
    else
    warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
    fi

    # For Darwin, add options to specify how the application appears in the dock
    if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
    fi

    # For Cygwin, switch paths to Windows format before running java
    if $cygwin ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
    JAVACMD=`cygpath --unix "$JAVACMD"`

    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
    ROOTDIRS="$ROOTDIRS$SEP$dir"
    SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
    OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
    CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
    CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option

    if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
    eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
    else
    eval `echo args$i`="\"$arg\""
    fi
    i=$((i+1))
    done
    case $i in
    (0) set -- ;;
    (1) set -- "$args0" ;;
    (2) set -- "$args0" "$args1" ;;
    (3) set -- "$args0" "$args1" "$args2" ;;
    (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
    (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
    (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
    (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
    (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
    (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
    fi

    # Escape application args
    save () {
    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
    echo " "
    }
    APP_ARGS=$(save "$@")

    # Collect all arguments for the java command, following the shell quoting and substitution rules
    eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $APKANALYZER_OPTS -classpath "\"$CLASSPATH\"" com.android.tools.apk.analyzer.ApkAnalyzerCli "$APP_ARGS"

    # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
    if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
    cd "$(dirname "$0")"
    fi

    exec "$JAVACMD" "$@"
    ===== < above is the original Linux/macOS apkanalyzer.bat script > =====
    NOTE: The single-line CLASSPATH in the macOS/Linux script was broken
    artificially into multiple lines so that nntp servers don't barf.

    While I would never have done this had I known two hours ago what I now know, this still serves as a rare example of porting Linux/macOS
    legacy scripts to work in a modern JAVA-based Windows environment.

    As always, please add value where you feel the team would benefit
    from your knowledge, suggestions, fixes, corrections and insight.
    --
    Usenet allows purposefully helpful people to pool their experiences.
    --- Synchronet 3.21d-Linux NewsLink 1.2