diff options
21 files changed, 475 insertions, 116 deletions
diff --git a/cmds/app_process/Android.mk b/cmds/app_process/Android.mk index 7ce0846f2e7c..51bbb813df65 100644 --- a/cmds/app_process/Android.mk +++ b/cmds/app_process/Android.mk @@ -65,7 +65,7 @@ LOCAL_MULTILIB := both 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 2a7c79bdfb5b..754d3f510bbd 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 @@ public class Input { 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 @@ public class Input { 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 9ea160680683..0a28690da8d2 100644 --- a/core/java/android/app/AlarmManager.java +++ b/core/java/android/app/AlarmManager.java @@ -817,7 +817,7 @@ public class AlarmManager { } /** - * 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 88b54bf14ebb..518718f9632a 100644 --- a/core/java/android/widget/Adapter.java +++ b/core/java/android/widget/Adapter.java @@ -130,8 +130,7 @@ public interface Adapter { * 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 @@ public interface Adapter { */ boolean isEmpty(); } - diff --git a/core/java/com/android/internal/app/ProcessStats.java b/core/java/com/android/internal/app/ProcessStats.java index 27aec4e6103a..17ca904d50b0 100644 --- a/core/java/com/android/internal/app/ProcessStats.java +++ b/core/java/com/android/internal/app/ProcessStats.java @@ -3366,10 +3366,11 @@ public final class ProcessStats implements Parcelable { + 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 0f5ba83fa19a..a14afa054c17 100644 --- a/core/jni/android_os_Parcel.cpp +++ b/core/jni/android_os_Parcel.cpp @@ -451,15 +451,11 @@ static jobject android_os_Parcel_openFileDescriptor(JNIEnv* env, jclass clazz, 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: @@ -482,7 +478,7 @@ static jobject android_os_Parcel_openFileDescriptor(JNIEnv* env, jclass clazz, 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 3703f4a4b7ca..2aa25adeda5d 100755 --- a/core/tests/overlaytests/testrunner.py +++ b/core/tests/overlaytests/testrunner.py @@ -301,7 +301,7 @@ class Md5Test: 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 898efe8f2dca..461683805058 100644 --- a/data/keyboards/Android.mk +++ b/data/keyboards/Android.mk @@ -32,7 +32,7 @@ $(LOCAL_BUILT_MODULE) : $(framework_keylayouts) $(framework_keycharmaps) $(frame $(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 d2baaf26ce84..e222b2a05cd4 100644 --- a/docs/html/tools/debugging/debugging-log.jd +++ b/docs/html/tools/debugging/debugging-log.jd @@ -284,22 +284,8 @@ adb logcat -b radio <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 f537022c6129..e7190dc90843 100644 --- a/packages/InputDevices/Android.mk +++ b/packages/InputDevices/Android.mk @@ -42,7 +42,7 @@ $(LOCAL_BUILT_MODULE) : $(input_devices_keymaps) | $(validatekeymaps) $(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 6acd13731b27..35ea9a840442 100644 --- a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java @@ -236,7 +236,9 @@ public class ImageWallpaper extends WallpaperService { 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 0a5059390449..a4876b92fadf 100644 --- a/rs/java/android/renderscript/Allocation.java +++ b/rs/java/android/renderscript/Allocation.java @@ -1505,7 +1505,7 @@ public class Allocation extends BaseObj { } 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 8b1a0324956a..f8028eeccacf 100644 --- a/rs/java/android/renderscript/RenderScript.java +++ b/rs/java/android/renderscript/RenderScript.java @@ -88,6 +88,13 @@ public class RenderScript { */ 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 @@ public class RenderScript { 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 1833a1cd87a1..26af574f32fe 100644 --- a/rs/jni/android_renderscript_RenderScript.cpp +++ b/rs/jni/android_renderscript_RenderScript.cpp @@ -1452,7 +1452,7 @@ nAllocationElementRead(JNIEnv *_env, jobject _this, jlong con, jlong alloc, 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 839b87a7afde..2066cdd01ceb 100644 --- a/services/core/java/com/android/server/AlarmManagerService.java +++ b/services/core/java/com/android/server/AlarmManagerService.java @@ -516,10 +516,10 @@ class AlarmManagerService extends SystemService { 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 @@ class AlarmManagerService extends SystemService { 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 bb4e38849a56..d26319b5b0b9 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 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne 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 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne 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 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne mRunningJob = null; mParams = null; mExecutionStartTimeElapsed = 0L; + mVerb = VERB_FINISHED; removeOpTimeOut(); return false; } @@ -307,8 +309,8 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne 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 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne * 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 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne 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 bbe68608527d..d551c8efa00f 100644 --- a/tools/aapt/Android.mk +++ b/tools/aapt/Android.mk @@ -50,9 +50,11 @@ aaptSources := \ 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 @@ LOCAL_SRC_FILES := $(aaptSources) include $(BUILD_HOST_STATIC_LIBRARY) - # ========================================================== # Build the host executable: aapt # ========================================================== diff --git a/tools/aapt/XMLNode.cpp b/tools/aapt/XMLNode.cpp index 6902a3036ca4..ca3f68748ef9 100644 --- a/tools/aapt/XMLNode.cpp +++ b/tools/aapt/XMLNode.cpp @@ -213,16 +213,14 @@ status_t parseStyledString(Bundle* /* bundle */, 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 @@ status_t parseStyledString(Bundle* /* bundle */, 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 @@ moveon: } } - // 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 60aa2b2d3e3d..c7fee2c19342 100644 --- a/tools/aapt/pseudolocalize.cpp +++ b/tools/aapt/pseudolocalize.cpp @@ -16,6 +16,80 @@ static const String16 k_pdf = String16("\xE2\x80\xac"); 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 @@ pseudolocalize_char(const char16_t c) } } -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 @@ is_possible_normal_placeholder_end(const char16_t c) { } } -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 @@ pseudo_generate_expansion(const unsigned int length) { 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 @@ pseudolocalize_string(const String16& source) } } 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 @@ pseudolocalize_string(const String16& source) 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); + } + if (!lastspace) { + // End of last word + result += k_pdf + k_rlm; } - result += k_pdf; - result += 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 e6ab18ef1747..71b974b0c269 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 000000000000..4670e9fd53ea --- /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)); +} |