tweaselORG / appstraction

An abstraction layer for common instrumentation functions (e.g. installing and starting apps, setting preferences, etc.) on Android and iOS.
MIT License
5 stars 1 forks source link

Installing split APKs #4

Closed baltpeter closed 1 year ago

baltpeter commented 1 year ago

We download Android apps as split APKs. For example, downloading DB Navigator using googleplay yields the following files:

de.hafas.android.db-221000003.apk
de.hafas.android.db-config.de-221000003.apk
de.hafas.android.db-config.mdpi-221000003.apk
de.hafas.android.db-config.x86-221000003.apk

These tend to include APKs for different architectures (de.hafas.android.db-config.x86-221000003.apk for x86 in this case). We were previously using an x86_64 emulator running Android 11 (API level 30). Here, we could just install all split APKs:

adb install-multiple *.apk

This works even when there are split APKs for architectures not supported by the device/emulator. This seems to have changed with Android 13 (API level 33). Now, running the above command results in an error:

adb: failed to finalize session
Failure [INSTALL_FAILED_NO_MATCHING_ABIS: INSTALL_FAILED_NO_MATCHING_ABIS: Failed to extract native libraries, res=-113]

We now have to explicitly specify only the parts that are actually supported by the device. So, in this case for an x86_64 emulator:

adb install-multiple de.hafas.android.db-221000003.apk de.hafas.android.db-config.de-221000003.apk de.hafas.android.db-config.mdpi-221000003.apk

That's quite annoying. Hopefully, there is a better way.

Update: I misunderstood things a little here.

mal-tee commented 1 year ago

are you aware of the -s flag of googleplay?

-s single APK

baltpeter commented 1 year ago

@mal-tee I'm a bit wary of that because I don't really understand what it does (mostly: does that ask the server for a single APK or does it do some kind of merging client-side?). But definitely something to consider, thanks!

baltpeter commented 1 year ago

Interestingly, I just observed the same thing on a physical phone running Android 10.

baltpeter commented 1 year ago

ChatGPT suggested:

You can use a tool such as aapt or apksigner to list the CPU architectures that an APK supports. For example, to list the supported CPU architectures of the de.hafas.android.db-221000003.apk file, you can use the following command:

aapt dump badging de.hafas.android.db-221000003.apk | grep -E 'package|native'

This will output information about the package and the native libraries it contains. Look for the line that starts with native-code to see the supported CPU architectures.

And, indeed:

❯ for f in *.apk; do echo "$f:"; aapt dump badging $f | grep -E 'package|native'; echo "\n"; done
de.hafas.android.db-221200000.apk:
package: name='de.hafas.android.db' versionCode='221200000' versionName='22.12.p00.00' compileSdkVersion='33' compileSdkVersionCodename='13'

de.hafas.android.db-config.de-221200000.apk:
package: name='de.hafas.android.db' versionCode='221200000' versionName='' split='config.de'

de.hafas.android.db-config.mdpi-221200000.apk:
package: name='de.hafas.android.db' versionCode='221200000' versionName='' split='config.mdpi'

de.hafas.android.db-config.x86-221200000.apk:
package: name='de.hafas.android.db' versionCode='221200000' versionName='' split='config.x86'
native-code: 'x86'
baltpeter commented 1 year ago

I'll add support for parsing the architecture to parseAppMeta().

I noticed that the version is currently being parsed incorrectly for split APKs (where the version is empty):

{ id: 'de.hafas.android.db', version: "' split=", architecture: 'x86' }
baltpeter commented 1 year ago

I don't know yet what the possible values for native-code are, and whether there can be native code for more than one architecture in an APK.

So, I ran this little script on ~9000 split APKs:

import glob from 'glob';
import { parseAppMeta } from './index';

(async () => {
    const counts = {};

    const files = glob.sync('/media/benni/storage2/tmp/ma-apps/android/**/*.apk');

    for (const file of files) {
        const appMeta = await parseAppMeta(file);

        if (!appMeta) {
            console.log('Invalid app:', file);
            continue;
        }

        if (!counts[appMeta.architectures]) counts[appMeta.architectures] = 0;
        counts[appMeta.architectures]++;
    }

    console.log();
    console.log(counts);
})();

Result:

{
  undefined: 6357,
  x86: 1169,
  'armeabi-v7a': 618,
  'arm64-v8a': 705,
  armeabi: 5,
  '': 1,
  '.DS_Store': 1
}
baltpeter commented 1 year ago

Actually, that wasn't quite correct. In parseAppMeta(), I matched on /native-code: '(.+)'/. But that way, I couldn't be sure whether multiple values are possible. So, I changed that to /native-code: (.+)/ and re-ran.

And indeed:

{
  undefined: 6357,
  "'x86'": 1168,
  "'armeabi-v7a'": 616,
  "'arm64-v8a' 'armeabi' 'armeabi-v7a' 'mips' 'mips64' 'x86' 'x86_64'": 64,
  "'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64'": 291,
  "'arm64-v8a' 'armeabi-v7a' 'mips' 'mips64' 'x86' 'x86_64'": 8,
  "'arm64-v8a' 'armeabi-v7a'": 179,
  "'arm64-v8a' 'armeabi' 'armeabi-v7a' 'x86' 'x86_64'": 81,
  "'arm64-v8a' 'armeabi' 'armeabi-v7a'": 11,
  "'arm64-v8a' 'armeabi-v7a' 'x86_64'": 12,
  "'arm64-v8a' 'armeabi' 'armeabi-v7a' 'mips' 'x86' 'x86_64'": 29,
  "'armeabi'": 5,
  "'arm64-v8a' 'armeabi-v7a' 'mips' 'x86' 'x86_64'": 15,
  "'arm64-v8a' 'armeabi' 'mips' 'x86' 'x86_64'": 1,
  "'arm64-v8a' 'armeabi'": 9,
  "'arm64-v8a' 'armeabi' 'armeabi-v7a' 'x86'": 1,
  "'arm64-v8a' 'armeabi-v7a' 'commons-io-2.4.jar' 'x86' 'x86_64'": 1,
  "'arm64-v8a' 'armeabi' 'armeabi-v7a' 'x86_64'": 1,
  "'armeabi-v7a' 'x86'": 2,
  "'x86' 'x86_64'": 1,
  "'' 'x86'": 1,
  "'arm64-v8a' 'armeabi-v7a' 'commons-io-2.4.jar'": 1,
  "'arm64-v8a' 'armeabi-v7a' 'commons-codec-1.8.jar' 'x86' 'x86_64'": 1,
  "'.DS_Store' 'arm64-v8a' 'armeabi' 'armeabi-v7a' 'mips' 'x86' 'x86_64'": 1
}
baltpeter commented 1 year ago

So, there can be native code for multiple architectures in one APK.

baltpeter commented 1 year ago

My current implementation in #52 is not quite correct.

I am using getprop ro.product.cpu.abi to check whether the device supports a particular architecture. But instead, I need to check ro.product.cpu.abilist, because a device can support more than its primary architecture:

❯ adb shell getprop | grep ro.product.cpu.abi
[ro.product.cpu.abi]: [x86_64]
[ro.product.cpu.abilist]: [x86_64,x86,arm64-v8a,armeabi-v7a,armeabi]
[ro.product.cpu.abilist32]: [x86,armeabi-v7a,armeabi]
[ro.product.cpu.abilist64]: [x86_64,arm64-v8a]
baltpeter commented 1 year ago

My observations in #54 explain why I initially thought this was a problem with Android 13 and was then confused when I observed the same thing on a physical Android 10 phone.

My assumption that install-multiple automatically filtered out split APKs for the wrong architecture at any point was wrong. It never did. I only thought it did because the Android 11 emulator I used had support for all relevant architectures:

❯ adb shell getprop | grep ro.product.cpu.abi
[ro.product.cpu.abi]: [x86_64]
[ro.product.cpu.abilist]: [x86_64,x86,arm64-v8a,armeabi-v7a,armeabi]
[ro.product.cpu.abilist32]: [x86,armeabi-v7a,armeabi]
[ro.product.cpu.abilist64]: [x86_64,arm64-v8a]

As such, it was able to install split APKs for all those architectures just fine. Meanwhile the Android 13 emulator and the Android 10 phone only supported x86_64 or arm64-v8a, respectively.

This means that it is still relevant that we fix this problem, but for different reasons than I initially thought. :D

baltpeter commented 1 year ago

My current implementation in #52 is not quite correct.

Fixed with https://github.com/tweaselORG/appstraction/compare/797271dcc7f77f404339cdfb4094d68c4c79978c..509f39c28ac89fb18e607f2a93045cc0e347eb93.