am 338ac4ac: am c5e43c50: Only current IME and spell checker can access user dictionary

* commit '338ac4ac978c299cf27059aaf1005f64b5497581':
  Only current IME and spell checker can access user dictionary
diff --git a/cmds/app_process/Android.mk b/cmds/app_process/Android.mk
index 7ce0846..51bbb81 100644
--- a/cmds/app_process/Android.mk
+++ b/cmds/app_process/Android.mk
@@ -65,7 +65,7 @@
 LOCAL_MODULE_STEM_32 := app_process32
 LOCAL_MODULE_STEM_64 := app_process64
 
-LOCAL_ADDRESS_SANITIZER := true
+LOCAL_SANITIZE := address
 LOCAL_CLANG := true
 LOCAL_MODULE_PATH := $(TARGET_OUT_EXECUTABLES)/asan
 
diff --git a/cmds/input/src/com/android/commands/input/Input.java b/cmds/input/src/com/android/commands/input/Input.java
index 2a7c79b..754d3f5 100644
--- a/cmds/input/src/com/android/commands/input/Input.java
+++ b/cmds/input/src/com/android/commands/input/Input.java
@@ -234,6 +234,18 @@
                 InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
     }
 
+    private int getInputDeviceId(int inputSource) {
+        final int DEFAULT_DEVICE_ID = 0;
+        int[] devIds = InputDevice.getDeviceIds();
+        for (int devId : devIds) {
+            InputDevice inputDev = InputDevice.getDevice(devId);
+            if (inputDev.supportsSource(inputSource)) {
+                return devId;
+            }
+        }
+        return DEFAULT_DEVICE_ID;
+    }
+
     /**
      * Builds a MotionEvent and injects it into the event stream.
      *
@@ -249,11 +261,10 @@
         final int DEFAULT_META_STATE = 0;
         final float DEFAULT_PRECISION_X = 1.0f;
         final float DEFAULT_PRECISION_Y = 1.0f;
-        final int DEFAULT_DEVICE_ID = 0;
         final int DEFAULT_EDGE_FLAGS = 0;
         MotionEvent event = MotionEvent.obtain(when, when, action, x, y, pressure, DEFAULT_SIZE,
-                DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y, DEFAULT_DEVICE_ID,
-                DEFAULT_EDGE_FLAGS);
+                DEFAULT_META_STATE, DEFAULT_PRECISION_X, DEFAULT_PRECISION_Y,
+                getInputDeviceId(inputSource), DEFAULT_EDGE_FLAGS);
         event.setSource(inputSource);
         Log.i(TAG, "injectMotionEvent: " + event);
         InputManager.getInstance().injectInputEvent(event,
diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java
index 9ea1606..0a28690 100644
--- a/core/java/android/app/AlarmManager.java
+++ b/core/java/android/app/AlarmManager.java
@@ -817,7 +817,7 @@
         }
 
         /**
-         * Returns an intent intent that can be used to show or edit details of the alarm clock in
+         * Returns an intent that can be used to show or edit details of the alarm clock in
          * the application that scheduled it.
          *
          * <p class="note">Beware that any application can retrieve and send this intent, 
diff --git a/core/java/android/widget/Adapter.java b/core/java/android/widget/Adapter.java
index 88b54bf..518718f 100644
--- a/core/java/android/widget/Adapter.java
+++ b/core/java/android/widget/Adapter.java
@@ -130,8 +130,7 @@
      * type of View for all items, this method should return 1.
      * </p>
      * <p>
-     * This method will only be called when when the adapter is set on the
-     * the {@link AdapterView}.
+     * This method will only be called when the adapter is set on the {@link AdapterView}.
      * </p>
      * 
      * @return The number of types of Views that will be created by this adapter
@@ -148,4 +147,3 @@
       */
      boolean isEmpty();
 }
-
diff --git a/core/java/com/android/internal/app/ProcessStats.java b/core/java/com/android/internal/app/ProcessStats.java
index 27aec4e..17ca904d 100644
--- a/core/java/com/android/internal/app/ProcessStats.java
+++ b/core/java/com/android/internal/app/ProcessStats.java
@@ -3366,10 +3366,11 @@
                             + pkgList.keyAt(index) + "/" + proc.mUid
                             + " for multi-proc " + proc.mName + " version " + proc.mVersion);
                 }
+                String savedName = proc.mName;
                 proc = pkg.mProcesses.get(proc.mName);
                 if (proc == null) {
                     throw new IllegalStateException("Didn't create per-package process "
-                            + proc.mName + " in pkg " + pkg.mPackageName + "/" + pkg.mUid);
+                            + savedName + " in pkg " + pkg.mPackageName + "/" + pkg.mUid);
                 }
                 holder.state = proc;
             }
diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp
index 2ee9283..d2befca 100644
--- a/core/jni/android_os_Parcel.cpp
+++ b/core/jni/android_os_Parcel.cpp
@@ -449,15 +449,11 @@
         jniThrowNullPointerException(env, NULL);
         return NULL;
     }
-    const jchar* str = env->GetStringCritical(name, 0);
-    if (str == NULL) {
-        // Whatever, whatever.
-        jniThrowException(env, "java/lang/IllegalStateException", NULL);
+    ScopedUtfChars name8(env, name);
+    if (name8.c_str() == NULL) {
         return NULL;
     }
-    String8 name8(reinterpret_cast<const char16_t*>(str),
-                  env->GetStringLength(name));
-    env->ReleaseStringCritical(name, str);
+
     int flags=0;
     switch (mode&0x30000000) {
         case 0:
@@ -480,7 +476,7 @@
     if (mode&0x00000001) realMode |= S_IROTH;
     if (mode&0x00000002) realMode |= S_IWOTH;
 
-    int fd = open(name8.string(), flags, realMode);
+    int fd = open(name8.c_str(), flags, realMode);
     if (fd < 0) {
         jniThrowException(env, "java/io/FileNotFoundException", strerror(errno));
         return NULL;
diff --git a/core/tests/overlaytests/testrunner.py b/core/tests/overlaytests/testrunner.py
index 3703f4a..2aa25ad 100755
--- a/core/tests/overlaytests/testrunner.py
+++ b/core/tests/overlaytests/testrunner.py
@@ -301,7 +301,7 @@
         return self.path
 
     def execute(self):
-        returncode, stdout, stderr = _adb_shell('md5 %s' % self.path)
+        returncode, stdout, stderr = _adb_shell('md5sum %s' % self.path)
         if returncode != 0:
             return returncode, stdout, stderr
         actual_md5 = stdout.split()[0]
diff --git a/data/keyboards/Android.mk b/data/keyboards/Android.mk
index 898efe8..4616838 100644
--- a/data/keyboards/Android.mk
+++ b/data/keyboards/Android.mk
@@ -32,7 +32,7 @@
 	$(hide) mkdir -p $(dir $@) && touch $@
 
 # Run validatekeymaps uncondionally for platform build.
-droidcore all_modules : $(LOCAL_BUILT_MODULE)
+droidcore : $(LOCAL_BUILT_MODULE)
 
 # Reset temp vars.
 validatekeymaps :=
diff --git a/docs/html/tools/debugging/debugging-log.jd b/docs/html/tools/debugging/debugging-log.jd
index d2baaf26..e222b2a 100644
--- a/docs/html/tools/debugging/debugging-log.jd
+++ b/docs/html/tools/debugging/debugging-log.jd
@@ -284,22 +284,8 @@
   <h2 id="viewingStd">Viewing stdout and stderr</h2>
 
   <p>By default, the Android system sends <code>stdout</code> and <code>stderr</code>
-  (<code>System.out</code> and <code>System.err</code>) output to <code>/dev/null</code>. In
-  processes that run the Dalvik VM, you can have the system write a copy of the output to the log
-  file. In this case, the system writes the messages to the log using the log tags
-  <code>stdout</code> and <code>stderr</code>, both with priority <code>I</code>.</p>
-
-  <p>To route the output in this way, you stop a running emulator/device instance and then use the
-  shell command <code>setprop</code> to enable the redirection of output. Here's how you do it:</p>
-  <pre>
-$ adb shell stop
-$ adb shell setprop log.redirect-stdio true
-$ adb shell start
-</pre>
-
-  <p>The system retains this setting until you terminate the emulator/device instance. To use the
-  setting as a default on the emulator/device instance, you can add an entry to
-  <code>/data/local.prop</code> on the device.</p>
+  output to <code>/dev/null</code>. (The Java <code>System.out</code> and <code>System.err</code>
+  streams go to the log.)
 
   <h2 id="DebuggingWebPages">Debugging Web Apps</h2>
   <p>
diff --git a/packages/InputDevices/Android.mk b/packages/InputDevices/Android.mk
index f537022..e7190dc 100644
--- a/packages/InputDevices/Android.mk
+++ b/packages/InputDevices/Android.mk
@@ -42,7 +42,7 @@
 	$(hide) mkdir -p $(dir $@) && touch $@
 
 # Run validatekeymaps unconditionally for platform build.
-droidcore all_modules : $(LOCAL_BUILT_MODULE)
+droidcore : $(LOCAL_BUILT_MODULE)
 
 # Reset temp vars.
 validatekeymaps :=
diff --git a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java
index 6acd137..35ea9a8 100644
--- a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java
@@ -236,7 +236,9 @@
                     Log.d(TAG, "Visibility changed to visible=" + visible);
                 }
                 mVisible = visible;
-                drawFrame();
+                if (visible) {
+                    drawFrame();
+                }
             }
         }
 
diff --git a/rs/java/android/renderscript/Allocation.java b/rs/java/android/renderscript/Allocation.java
index 0a50593..a4876b9 100644
--- a/rs/java/android/renderscript/Allocation.java
+++ b/rs/java/android/renderscript/Allocation.java
@@ -1505,7 +1505,7 @@
         }
 
         final byte[] data = fp.getData();
-        int data_length = fp.getPos();
+        int data_length = data.length;
         int eSize = mType.mElement.mElements[component_number].getBytesSize();
         eSize *= mType.mElement.mArraySizes[component_number];
 
diff --git a/rs/java/android/renderscript/RenderScript.java b/rs/java/android/renderscript/RenderScript.java
index 8b1a032..f8028ee 100644
--- a/rs/java/android/renderscript/RenderScript.java
+++ b/rs/java/android/renderscript/RenderScript.java
@@ -88,6 +88,13 @@
     */
     public static final int CREATE_FLAG_LOW_POWER = 0x0004;
 
+    /**
+     * @hide
+     * Context creation flag which instructs the implementation to wait for
+     * a debugger to be attached before continuing execution.
+    */
+    public static final int CREATE_FLAG_WAIT_FOR_ATTACH = 0x0008;
+
     /*
      * Detect the bitness of the VM to allow FieldPacker to do the right thing.
      */
@@ -1356,7 +1363,7 @@
             return null;
         }
 
-        if ((flags & ~(CREATE_FLAG_LOW_LATENCY | CREATE_FLAG_LOW_POWER)) != 0) {
+        if ((flags & ~(CREATE_FLAG_LOW_LATENCY | CREATE_FLAG_LOW_POWER | CREATE_FLAG_WAIT_FOR_ATTACH)) != 0) {
             throw new RSIllegalArgumentException("Invalid flags passed.");
         }
 
diff --git a/rs/jni/android_renderscript_RenderScript.cpp b/rs/jni/android_renderscript_RenderScript.cpp
index 1833a1c..26af574 100644
--- a/rs/jni/android_renderscript_RenderScript.cpp
+++ b/rs/jni/android_renderscript_RenderScript.cpp
@@ -1452,7 +1452,7 @@
     rsAllocationElementRead((RsContext)con, (RsAllocation)alloc,
                             xoff, yoff, zoff,
                             lod, ptr, sizeBytes, compIdx);
-    _env->ReleaseByteArrayElements(data, ptr, JNI_ABORT);
+    _env->ReleaseByteArrayElements(data, ptr, 0);
 }
 
 // Copies from the Allocation pointed to by _alloc into the Java object data.
diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java
index 839b87a..2066cdd 100644
--- a/services/core/java/com/android/server/AlarmManagerService.java
+++ b/services/core/java/com/android/server/AlarmManagerService.java
@@ -516,10 +516,10 @@
         public int compare(Batch b1, Batch b2) {
             long when1 = b1.start;
             long when2 = b2.start;
-            if (when1 - when2 > 0) {
+            if (when1 > when2) {
                 return 1;
             }
-            if (when1 - when2 < 0) {
+            if (when1 < when2) {
                 return -1;
             }
             return 0;
@@ -1934,10 +1934,10 @@
         public int compare(Alarm a1, Alarm a2) {
             long when1 = a1.whenElapsed;
             long when2 = a2.whenElapsed;
-            if (when1 - when2 > 0) {
+            if (when1 > when2) {
                 return 1;
             }
-            if (when1 - when2 < 0) {
+            if (when1 < when2) {
                 return -1;
             }
             return 0;
diff --git a/services/core/java/com/android/server/job/JobServiceContext.java b/services/core/java/com/android/server/job/JobServiceContext.java
index bb4e388..d26319b 100644
--- a/services/core/java/com/android/server/job/JobServiceContext.java
+++ b/services/core/java/com/android/server/job/JobServiceContext.java
@@ -73,7 +73,7 @@
     private static final long OP_TIMEOUT_MILLIS = 8 * 1000;
 
     private static final String[] VERB_STRINGS = {
-            "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING"
+            "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED"
     };
 
     // States that a job occupies while interacting with the client.
@@ -81,6 +81,7 @@
     static final int VERB_STARTING = 1;
     static final int VERB_EXECUTING = 2;
     static final int VERB_STOPPING = 3;
+    static final int VERB_FINISHED = 4;
 
     // Messages that result from interactions with the client service.
     /** System timed out waiting for a response. */
@@ -172,6 +173,7 @@
                 mRunningJob = null;
                 mParams = null;
                 mExecutionStartTimeElapsed = 0L;
+                mVerb = VERB_FINISHED;
                 removeOpTimeOut();
                 return false;
             }
@@ -307,8 +309,8 @@
                     break;
                 case MSG_CALLBACK:
                     if (DEBUG) {
-                        Slog.d(TAG, "MSG_CALLBACK of : " + mRunningJob + " v:" +
-                                (mVerb >= 0 ? VERB_STRINGS[mVerb] : "[invalid]"));
+                        Slog.d(TAG, "MSG_CALLBACK of : " + mRunningJob
+                                + " v:" + VERB_STRINGS[mVerb]);
                     }
                     removeOpTimeOut();
 
@@ -524,8 +526,12 @@
          * we want to clean up internally.
          */
         private void closeAndCleanupJobH(boolean reschedule) {
-            final JobStatus completedJob = mRunningJob;
+            final JobStatus completedJob;
             synchronized (mLock) {
+                if (mVerb == VERB_FINISHED) {
+                    return;
+                }
+                completedJob = mRunningJob;
                 try {
                     mBatteryStats.noteJobFinish(mRunningJob.getName(), mRunningJob.getUid());
                 } catch (RemoteException e) {
@@ -538,7 +544,7 @@
                 mWakeLock = null;
                 mRunningJob = null;
                 mParams = null;
-                mVerb = -1;
+                mVerb = VERB_FINISHED;
                 mCancelled.set(false);
                 service = null;
                 mAvailable = true;
diff --git a/tools/aapt/Android.mk b/tools/aapt/Android.mk
index bbe6860..d551c8e 100644
--- a/tools/aapt/Android.mk
+++ b/tools/aapt/Android.mk
@@ -50,9 +50,11 @@
 aaptTests := \
     tests/AaptConfig_test.cpp \
     tests/AaptGroupEntry_test.cpp \
+    tests/Pseudolocales_test.cpp \
     tests/ResourceFilter_test.cpp
 
 aaptCIncludes := \
+    system/core/base/include \
     external/libpng \
     external/zlib
 
@@ -99,7 +101,6 @@
 
 include $(BUILD_HOST_STATIC_LIBRARY)
 
-
 # ==========================================================
 # Build the host executable: aapt
 # ==========================================================
diff --git a/tools/aapt/XMLNode.cpp b/tools/aapt/XMLNode.cpp
index 6902a30..ca3f687 100644
--- a/tools/aapt/XMLNode.cpp
+++ b/tools/aapt/XMLNode.cpp
@@ -213,16 +213,14 @@
     Vector<StringPool::entry_style_span> spanStack;
     String16 curString;
     String16 rawString;
+    Pseudolocalizer pseudo(pseudolocalize);
     const char* errorMsg;
     int xliffDepth = 0;
     bool firstTime = true;
 
     size_t len;
     ResXMLTree::event_code_t code;
-    // Bracketing if pseudolocalization accented method specified.
-    if (pseudolocalize == PSEUDO_ACCENTED) {
-        curString.append(String16(String8("[")));
-    }
+    curString.append(pseudo.start());
     while ((code=inXml->next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) {
         if (code == ResXMLTree::TEXT) {
             String16 text(inXml->getText(&len));
@@ -231,18 +229,12 @@
                 if (text.string()[0] == '@') {
                     // If this is a resource reference, don't do the pseudoloc.
                     pseudolocalize = NO_PSEUDOLOCALIZATION;
+                    pseudo.setMethod(pseudolocalize);
+                    curString = String16();
                 }
             }
             if (xliffDepth == 0 && pseudolocalize > 0) {
-                String16 pseudo;
-                if (pseudolocalize == PSEUDO_ACCENTED) {
-                    pseudo = pseudolocalize_string(text);
-                } else if (pseudolocalize == PSEUDO_BIDI) {
-                    pseudo = pseudobidi_string(text);
-                } else {
-                    pseudo = text;
-                }
-                curString.append(pseudo);
+                curString.append(pseudo.text(text));
             } else {
                 if (isFormatted && hasSubstitutionErrors(fileName, inXml, text) != NO_ERROR) {
                     return UNKNOWN_ERROR;
@@ -382,24 +374,7 @@
         }
     }
 
-    // Bracketing if pseudolocalization accented method specified.
-    if (pseudolocalize == PSEUDO_ACCENTED) {
-        const char16_t* str = outString->string();
-        const char16_t* p = str;
-        const char16_t* e = p + outString->size();
-        int words_cnt = 0;
-        while (p < e) {
-            if (isspace(*p)) {
-                words_cnt++;
-            }
-            p++;
-        }
-        unsigned int length = words_cnt > 3 ? outString->size() :
-            outString->size() / 2;
-        curString.append(String16(String8(" ")));
-        curString.append(pseudo_generate_expansion(length));
-        curString.append(String16(String8("]")));
-    }
+    curString.append(pseudo.end());
 
     if (code == ResXMLTree::BAD_DOCUMENT) {
             SourcePos(String8(fileName), inXml->getLineNumber()).error(
diff --git a/tools/aapt/pseudolocalize.cpp b/tools/aapt/pseudolocalize.cpp
index 60aa2b2..c7fee2c 100644
--- a/tools/aapt/pseudolocalize.cpp
+++ b/tools/aapt/pseudolocalize.cpp
@@ -16,6 +16,80 @@
 static const String16 k_placeholder_open = String16("\xc2\xbb");
 static const String16 k_placeholder_close = String16("\xc2\xab");
 
+static const char16_t k_arg_start = '{';
+static const char16_t k_arg_end = '}';
+
+Pseudolocalizer::Pseudolocalizer(PseudolocalizationMethod m)
+    : mImpl(nullptr), mLastDepth(0) {
+  setMethod(m);
+}
+
+void Pseudolocalizer::setMethod(PseudolocalizationMethod m) {
+  if (mImpl) {
+    delete mImpl;
+  }
+  if (m == PSEUDO_ACCENTED) {
+    mImpl = new PseudoMethodAccent();
+  } else if (m == PSEUDO_BIDI) {
+    mImpl = new PseudoMethodBidi();
+  } else {
+    mImpl = new PseudoMethodNone();
+  }
+}
+
+String16 Pseudolocalizer::text(const String16& text) {
+  String16 out;
+  size_t depth = mLastDepth;
+  size_t lastpos, pos;
+  const size_t length= text.size();
+  const char16_t* str = text.string();
+  bool escaped = false;
+  for (lastpos = pos = 0; pos < length; pos++) {
+    char16_t c = str[pos];
+    if (escaped) {
+      escaped = false;
+      continue;
+    }
+    if (c == '\'') {
+      escaped = true;
+      continue;
+    }
+
+    if (c == k_arg_start) {
+      depth++;
+    } else if (c == k_arg_end && depth) {
+      depth--;
+    }
+
+    if (mLastDepth != depth || pos == length - 1) {
+      bool pseudo = ((mLastDepth % 2) == 0);
+      size_t nextpos = pos;
+      if (!pseudo || depth == mLastDepth) {
+        nextpos++;
+      }
+      size_t size = nextpos - lastpos;
+      if (size) {
+        String16 chunk = String16(text, size, lastpos);
+        if (pseudo) {
+          chunk = mImpl->text(chunk);
+        } else if (str[lastpos] == k_arg_start &&
+                   str[nextpos - 1] == k_arg_end) {
+          chunk = mImpl->placeholder(chunk);
+        }
+        out.append(chunk);
+      }
+      if (pseudo && depth < mLastDepth) { // End of message
+        out.append(mImpl->end());
+      } else if (!pseudo && depth > mLastDepth) { // Start of message
+        out.append(mImpl->start());
+      }
+      lastpos = nextpos;
+      mLastDepth = depth;
+    }
+  }
+  return out;
+}
+
 static const char*
 pseudolocalize_char(const char16_t c)
 {
@@ -78,8 +152,7 @@
     }
 }
 
-static bool
-is_possible_normal_placeholder_end(const char16_t c) {
+static bool is_possible_normal_placeholder_end(const char16_t c) {
     switch (c) {
         case 's': return true;
         case 'S': return true;
@@ -106,8 +179,7 @@
     }
 }
 
-String16
-pseudo_generate_expansion(const unsigned int length) {
+static String16 pseudo_generate_expansion(const unsigned int length) {
     String16 result = k_expansion_string;
     const char16_t* s = result.string();
     if (result.size() < length) {
@@ -127,18 +199,47 @@
     return result;
 }
 
+static bool is_space(const char16_t c) {
+  return (c == ' ' || c == '\t' || c == '\n');
+}
+
+String16 PseudoMethodAccent::start() {
+  String16 result;
+  if (mDepth == 0) {
+    result = String16(String8("["));
+  }
+  mWordCount = mLength = 0;
+  mDepth++;
+  return result;
+}
+
+String16 PseudoMethodAccent::end() {
+  String16 result;
+  if (mLength) {
+    result.append(String16(String8(" ")));
+    result.append(pseudo_generate_expansion(
+        mWordCount > 3 ? mLength : mLength / 2));
+  }
+  mWordCount = mLength = 0;
+  mDepth--;
+  if (mDepth == 0) {
+    result.append(String16(String8("]")));
+  }
+  return result;
+}
+
 /**
  * Converts characters so they look like they've been localized.
  *
  * Note: This leaves escape sequences untouched so they can later be
  * processed by ResTable::collectString in the normal way.
  */
-String16
-pseudolocalize_string(const String16& source)
+String16 PseudoMethodAccent::text(const String16& source)
 {
     const char16_t* s = source.string();
     String16 result;
     const size_t I = source.size();
+    bool lastspace = true;
     for (size_t i=0; i<I; i++) {
         char16_t c = s[i];
         if (c == '\\') {
@@ -170,23 +271,24 @@
             }
         } else if (c == '%') {
             // Placeholder syntax, no need to pseudolocalize
-            result += k_placeholder_open;
+            String16 chunk;
             bool end = false;
-            result.append(&c, 1);
+            chunk.append(&c, 1);
             while (!end && i < I) {
                 ++i;
                 c = s[i];
-                result.append(&c, 1);
+                chunk.append(&c, 1);
                 if (is_possible_normal_placeholder_end(c)) {
                     end = true;
                 } else if (c == 't') {
                     ++i;
                     c = s[i];
-                    result.append(&c, 1);
+                    chunk.append(&c, 1);
                     end = true;
                 }
             }
-            result += k_placeholder_close;
+            // Treat chunk as a placeholder unless it ends with %.
+            result += ((c == '%') ? chunk : placeholder(chunk));
         } else if (c == '<' || c == '&') {
             // html syntax, no need to pseudolocalize
             bool tag_closed = false;
@@ -234,35 +336,52 @@
             if (p != NULL) {
                 result += String16(p);
             } else {
+                bool space = is_space(c);
+                if (lastspace && !space) {
+                  mWordCount++;
+                }
+                lastspace = space;
                 result.append(&c, 1);
             }
+            // Count only pseudolocalizable chars and delimiters
+            mLength++;
         }
     }
     return result;
 }
+String16 PseudoMethodAccent::placeholder(const String16& source) {
+  // Surround a placeholder with brackets
+  return k_placeholder_open + source + k_placeholder_close;
+}
 
-String16
-pseudobidi_string(const String16& source)
+String16 PseudoMethodBidi::text(const String16& source)
 {
     const char16_t* s = source.string();
     String16 result;
-    result += k_rlm;
-    result += k_rlo;
+    bool lastspace = true;
+    bool space = true;
     for (size_t i=0; i<source.size(); i++) {
         char16_t c = s[i];
-        switch(c) {
-            case ' ': result += k_pdf;
-                      result += k_rlm;
-                      result.append(&c, 1);
-                      result += k_rlm;
-                      result += k_rlo;
-                      break;
-            default: result.append(&c, 1);
-                     break;
+        space = is_space(c);
+        if (lastspace && !space) {
+          // Word start
+          result += k_rlm + k_rlo;
+        } else if (!lastspace && space) {
+          // Word end
+          result += k_pdf + k_rlm;
         }
+        lastspace = space;
+        result.append(&c, 1);
     }
-    result += k_pdf;
-    result += k_rlm;
+    if (!lastspace) {
+      // End of last word
+      result += k_pdf + k_rlm;
+    }
     return result;
 }
 
+String16 PseudoMethodBidi::placeholder(const String16& source) {
+  // Surround a placeholder with directionality change sequence
+  return k_rlm + k_rlo + source + k_pdf + k_rlm;
+}
+
diff --git a/tools/aapt/pseudolocalize.h b/tools/aapt/pseudolocalize.h
index e6ab18e..71b974b 100644
--- a/tools/aapt/pseudolocalize.h
+++ b/tools/aapt/pseudolocalize.h
@@ -1,18 +1,58 @@
 #ifndef HOST_PSEUDOLOCALIZE_H
 #define HOST_PSEUDOLOCALIZE_H
 
+#include <base/macros.h>
 #include "StringPool.h"
 
-#include <string>
+class PseudoMethodImpl {
+ public:
+  virtual ~PseudoMethodImpl() {}
+  virtual String16 start() { return String16(); }
+  virtual String16 end() { return String16(); }
+  virtual String16 text(const String16& text) = 0;
+  virtual String16 placeholder(const String16& text) = 0;
+};
 
-String16 pseudolocalize_string(const String16& source);
-// Surrounds every word in the sentance with specific characters that makes
-// the word directionality RTL.
-String16 pseudobidi_string(const String16& source);
-// Generates expansion string based on the specified lenght.
-// Generated string could not be shorter that length, but it could be slightly
-// longer.
-String16 pseudo_generate_expansion(const unsigned int length);
+class PseudoMethodNone : public PseudoMethodImpl {
+ public:
+  PseudoMethodNone() {}
+  String16 text(const String16& text) { return text; }
+  String16 placeholder(const String16& text) { return text; }
+ private:
+  DISALLOW_COPY_AND_ASSIGN(PseudoMethodNone);
+};
+
+class PseudoMethodBidi : public PseudoMethodImpl {
+ public:
+  String16 text(const String16& text);
+  String16 placeholder(const String16& text);
+};
+
+class PseudoMethodAccent : public PseudoMethodImpl {
+ public:
+  PseudoMethodAccent() : mDepth(0), mWordCount(0), mLength(0) {}
+  String16 start();
+  String16 end();
+  String16 text(const String16& text);
+  String16 placeholder(const String16& text);
+ private:
+  size_t mDepth;
+  size_t mWordCount;
+  size_t mLength;
+};
+
+class Pseudolocalizer {
+ public:
+  Pseudolocalizer(PseudolocalizationMethod m);
+  ~Pseudolocalizer() { if (mImpl) delete mImpl; }
+  void setMethod(PseudolocalizationMethod m);
+  String16 start() { return mImpl->start(); }
+  String16 end() { return mImpl->end(); }
+  String16 text(const String16& text);
+ private:
+  PseudoMethodImpl *mImpl;
+  size_t mLastDepth;
+};
 
 #endif // HOST_PSEUDOLOCALIZE_H
 
diff --git a/tools/aapt/tests/Pseudolocales_test.cpp b/tools/aapt/tests/Pseudolocales_test.cpp
new file mode 100644
index 0000000..4670e9f
--- /dev/null
+++ b/tools/aapt/tests/Pseudolocales_test.cpp
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#include <androidfw/ResourceTypes.h>
+#include <utils/String8.h>
+#include <gtest/gtest.h>
+
+#include "Bundle.h"
+#include "pseudolocalize.h"
+
+using android::String8;
+
+// In this context, 'Axis' represents a particular field in the configuration,
+// such as language or density.
+
+static void simple_helper(const char* input, const char* expected, PseudolocalizationMethod method) {
+    Pseudolocalizer pseudo(method);
+    String16 result = pseudo.start() + pseudo.text(String16(String8(input))) + pseudo.end();
+    //std::cout << String8(result).string() << std::endl;
+    ASSERT_EQ(String8(expected), String8(result));
+}
+
+static void compound_helper(const char* in1, const char* in2, const char *in3,
+                            const char* expected, PseudolocalizationMethod method) {
+    Pseudolocalizer pseudo(method);
+    String16 result = pseudo.start() + \
+                      pseudo.text(String16(String8(in1))) + \
+                      pseudo.text(String16(String8(in2))) + \
+                      pseudo.text(String16(String8(in3))) + \
+                      pseudo.end();
+    ASSERT_EQ(String8(expected), String8(result));
+}
+
+TEST(Pseudolocales, NoPseudolocalization) {
+  simple_helper("", "", NO_PSEUDOLOCALIZATION);
+  simple_helper("Hello, world", "Hello, world", NO_PSEUDOLOCALIZATION);
+
+  compound_helper("Hello,", " world", "",
+                  "Hello, world", NO_PSEUDOLOCALIZATION);
+}
+
+TEST(Pseudolocales, PlaintextAccent) {
+  simple_helper("", "[]", PSEUDO_ACCENTED);
+  simple_helper("Hello, world",
+                "[Ĥéļļö, ŵöŕļð one two]", PSEUDO_ACCENTED);
+
+  simple_helper("Hello, %1d",
+                "[Ĥéļļö, »%1d« one two]", PSEUDO_ACCENTED);
+
+  simple_helper("Battery %1d%%",
+                "[βåţţéŕý »%1d«%% one two]", PSEUDO_ACCENTED);
+
+  compound_helper("", "", "", "[]", PSEUDO_ACCENTED);
+  compound_helper("Hello,", " world", "",
+                  "[Ĥéļļö, ŵöŕļð one two]", PSEUDO_ACCENTED);
+}
+
+TEST(Pseudolocales, PlaintextBidi) {
+  simple_helper("", "", PSEUDO_BIDI);
+  simple_helper("word",
+                "\xe2\x80\x8f\xE2\x80\xaeword\xE2\x80\xac\xe2\x80\x8f",
+                PSEUDO_BIDI);
+  simple_helper("  word  ",
+                "  \xe2\x80\x8f\xE2\x80\xaeword\xE2\x80\xac\xe2\x80\x8f  ",
+                PSEUDO_BIDI);
+  simple_helper("  word  ",
+                "  \xe2\x80\x8f\xE2\x80\xaeword\xE2\x80\xac\xe2\x80\x8f  ",
+                PSEUDO_BIDI);
+  simple_helper("hello\n  world\n",
+                "\xe2\x80\x8f\xE2\x80\xaehello\xE2\x80\xac\xe2\x80\x8f\n" \
+                "  \xe2\x80\x8f\xE2\x80\xaeworld\xE2\x80\xac\xe2\x80\x8f\n",
+                PSEUDO_BIDI);
+  compound_helper("hello", "\n ", " world\n",
+                "\xe2\x80\x8f\xE2\x80\xaehello\xE2\x80\xac\xe2\x80\x8f\n" \
+                "  \xe2\x80\x8f\xE2\x80\xaeworld\xE2\x80\xac\xe2\x80\x8f\n",
+                PSEUDO_BIDI);
+}
+
+TEST(Pseudolocales, SimpleICU) {
+  // Single-fragment messages
+  simple_helper("{placeholder}", "[»{placeholder}«]", PSEUDO_ACCENTED);
+  simple_helper("{USER} is offline",
+              "[»{USER}« îš öƒƒļîñé one two]", PSEUDO_ACCENTED);
+  simple_helper("Copy from {path1} to {path2}",
+              "[Çöþý ƒŕöḿ »{path1}« ţö »{path2}« one two three]", PSEUDO_ACCENTED);
+  simple_helper("Today is {1,date} {1,time}",
+              "[Ţöðåý îš »{1,date}« »{1,time}« one two]", PSEUDO_ACCENTED);
+
+  // Multi-fragment messages
+  compound_helper("{USER}", " ", "is offline",
+                  "[»{USER}« îš öƒƒļîñé one two]",
+                  PSEUDO_ACCENTED);
+  compound_helper("Copy from ", "{path1}", " to {path2}",
+                  "[Çöþý ƒŕöḿ »{path1}« ţö »{path2}« one two three]",
+                  PSEUDO_ACCENTED);
+}
+
+TEST(Pseudolocales, ICUBidi) {
+  // Single-fragment messages
+  simple_helper("{placeholder}",
+                "\xe2\x80\x8f\xE2\x80\xae{placeholder}\xE2\x80\xac\xe2\x80\x8f",
+                PSEUDO_BIDI);
+  simple_helper(
+      "{COUNT, plural, one {one} other {other}}",
+      "{COUNT, plural, " \
+          "one {\xe2\x80\x8f\xE2\x80\xaeone\xE2\x80\xac\xe2\x80\x8f} " \
+        "other {\xe2\x80\x8f\xE2\x80\xaeother\xE2\x80\xac\xe2\x80\x8f}}",
+      PSEUDO_BIDI
+  );
+}
+
+TEST(Pseudolocales, Escaping) {
+  // Single-fragment messages
+  simple_helper("'{USER'} is offline",
+                "['{ÛŠÉŔ'} îš öƒƒļîñé one two three]", PSEUDO_ACCENTED);
+
+  // Multi-fragment messages
+  compound_helper("'{USER}", " ", "''is offline",
+                  "['{ÛŠÉŔ} ''îš öƒƒļîñé one two three]", PSEUDO_ACCENTED);
+}
+
+TEST(Pseudolocales, PluralsAndSelects) {
+  simple_helper(
+      "{COUNT, plural, one {Delete a file} other {Delete {COUNT} files}}",
+      "[{COUNT, plural, one {Ðéļéţé å ƒîļé one two} " \
+                     "other {Ðéļéţé »{COUNT}« ƒîļéš one two}}]",
+      PSEUDO_ACCENTED
+  );
+  simple_helper(
+      "Distance is {COUNT, plural, one {# mile} other {# miles}}",
+      "[Ðîšţåñçé îš {COUNT, plural, one {# ḿîļé one two} " \
+                                 "other {# ḿîļéš one two}}]",
+      PSEUDO_ACCENTED
+  );
+  simple_helper(
+      "{1, select, female {{1} added you} " \
+        "male {{1} added you} other {{1} added you}}",
+      "[{1, select, female {»{1}« åððéð ýöû one two} " \
+        "male {»{1}« åððéð ýöû one two} other {»{1}« åððéð ýöû one two}}]",
+      PSEUDO_ACCENTED
+  );
+
+  compound_helper(
+      "{COUNT, plural, one {Delete a file} " \
+        "other {Delete ", "{COUNT}", " files}}",
+      "[{COUNT, plural, one {Ðéļéţé å ƒîļé one two} " \
+        "other {Ðéļéţé »{COUNT}« ƒîļéš one two}}]",
+      PSEUDO_ACCENTED
+  );
+}
+
+TEST(Pseudolocales, NestedICU) {
+  simple_helper(
+      "{person, select, " \
+        "female {" \
+          "{num_circles, plural," \
+            "=0{{person} didn't add you to any of her circles.}" \
+            "=1{{person} added you to one of her circles.}" \
+            "other{{person} added you to her # circles.}}}" \
+        "male {" \
+          "{num_circles, plural," \
+            "=0{{person} didn't add you to any of his circles.}" \
+            "=1{{person} added you to one of his circles.}" \
+            "other{{person} added you to his # circles.}}}" \
+        "other {" \
+          "{num_circles, plural," \
+            "=0{{person} didn't add you to any of their circles.}" \
+            "=1{{person} added you to one of their circles.}" \
+            "other{{person} added you to their # circles.}}}}",
+      "[{person, select, " \
+        "female {" \
+          "{num_circles, plural," \
+            "=0{»{person}« ðîðñ'ţ åðð ýöû ţö åñý öƒ ĥéŕ çîŕçļéš." \
+              " one two three four five}" \
+            "=1{»{person}« åððéð ýöû ţö öñé öƒ ĥéŕ çîŕçļéš." \
+              " one two three four}" \
+            "other{»{person}« åððéð ýöû ţö ĥéŕ # çîŕçļéš." \
+              " one two three four}}}" \
+        "male {" \
+          "{num_circles, plural," \
+            "=0{»{person}« ðîðñ'ţ åðð ýöû ţö åñý öƒ ĥîš çîŕçļéš." \
+              " one two three four five}" \
+            "=1{»{person}« åððéð ýöû ţö öñé öƒ ĥîš çîŕçļéš." \
+              " one two three four}" \
+            "other{»{person}« åððéð ýöû ţö ĥîš # çîŕçļéš." \
+              " one two three four}}}" \
+        "other {{num_circles, plural," \
+          "=0{»{person}« ðîðñ'ţ åðð ýöû ţö åñý öƒ ţĥéîŕ çîŕçļéš." \
+            " one two three four five}" \
+          "=1{»{person}« åððéð ýöû ţö öñé öƒ ţĥéîŕ çîŕçļéš." \
+            " one two three four}" \
+          "other{»{person}« åððéð ýöû ţö ţĥéîŕ # çîŕçļéš." \
+            " one two three four}}}}]",
+      PSEUDO_ACCENTED
+  );
+}
+
+TEST(Pseudolocales, RedefineMethod) {
+  Pseudolocalizer pseudo(PSEUDO_ACCENTED);
+  String16 result = pseudo.text(String16(String8("Hello, ")));
+  pseudo.setMethod(NO_PSEUDOLOCALIZATION);
+  result.append(pseudo.text(String16(String8("world!"))));
+  ASSERT_EQ(String8("Ĥéļļö, world!"), String8(result));
+}