Adopt Hugo Hudson's AppDataDirGuesser in the core DexMaker. It's far too cumbersome to rely on frameworks to include such heuristics.
diff --git a/src/main/java/com/google/dexmaker/AppDataDirGuesser.java b/src/main/java/com/google/dexmaker/AppDataDirGuesser.java
new file mode 100644
index 0000000..2492ea0
--- /dev/null
+++ b/src/main/java/com/google/dexmaker/AppDataDirGuesser.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.dexmaker;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Uses heuristics to guess the application's private data directory.
+ */
+class AppDataDirGuesser {
+    public File guess() {
+        try {
+            ClassLoader classLoader = guessSuitableClassLoader();
+            // Check that we have an instance of the PathClassLoader.
+            Class<?> clazz = Class.forName("dalvik.system.PathClassLoader");
+            clazz.cast(classLoader);
+            // Use the toString() method to calculate the data directory.
+            String pathFromThisClassLoader = getPathFromThisClassLoader(classLoader);
+            File[] results = guessPath(pathFromThisClassLoader);
+            if (results.length > 0) {
+                return results[0];
+            }
+        } catch (ClassCastException ignored) {
+        } catch (ClassNotFoundException ignored) {
+        }
+        return null;
+    }
+
+    private ClassLoader guessSuitableClassLoader() {
+        return AppDataDirGuesser.class.getClassLoader();
+    }
+
+    private String getPathFromThisClassLoader(ClassLoader classLoader) {
+        // Parsing toString() method: yuck.  But no other way to get the path.
+        // Strip out the bit between angle brackets, that's our path.
+        String result = classLoader.toString();
+        int index = result.lastIndexOf('[');
+        result = (index == -1) ? result : result.substring(index + 1);
+        index = result.indexOf(']');
+        return (index == -1) ? result : result.substring(0, index);
+    }
+
+    File[] guessPath(String input) {
+        List<File> results = new ArrayList<File>();
+        for (String potential : input.split(":")) {
+            if (!potential.startsWith("/data/app/")) {
+                continue;
+            }
+            int start = "/data/app/".length();
+            int end = potential.lastIndexOf(".apk");
+            if (end != potential.length() - 4) {
+                continue;
+            }
+            int dash = potential.indexOf("-");
+            if (dash != -1) {
+                end = dash;
+            }
+            File file = new File("/data/data/" + potential.substring(start, end) + "/cache");
+            if (isWriteableDirectory(file)) {
+                results.add(file);
+            }
+        }
+        return results.toArray(new File[results.size()]);
+    }
+
+    boolean isWriteableDirectory(File file) {
+        return file.isDirectory() && file.canWrite();
+    }
+}
diff --git a/src/main/java/com/google/dexmaker/DexMaker.java b/src/main/java/com/google/dexmaker/DexMaker.java
index 3566fb6..ae59740 100644
--- a/src/main/java/com/google/dexmaker/DexMaker.java
+++ b/src/main/java/com/google/dexmaker/DexMaker.java
@@ -326,20 +326,42 @@
     /**
      * Generates a dex file and loads its types into the current process.
      *
-     * <p>All parameters are optional; you may pass {@code null} and suitable
-     * defaults will be used.
+     * <h3>Picking a dex cache directory</h3>
+     * The {@code dexCache} should be an application-private directory. If
+     * you pass a world-writable directory like {@code /sdcard} a malicious app
+     * could inject code into your process. Most applications should use this:
+     * <pre>   {@code
      *
-     * <p>If you opt to provide your own {@code dexDir}, take care to ensure
-     * that it is not world-writable, otherwise a malicious app may be able
-     * to inject code into your process.  A suitable parameter is:
-     * {@code getApplicationContext().getDir("dx", Context.MODE_PRIVATE); }
+     *     File dexCache = getApplicationContext().getDir("dx", Context.MODE_PRIVATE);
+     * }</pre>
+     * If the {@code dexCache} is null, this method will consult the {@code
+     * dexmaker.dexcache} system property. If that exists, it will be used for
+     * the dex cache. If it doesn't exist, this method will attempt to guess
+     * the application's private data directory as a last resort. If that fails,
+     * this method will fail with an unchecked exception. You can avoid the
+     * exception by either providing a non-null value or setting the system
+     * property.
      *
-     * @param parent the parent ClassLoader to be used when loading
-     *     our generated types
-     * @param dexDir the destination directory where generated and
-     *     optimized dex files will be written.
+     * @param parent the parent ClassLoader to be used when loading our
+     *     generated types
+     * @param dexCache the destination directory where generated and optimized
+     *     dex files will be written. If null, this class will try to guess the
+     *     application's private data dir.
      */
-    public ClassLoader generateAndLoad(ClassLoader parent, File dexDir) throws IOException {
+    public ClassLoader generateAndLoad(ClassLoader parent, File dexCache) throws IOException {
+        if (dexCache == null) {
+            String property = System.getProperty("dexmaker.dexcache");
+            if (property != null) {
+                dexCache = new File(property);
+            } else {
+                dexCache = new AppDataDirGuesser().guess();
+                if (dexCache == null) {
+                    throw new IllegalArgumentException("dexcache == null (and no default could be"
+                            + " found; consider setting the 'dexmaker.dexcache' system property)");
+                }
+            }
+        }
+
         byte[] dex = generate();
 
         /*
@@ -349,7 +371,7 @@
          *
          * TODO: load the dex from memory where supported.
          */
-        File result = File.createTempFile("Generated", ".jar", dexDir);
+        File result = File.createTempFile("Generated", ".jar", dexCache);
         result.deleteOnExit();
         JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(result));
         jarOut.putNextEntry(new JarEntry(DexFormat.DEX_IN_JAR_NAME));
@@ -359,7 +381,7 @@
         try {
             return (ClassLoader) Class.forName("dalvik.system.DexClassLoader")
                     .getConstructor(String.class, String.class, String.class, ClassLoader.class)
-                    .newInstance(result.getPath(), dexDir.getAbsolutePath(), null, parent);
+                    .newInstance(result.getPath(), dexCache.getAbsolutePath(), null, parent);
         } catch (ClassNotFoundException e) {
             throw new UnsupportedOperationException("load() requires a Dalvik VM", e);
         } catch (InvocationTargetException e) {
diff --git a/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java b/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
index f5b9c39..ad702e4 100644
--- a/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
+++ b/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
@@ -126,7 +126,6 @@
             = Collections.synchronizedMap(new HashMap<Class<?>, Class<?>>());
 
     private final Class<T> baseClass;
-    // TODO: make DexMaker do the defaulting here
     private ClassLoader parentClassLoader = ProxyBuilder.class.getClassLoader();
     private InvocationHandler handler;
     private File dexCache;
@@ -156,6 +155,11 @@
         return this;
     }
 
+    /**
+     * Sets the directory where executable code is stored. See {@link
+     * DexMaker#generateAndLoad DexMaker.generateAndLoad()} for guidance on
+     * choosing a secure location for the dex cache.
+     */
     public ProxyBuilder<T> dexCache(File dexCache) {
         this.dexCache = dexCache;
         return this;
@@ -310,6 +314,19 @@
         }
     }
 
+    /**
+     * Returns true if {@code c} is a proxy class created by this builder.
+     */
+    public static boolean isProxyClass(Class<?> c) {
+        // TODO: use a marker interface instead?
+        try {
+            c.getDeclaredField(FIELD_NAME_HANDLER);
+            return true;
+        } catch (NoSuchFieldException e) {
+            return false;
+        }
+    }
+
     private static <T, G extends T> void generateCodeForAllMethods(DexMaker dexMaker,
             TypeId<G> generatedType, Method[] methodsToProxy, TypeId<T> superclassType) {
         TypeId<InvocationHandler> handlerType = TypeId.get(InvocationHandler.class);
diff --git a/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java b/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java
new file mode 100644
index 0000000..5c92f34
--- /dev/null
+++ b/src/test/java/com/google/dexmaker/AppDataDirGuesserTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.dexmaker;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import junit.framework.TestCase;
+
+public final class AppDataDirGuesserTest extends TestCase {
+    public void testGuessCacheDir_SimpleExample() {
+        guessCacheDirFor("/data/app/a.b.c.apk").shouldGive("/data/data/a.b.c/cache");
+        guessCacheDirFor("/data/app/a.b.c.tests.apk").shouldGive("/data/data/a.b.c.tests/cache");
+    }
+
+    public void testGuessCacheDir_MultipleResultsSeparatedByColon() {
+        guessCacheDirFor("/data/app/a.b.c.apk:/data/app/d.e.f.apk")
+                .shouldGive("/data/data/a.b.c/cache", "/data/data/d.e.f/cache");
+    }
+
+    public void testGuessCacheDir_NotWriteableSkipped() {
+        guessCacheDirFor("/data/app/a.b.c.apk:/data/app/d.e.f.apk")
+                .withNonWriteable("/data/data/a.b.c/cache")
+                .shouldGive("/data/data/d.e.f/cache");
+    }
+
+    public void testGuessCacheDir_StripHyphenatedSuffixes() {
+        guessCacheDirFor("/data/app/a.b.c-2.apk").shouldGive("/data/data/a.b.c/cache");
+    }
+
+    public void testGuessCacheDir_LeadingAndTrailingColonsIgnored() {
+        guessCacheDirFor("/data/app/a.b.c.apk:asdf:").shouldGive("/data/data/a.b.c/cache");
+        guessCacheDirFor(":asdf:/data/app/a.b.c.apk").shouldGive("/data/data/a.b.c/cache");
+    }
+
+    public void testGuessCacheDir_InvalidInputsGiveEmptyArray() {
+        guessCacheDirFor("").shouldGive();
+    }
+
+    public void testGuessCacheDir_JarsIgnored() {
+        guessCacheDirFor("/data/app/a.b.c.jar").shouldGive();
+        guessCacheDirFor("/system/framework/android.test.runner.jar").shouldGive();
+    }
+
+    public void testGuessCacheDir_RealWorldExample() {
+        String realPath = "/system/framework/android.test.runner.jar:" +
+                "/data/app/com.google.android.voicesearch.tests-2.apk:" +
+                "/data/app/com.google.android.voicesearch-1.apk";
+        guessCacheDirFor(realPath)
+                .withNonWriteable("/data/data/com.google.android.voicesearch.tests/cache")
+                .shouldGive("/data/data/com.google.android.voicesearch/cache");
+    }
+
+    private interface TestCondition {
+        TestCondition withNonWriteable(String... files);
+        void shouldGive(String... files);
+    }
+
+    private TestCondition guessCacheDirFor(final String path) {
+        final Set<String> notWriteable = new HashSet<String>();
+        return new TestCondition() {
+            public void shouldGive(String... files) {
+                AppDataDirGuesser guesser = new AppDataDirGuesser() {
+                    @Override
+                    public boolean isWriteableDirectory(File file) {
+                        return !notWriteable.contains(file.getAbsolutePath());
+                    }
+                };
+                File[] results = guesser.guessPath(path);
+                assertNotNull("Null results for " + path, results);
+                assertEquals("Bad lengths for " + path, files.length, results.length);
+                for (int i = 0; i < files.length; ++i) {
+                    assertEquals("Element " + i, new File(files[i]), results[i]);
+                }
+            }
+
+            public TestCondition withNonWriteable(String... files) {
+                notWriteable.addAll(Arrays.asList(files));
+                return this;
+            }
+        };
+    }
+}