RK3399预置APK及问题

背景

  • 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源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/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 配置,原始的如下:

    1
    2
    3
    4
    5
    6
    7
    ...
    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完整性是抛出了异常:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    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文件对比

有问题的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

正常的:

1
2
3
4
5
6
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安装失败提示证书有问题
Donate comment here