背景

  • Platform: RK3399
  • OS: Android7.1.2
  • Kernel: v4.4.103

正常流程

按照官方文档,步骤如下:

  1. device/rockchip/rk3399/xxx/目录下新建preinstall_delpreinstall_del_foreverpreinstall文件夹。
    • preinstall_del:可卸载预装,恢复出厂后应用会恢复
    • preinstall_del_forever:可卸载预装,恢复出厂后应用不会恢复
    • preinstall:不可卸载预装
  2. 拷贝预安装的APK到上述新建的文件夹中,注意文件名尽量使用英文,避免空格
  3. 编译。auto_generator.py会在编译过程中,将拷贝的apk解包,自动生成编译mk文件。编译完之后预置的APK会拷贝到system固件中,烧录后,系统启动的时候会自动安装到data/app目录中(通过PackageManager)

注:预置的 APK 应用需要得到对应厂商授权

auto_generator.py源码:

#!/usr/bin/env python
import sys
import os
import re
import zipfile
import shutil

templet = """include $(CLEAR_VARS)
LOCAL_MODULE := %s
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR)/%s
LOCAL_SRC_FILES := $(LOCAL_MODULE)$(COMMON_ANDROID_PACKAGE_SUFFIX)
LOCAL_CERTIFICATE := PRESIGNED
#LOCAL_DEX_PREOPT := false
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
LOCAL_JNI_SHARED_LIBRARIES_ABI := %s
MY_LOCAL_PREBUILT_JNI_LIBS := %s
MY_APP_LIB_PATH := $(TARGET_OUT_VENDOR)/%s/$(LOCAL_MODULE)/lib/$(LOCAL_JNI_SHARED_LIBRARIES_ABI)
ifneq ($(LOCAL_JNI_SHARED_LIBRARIES_ABI), None)
$(warning MY_APP_LIB_PATH=$(MY_APP_LIB_PATH))
LOCAL_POST_INSTALL_CMD := \
    mkdir -p $(MY_APP_LIB_PATH) \
    $(foreach lib, $(MY_LOCAL_PREBUILT_JNI_LIBS), ; cp -f $(LOCAL_PATH)/$(lib) $(MY_APP_LIB_PATH)/$(notdir $(lib)))
endif
include $(BUILD_PREBUILT)

"""

copy_app_templet = """LOCAL_PATH := $(my-dir)
include $(CLEAR_VARS)
LOCAL_APK_NAME := %s
LOCAL_POST_PROCESS_COMMAND := $(shell mkdir -p $(TARGET_OUT_VENDOR)/%s/$(LOCAL_APK_NAME) && cp $(LOCAL_PATH)/$(LOCAL_APK_NAME).apk $(TARGET_OUT_VENDOR)/%s/$(LOCAL_APK_NAME)/)
"""

def main(argv):
    preinstall_dir = os.path.join(argv[1],argv[2])
    if os.path.exists(preinstall_dir):
        #Use to include modules
        isfound = 'not_found_lib'
        include_path = preinstall_dir + '/preinstall.mk'
        android_path = preinstall_dir + '/Android.mk'

        if os.path.exists(include_path):
            os.remove(include_path)
        if os.path.exists(android_path):
            os.remove(android_path)

        includefile = file(include_path, 'w')
        androidfile = file(android_path, 'w')

        androidfile.write("include $(call all-subdir-makefiles)\n\n")

        MY_LOCAL_PREBUILT_JNI_LIBS = '\\' + '\n'

        for root, dirs, files in os.walk(preinstall_dir):
            for file_name in files:
                p = re.compile(r'\S*(?=.apk\b)')
                found = p.search(file_name)
                if found:
                    include_apk_path = preinstall_dir + '/' + found.group()
                    makefile_path = include_apk_path + '/Android.mk'
                    apk = preinstall_dir + '/' + found.group() + '.apk'
                    try:
                        zfile = zipfile.ZipFile(apk,'r')
                    except:
                        if os.path.exists(include_apk_path):
                            shutil.rmtree(include_apk_path)
                        os.makedirs(include_apk_path)
                        apkpath = preinstall_dir + '/' + found.group() + '/'
                        shutil.move(apk,apkpath)
                        makefile = file(makefile_path,'w')
                        makefile.write("LOCAL_PATH := $(my-dir)\n\n")
                        makefile.write(templet % (found.group(),argv[3],'None',MY_LOCAL_PREBUILT_JNI_LIBS,argv[3]))
                        continue
                    for lib_name in zfile.namelist():
                        include_apklib_path = include_apk_path + '/lib' + '/arm'
                        if os.path.exists(include_apk_path):
                            shutil.rmtree(include_apk_path)
                        os.makedirs(include_apklib_path)
                        makefile = file(makefile_path,'w')
                        makefile.write("LOCAL_PATH := $(my-dir)\n\n")
                        apkpath = preinstall_dir + '/' + found.group() + '/'
                    for lib_name in zfile.namelist():
                        lib = re.compile(r'\A(lib/armeabi-v7a/)+?')
                        find_name = 'lib/armeabi-v7a/'
                        if not cmp(lib_name,find_name):
                            continue
                        libfound = lib.search(lib_name)
                        if libfound:
                            isfound = 'armeabi-v7a'
                            data = zfile.read(lib_name)
                            string = lib_name.split(libfound.group())
                            libfile = include_apklib_path + '/' + string[1]
                            MY_LOCAL_PREBUILT_JNI_LIBS += '\t' + 'lib/arm' + '/' + string[1] + '\\' + '\n'
                            if(os.path.isdir(libfile)):
                                continue
                            else:
                                includelib = file(libfile,'w')
                                includelib.write(data)
                    if not cmp(isfound,'not_found_lib'):
                        for lib_name in zfile.namelist():
                            lib = re.compile(r'\A(lib/armeabi/)+?')
                            find_name = 'lib/armeabi/'
                            if not cmp(lib_name,find_name):
                                continue
                            libfound = lib.search(lib_name)
                            if libfound:
                                data = zfile.read(lib_name)
                                string = lib_name.split(libfound.group())
                                libfile = include_apklib_path + '/' + string[1]
                                MY_LOCAL_PREBUILT_JNI_LIBS += '\t' + 'lib/arm' + '/' + string[1] + '\\' + '\n'
                                if(os.path.isdir(libfile)):
                                    continue
                                else:
                                    includelib = file(libfile,'w')
                                    includelib.write(data)
                    tmp_jni_libs = '\\' + '\n'
                    if not cmp(MY_LOCAL_PREBUILT_JNI_LIBS,tmp_jni_libs):
                        nolibpath = preinstall_dir + '/' + found.group() + '/lib'
                        shutil.rmtree(nolibpath)
                        makefile.write(templet % (found.group(),argv[3],'None',MY_LOCAL_PREBUILT_JNI_LIBS,argv[3]))
                    else:
                        if argv[2]=='preinstall_del' or argv[2]=='preinstall_del_forever':
                            makefile.write(copy_app_templet % (found.group(), argv[3], argv[3]))
                        else:
                            makefile.write(templet % (found.group(),argv[3],'arm',MY_LOCAL_PREBUILT_JNI_LIBS,argv[3]))
                    shutil.move(apk,apkpath)
                    isfound = 'not_found_lib'
                    MY_LOCAL_PREBUILT_JNI_LIBS = '\\' + '\n'
                    makefile.close()
            break
        for root, dirs,files in os.walk(preinstall_dir):
            for dir_file in dirs:
                includefile.write('PRODUCT_PACKAGES += %s\n' %dir_file)
            break
        includefile.close()

if __name__=="__main__":
  main(sys.argv)

排坑记录

问题

预置打包我们公司一个产品的APK进去,发现系统起来以后没有安装。查看日志,发现报错:

PackageManager: Failed to parse /system/vendor/bundled_uninstall_back-app/xxx: Failed to collect certificates from /system/vendor/bundled_uninstall_back-app/xxx/xxx.apk

分析过程

  1. 参照网上别人遇到的坑,由于apk的Signature Scheme v2标识导致。但我们的APK是debug版本,使用的是开发工具默认的签名,且通过查看源码(/frameworks/base/core/java/android/content/pm/PackageParser.java),如果是Signature Scheme v2 signature问题,log后面会加"using APK Signature Scheme v2",所以这个原因排除。

  2. 通过adb install手动安装没问题,如果是上面的签名问题,手动安装都会报错,所有进一步证实了前面一点。如果手动安装没有问题,则很有可能是预置apk后系统编译阶段出问题。

  3. 进一步分析上面的auto_generator.py脚本,发现该脚本不支持仅包含arm64-v8a库的apk,如果仅包含arm64库,lib不会被解压抽取

  4. 进一步分析APK的编译配置。在该APK的build.gradle文件中指定了ABI 配置,原始的如下:

    ...
    defaultConfig {
        ...
        ndk {
                abiFilters 'armeabi', 'arm64-v8a'
            }
    }

    而刚好该apk依赖的库文件里面包括'armeabi-v7a',且配置里面没有添加'armeabi-v7a'。解压编译出来的APK,发现确实没有/lib/armeabi-v7a目录,仅有/lib/arm64-v8a。解压后面正常的APK,2个目录都有。

  5. 进一步分析PackageManager源码,出现Failed to collect certificates from的地方共有4处,log后面加apkPath的有3处,且上面Signature Scheme v2的地方排除了1处。主要是在检查APK完整性是抛出了异常:

    StrictJarFile jarFile = null;
    try {
        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor");
        // Ignore signature stripping protections when verifying APKs from system partition.
        // For those APKs we only care about extracting signer certificates, and don't care
        // about verifying integrity.
        boolean signatureSchemeRollbackProtectionsEnforced =
                (parseFlags & PARSE_IS_SYSTEM_DIR) == 0;
        jarFile = new StrictJarFile(
                apkPath,
                !verified, // whether to verify JAR signature
                signatureSchemeRollbackProtectionsEnforced);
        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
    
        // Always verify manifest, regardless of source
        final ZipEntry manifestEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME);
        if (manifestEntry == null) {
            throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
                    "Package " + apkPath + " has no manifest");
        }
    
        // Optimization: early termination when APK already verified
        if (verified) {
            return;
        }
    
        // APK's integrity needs to be verified using JAR signature scheme.
        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV1");
        final List<ZipEntry> toVerify = new ArrayList<>();
        toVerify.add(manifestEntry);
    
        // If we're parsing an untrusted package, verify all contents
        if ((parseFlags & PARSE_IS_SYSTEM_DIR) == 0) {
            final Iterator<ZipEntry> i = jarFile.iterator();
            while (i.hasNext()) {
                final ZipEntry entry = i.next();
    
                if (entry.isDirectory()) continue;
    
                final String entryName = entry.getName();
                if (entryName.startsWith("META-INF/")) continue;
                if (entryName.equals(ANDROID_MANIFEST_FILENAME)) continue;
    
                toVerify.add(entry);
            }
        }
    
        // Verify that entries are signed consistently with the first entry
        // we encountered. Note that for splits, certificates may have
        // already been populated during an earlier parse of a base APK.
        for (ZipEntry entry : toVerify) {
            final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
            if (ArrayUtils.isEmpty(entryCerts)) {
                throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                        "Package " + apkPath + " has no certificates at entry "
                        + entry.getName());
            }
            final Signature[] entrySignatures = convertToSignatures(entryCerts);
    
            if (pkg.mCertificates == null) {
                pkg.mCertificates = entryCerts;
                pkg.mSignatures = entrySignatures;
                pkg.mSigningKeys = new ArraySet<PublicKey>();
                for (int i=0; i < entryCerts.length; i++) {
                    pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
                }
            } else {
                if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
                    throw new PackageParserException(
                            INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
                                    + " has mismatched certificates at entry "
                                    + entry.getName());
                }
            }
        }
        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
    } catch (GeneralSecurityException e) {
        throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
                "Failed to collect certificates from " + apkPath, e);
    } catch (IOException | RuntimeException e) {
        throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                "Failed to collect certificates from " + apkPath, e);
    } finally {
        closeQuietly(jarFile);
    }

    生成的Android.mk文件对比

    有问题的:

    LOCAL_PATH := $(my-dir)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := LVI8031A_error
    LOCAL_MODULE_CLASS := APPS
    LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR)/bundled_uninstall_back-app
    LOCAL_SRC_FILES := $(LOCAL_MODULE)$(COMMON_ANDROID_PACKAGE_SUFFIX)
    LOCAL_CERTIFICATE := PRESIGNED
    #LOCAL_DEX_PREOPT := false
    LOCAL_MODULE_TAGS := optional
    LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
    LOCAL_JNI_SHARED_LIBRARIES_ABI := None
    MY_LOCAL_PREBUILT_JNI_LIBS := \
    
    MY_APP_LIB_PATH := $(TARGET_OUT_VENDOR)/bundled_uninstall_back-app/$(LOCAL_MODULE)/lib/$(LOCAL_JNI_SHARED_LIBRARIES_ABI)
    ifneq ($(LOCAL_JNI_SHARED_LIBRARIES_ABI), None)
    $(warning MY_APP_LIB_PATH=$(MY_APP_LIB_PATH))
    LOCAL_POST_INSTALL_CMD :=     mkdir -p $(MY_APP_LIB_PATH)     $(foreach lib, $(MY_LOCAL_PREBUILT_JNI_LIBS), ; cp -f $(LOCAL_PATH)/$(lib) $(MY_APP_LIB_PATH)/$(notdir $(lib)))
    endif
    include $(BUILD_PREBUILT)
    

    正常的:

    LOCAL_PATH := $(my-dir)
    
    LOCAL_PATH := $(my-dir)
    include $(CLEAR_VARS)
    LOCAL_APK_NAME := LVI8031A
    LOCAL_POST_PROCESS_COMMAND := $(shell mkdir -p $(TARGET_OUT_VENDOR)/bundled_uninstall_back-app/$(LOCAL_APK_NAME) && cp $(LOCAL_PATH)/$(LOCAL_APK_NAME).apk $(TARGET_OUT_VENDOR)/bundled_uninstall_back-app/$(LOCAL_APK_NAME)/)

    解决

    在配置中删除ndk相关的配置,或者在ndk的配置中添加'armeabi-v7a',均已验证通过。

扩展

APK安装过程
APK签名校验过程

参考

  1. [RK3399] [android 7.1.2]添加预装应用
  2. [RK3399][Android7.1] 调试笔记 — app安装失败提示证书有问题