Skip to content

Commit

Permalink
Fixes linkedin#128
Browse files Browse the repository at this point in the history
Correctly guesses data directory when running in secondary users (user ID >= 10) in Android.
  • Loading branch information
mahuna13 authored and drewhannay committed Dec 5, 2018
1 parent 1e417ef commit 8ff85ed
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.android.dx;

import android.os.Build;
import android.os.Process;
import org.junit.Test;

import java.io.File;
Expand Down Expand Up @@ -48,6 +49,14 @@ public void testGuessCacheDir_NotWriteableSkipped() {
.shouldGive("/data/data/d.e.f/cache");
}

@Test
public void testGuessCacheDir_ForSecondaryUser() {
guessCacheDirFor("/data/app/a.b.c.apk:/data/app/d.e.f.apk")
.withNonWriteable("/data/data/a.b.c", "/data/data/d.e.f")
.withProcessUid(1110009)
.shouldGive("/data/user/11/a.b.c/cache", "/data/user/11/d.e.f/cache");
}

@Test
public void testGuessCacheDir_StripHyphenatedSuffixes() {
guessCacheDirFor("/data/app/a.b.c-2.apk").shouldGive("/data/data/a.b.c/cache");
Expand Down Expand Up @@ -121,14 +130,22 @@ public void testApiLevel17PlusPathProcessing() {
}
}

@Test
public void testGetProcessUid() {
AppDataDirGuesser guesser = new AppDataDirGuesser();
assertTrue(guesser.getProcessUid() == Process.myUid());
}

private interface TestCondition {
TestCondition withNonWriteable(String... files);
TestCondition withProcessUid(Integer uid);
void shouldGive(String... files);
}

private TestCondition guessCacheDirFor(final String path) {
final Set<String> notWriteable = new HashSet<>();
return new TestCondition() {
private Integer processUid;
@Override
public void shouldGive(String... files) {
AppDataDirGuesser guesser = new AppDataDirGuesser() {
Expand All @@ -140,6 +157,10 @@ public boolean isWriteableDirectory(File file) {
boolean fileOrDirExists(File file) {
return true;
}
@Override
Integer getProcessUid() {
return processUid;
}
};
File[] results = guesser.guessPath(path);
assertNotNull("Null results for " + path, results);
Expand All @@ -154,6 +175,12 @@ public TestCondition withNonWriteable(String... files) {
notWriteable.addAll(Arrays.asList(files));
return this;
}

@Override
public TestCondition withProcessUid(Integer uid) {
processUid = uid;
return this;
}
};
}
}
47 changes: 45 additions & 2 deletions dexmaker/src/main/java/com/android/dx/AppDataDirGuesser.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
* Uses heuristics to guess the application's private data directory.
*/
class AppDataDirGuesser {
// Copied from UserHandle, indicates range of uids allocated for a user.
public static final int PER_USER_RANGE = 100000;

public File guess() {
try {
ClassLoader classLoader = guessSuitableClassLoader();
Expand Down Expand Up @@ -146,8 +150,14 @@ File[] guessPath(String input) {
end = dash;
}
String packageName = potential.substring(start, end);
File dataDir = new File("/data/data/" + packageName);
if (isWriteableDirectory(dataDir)) {
File dataDir = getWriteableDirectory("/data/data/" + packageName);

if (dataDir == null) {
// If we can't access "/data/data", try to guess user specific data directory.
dataDir = guessUserDataDirectory(packageName);
}

if (dataDir != null) {
File cacheDir = new File(dataDir, "cache");
// The cache directory might not exist -- create if necessary
if (fileOrDirExists(cacheDir) || cacheDir.mkdir()) {
Expand Down Expand Up @@ -179,4 +189,37 @@ boolean fileOrDirExists(File file) {
boolean isWriteableDirectory(File file) {
return file.isDirectory() && file.canWrite();
}

Integer getProcessUid() {
/* Uses reflection to try to fetch process UID. It will only work when executing on
* Android device. Otherwise, returns null.
*/
try {
Method myUid = Class.forName("android.os.Process").getMethod("myUid");

// Invoke the method on a null instance, since it's a static method.
return (Integer) myUid.invoke(/* instance= */ null);
} catch (Exception e) {
// Catch any exceptions thrown and default to returning a null.
return null;
}
}

File guessUserDataDirectory(String packageName) {
Integer uid = getProcessUid();
if (uid == null) {
// If we couldn't retrieve process uid, return null.
return null;
}

// We're trying to get the ID of the Android user that's running the process. It can be
// inferred from the UID of the current process.
int userId = uid / PER_USER_RANGE;
return getWriteableDirectory(String.format("/data/user/%d/%s", userId, packageName));
}

private File getWriteableDirectory(String pathName) {
File dir = new File(pathName);
return isWriteableDirectory(dir) ? dir : null;
}
}

0 comments on commit 8ff85ed

Please sign in to comment.