Skip to content

Commit ba7bd22

Browse files
authored
Merge branch 'master' into fix/refactor-db-retry-timer
2 parents db6c8b1 + 1602401 commit ba7bd22

File tree

4 files changed

+240
-2
lines changed

4 files changed

+240
-2
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Backtrace Android Release Notes
22

3+
## Version 3.10.5
4+
- Fix native-library loading for AAB / split APK installs
5+
36
## Version 3.10.4
47
- Add support for Crashpad offline native crash replay
58

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package backtraceio.library.crashHandler;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import androidx.test.ext.junit.runners.AndroidJUnit4;
6+
import androidx.test.platform.app.InstrumentationRegistry;
7+
8+
import org.junit.Test;
9+
import org.junit.runner.RunWith;
10+
11+
import java.io.File;
12+
import java.io.FileOutputStream;
13+
import java.util.List;
14+
import java.util.zip.ZipEntry;
15+
import java.util.zip.ZipOutputStream;
16+
17+
import android.content.pm.ApplicationInfo;
18+
19+
import backtraceio.library.common.AbiHelper;
20+
import backtraceio.library.models.nativeHandler.CrashHandlerConfiguration;
21+
22+
@RunWith(AndroidJUnit4.class)
23+
public class CrashHandlerNativeLibraryResolutionTest {
24+
25+
private static final String LIB = "libbacktrace-native.so";
26+
27+
private File tempDir(String name) {
28+
File cache = InstrumentationRegistry.getInstrumentation()
29+
.getTargetContext().getCacheDir();
30+
File d = new File(cache, name);
31+
//noinspection ResultOfMethodCallIgnored
32+
d.mkdirs();
33+
return d;
34+
}
35+
36+
private File makeApk(File dir, String name, String abi, boolean includeLib) throws Exception {
37+
File apk = new File(dir, name);
38+
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(apk))) {
39+
if (includeLib) {
40+
String entry = "lib/" + abi + "/" + LIB;
41+
zos.putNextEntry(new ZipEntry(entry));
42+
zos.write(new byte[]{1, 2, 3, 4});
43+
zos.closeEntry();
44+
} else {
45+
zos.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
46+
zos.write(0);
47+
zos.closeEntry();
48+
}
49+
}
50+
return apk;
51+
}
52+
53+
private static String getEnv(List<String> env, String key) {
54+
String prefix = key + "=";
55+
for (String kv : env) {
56+
if (kv.startsWith(prefix)) return kv.substring(prefix.length());
57+
}
58+
return null;
59+
}
60+
61+
@Test
62+
public void usesSplitWhenBaseDoesNotContainLib() throws Exception {
63+
final String abi = AbiHelper.getCurrentAbi();
64+
File root = tempDir("bt_split_pref");
65+
File base = makeApk(root, "base.apk", abi, false);
66+
File split = makeApk(root, "split_config." + abi + ".apk", abi, true);
67+
68+
ApplicationInfo ai = new ApplicationInfo();
69+
ai.sourceDir = base.getAbsolutePath();
70+
ai.splitSourceDirs = new String[]{split.getAbsolutePath()};
71+
ai.nativeLibraryDir = "/nonexistent";
72+
73+
CrashHandlerConfiguration cfg = new CrashHandlerConfiguration();
74+
List<String> env = cfg.getCrashHandlerEnvironmentVariables(ai);
75+
76+
String libPath = getEnv(env, CrashHandlerConfiguration.BACKTRACE_CRASH_HANDLER);
77+
String apkLib = split.getAbsolutePath() + "!/lib/" + abi + "/" + LIB;
78+
assertEquals(apkLib, libPath);
79+
80+
String classPath = getEnv(env, "CLASSPATH");
81+
assertEquals(base.getAbsolutePath(), classPath);
82+
}
83+
84+
@Test
85+
public void prefersExtractedOverApkContainers() throws Exception {
86+
final String abi = AbiHelper.getCurrentAbi();
87+
File root = tempDir("bt_extracted_pref");
88+
File base = makeApk(root, "base.apk", abi, false);
89+
File split = makeApk(root, "split_config." + abi + ".apk", abi, true);
90+
91+
File nativeDir = new File(root, "lib/" + abi);
92+
//noinspection ResultOfMethodCallIgnored
93+
nativeDir.mkdirs();
94+
File extracted = new File(nativeDir, LIB);
95+
try (FileOutputStream fos = new FileOutputStream(extracted)) {
96+
fos.write(new byte[]{9, 9, 9});
97+
}
98+
99+
ApplicationInfo ai = new ApplicationInfo();
100+
ai.sourceDir = base.getAbsolutePath();
101+
ai.splitSourceDirs = new String[]{split.getAbsolutePath()};
102+
ai.nativeLibraryDir = nativeDir.getAbsolutePath();
103+
104+
CrashHandlerConfiguration cfg = new CrashHandlerConfiguration();
105+
List<String> env = cfg.getCrashHandlerEnvironmentVariables(ai);
106+
107+
String libPath = getEnv(env, CrashHandlerConfiguration.BACKTRACE_CRASH_HANDLER);
108+
assertEquals(extracted.getAbsolutePath(), libPath);
109+
110+
String classPath = getEnv(env, "CLASSPATH");
111+
assertEquals(base.getAbsolutePath(), classPath);
112+
}
113+
114+
@Test
115+
public void prefersBaseWhenBaseContainsLib() throws Exception {
116+
final String abi = AbiHelper.getCurrentAbi();
117+
File root = tempDir("bt_base_pref");
118+
File base = makeApk(root, "base.apk", abi, true);
119+
File split = makeApk(root, "split_config." + abi + ".apk", abi, true);
120+
121+
ApplicationInfo ai = new ApplicationInfo();
122+
ai.sourceDir = base.getAbsolutePath();
123+
ai.splitSourceDirs = new String[]{split.getAbsolutePath()};
124+
ai.nativeLibraryDir = "/nonexistent";
125+
126+
CrashHandlerConfiguration cfg = new CrashHandlerConfiguration();
127+
List<String> env = cfg.getCrashHandlerEnvironmentVariables(ai);
128+
129+
String libPath = getEnv(env, CrashHandlerConfiguration.BACKTRACE_CRASH_HANDLER);
130+
String apkLib = base.getAbsolutePath() + "!/lib/" + abi + "/" + LIB;
131+
assertEquals(apkLib, libPath);
132+
}
133+
134+
@Test
135+
public void fallsBackToBasePathWhenNoContainerHasLib() throws Exception {
136+
final String abi = AbiHelper.getCurrentAbi();
137+
File root = tempDir("bt_fallback");
138+
File base = makeApk(root, "base.apk", abi, false);
139+
140+
ApplicationInfo ai = new ApplicationInfo();
141+
ai.sourceDir = base.getAbsolutePath();
142+
ai.splitSourceDirs = null;
143+
ai.nativeLibraryDir = "/nonexistent";
144+
145+
CrashHandlerConfiguration cfg = new CrashHandlerConfiguration();
146+
List<String> env = cfg.getCrashHandlerEnvironmentVariables(ai);
147+
148+
String libPath = getEnv(env, CrashHandlerConfiguration.BACKTRACE_CRASH_HANDLER);
149+
String apkLib = base.getAbsolutePath() + "!/lib/" + abi + "/" + LIB;
150+
assertEquals(apkLib, libPath);
151+
}
152+
}
Submodule crashpad updated 366 files

backtrace-library/src/main/java/backtraceio/library/models/nativeHandler/CrashHandlerConfiguration.java

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
import android.text.TextUtils;
55

66
import java.io.File;
7+
import java.io.IOException;
78
import java.util.ArrayList;
89
import java.util.Arrays;
910
import java.util.HashSet;
1011
import java.util.List;
1112
import java.util.Map;
1213
import java.util.Set;
14+
import java.util.zip.ZipEntry;
15+
import java.util.zip.ZipFile;
1316

1417
import backtraceio.library.common.AbiHelper;
1518
import backtraceio.library.services.BacktraceCrashHandlerRunner;
@@ -36,9 +39,42 @@ public String getClassPath() {
3639
}
3740

3841
public List<String> getCrashHandlerEnvironmentVariables(ApplicationInfo applicationInfo) {
39-
return getCrashHandlerEnvironmentVariables(applicationInfo.sourceDir, applicationInfo.nativeLibraryDir, AbiHelper.getCurrentAbi());
42+
final String classPathApk = applicationInfo.sourceDir;
43+
final String nativeLibraryDirPath = applicationInfo.nativeLibraryDir;
44+
final String arch = AbiHelper.getCurrentAbi();
45+
46+
final List<String> environmentVariables = new ArrayList<>();
47+
48+
// system environment variables
49+
for (Map.Entry<String, String> variable : System.getenv().entrySet()) {
50+
environmentVariables.add(String.format("%s=%s", variable.getKey(), variable.getValue()));
51+
}
52+
53+
// LD_LIBRARY_PATH
54+
File nativeLibraryDirectory = new File(nativeLibraryDirPath);
55+
File allNativeLibrariesDirectory = nativeLibraryDirectory.getParentFile();
56+
String allPossibleLibrarySearchPaths = TextUtils.join(File.pathSeparator, new String[]{
57+
nativeLibraryDirPath,
58+
allNativeLibrariesDirectory.getPath(),
59+
System.getProperty("java.library.path"),
60+
"/data/local"
61+
});
62+
63+
final String backtraceNativeLibraryPath = resolveBacktraceNativeLibraryPath(applicationInfo, arch);
64+
65+
environmentVariables.add(String.format("CLASSPATH=%s", classPathApk));
66+
environmentVariables.add(String.format("%s=%s", BACKTRACE_CRASH_HANDLER, backtraceNativeLibraryPath));
67+
environmentVariables.add(String.format("LD_LIBRARY_PATH=%s", allPossibleLibrarySearchPaths));
68+
environmentVariables.add("ANDROID_DATA=/data");
69+
70+
return environmentVariables;
4071
}
4172

73+
/**
74+
* @deprecated Prefer {@link #getCrashHandlerEnvironmentVariables(android.content.pm.ApplicationInfo)} which correctly resolves split APKs on GooglePlay/AAB installs.
75+
* This method may be removed in a future release.
76+
*/
77+
@Deprecated
4278
public List<String> getCrashHandlerEnvironmentVariables(String apkPath, String nativeLibraryDirPath, String arch) {
4379
final List<String> environmentVariables = new ArrayList<>();
4480

@@ -86,4 +122,51 @@ private String getBacktraceNativeLibraryPath(String nativeLibraryDirPath, String
86122
? backtraceNativeLibraryPath
87123
: String.format("%s!/lib/%s/%s", apkPath, arch, BACKTRACE_NATIVE_LIBRARY_NAME);
88124
}
125+
126+
/**
127+
* Resolve native lib container:
128+
* extracted dir if present,
129+
* base.apk if it contains the entry,
130+
* first split that contains the entry,
131+
* fallback to base.apk path format.
132+
*/
133+
private String resolveBacktraceNativeLibraryPath(ApplicationInfo appInfo, String arch) {
134+
final String entry = "lib/" + arch + "/" + BACKTRACE_NATIVE_LIBRARY_NAME;
135+
136+
// extracted dir if present
137+
if (appInfo.nativeLibraryDir != null) {
138+
File extracted = new File(appInfo.nativeLibraryDir, BACKTRACE_NATIVE_LIBRARY_NAME);
139+
if (extracted.exists()) {
140+
return extracted.getAbsolutePath();
141+
}
142+
}
143+
144+
// base.apk if it contains the lib
145+
if (apkContains(appInfo.sourceDir, entry)) {
146+
return appInfo.sourceDir + "!/" + entry;
147+
}
148+
149+
// first split that contains the entry
150+
if (appInfo.splitSourceDirs != null) {
151+
for (String split : appInfo.splitSourceDirs) {
152+
if (apkContains(split, entry)) {
153+
return split + "!/" + entry;
154+
}
155+
}
156+
}
157+
158+
// fallback to base.apk path format
159+
return appInfo.sourceDir + "!/" + entry;
160+
}
161+
162+
private static boolean apkContains(String apkPath, String entry) {
163+
if (apkPath == null || apkPath.isEmpty()) return false;
164+
try (ZipFile zf = new ZipFile(apkPath)) {
165+
ZipEntry ze = zf.getEntry(entry);
166+
return ze != null;
167+
} catch (IOException ignored) {
168+
return false;
169+
}
170+
}
171+
89172
}

0 commit comments

Comments
 (0)