Enable Material theme for Android

With this CL, Mozc for Android supports Material theme.

Note that this CL also contains a lot of other improvements and bug fixes that might not be directly related to the Material theme.  Here are some examples:
  * Floating candidate window support in Android 5.0.
  * Improved accessibility support.
  * Start bundling Key Character Map (KCM) file of Japanese 109 keyboard.

See the release note for details.

As for desktop versions, no behavior change is intended.

BUG=none
TEST=manually done with Nexus 5 / Android 5.0.1 (LRX22C)

git-svn-id: https://mozc.googlecode.com/svn/trunk@467 a6090854-d499-a067-5803-1114d4e51264
diff --git a/src/DEPS b/src/DEPS
index d58edc4..03c4ff4 100644
--- a/src/DEPS
+++ b/src/DEPS
@@ -29,6 +29,7 @@
 
 vars = {
   "breakpad_revision": "1391",
+  "fonttools_revision": "5ba7d98a4153fad57258fca23b0bcb238717aec3",
   "gtest_revision": "700",
   "gmock_revision": "501",
   "gyp_revision": "2012",
@@ -82,6 +83,9 @@
       File("http://findbugs.googlecode.com/"
            + "svn/repos/release-repository/com/google/code/findbugs/jsr305/"
            + Var("jsr305_version") + "/jsr305-" + Var("jsr305_version") + ".jar"),
+    "src/third_party/fontTools":
+      "https://github.com/googlei18n/fonttools.git@" +
+      Var("fonttools_revision"),
     "src/third_party/zlib/v1_2_8":
       "https://src.chromium.org/chrome/trunk/src/third_party/zlib@" +
       Var("zlib_revision"),
diff --git a/src/android/AndroidManifest_template.xml b/src/android/AndroidManifest_template.xml
index 988b823..5d3a845 100644
--- a/src/android/AndroidManifest_template.xml
+++ b/src/android/AndroidManifest_template.xml
@@ -33,15 +33,21 @@
           package="@ANDROID_APPLICATION_ID@"
           android:versionCode="@ANDROID_VERSION_CODE@"
           android:versionName="@MAJOR@.@MINOR@.@BUILD@.@REVISION@-@ANDROID_ARCH@">
-  <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="19" />
+  <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21" />
 
   <!-- Google Japanese Input (the main code) requires the following permissions. -->
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
   <uses-permission android:name="android.permission.INTERNET" />
   <uses-permission android:name="android.permission.VIBRATE" />
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
 
 
-  <application android:icon="@drawable/application_icon" android:label="@string/app_full_name" >
+  <application android:icon="@drawable/application_icon"
+               android:label="@string/app_full_name"
+               android:allowBackup="false"
+               android:theme="@style/AppThemeSelector" >
     <service android:name="@ANDROID_SERVICE_NAME@"
              android:permission="android.permission.BIND_INPUT_METHOD"
              android:label="@string/app_full_name">
@@ -51,18 +57,32 @@
       <meta-data android:name="android.view.im" android:resource="@xml/method" />
     </service>
 
-    <!-- Preference -->
-    <activity android:name="org.mozc.android.inputmethod.japanese.preference.MozcProxyPreferenceActivity"
+    <!-- Launcher Icon -->
+    <activity android:name="org.mozc.android.inputmethod.japanese.LauncherActivity"
+              android:enabled="@bool/show_launcher_icon"
               android:label="@string/app_full_name">
       <intent-filter>
         <action android:name="android.intent.action.MAIN" />
         <category android:name="android.intent.category.LAUNCHER" />
       </intent-filter>
     </activity>
+
+    <receiver android:name="org.mozc.android.inputmethod.japanese.LauncherIconVisibilityInitializer">
+      <intent-filter>
+        <action android:name="android.intent.action.BOOT_COMPLETED" />
+        <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
+        <action android:name="android.intent.action.USER_INITIALIZE" />
+      </intent-filter>
+    </receiver>
+
+    <!-- Preference -->
+    <activity android:name="org.mozc.android.inputmethod.japanese.preference.MozcProxyPreferenceActivity" />
     <activity android:name="org.mozc.android.inputmethod.japanese.preference.MozcFragmentPreferenceActivity"
-              android:launchMode="singleTask" />
+              android:launchMode="singleTask"
+              android:theme="@style/AppThemeSelectorSettings"/>
     <activity android:name="org.mozc.android.inputmethod.japanese.preference.MozcFragmentSoftwareKeyboardAdvancedSettingsPreferenceActivity"
-              android:launchMode="singleTop" />
+              android:launchMode="singleTop"
+              android:theme="@style/AppThemeSelectorSettings"/>
 
     <activity android:name="org.mozc.android.inputmethod.japanese.FirstTimeLaunchActivity" />
     <activity android:name="org.mozc.android.inputmethod.japanese.preference.MiniBrowserActivity"
@@ -70,11 +90,11 @@
 
     <!-- User dictionary tool -->
     <activity android:name="org.mozc.android.inputmethod.japanese.userdictionary.UserDictionaryToolActivity"
-              android:enabled="@bool/enable_user_dictionary_tools_activity"
               android:label="@string/user_dictionary_tool_app_name"
-              android:theme="@style/MozcLightTheme"
               android:uiOptions="splitActionBarWhenNarrow"
-              android:configChanges="orientation|screenSize">
+              android:configChanges="orientation|screenSize"
+              android:theme="@style/AppThemeSelectorSettings">
+
       <!-- Intent filters to receive a file-sending intent for importing user dictionary data.
            UserDictionaryToolActivity accepts only file scheme.
       -->
@@ -111,11 +131,20 @@
     <activity android:name="org.mozc.android.inputmethod.japanese.mushroom.MushroomSelectionActivity"
               android:launchMode="singleTask"
               android:taskAffinity=".mushroom"
-              android:theme="@style/MozcMushroomDialogTheme"
+              android:theme="@android:style/Theme.DeviceDefault.Light.Dialog"
               android:windowSoftInputMode="stateAlwaysHidden"
               android:excludeFromRecents="true" />
 
 
+    <!-- Input device manager -->
+    <receiver android:name="org.mozc.android.inputmethod.japanese.InputDeviceReceiver">
+      <intent-filter>
+        <action android:name="android.hardware.input.action.QUERY_KEYBOARD_LAYOUTS" />
+      </intent-filter>
+      <meta-data android:name="android.hardware.input.metadata.KEYBOARD_LAYOUTS"
+                 android:resource="@xml/keyboard_layouts" />
+    </receiver>
+
     <!-- Following entry is for unit tests. Do not remove. -->
     <!-- The activity is used for testing views. -->
     <activity android:name="android.app.Activity" />
diff --git a/src/android/android.gyp b/src/android/android.gyp
index 9943cf8..147f2a3 100644
--- a/src/android/android.gyp
+++ b/src/android/android.gyp
@@ -100,6 +100,7 @@
       'type': 'none',
       'dependencies': [
         'protobuf/protobuf.gyp:protobuf_java',
+        'resources/resources.gyp:resources',
         'sdk_apk_dependencies',
         'userfeedback/userfeedback.gyp:userfeedback',
       ],
@@ -153,9 +154,10 @@
         'android_manifest',
         'assets',
         'mozc',
-        'gen_mozc_drawable',
         'guava_library',
         'userfeedback/userfeedback.gyp:userfeedback_project',
+        'subset_font',
+        'resources/resources.gyp:resources_project',
         'support_libraries',
       ],
     },
@@ -219,7 +221,6 @@
         'files': [
           # Copies the copyright and credit info.
           '../data/installer/credits_en.html',
-          '../data/installer/credits_ja.html',
         ],
       }],
     },
@@ -407,7 +408,7 @@
           'outputs': ['dummy_touch_stat_data'],
           'action': [
             'python', 'gen_touch_event_stats.py',
-            '--output_dir', 'assets',
+            '--output_dir', '<(sdk_asset_dir)',
             '--stats_data', '../data/typing/touch_event_stats.csv',
             '--collected_keyboards', 'collected_keyboards.csv',
           ],
@@ -528,22 +529,34 @@
       ],
     },
     {
-      'target_name': 'gen_mozc_drawable',
+      'target_name': 'subset_font',
       'type': 'none',
+      'dependencies': [
+        # TODO(komatsu): Is it better to move android_base.gyp?
+        'resources/resources.gyp:copy_asis_svg',
+        'resources/resources.gyp:transform_template_svg',
+      ],
+      'variables': {
+        'input_font': '<(font_dir)/Noto-Roboto2-Regular.otf',
+        'fonttools_path': '<(third_party_dir)/fontTools/Lib/fontTools',
+      },
       'actions': [
         {
-          'action_name': 'generate_pic_files',
+          'action_name': 'make_subset_font',
           'inputs': [
-            '<(dummy_input_file)',
-            'gen_mozc_drawable.py',
+            '<(input_font)',
+            'gen_subset_font.py',
           ],
           'outputs': [
-            'dummy_gen_mozc_drawable_output',
+            '<(sdk_asset_dir)/subset_font.otf',
           ],
           'action': [
-            'python', 'gen_mozc_drawable.py',
-            '--svg_dir=../data/images/android/svg',
-            '--output_dir=<(resources_project_path)/res/raw',
+            'python',
+            'gen_subset_font.py',
+            '--svg_paths=<(shared_intermediate_mozc_dir)/data/images/android/svg/transformed.zip,<(shared_intermediate_mozc_dir)/data/images/android/svg/asis.zip',
+            '--input_font', '<(input_font)',
+            '--output_font', '<@(_outputs)',
+            '--fonttools_path', '<(fonttools_path)',
           ],
         },
       ],
diff --git a/src/android/android_env.gypi b/src/android/android_env.gypi
index c92a1ba..579b53f 100644
--- a/src/android/android_env.gypi
+++ b/src/android/android_env.gypi
@@ -78,7 +78,7 @@
         'native_test_small_targets': [
           'oss_data_manager_test',
         ],
-        'resources_project_path': 'static_resources/resources_oss',
+        'font_dir': '<(third_party_dir)/noto_font',
       },
     }],
     ['android_arch=="arm" and android_compiler=="gcc"', {
diff --git a/src/android/android_resources.gypi b/src/android/android_resources.gypi
new file mode 100644
index 0000000..853b35d
--- /dev/null
+++ b/src/android/android_resources.gypi
@@ -0,0 +1,60 @@
+# Copyright 2010-2014, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+{
+  'variables': {
+    'android_translations': [
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values-ja/*.xml)',
+    ],
+
+    'android_resources': [
+      '<@(android_translations)',
+
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/drawable/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/drawable-hdpi/*.png)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/drawable-xhdpi/*.png)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/layout/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/layout-land/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/menu/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values-h380dp-land/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values-h500dp-land/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values-h570dp-land/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values-h650dp-port/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values-h720dp-land/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values-h800dp-port/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values-h900dp-port/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values-land/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values-v16/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/values-v21/*.xml)',
+      '<!@(ls -1 <(original_sdk_resources_dir)/res/xml/*.xml)',
+    ],
+  },
+}
+
diff --git a/src/android/gen_mozc_drawable.py b/src/android/gen_mozc_drawable.py
index ad11a4b..b4f7699 100644
--- a/src/android/gen_mozc_drawable.py
+++ b/src/android/gen_mozc_drawable.py
@@ -70,6 +70,7 @@
 CMD_PICTURE_DRAW_ELLIPSE = 7
 CMD_PICTURE_DRAW_GROUP_START = 8
 CMD_PICTURE_DRAW_GROUP_END = 9
+CMD_PICTURE_DRAW_TEXT = 10
 
 CMD_PICTURE_PATH_EOP = 0
 CMD_PICTURE_PATH_MOVE = 1
@@ -88,6 +89,20 @@
 CMD_PICTURE_PAINT_STROKE_CAP = 5
 CMD_PICTURE_PAINT_STROKE_JOIN = 6
 CMD_PICTURE_PAINT_SHADER = 7
+CMD_PICTURE_PAINT_FONT_SIZE = 8
+CMD_PICTURE_PAINT_TEXT_ANCHOR = 9
+CMD_PICTURE_PAINT_DOMINANT_BASELINE = 10
+CMD_PICTURE_PAINT_FONT_WEIGHT = 11
+
+CMD_PICTURE_PAINT_TEXT_ANCHOR_START = 0
+CMD_PICTURE_PAINT_TEXT_ANCHOR_MIDDLE = 1
+CMD_PICTURE_PAINT_TEXT_ANCHOR_END = 2
+
+CMD_PICTURE_PAINT_DOMINANTE_BASELINE_AUTO = 0
+CMD_PICTURE_PAINT_DOMINANTE_BASELINE_CENTRAL = 1
+
+CMD_PICTURE_PAINT_FONT_WEIGHT_NORMAL = 0
+CMD_PICTURE_PAINT_FONT_WEIGHT_BOLD = 1
 
 CMD_PICTURE_SHADER_LINEAR_GRADIENT = 1
 CMD_PICTURE_SHADER_RADIAL_GRADIENT = 2
@@ -99,19 +114,22 @@
 STYLE_CATEGORY_KEYICON_MAIN_HIGHLIGHT = 3
 STYLE_CATEGORY_KEYICON_GUIDE_HIGHLIGHT = 4
 STYLE_CATEGORY_KEYICON_BOUND = 5
-STYLE_CATEGORY_KEYICON_FUNCTION = 6
-STYLE_CATEGORY_KEYICON_FUNCTION_DARK = 7
-STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW = 8
-STYLE_CATEGORY_KEYICON_QWERTY_CAPS_ON_ARROW = 9
-STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT = 10
-STYLE_CATEGORY_KEYICON_POPUP_FUNCTION = 11
-STYLE_CATEGORY_KEYICON_POPUP_FUNCTION_DARK = 12
+STYLE_CATEGORY_KEYICON_TWELVEKEYS_FUNCTION = 6
+STYLE_CATEGORY_KEYICON_TWELVEKEYS_GLOBE = 7
+STYLE_CATEGORY_KEYICON_QWERTY_FUNCTION = 8
+STYLE_CATEGORY_KEYICON_FUNCTION_DARK = 9
+STYLE_CATEGORY_KEYICON_ENTER = 10
+STYLE_CATEGORY_KEYICON_ENTER_CIRCLE = 11
+STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW = 12
+STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT = 13
 # We may be able to reuse same resources for symbol major/minor icons.
-STYLE_CATEGORY_SYMBOL_MAJOR = 13
-STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED = 14
-STYLE_CATEGORY_SYMBOL_MINOR = 15
-STYLE_CATEGORY_SYMBOL_MINOR_SELECTED = 16
-STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND = 17
+STYLE_CATEGORY_SYMBOL_MAJOR = 14
+STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED = 15
+STYLE_CATEGORY_SYMBOL_MAJOR_EMOJI_DISABLE_CIRCLE = 16
+STYLE_CATEGORY_SYMBOL_MINOR = 17
+STYLE_CATEGORY_SYMBOL_MINOR_SELECTED = 18
+STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_DEFAULT = 19
+STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_SCROLLED = 20
 
 # We'll check the category id by reverse sorted order, to resolve prefix match
 # confliction.
@@ -124,23 +142,28 @@
         ('style-keyicon-guide-highlight',
          STYLE_CATEGORY_KEYICON_GUIDE_HIGHLIGHT),
         ('style-keyicon-bound', STYLE_CATEGORY_KEYICON_BOUND),
-        ('style-keyicon-function', STYLE_CATEGORY_KEYICON_FUNCTION),
+        ('style-keyicon-twelvekeys-function',
+         STYLE_CATEGORY_KEYICON_TWELVEKEYS_FUNCTION),
+        ('style-keyicon-twelvekeys-globe',
+         STYLE_CATEGORY_KEYICON_TWELVEKEYS_GLOBE),
+        ('style-keyicon-qwerty-function',
+         STYLE_CATEGORY_KEYICON_QWERTY_FUNCTION),
         ('style-keyicon-function-dark', STYLE_CATEGORY_KEYICON_FUNCTION_DARK),
+        ('style-keyicon-enter', STYLE_CATEGORY_KEYICON_ENTER),
+        ('style-keyicon-enter-circle', STYLE_CATEGORY_KEYICON_ENTER_CIRCLE),
         ('style-keyicon-qwerty-shift-on-arrow',
          STYLE_CATEGORY_KEYICON_QWERTY_SHIFT_ON_ARROW),
-        ('style-keyicon-qwerty-caps-on-arrow',
-         STYLE_CATEGORY_KEYICON_QWERTY_CAPS_ON_ARROW),
         ('style-keypopup-highlight', STYLE_CATEGORY_KEYPOPUP_HIGHLIGHT),
-        ('style-keyicon-popup-function',
-         STYLE_CATEGORY_KEYICON_POPUP_FUNCTION),
-        ('style-keyicon-popup-function-dark',
-         STYLE_CATEGORY_KEYICON_POPUP_FUNCTION_DARK),
         ('style-symbol-major', STYLE_CATEGORY_SYMBOL_MAJOR),
         ('style-symbol-major-selected', STYLE_CATEGORY_SYMBOL_MAJOR_SELECTED),
+        ('style-symbol-major-emoji-disable-circle',
+         STYLE_CATEGORY_SYMBOL_MAJOR_EMOJI_DISABLE_CIRCLE),
         ('style-symbol-minor', STYLE_CATEGORY_SYMBOL_MINOR),
         ('style-symbol-minor-selected', STYLE_CATEGORY_SYMBOL_MINOR_SELECTED),
-        ('style-keyboard-folding-button-background',
-         STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND),
+        ('style-keyboard-folding-button-background-default',
+         STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_DEFAULT),
+        ('style-keyboard-folding-button-background-scrolled',
+         STYLE_CATEGORY_KEYBOARD_FOLDING_BUTTON_BACKGROUND_SCROLLED),
     ],
     reverse=True)
 
@@ -150,11 +173,11 @@
 #
 # Format of StateListDrawable:
 # 2, [[state_list], drawable]
-COLOR_PATTERN = re.compile(r'#([0-9A-Fa-f]{6})')
+COLOR_PATTERN = re.compile(r'#([0-9A-Fa-f]{3,8}(?![0-9A-Fa-f]))')
 PIXEL_PATTERN = re.compile(r'(\d+)(?:px)?')
 FLOAT_PATTERN = re.compile(r'^\s*,?\s*([+-]?\d+(?:\.\d*)?(:?e[+-]\d+)?)')
 SHADER_PATTERN = re.compile(r'url\(#(.*)\)')
-TRANSFORM_PATTERN = re.compile(r'(matrix|translate)\((.*)\)')
+TRANSFORM_PATTERN = re.compile(r'(matrix|translate|scale)\((.*)\)')
 MATRIX_PATTERN = re.compile(r'matrix\((.*)\)')
 STOP_COLOR_PATTERN = re.compile(r'stop-color:(.*)')
 
@@ -162,14 +185,16 @@
 HAS_SHADOW = 1
 HAS_BELOW_SHADOW = 2
 
+
 class _OutputStream(object):
   """Simple wrapper of output stream by by packing value in big endian."""
+
   def __init__(self, output):
     self.output = output
 
   def WriteByte(self, value):
     if not (0 <= value <= 255):
-      logging.critical('overflow')
+      logging.critical('overflow byte')
       sys.exit(1)
     self.output.write(struct.pack('>B', value & 0xFF))
 
@@ -187,15 +212,21 @@
     # so we can compress the data by using fixed-precision values.
     self.output.write(struct.pack('>f', value))
 
+  def WriteString(self, value):
+    utf8_string = value.encode('utf-8')
+    self.WriteInt16(len(utf8_string))
+    self.output.write(utf8_string)
+
   def __enter__(self):
     self.output.__enter__()
 
-  def __exit__(self):
-    self.output.__exit__()
+  def __exit__(self, exec_type, exec_value, traceback):
+    self.output.__exit__(exec_type, exec_value, traceback)
 
 
 class MozcDrawableConverter(object):
   """Converter from .svg file to .pic file."""
+
   def __init__(self):
     pass
 
@@ -209,12 +240,22 @@
     if not m:
       return None
 
-    c = int(m.group(1), 16)
-    if c < 0 or 0x1000000 <= c:
+    c_str = m.group(1)
+    if 3 <= len(c_str) <= 4:
+      expanded_c_str = ''
+      for ch in c_str:
+        expanded_c_str = expanded_c_str + ch + ch
+      c_str = expanded_c_str
+    if len(c_str) == 6:
+      c_str = 'FF' + c_str
+    if len(c_str) != 8:
+      logging.critical('Invalid color format.')
+      sys.exit(1)
+    c = int(c_str, 16)
+    if c < 0 or 0x100000000 <= c:
       logging.critical('Out of color range: %s', color)
       sys.exit(1)
-    # Set alpha.
-    return c | 0xFF000000
+    return c
 
   def _ParseShader(self, color, shader_map):
     """Parses shader attribute and returns a shader name from the given map."""
@@ -278,77 +319,104 @@
       y1 = float(node.get('y1'))
       x2 = float(node.get('x2'))
       y2 = float(node.get('y2'))
-      gradientTransform = node.get('gradientTransform')
-      if gradientTransform:
-        m = MATRIX_PATTERN.match(gradientTransform)
-        (m11, m21, m12, m22, m13, m23) = self._ParseFloatList(m.group(1))
+      gradient_transform = node.get('gradientTransform')
+      if gradient_transform:
+        m = MATRIX_PATTERN.match(gradient_transform)
+        (m11, m21, m12, m22, m13, m23) = tuple(self._ParseFloatList(m.group(1)))
         (x1, y1) = (m11 * x1 + m12 * y1 + m13, m21 * x1 + m22 * y1 + m23)
         (x2, y2) = (m11 * x2 + m12 * y2 + m13, m21 * x2 + m22 * y2 + m23)
 
       color_list = self._ParseStopList(node)
-      return { element_id: ('linear', x1, y1, x2, y2, color_list) }
+      return {element_id: ('linear', x1, y1, x2, y2, color_list)}
 
     if node.tag == '{http://www.w3.org/2000/svg}radialGradient':
       element_id = node.get('id')
       cx = float(node.get('cx'))
       cy = float(node.get('cy'))
       r = float(node.get('r'))
-      gradientTransform = node.get('gradientTransform')
-      if gradientTransform:
-        m = MATRIX_PATTERN.match(gradientTransform)
+      gradient_transform = node.get('gradientTransform')
+      if gradient_transform:
+        m = MATRIX_PATTERN.match(gradient_transform)
         matrix = self._ParseFloatList(m.group(1))
       else:
         matrix = None
 
       color_list = self._ParseStopList(node)
-      return { element_id: ('radial', cx, cy, r, matrix, color_list) }
+      return {element_id: ('radial', cx, cy, r, matrix, color_list)}
 
     return {}
 
   def _ParseStyle(self, node, has_shadow, shader_map):
     """Parses style attribute of the given node."""
-    result = {}
+    common_map = {}
+    # Default fill color is black (SVG's spec)
+    fill_map = {'style': 'fill', 'color': 0xFF000000, 'shadow': has_shadow}
+    # Default stroke color is none (SVG's spec)
+    stroke_map = {'style': 'stroke', 'shadow': has_shadow}
+
+    # Special warning for font-size.
+    # Inkscape often unexpectedly converts from sytle to font-size attribute.
+    if node.get('font-size', ''):
+      logging.warning('font-size attribute is not supported.')
+
     for attr in node.get('style', '').split(';'):
       attr = attr.strip()
       if not attr:
         continue
       command, arg = attr.split(':')
       if command == 'fill' or command == 'stroke':
-        shader = self._ParseShader(arg, shader_map)
-        color = self._ParseColor(arg)
-
-        if shader is None and color is None:
-          if arg != 'none':
-            logging.error('Unknown pattern: %s', arg)
+        paint_map = fill_map if command == 'fill' else stroke_map
+        if arg == 'none':
+          paint_map.pop('color', None)
+          paint_map.pop('shader', None)
           continue
 
-        paint_map = {}
+        shader = self._ParseShader(arg, shader_map)
+        color = self._ParseColor(arg)
+        if shader is None and color is None:
+          if arg != 'none':
+            logging.critical('Unknown pattern: %s', arg)
+            sys.exit(1)
+          continue
+
         paint_map['style'] = command
         if shader is not None:
           paint_map['shader'] = shader
         if color is not None:
           paint_map['color'] = color
-        paint_map['shadow'] = has_shadow
-
-        result[command] = paint_map
         continue
 
       if command == 'stroke-width':
-        paint_map = result['stroke']
-        paint_map['stroke-width'] = float(arg)
+        stroke_map['stroke-width'] = float(arg)
         continue
 
       if command == 'stroke-linecap':
-        paint_map = result['stroke']
-        paint_map['stroke-linecap'] = arg
+        stroke_map['stroke-linecap'] = arg
         continue
 
       if command == 'stroke-linejoin':
-        paint_map = result['stroke']
-        paint_map['stroke-linejoin'] = arg
+        stroke_map['stroke-linejoin'] = arg
         continue
 
-    return sorted(result.values(), key=lambda e: e['style'])
+      # font relating attributes are common to all commands.
+      if command == 'font-size':
+        common_map['font-size'] = self._ParsePixel(arg)
+      if command == 'text-anchor':
+        common_map['text-anchor'] = arg
+      if command == 'dominant-baseline':
+        common_map['dominant-baseline'] = arg
+      if command == 'font-weight':
+        common_map['font-weight'] = arg
+
+    # 'fill' comes first in order to draw 'fill' first (SVG specification).
+    result = []
+    if 'color' in fill_map or 'shader' in fill_map:
+      fill_map.update(common_map)
+      result.append(fill_map)
+    if 'color' in stroke_map or 'shader' in stroke_map:
+      stroke_map.update(common_map)
+      result.append(stroke_map)
+    return result
 
   def _ParseStopList(self, parent_node):
     result = []
@@ -390,7 +458,8 @@
       output.WriteFloat(2.)
       output.WriteInt32(0xFF292929)
 
-  def _ConvertStyle(self, style, output):
+  def _ConvertStyleCommand(self, style, output):
+    """Converts style attribute."""
     if style == 'fill':
       output.WriteByte(CMD_PICTURE_PAINT_STYLE)
       output.WriteByte(0)
@@ -490,23 +559,57 @@
     logging.critical('unknown stroke-linejoin: %s', stroke_linejoin)
     sys.exit(1)
 
-  def _ConvertStyleMap(self, style_map, output):
-    self._ConvertStyle(style_map['style'], output)
-    self._MaybeConvertColor(style_map.get('color'), output)
-    self._MaybeConvertShader(style_map.get('shader'), output)
-    self._MaybeConvertShadow(style_map['shadow'], output)
-    self._MaybeConvertStrokeWidth(style_map.get('stroke-width'), output)
-    self._MaybeConvertStrokeLinecap(style_map.get('stroke-linecap'), output)
-    self._MaybeConvertStrokeLinejoin(style_map.get('stroke-linejoin'), output)
-    output.WriteByte(CMD_PICTURE_PAINT_EOP)
+  def _MaybeConvertFontSize(self, font_size, output):
+    if font_size is None:
+      return
+    output.WriteByte(CMD_PICTURE_PAINT_FONT_SIZE)
+    output.WriteFloat(font_size)
+    return
 
-  def _ConvertStyleList(self, style_list, output):
-    output.WriteByte(len(style_list))
-    for style_map in style_list:
-      self._ConvertStyleMap(style_map, output)
+  def _MaybeConvertTextAnchor(self, text_anchor, output):
+    if text_anchor is None:
+      return
+    output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR)
+    if text_anchor == 'start':
+      output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR_START)
+    elif text_anchor == 'middle':
+      output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR_MIDDLE)
+    elif text_anchor == 'end':
+      output.WriteByte(CMD_PICTURE_PAINT_TEXT_ANCHOR_END)
+    else:
+      logging.critical('text-anchor is invalid (%s)', text_anchor)
+      sys.exit(1)
+    return
 
-  def _ConvertStyleCategory(self, style_category, output):
-    output.WriteByte(1)
+  def _MaybeConvertDominantBaseline(self, baseline, output):
+    if baseline is None:
+      return
+    output.WriteByte(CMD_PICTURE_PAINT_DOMINANT_BASELINE)
+    if baseline == 'auto':
+      output.WriteByte(CMD_PICTURE_PAINT_DOMINANTE_BASELINE_AUTO)
+    elif baseline == 'central':
+      output.WriteByte(CMD_PICTURE_PAINT_DOMINANTE_BASELINE_CENTRAL)
+    else:
+      logging.critical('dominant-baseline is invalid (%s)', baseline)
+      sys.exit(1)
+    return
+
+  def _MaybeConvertFontWeight(self, weight, output):
+    if weight is None:
+      return
+    output.WriteByte(CMD_PICTURE_PAINT_FONT_WEIGHT)
+    if weight == 'normal':
+      output.WriteByte(CMD_PICTURE_PAINT_FONT_WEIGHT_NORMAL)
+    elif weight == 'bold':
+      output.WriteByte(CMD_PICTURE_PAINT_FONT_WEIGHT_BOLD)
+    else:
+      logging.critical('font-weight is invalid (%s)', weight)
+      sys.exit(1)
+    return
+
+  def _MaybeConvertStyleCategory(self, style_category, output):
+    if style_category is None:
+      return
     for id_prefix, category in STYLE_CATEGORY_MAP:
       if style_category.startswith(id_prefix):
         output.WriteByte(STYLE_CATEGORY_TAG + category)
@@ -514,6 +617,25 @@
     logging.critical('unknown style_category: "%s"', style_category)
     sys.exit(1)
 
+  def _ConvertStyle(self, style_category, style_list, output):
+    style_len = len(style_list)
+    output.WriteByte(style_len)
+    for style_map in style_list:
+      self._ConvertStyleCommand(style_map['style'], output)
+      self._MaybeConvertColor(style_map.get('color'), output)
+      self._MaybeConvertShader(style_map.get('shader'), output)
+      self._MaybeConvertShadow(style_map['shadow'], output)
+      self._MaybeConvertStrokeWidth(style_map.get('stroke-width'), output)
+      self._MaybeConvertStrokeLinecap(style_map.get('stroke-linecap'), output)
+      self._MaybeConvertStrokeLinejoin(style_map.get('stroke-linejoin'), output)
+      self._MaybeConvertFontSize(style_map.get('font-size'), output)
+      self._MaybeConvertTextAnchor(style_map.get('text-anchor'), output)
+      self._MaybeConvertDominantBaseline(style_map.get('dominant-baseline'),
+                                         output)
+      self._MaybeConvertFontWeight(style_map.get('font-weight'), output)
+      self._MaybeConvertStyleCategory(style_category, output)
+      output.WriteByte(CMD_PICTURE_PAINT_EOP)
+
   def _ConvertPath(self, node, output):
     path = node.get('d')
     if path is None:
@@ -650,10 +772,7 @@
       self, node, style_category, has_shadow, shader_map, output):
     style_list = self._ParseStyle(node, has_shadow, shader_map)
     self._ConvertPath(node, output)
-    if style_category is not None:
-      self._ConvertStyleCategory(style_category, output)
-    else:
-      self._ConvertStyleList(style_list, output)
+    self._ConvertStyle(style_category, style_list, output)
 
   def _ConvertPolylineElement(
       self, node, style_category, has_shadow, shader_map, output):
@@ -667,10 +786,7 @@
     output.WriteByte(len(point_list))
     for coord in point_list:
       output.WriteFloat(coord)
-    if style_category is not None:
-      self._ConvertStyleCategory(style_category, output)
-    else:
-      self._ConvertStyleList(style_list, output)
+    self._ConvertStyle(style_category, style_list, output)
 
   def _ConvertPolygonElement(
       self, node, style_category, has_shadow, shader_map, output):
@@ -681,10 +797,7 @@
     output.WriteByte(len(point_list))
     for coord in point_list:
       output.WriteFloat(coord)
-    if style_category is not None:
-      self._ConvertStyleCategory(style_category, output)
-    else:
-      self._ConvertStyleList(style_list, output)
+    self._ConvertStyle(style_category, style_list, output)
 
   def _ConvertLineElement(
       self, node, style_category, has_shadow, shader_map, output):
@@ -698,10 +811,7 @@
     output.WriteFloat(y1)
     output.WriteFloat(x2)
     output.WriteFloat(y2)
-    if style_category is not None:
-      self._ConvertStyleCategory(style_category, output)
-    else:
-      self._ConvertStyleList(style_list, output)
+    self._ConvertStyle(style_category, style_list, output)
 
   def _ConvertCircleElement(
       self, node, style_category, has_shadow, shader_map, output):
@@ -713,10 +823,7 @@
     output.WriteFloat(cx)
     output.WriteFloat(cy)
     output.WriteFloat(r)
-    if style_category is not None:
-      self._ConvertStyleCategory(style_category, output)
-    else:
-      self._ConvertStyleList(style_list, output)
+    self._ConvertStyle(style_category, style_list, output)
 
   def _ConvertEllipseElement(
       self, node, style_category, has_shadow, shader_map, output):
@@ -730,10 +837,7 @@
     output.WriteFloat(cy)
     output.WriteFloat(rx)
     output.WriteFloat(ry)
-    if style_category is not None:
-      self._ConvertStyleCategory(style_category, output)
-    else:
-      self._ConvertStyleList(style_list, output)
+    self._ConvertStyle(style_category, style_list, output)
 
   def _ConvertRectElement(
       self, node, style_category, has_shadow, shader_map, output):
@@ -747,14 +851,39 @@
     output.WriteFloat(y)
     output.WriteFloat(w)
     output.WriteFloat(h)
-    if style_category is not None:
-      self._ConvertStyleCategory(style_category, output)
-    else:
-      self._ConvertStyleList(style_list, output)
+    self._ConvertStyle(style_category, style_list, output)
+
+  def _ConvertTextElement(
+      self, node, style_category, has_shadow, shader_map, output):
+    """Converts text element.
+
+    The text element must not have any children.
+
+    Args:
+      node: node
+      style_category: style_category(optional)
+      has_shadow: shadow value
+      shader_map: shader map
+      output: output stream
+    """
+    if not node.text:
+      # Ignore empty text node
+      return
+
+    style_list = self._ParseStyle(node, has_shadow, shader_map)
+    x = float(node.get('x', 0))
+    y = float(node.get('y', 0))
+    output.WriteByte(CMD_PICTURE_DRAW_TEXT)
+    output.WriteFloat(x)
+    output.WriteFloat(y)
+    if node:
+      logging.critical('<text> with children is not supported.')
+      sys.exit(1)
+    output.WriteString(node.text)
+    self._ConvertStyle(style_category, style_list, output)
 
   def _ConvertGroupElement(
       self, node, style_category, has_shadow, shader_map, output):
-
     transform = node.get('transform')
     if transform:
       # Output order of 3x3 matrix;
@@ -794,6 +923,25 @@
         output.WriteFloat(tx)
         output.WriteFloat(ty)
         output.WriteFloat(1)
+      elif transformation == 'scale':
+        parsed_coords = tuple(self._ParseFloatList(coords))
+        if len(parsed_coords) != 1 and len(parsed_coords) != 2:
+          logging.critical('Invalid argument for scale: %s', coords)
+          sys.exit(1)
+        sx = parsed_coords[0]
+        if len(parsed_coords) == 1:
+          sy = sx
+        else:
+          sy = parsed_coords[1]
+        output.WriteFloat(sx)
+        output.WriteFloat(0)
+        output.WriteFloat(0)
+        output.WriteFloat(0)
+        output.WriteFloat(sy)
+        output.WriteFloat(0)
+        output.WriteFloat(0)
+        output.WriteFloat(0)
+        output.WriteFloat(1)
       else:
         # Never reach here. Just in case.
         logging.critical('Unsupported transform: %s', transform)
@@ -855,17 +1003,26 @@
           node, style_category, has_shadow, shader_map, output)
       return
 
+    if node.tag == '{http://www.w3.org/2000/svg}text':
+      self._ConvertTextElement(
+          node, style_category, has_shadow, shader_map, output)
+      return
+
     if node.tag in ['{http://www.w3.org/2000/svg}g',
                     '{http://www.w3.org/2000/svg}svg']:
       self._ConvertGroupElement(
           node, style_category, has_shadow, shader_map, output)
       return
 
+    # Ignore following tags.
     if node.tag in ['{http://www.w3.org/2000/svg}linearGradient',
-                    '{http://www.w3.org/2000/svg}radialGradient']:
+                    '{http://www.w3.org/2000/svg}radialGradient',
+                    '{http://www.w3.org/2000/svg}metadata',
+                    '{http://www.w3.org/2000/svg}defs',]:
       return
 
-    logging.warning('Unknown element: %s', node.tag)
+    logging.critical('Unknown element: %s', node.tag)
+    sys.exit(1)
 
   def _OutputEOP(self, output):
     output.WriteByte(CMD_PICTURE_EOP)
@@ -899,21 +1056,22 @@
     return output.output.getvalue()
 
 
-def ConvertFiles(svg_dir, output_dir):
+def ConvertFiles(svg_paths, output_dir):
   """Converts SVG files into MechaMozc specific *pic* files.
 
   Args:
-    svg_dir: Path to a directory which has svg files (recursively).
+    svg_paths: Comma separated paths to a directory/zip which has svg files
+               (recursively).
     output_dir: Path of the destination directory.
   """
-  logging.debug('Start SVG conversion. From:%s, To:%s', svg_dir, output_dir)
+  logging.debug('Start SVG conversion. From:%s, To:%s', svg_paths, output_dir)
   # Ensure that the output directory exists.
   if not os.path.exists(output_dir):
     os.makedirs(output_dir)
 
   converter = MozcDrawableConverter()
   number_of_conversion = 0
-  for dirpath, dirnames, filenames in os.walk(svg_dir):
+  for dirpath, _, filenames in util.WalkFileContainers(svg_paths):
     for filename in filenames:
       basename, ext = os.path.splitext(filename)
       if ext != '.svg':
@@ -971,11 +1129,16 @@
 
 
 def ParseOptions():
+  """Parses options."""
   parser = optparse.OptionParser()
-  parser.add_option('--svg_dir', dest='svg_dir',
-                    help='Path to a directory containing .svg files.')
+  parser.add_option('--svg_paths', dest='svg_paths',
+                    help='Comma separated paths to a directory or'
+                    ' a .zip file containing .svg files.')
   parser.add_option('--output_dir', dest='output_dir',
                     help='Path to the output directory,')
+  parser.add_option('--build_log', dest='build_log',
+                    help='(Optional) Path to build log to generate.'
+                    ' If set, nothing will be sent to stderr.')
   parser.add_option('--verbose', '-v', dest='verbose',
                     action='store_true', default=False,
                     help='Shows verbose message.')
@@ -987,9 +1150,12 @@
   logging.getLogger().addFilter(util.ColoredLoggingFilter())
 
   options = ParseOptions()
-  if options.verbose:
+  if options.build_log:
+    logging.getLogger().handlers = []
+    logging.getLogger().addHandler(logging.FileHandler(options.build_log, 'w'))
+  if options.verbose or options.build_log:
     logging.getLogger().setLevel(logging.DEBUG)
-  ConvertFiles(options.svg_dir, options.output_dir)
+  ConvertFiles(options.svg_paths, options.output_dir)
 
 
 if __name__ == '__main__':
diff --git a/src/android/gen_subset_font.py b/src/android/gen_subset_font.py
new file mode 100644
index 0000000..67db258
--- /dev/null
+++ b/src/android/gen_subset_font.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+# Copyright 2010-2014, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Generates subset font.
+
+1. Corrects characters from .svg files in given directory.
+  Only text nodes in <text> element are taken into account.
+2. Make subset font which contains only the characters corrected above.
+"""
+
+import logging
+import optparse
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+from xml.etree import cElementTree as ElementTree
+
+from build_tools import util
+
+
+def ExtractCharactersFromSvgFile(svg_file):
+  result = set()
+  for text in ElementTree.parse(svg_file).findall(
+      './/{http://www.w3.org/2000/svg}text'):
+    if text.text:
+      result.update(tuple(text.text))  # Split into characters.
+  logging.debug('%s: %s', svg_file, result)
+  return result
+
+
+def ExtractCharactersFromDirectory(svg_paths):
+  result = set()
+  for dirpath, _, filenames in util.WalkFileContainers(svg_paths):
+    for filename in filenames:
+      if os.path.splitext(filename)[1] != '.svg':
+        # Do nothing for files other than svg.
+        continue
+      result.update(ExtractCharactersFromSvgFile(
+          os.path.join(dirpath, filename)))
+  return list(result)
+
+
+def MakeSubsetFont(fonttools_path, unicodes, input_font, output_font):
+  """Makes subset font using fontTools toolchain.
+
+  Args:
+    fonttools_path: Path to fontTools library.
+    unicodes: A list of unicode characters of which glyph should be in
+              the output.
+    input_font: Path to input font.
+    output_font: Path to output font.
+  """
+  if not unicodes:
+    # subset.py requires at least one unicode character.
+    logging.debug('No unicode character is specified. Use "A" as stub.')
+    unicodes = [u'A']
+
+  tempdir = tempfile.mkdtemp()
+  try:
+    # fontTools's output file name is fixed (input file path + '.subset').
+    # To get result with specified file name, copy the input font
+    # to temporary directory and copy the result with renaming.
+    shutil.copy(input_font, tempdir)
+    temp_input_font = os.path.join(tempdir, os.path.basename(input_font))
+    commands = ['python', os.path.join(fonttools_path, 'subset.py'),
+                temp_input_font]
+    commands.extend(['U+%08x' % ord(char) for char in unicodes])
+    env = os.environ.copy()
+    # In fontTools toolchain, "from fontTools import XXXX" is executed.
+    # In order for successfull import, add parent directory of the toolchain
+    # into PYTHONPATH.
+    env['PYTHONPATH'] += ':%s' % os.path.join(fonttools_path, '..')
+    if subprocess.call(commands, env=env) != 0:
+      sys.exit(1)
+    shutil.copyfile('%s.subset' % temp_input_font, output_font)
+  finally:
+    shutil.rmtree(tempdir)
+
+
+def ParseOptions():
+  """Parses options."""
+  parser = optparse.OptionParser()
+  parser.add_option('--svg_paths', dest='svg_paths',
+                    help='Comma separated paths to a directory or'
+                    ' a .zip file containing .svg files.')
+  parser.add_option('--input_font', dest='input_font',
+                    help='Path to the input font.')
+  parser.add_option('--output_font', dest='output_font',
+                    help='Path to the output font.')
+  parser.add_option('--fonttools_path', dest='fonttools_path',
+                    help='Path to fontTools toolchain.')
+  parser.add_option('--verbose', dest='verbose',
+                    help='Verbosity')
+  return parser.parse_args()[0]
+
+
+def main():
+  logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
+
+  options = ParseOptions()
+  if options.verbose:
+    logging.getLogger().setLevel(logging.DEBUG)
+  unicodes = ExtractCharactersFromDirectory(options.svg_paths)
+  MakeSubsetFont(options.fonttools_path, unicodes,
+                 options.input_font, options.output_font)
+
+
+if __name__ == '__main__':
+  main()
+
diff --git a/src/android/proguard-project.txt b/src/android/proguard-project.txt
index f401f16..26c35b4 100644
--- a/src/android/proguard-project.txt
+++ b/src/android/proguard-project.txt
@@ -54,6 +54,11 @@
     public static <methods>;
 }
 
+# Skin's fields are accessed by reflection.
+-keepclassmembers class org.mozc.android.inputmethod.japanese.view.Skin {
+    <fields>;
+}
+
 # Needed for Guava library.
 -libraryjars libs/jsr305.jar
 -dontwarn sun.misc.Unsafe
diff --git a/src/android/project.properties b/src/android/project.properties
index 8d01942..8cef1f3 100644
--- a/src/android/project.properties
+++ b/src/android/project.properties
@@ -30,7 +30,7 @@
 
 
 # Project target.
-target=android-19
+target=android-21
 
 # Location of the ProGuard configuration.
 proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt
@@ -38,5 +38,5 @@
 # Library projects
 # 'resources' project must not be placed at the tail of reference definition.
 # Otherwise aapt command crashes.
-android.library.reference.1=static_resources/resources_oss
+android.library.reference.1=resources
 android.library.reference.2=protobuf
diff --git a/src/android/protobuf/project.properties b/src/android/protobuf/project.properties
index 9aacf2f..c3b6d82 100644
--- a/src/android/protobuf/project.properties
+++ b/src/android/protobuf/project.properties
@@ -39,4 +39,4 @@
 android.library=true
 
 # Project target.
-target=android-19
+target=android-21
diff --git a/src/android/resources/AndroidManifest.xml b/src/android/resources/AndroidManifest.xml
new file mode 100644
index 0000000..b060f87
--- /dev/null
+++ b/src/android/resources/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2010-2014, Google Inc.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+     * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+     * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following disclaimer
+ in the documentation and/or other materials provided with the
+ distribution.
+     * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.mozc.android.inputmethod.japanese.resources">
+  <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21" />
+</manifest>
diff --git a/src/android/static_resources/resources_oss/ant.properties b/src/android/resources/ant.properties
similarity index 100%
rename from src/android/static_resources/resources_oss/ant.properties
rename to src/android/resources/ant.properties
diff --git a/src/android/static_resources/resources_oss/build.xml b/src/android/resources/build.xml
similarity index 100%
rename from src/android/static_resources/resources_oss/build.xml
rename to src/android/resources/build.xml
diff --git a/src/android/resources/proguard-project.txt b/src/android/resources/proguard-project.txt
new file mode 100644
index 0000000..5317df3
--- /dev/null
+++ b/src/android/resources/proguard-project.txt
@@ -0,0 +1,49 @@
+# Copyright 2010-2014, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
diff --git a/src/android/resources/project.properties b/src/android/resources/project.properties
new file mode 100644
index 0000000..613d5c2
--- /dev/null
+++ b/src/android/resources/project.properties
@@ -0,0 +1,44 @@
+# Copyright 2010-2014, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-21
+android.library=true
diff --git a/src/android/resources/resources.gyp b/src/android/resources/resources.gyp
new file mode 100644
index 0000000..fa6b0b9
--- /dev/null
+++ b/src/android/resources/resources.gyp
@@ -0,0 +1,304 @@
+# Copyright 2010-2014, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+{
+  'variables': {
+    'relative_dir': 'android/resources',
+    'abs_android_dir': '<(abs_depth)/<(relative_dir)',
+    # Actions with an existing input and non-existing output behave like
+    # phony rules.  Nothing matters for an input but its existence, so
+    # we use 'resources.gyp' as a dummy input since it must exist.
+    'dummy_input_file': 'resources.gyp',
+    # GYP's 'copies' rule cannot copy a whole directory recursively, so we use
+    # our own script to copy files.
+    'copy_file': ['python', '../../build_tools/copy_file.py'],
+    'shared_intermediate_mozc_dir': '<(SHARED_INTERMEDIATE_DIR)/',
+    'static_resources_dir': '<(abs_depth)/android/static_resources',
+    'sdk_resources_dir': '<(shared_intermediate_mozc_dir)/android/resources',
+  },
+  'conditions': [
+    ['branding=="GoogleJapaneseInput"', {
+      'conditions': [
+        ['android_hide_icon==1', {
+          'variables': {
+            'launcher_icon_bools': '<(static_resources_dir)/launcher_icon_resources/launcher_icon_preinstall_bools.xml',
+          },
+        }, { # else
+          'variables': {
+            'launcher_icon_bools': '<(static_resources_dir)/launcher_icon_resources/launcher_icon_standard_bools.xml',
+          },
+        }],
+        ['android_release_icon==1', {
+          'variables': {
+            'application_icon_dir': '<(static_resources_dir)/application_icon/release_icon/',
+          },
+        }, {  # else
+          'variables': {
+            'application_icon_dir': '<(static_resources_dir)/application_icon/dogfood_icon/',
+          },
+        }],
+      ],
+      'variables': {
+        'original_sdk_resources_dir': '<(static_resources_dir)/resources',
+      },
+    }, {  # 'branding!="GoogleJapaneseInput"'
+      'variables': {
+        'launcher_icon_bools': '<(static_resources_dir)/launcher_icon_resources/launcher_icon_standard_bools.xml',
+        'original_sdk_resources_dir': '<(static_resources_dir)/resources_oss',
+        'application_icon_dir': '<(static_resources_dir)/application_icon/oss_icon/',
+      },
+    }],
+  ],
+  'targets': [
+    {
+      'target_name': 'resources',
+      'type': 'none',
+      'dependencies': [
+        'resources_project',
+      ],
+      'actions': [
+        {
+          'action_name': 'build_resources',
+          'inputs': [
+            'AndroidManifest.xml',
+            'build.xml',
+            'project.properties',
+            'ant.properties',
+            'proguard-project.txt',
+          ],
+          'outputs': [
+            'bin/classes.jar',
+            'gen/org/mozc/android/inputmethod/japanese/resources/R.java',
+          ],
+          'includes': ['../ant.gypi'],
+        },
+      ],
+    },
+    {
+      'target_name': 'resources_project',
+      'type': 'none',
+      'dependencies': [
+        'copy_resources',
+        'gen_kcm_data',
+        'gen_mozc_drawable',
+      ],
+    },
+    {
+      # Copies original resources to intermediate directory.
+      # Then make symbolic link from android/resources to the intermediate directory.
+      # The symbolic link is required to build both by ADT and ant.
+      'target_name': 'copy_resources',
+      'type': 'none',
+      'includes': ['../android_resources.gypi'],
+      'inputs': ['<@(android_resources)'],
+      'outputs': ['dummy_copy_resources'],
+      'actions': [
+        {
+          'action_name': 'copy_files',
+          'inputs': ['<@(android_resources)'],
+          'outputs': ['dummy_copy_files'],
+          'action': [
+            '<@(copy_file)', '-pr',
+            '<@(android_resources)',
+            '<(sdk_resources_dir)',
+            '--src_base', '<(original_sdk_resources_dir)',
+          ],
+        },
+        {
+          # Make sure the link of resources/res does not exist.
+          # the new link is created in the next step.
+          'action_name': 'remove_symbolic_link',
+          'inputs': [
+            'dummy_copy_files'
+          ],
+          'outputs': [
+            'dummy_remove_symbolic_link',
+          ],
+          'action': [
+            'rm', '-f', 'res',
+          ],
+        },
+        {
+          'action_name': 'make_symbolic_link',
+          'inputs': [
+            'dummy_remove_symbolic_link'
+          ],
+          'outputs': [
+            'dummy_make_symbolic_link',
+          ],
+          'action': [
+            'ln', '-r', '-s', '-f',
+            '<(sdk_resources_dir)/res',
+            'res',
+          ],
+        },
+        {
+          'action_name': 'copy_configuration_dependent_resources',
+          'inputs': [
+            'dummy_make_symbolic_link',
+          ],
+          'outputs': [
+            'dummy_copy_configuration_dependent_resources',
+          ],
+          'action': [
+            'ln', '-s', '-f',
+            '<(launcher_icon_bools)',
+            '<(sdk_resources_dir)/res/values/',
+          ],
+        },
+        {
+          'action_name': 'copy_application_icons',
+          'variables': {
+            'icon_files': [
+              '<(application_icon_dir)/drawable-hdpi/application_icon.png',
+              '<(application_icon_dir)/drawable-mdpi/application_icon.png',
+              '<(application_icon_dir)/drawable-xhdpi/application_icon.png',
+              '<(application_icon_dir)/drawable-xxhdpi/application_icon.png',
+              '<(application_icon_dir)/drawable-xxxhdpi/application_icon.png',
+            ],
+          },
+          'inputs': [
+            'dummy_make_symbolic_link',
+            '<@(icon_files)',
+          ],
+          'outputs': [
+            '<(sdk_resources_dir)/res/drawable-hdpi/application_icon.png',
+            '<(sdk_resources_dir)/res/drawable-mdpi/application_icon.png',
+            '<(sdk_resources_dir)/res/drawable-xhdpi/application_icon.png',
+            '<(sdk_resources_dir)/res/drawable-xxhdpi/application_icon.png',
+            '<(sdk_resources_dir)/res/drawable-xxxhdpi/application_icon.png',
+          ],
+          'action': ['<@(copy_file)', '-pr',
+                     '<@(icon_files)',
+                     '<(sdk_resources_dir)/res/',
+                     '--src_base', '<(application_icon_dir)'],
+        },
+      ],
+    },
+    {
+      'target_name': 'gen_kcm_data',
+      'type': 'none',
+      'dependencies': ['copy_resources'],
+      'copies': [{
+        'destination': '<(sdk_resources_dir)/res/raw',
+        'files': [
+          '../../data/android/keyboard_layout_japanese109a.kcm',
+        ],
+      }],
+    },
+    {
+      # This (and 'copy_asis_svg') tareget outputs single .zip file
+      # in order to make build system work correctly.
+      # Without this hack, all the names of the generated files
+      # should be listed up in 'inputs' and/or 'outputs' field.
+      'target_name': 'transform_template_svg',
+      'type': 'none',
+      'actions': [
+        {
+          'action_name': 'transform_template_svg',
+          'inputs': [
+            '../../data/images/android/template/transform.py',
+          ],
+          'outputs': [
+            '<(shared_intermediate_mozc_dir)/data/images/android/svg/transformed.zip',
+          ],
+          'conditions': [
+            ['OS=="linux" or OS=="mac"', {
+              'inputs': ['<!@(ls -1 ../../data/images/android/template/*.svg)']
+            }, {
+              # TODO: Support Windows.
+              'inputs': ['<(dummy_input_file)']
+            }],
+          ],
+          'action': [
+            'python', '../../data/images/android/template/transform.py',
+            '--output_zip', '<@(_outputs)',
+          ],
+        },
+      ],
+    },
+    {
+      'target_name': 'copy_asis_svg',
+      'type': 'none',
+      'actions': [
+        {
+          'action_name': 'archive_asis_svg',
+          'outputs': [
+            '<(shared_intermediate_mozc_dir)/data/images/android/svg/asis.zip',
+          ],
+          'conditions': [
+            ['OS=="linux" or OS=="mac"', {
+              'inputs': ['<!@(ls -1 ../../data/images/android/svg/*.svg)']
+            }, {
+              # TODO: Support Windows.
+              'inputs': ['<(dummy_input_file)']
+            }],
+          ],
+          'action': [
+            # TODO: Support Windows.
+            'zip',
+            '-q',  # quiet operation
+            '-1',  # compress faster
+            '-j',  # don't record directory names
+            '<@(_outputs)',
+            '<@(_inputs)',
+          ],
+        },
+      ],
+    },
+    {
+      'target_name': 'gen_mozc_drawable',
+      'type': 'none',
+      'dependencies': [
+        'copy_asis_svg',
+        'transform_template_svg',
+      ],
+      'actions': [
+        {
+          'action_name': 'generate_pic_files',
+          'inputs': [
+            '<(shared_intermediate_mozc_dir)/data/images/android/svg/asis.zip',
+            '<(shared_intermediate_mozc_dir)/data/images/android/svg/transformed.zip',
+            '../gen_mozc_drawable.py',
+          ],
+          'outputs': [
+            '<(shared_intermediate_mozc_dir)/data/images/android/svg/gen_mozc_drawable.log',
+          ],
+          'dependencies': ['copy_resources'],
+          'action': [
+            'python', '../gen_mozc_drawable.py',
+            '--svg_paths=<(shared_intermediate_mozc_dir)/data/images/android/svg/asis.zip,<(shared_intermediate_mozc_dir)/data/images/android/svg/transformed.zip',
+            '--output_dir=<(sdk_resources_dir)/res/raw',
+            '--build_log=<(shared_intermediate_mozc_dir)/data/images/android/svg/gen_mozc_drawable.log',
+          ],
+        },
+      ],
+    },
+  ],
+}
diff --git a/src/android/resources/src/DONT_REMOVE_THIS_DIRECTORY b/src/android/resources/src/DONT_REMOVE_THIS_DIRECTORY
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/android/resources/src/DONT_REMOVE_THIS_DIRECTORY
diff --git a/src/android/src/com/google/android/inputmethod/japanese/ApplicationInitializerFactory.java b/src/android/src/com/google/android/inputmethod/japanese/ApplicationInitializerFactory.java
index d16377a..cd74d4e 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/ApplicationInitializerFactory.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/ApplicationInitializerFactory.java
@@ -32,7 +32,9 @@
 import org.mozc.android.inputmethod.japanese.MozcUtil.TelephonyManagerInterface;
 import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType;
 import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil;
+import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil.PreferenceManagerStaticInterface;
 import org.mozc.android.inputmethod.japanese.resources.R;
+import org.mozc.android.inputmethod.japanese.util.LauncherIconManagerFactory.LauncherIconManager;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
@@ -90,9 +92,6 @@
   @VisibleForTesting
   static final String PREF_LAUNCHED_AT_LEAST_ONCE = "pref_launched_at_least_once";
   @VisibleForTesting
-  static final String PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE =
-      "pref_last_launch_abi_independent_version_code";
-  @VisibleForTesting
   static final String PREF_WELCOME_ACTIVITY_SHOWN = "pref_welcome_activity_shown";
 
   private static class ApplicationInitializerImpl implements ApplicationInitializationStatus {
@@ -113,11 +112,13 @@
 
     @Override
     public Optional<Integer> getLastLaunchAbiIndependentVersionCode() {
-      if (!this.sharedPreferences.contains(PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE)) {
+      if (!this.sharedPreferences.contains(
+          PreferenceUtil.PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE)) {
         return Optional.absent();
       }
       return Optional.of(
-          this.sharedPreferences.getInt(PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE, 0));
+          this.sharedPreferences.getInt(
+              PreferenceUtil.PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE, 0));
     }
 
     @Override
@@ -149,10 +150,45 @@
       this.telephonyManager = Preconditions.checkNotNull(telephonyManager);
     }
 
-    public Optional<Intent> initialize(boolean omitWelcomeActivity,
+    /**
+     * Initializes the application.
+     *
+     * <p>Updates some preferences.
+     * Here we use three preference items.
+     * <ul>
+     * <li>pref_welcome_activity_shown: True if the "Welcome" activity has shown at least once.
+     * <li>pref_last_launch_abi_independent_version_code: The latest version number which
+     * has launched at least once.
+     * <li>pref_launched_at_least_once: Deprecated. True if the the IME has launched at least once.
+     * </ul>
+     * Some preferences should be set at the first time launch.
+     * If the IME is a system application (preinstalled), it shouldn't show "Welcome" activity.
+     * If an update is performed (meaning that the IME becomes non-system app),
+     * the activity should be shown at the first time launch.
+     *
+     * We have to do migration process.
+     * If pref_launched_at_least_once exists, pref_welcome_activity_shown is recognized as
+     * true and pref_last_launch_abi_independent_version_code is recognized as
+     * LAUNCHED_AT_LEAST_ONCE_DEPRECATED_VERSION_CODE. And then pref_launched_at_least_once is
+     * removed.
+     *
+     * @param isSystemApplication true if the app is a system application (== preinstall)
+     * @param isDevChannel true if the app is built for dev channel
+     * @param isWelcomeActivityPreferred true if the configuration prefers to shown welcome activity
+     *        if it's not been shown yet.
+     * @param abiIndependentVersionCode ABI independent version code, typically obtained
+     *        from {@link MozcUtil#getAbiIndependentVersionCode(Context)}
+     *
+     * @return if forwarding is needed Intent is returned. The caller side should invoke the Intent.
+     */
+    public Optional<Intent> initialize(boolean isSystemApplication,
                                        boolean isDevChannel,
                                        boolean isWelcomeActivityPreferred,
-                                       int abiIndependentVersionCode) {
+                                       int abiIndependentVersionCode,
+                                       LauncherIconManager launcherIconManager,
+                                       PreferenceManagerStaticInterface preferenceManager) {
+      Preconditions.checkNotNull(launcherIconManager);
+      Preconditions.checkNotNull(preferenceManager);
       SharedPreferences.Editor editor = sharedPreferences.edit();
       Resources resources = context.getResources();
       try {
@@ -175,27 +211,43 @@
         // Preferences: Update if this is the first launch
         if (!lastVersionCode.isPresent()) {
           // Store full-screen relating preferences.
+          DisplayMetrics portraitMetrics = getPortraitDisplayMetrics(
+              resources.getDisplayMetrics(), resources.getConfiguration().orientation);
           storeDefaultFullscreenMode(
-              sharedPreferences,
-              getPortraitDisplayMetrics(resources.getDisplayMetrics(),
-                                        resources.getConfiguration().orientation),
-              resources.getDimension(R.dimen.fullscreen_threshold),
-              resources.getDimension(R.dimen.ime_window_height_portrait),
-              resources.getDimension(R.dimen.ime_window_height_landscape));
+              sharedPreferences, portraitMetrics.heightPixels, portraitMetrics.widthPixels,
+              (int) Math.ceil(getDimensionForOrientation(
+                  resources, R.dimen.input_frame_height, Configuration.ORIENTATION_PORTRAIT)),
+              (int) Math.ceil(getDimensionForOrientation(
+                  resources, R.dimen.input_frame_height, Configuration.ORIENTATION_LANDSCAPE)),
+              resources.getDimensionPixelOffset(R.dimen.fullscreen_threshold));
 
           // Run emoji provider type detection, so that the detected provider will be
           // used as the default values of the preference activity.
           EmojiProviderType.maybeSetDetectedEmojiProviderType(
               sharedPreferences, telephonyManager);
         }
+        // Update launcher icon visibility and relating preference.
+        launcherIconManager.updateLauncherIconVisibility(context);
+        // Save default preference to the storage.
+        // NOTE: This method must NOT be called before updateLauncherIconVisibility() above.
+        //       Above method requires PREF_LAUNCHER_ICON_VISIBILITY_KEY is not filled with
+        //       the default value.
+        //       If PREF_LAUNCHER_ICON_VISIBILITY_KEY is filled prior to
+        //       updateLauncherIconVisibility(), the launcher icon will be unexpectedly shown
+        //       when 2.16.1955.3 (preinstall version) is overwritten by PlayStore version.
+        PreferenceUtil.setDefaultValues(
+            preferenceManager, context, MozcUtil.isDebug(context),
+            resources.getBoolean(R.bool.sending_information_features_enabled));
 
         if (isDevChannel) {
           // Usage Stats: Make pref_other_usage_stats_key enabled when dev channel.
           editor.putBoolean(PreferenceUtil.PREF_OTHER_USAGE_STATS_KEY, true);
+          maybeShowNotificationForDevChannel(abiIndependentVersionCode,
+              lastVersionCode);
         }
 
         // Welcome Activity
-        if (!isActivityShown && !omitWelcomeActivity && isWelcomeActivityPreferred) {
+        if (!isActivityShown && !isSystemApplication && isWelcomeActivityPreferred) {
           editor.putBoolean(PREF_WELCOME_ACTIVITY_SHOWN, true);
           Intent intent = new Intent(context, FirstTimeLaunchActivity.class);
           intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -204,18 +256,21 @@
         return Optional.absent();
       } finally {
         editor.remove(PREF_LAUNCHED_AT_LEAST_ONCE);
-        editor.putInt(PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE, abiIndependentVersionCode);
+        editor.putInt(PreferenceUtil.PREF_LAST_LAUNCH_ABI_INDEPENDENT_VERSION_CODE,
+                      abiIndependentVersionCode);
         editor.commit();
       }
     }
 
+    private void maybeShowNotificationForDevChannel(
+        int abiIndependentVersionCode, Optional<Integer> lastVersionCode) {
+    }
+
     /**
      * Returns a modified {@code DisplayMetrics} which equals to portrait modes's one.
      *
      * If current orientation is PORTRAIT, given {@code currentMetrics} is returned.
      * Otherwise {@code currentMetrics}'s {@code heightPixels} and {@code widthPixels} are swapped.
-     *
-     * Package private for testing purpose.
      */
     @VisibleForTesting
     static DisplayMetrics getPortraitDisplayMetrics(DisplayMetrics currentMetrics,
@@ -232,27 +287,43 @@
     }
 
     /**
+     * Get a dimension for the specified orientation.
+     * This method may be heavy since it updates the {@code resources} twice.
+     */
+    @VisibleForTesting
+    static float getDimensionForOrientation(Resources resources, int id, int orientation) {
+      Configuration configuration = resources.getConfiguration();
+      if (configuration.orientation == orientation) {
+        return resources.getDimension(id);
+      }
+
+      Configuration originalConfiguration = new Configuration(resources.getConfiguration());
+      try {
+        configuration.orientation = orientation;
+        resources.updateConfiguration(configuration, null);
+        return resources.getDimension(id);
+      } finally {
+        resources.updateConfiguration(originalConfiguration, null);
+      }
+    }
+
+    /**
      * Stores the default value of "fullscreen mode" to the shared preference.
-     *
-     * Package private for testing purpose.
      */
     @VisibleForTesting
     static void storeDefaultFullscreenMode(
-        SharedPreferences sharedPreferences, DisplayMetrics displayMetrics,
-        float fullscreenThresholdInPixel,
-        float portraitImeHeightInPixel, float landscapeImeHeightInPixel) {
+        SharedPreferences sharedPreferences,
+        int portraitDisplayHeight, int landscapeDisplayHeight,
+        int portraitInputFrameHeight, int landscapeInputFrameHeight, int fullscreenThreshold) {
       Preconditions.checkNotNull(sharedPreferences);
-      Preconditions.checkNotNull(displayMetrics);
 
       SharedPreferences.Editor editor = sharedPreferences.edit();
       editor.putBoolean(
           "pref_portrait_fullscreen_key",
-          displayMetrics.heightPixels - portraitImeHeightInPixel
-              < fullscreenThresholdInPixel);
+          portraitDisplayHeight - portraitInputFrameHeight < fullscreenThreshold);
       editor.putBoolean(
           "pref_landscape_fullscreen_key",
-          displayMetrics.widthPixels - landscapeImeHeightInPixel
-              < fullscreenThresholdInPixel);
+          landscapeDisplayHeight - landscapeInputFrameHeight < fullscreenThreshold);
       editor.commit();
     }
   }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/CandidateView.java b/src/android/src/com/google/android/inputmethod/japanese/CandidateView.java
index 74ceecd..8e3ac7f 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/CandidateView.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/CandidateView.java
@@ -29,6 +29,7 @@
 
 package org.mozc.android.inputmethod.japanese;
 
+import org.mozc.android.inputmethod.japanese.MozcView.InputFrameFoldButtonClickListener;
 import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateList;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord;
@@ -40,19 +41,22 @@
 import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer.DescriptionLayoutPolicy;
 import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer.ValueScalingPolicy;
 import org.mozc.android.inputmethod.japanese.ui.ConversionCandidateLayouter;
+import org.mozc.android.inputmethod.japanese.ui.InputFrameFoldButtonView;
 import org.mozc.android.inputmethod.japanese.ui.ScrollGuideView;
 import org.mozc.android.inputmethod.japanese.ui.SpanFactory;
-import org.mozc.android.inputmethod.japanese.view.MozcDrawableFactory;
-import org.mozc.android.inputmethod.japanese.view.SkinType;
+import org.mozc.android.inputmethod.japanese.view.Skin;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
+import android.view.View;
 import android.view.animation.Animation;
-import android.widget.CompoundButton;
+import android.widget.LinearLayout;
 
 /**
  * The view to show candidates.
@@ -60,17 +64,13 @@
  */
 public class CandidateView extends InOutAnimatedFrameLayout implements MemoryManageable {
 
-  /**
-   * Adapter for conversion candidate selection.
-   */
+  /** Adapter for conversion candidate selection. */
+  @VisibleForTesting
   static class ConversionCandidateSelectListener implements CandidateSelectListener {
     private final ViewEventListener viewEventListener;
 
     ConversionCandidateSelectListener(ViewEventListener viewEventListener) {
-      if (viewEventListener == null) {
-        throw new NullPointerException("viewEventListener should be non-null.");
-      }
-      this.viewEventListener = viewEventListener;
+      this.viewEventListener = Preconditions.checkNotNull(viewEventListener);
     }
 
     @Override
@@ -80,7 +80,6 @@
     }
   }
 
-
   private class OutAnimationAdapter extends AnimationAdapter {
     @Override
     public void onAnimationEnd(Animation animation) {
@@ -94,6 +93,8 @@
     private static final String DESCRIPTION_DELIMITER = " \t\n\r\f";
 
     ScrollGuideView scrollGuideView = null;
+    InputFrameFoldButtonView inputFrameFoldButtonView = null;
+    @VisibleForTesting int foldButtonBackgroundVisibilityThreshold = 0;
 
     // TODO(hidehiko): Simplify the interface as this is needed just for expandSuggestion.
     private ViewEventListener viewEventListener;
@@ -104,7 +105,7 @@
     private boolean isExpanded = false;
 
     {
-      setBackgroundDrawableType(DrawableType.CANDIDATE_BACKGROUND);
+      setSpanBackgroundDrawableType(DrawableType.CANDIDATE_BACKGROUND);
       layouter = new ConversionCandidateLayouter();
     }
 
@@ -117,6 +118,7 @@
           resources.getInteger(R.integer.candidate_scroller_minimum_velocity));
     }
 
+    @VisibleForTesting
     void setCandidateTextDimension(float candidateTextSize, float descriptionTextSize) {
       Preconditions.checkArgument(candidateTextSize > 0);
       Preconditions.checkArgument(descriptionTextSize > 0);
@@ -130,7 +132,9 @@
           resources.getDimension(R.dimen.symbol_description_right_padding);
       float descriptionVerticalPadding =
           resources.getDimension(R.dimen.symbol_description_bottom_padding);
+      float separatorWidth = resources.getDimensionPixelSize(R.dimen.candidate_separator_width);
 
+      carrierEmojiRenderHelper.setCandidateTextSize(candidateTextSize);
       candidateLayoutRenderer.setValueTextSize(candidateTextSize);
       candidateLayoutRenderer.setValueHorizontalPadding(valueHorizontalPadding);
       candidateLayoutRenderer.setValueScalingPolicy(ValueScalingPolicy.HORIZONTAL);
@@ -138,6 +142,7 @@
       candidateLayoutRenderer.setDescriptionHorizontalPadding(descriptionHorizontalPadding);
       candidateLayoutRenderer.setDescriptionVerticalPadding(descriptionVerticalPadding);
       candidateLayoutRenderer.setDescriptionLayoutPolicy(DescriptionLayoutPolicy.EXCLUSIVE);
+      candidateLayoutRenderer.setSeparatorWidth(separatorWidth);
 
       SpanFactory spanFactory = new SpanFactory();
       spanFactory.setValueTextSize(candidateTextSize);
@@ -160,6 +165,8 @@
       layouter.setValueHeight(candidateTextSize);
       layouter.setValueHorizontalPadding(valueHorizontalPadding);
       layouter.setValueVerticalPadding(valueVerticalPadding);
+
+      foldButtonBackgroundVisibilityThreshold = (int) (1.8 * valueVerticalPadding);
     }
 
     @Override
@@ -171,7 +178,9 @@
       this.viewEventListener = viewEventListener;
     }
 
+    @Override
     void reset() {
+      super.reset();
       isExpanded = false;
     }
 
@@ -179,6 +188,10 @@
     protected void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
       super.onScrollChanged(scrollX, scrollY, oldScrollX, oldScrollY);
       updateScrollGuide();
+      if (inputFrameFoldButtonView != null) {
+        inputFrameFoldButtonView.showBackgroundForScrolled(
+            scrollY > foldButtonBackgroundVisibilityThreshold);
+      }
       expandSuggestionIfNeeded();
     }
 
@@ -228,6 +241,17 @@
       super.update(candidateList);
       updateScrollGuide();
     }
+
+    @Override
+    protected Drawable getViewBackgroundDrawable(Skin skin) {
+      return skin.conversionCandidateViewBackgroundDrawable;
+    }
+
+    @Override
+    public void setSkin(Skin skin) {
+      super.setSkin(skin);
+      candidateLayoutRenderer.setSeparatorColor(skin.candidateBackgroundSeparatorColor);
+    }
   }
 
   public CandidateView(Context context) {
@@ -250,32 +274,36 @@
     ConversionCandidateWordView conversionCandidateWordView = getConversionCandidateWordView();
     scrollGuideView.setScroller(conversionCandidateWordView.scroller);
     conversionCandidateWordView.scrollGuideView = scrollGuideView;
-
-    // Initialize inputFrameFoldButton.
-    CompoundButton inputFrameFoldButton = getInputFrameFoldButton();
-    inputFrameFoldButton.setBackgroundDrawable(
-        new MozcDrawableFactory(getResources()).getDrawable(R.raw.keyboard__fold__tab).orNull());
+    conversionCandidateWordView.inputFrameFoldButtonView = getInputFrameFoldButton();
+    // To use Canvas#drawPicture(), the view shouldn't be h/w accelerated.
+    getInputFrameFoldButton().setLayerType(View.LAYER_TYPE_SOFTWARE, null);
 
     reset();
   }
 
-  CompoundButton getInputFrameFoldButton() {
-    return CompoundButton.class.cast(findViewById(R.id.input_frame_fold_button));
+  @VisibleForTesting InputFrameFoldButtonView getInputFrameFoldButton() {
+    return InputFrameFoldButtonView.class.cast(findViewById(R.id.input_frame_fold_button));
   }
 
-  ConversionCandidateWordView getConversionCandidateWordView() {
+  @VisibleForTesting ConversionCandidateWordView getConversionCandidateWordView() {
     return ConversionCandidateWordView.class.cast(findViewById(R.id.candidate_word_view));
   }
 
-  ScrollGuideView getScrollGuideView() {
+  private ConversionCandidateWordContainerView getConversionCandidateWordContainerView() {
+    return ConversionCandidateWordContainerView.class.cast(
+        findViewById(R.id.conversion_candidate_word_container_view));
+  }
+
+  @VisibleForTesting ScrollGuideView getScrollGuideView() {
     return ScrollGuideView.class.cast(findViewById(R.id.candidate_scroll_guide_view));
   }
 
-  /**
-   * Updates the view based on {@code Command}.
-   * Exposed as protected for testing purpose.
-   */
-  protected void update(Command outCommand) {
+  @VisibleForTesting LinearLayout getCandidateWordFrame() {
+    return LinearLayout.class.cast(findViewById(R.id.candidate_word_frame));
+  }
+
+  /** Updates the view based on {@code Command}. */
+  void update(Command outCommand) {
     if (outCommand == null) {
       getConversionCandidateWordView().update(null);
       return;
@@ -283,8 +311,8 @@
 
     Input input = outCommand.getInput();
     CandidateList allCandidateWords = outCommand.getOutput().getAllCandidateWords();
-    if (input.getType() == CommandType.SEND_COMMAND &&
-        input.getCommand().getType() == SessionCommand.CommandType.EXPAND_SUGGESTION) {
+    if (input.getType() == CommandType.SEND_COMMAND
+        && input.getCommand().getType() == SessionCommand.CommandType.EXPAND_SUGGESTION) {
       getConversionCandidateWordView().updateForExpandSuggestion(allCandidateWords);
     } else {
       getConversionCandidateWordView().update(allCandidateWords);
@@ -293,19 +321,18 @@
 
   /**
    * Register callback object.
-   * Note: exposed as a protected method for testing purpose.
    * @param listener
    */
-  protected void setViewEventListener(ViewEventListener listener,
-                                      OnClickListener inputFrameFoldButtonClickListner) {
-    if (listener == null) {
-      throw new NullPointerException("lister must be non-null.");
-    }
+  void setViewEventListener(ViewEventListener listener) {
+    Preconditions.checkNotNull(listener);
     ConversionCandidateWordView conversionCandidateWordView = getConversionCandidateWordView();
     conversionCandidateWordView.setViewEventListener(listener);
     conversionCandidateWordView.setCandidateSelectListener(
         new ConversionCandidateSelectListener(listener));
-    getInputFrameFoldButton().setOnClickListener(inputFrameFoldButtonClickListner);
+  }
+
+  void setInputFrameFoldButtonOnClickListener(InputFrameFoldButtonClickListener listener) {
+    getInputFrameFoldButton().setOnClickListener(Preconditions.checkNotNull(listener));
   }
 
   void reset() {
@@ -317,9 +344,14 @@
     getInputFrameFoldButton().setChecked(checked);
   }
 
-  void setSkinType(SkinType skinType) {
-    getScrollGuideView().setSkinType(skinType);
-    getConversionCandidateWordView().setSkinType(skinType);
+  @SuppressWarnings("deprecation")
+  void setSkin(Skin skin) {
+    Preconditions.checkNotNull(skin);
+    getScrollGuideView().setSkin(skin);
+    getConversionCandidateWordView().setSkin(skin);
+    getInputFrameFoldButton().setSkin(skin);
+    getCandidateWordFrame().setBackgroundColor(skin.candidateBackgroundBottomColor);
+    invalidate();
   }
 
   void setCandidateTextDimension(float candidateTextSize, float descriptionTextSize) {
@@ -328,12 +360,13 @@
 
     getConversionCandidateWordView().setCandidateTextDimension(candidateTextSize,
                                                                descriptionTextSize);
+    getConversionCandidateWordContainerView().setCandidateTextDimension(candidateTextSize);
   }
 
-  void setNarrowMode(boolean narrowMode) {
-    getInputFrameFoldButton().setVisibility(narrowMode ? GONE : VISIBLE);
+  void enableFoldButton(boolean enabled) {
+    getInputFrameFoldButton().setVisibility(enabled ? VISIBLE : GONE);
     getConversionCandidateWordView().getCandidateLayouter()
-        .reserveEmptySpanForInputFoldButton(!narrowMode);
+        .reserveEmptySpanForInputFoldButton(enabled);
   }
 
   @Override
diff --git a/src/android/src/com/google/android/inputmethod/japanese/CandidateViewManager.java b/src/android/src/com/google/android/inputmethod/japanese/CandidateViewManager.java
new file mode 100644
index 0000000..6b875f9
--- /dev/null
+++ b/src/android/src/com/google/android/inputmethod/japanese/CandidateViewManager.java
@@ -0,0 +1,456 @@
+// Copyright 2010-2014, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package org.mozc.android.inputmethod.japanese;
+
+import org.mozc.android.inputmethod.japanese.InOutAnimatedFrameLayout.VisibilityChangeListener;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
+import org.mozc.android.inputmethod.japanese.view.Skin;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.res.Resources;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.TranslateAnimation;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+
+/**
+ * Manages candidate views (floating, on-keyboard).
+ */
+class CandidateViewManager implements MemoryManageable {
+
+  /** Listener interface to handle the height change of a keyboard candidate view. */
+  public interface KeyboardCandidateViewHeightListener {
+    public void onExpanded();
+    public void onCollapse();
+  }
+
+  private static class ClearCandidateAnimationListener implements Animation.AnimationListener {
+    private final CandidateView candidateView;
+
+    public ClearCandidateAnimationListener(CandidateView candidateView) {
+      this.candidateView = Preconditions.checkNotNull(candidateView);
+    }
+
+    @Override
+    public void onAnimationEnd(Animation animation) {
+      candidateView.update(EMPTY_COMMAND);
+    }
+
+    @Override
+    public void onAnimationRepeat(Animation animation) {
+    }
+
+    @Override
+    public void onAnimationStart(Animation animation) {
+    }
+  }
+
+  /** {@link CandidateMode#FLOATING} is only available on Lollipop or later. */
+  private enum CandidateMode {
+    KEYBOARD, NUMBER, FLOATING,
+  }
+
+  private static final Animation NO_ANIMATION = new Animation() {};
+  @VisibleForTesting static final Command EMPTY_COMMAND = Command.getDefaultInstance();
+
+  @VisibleForTesting final CandidateView keyboardCandidateView;
+  @VisibleForTesting final FloatingCandidateView floatingCandidateView;
+  /**
+   * SymbolInputView which number candidate view belongs to is created lazily.
+   * Therefore number candidate view is not accessible when CandidateViewManager is instantiated.
+   */
+  @VisibleForTesting Optional<CandidateView> numberCandidateView = Optional.absent();
+
+  private Optional<KeyboardCandidateViewHeightListener> keyboardCandidateViewHeightListener =
+      Optional.absent();
+  /**
+   * Current active candidate view.
+   * {@link CandidateMode#FLOATING} is only available on Lollipop or later.
+   */
+  private CandidateMode candidateMode = CandidateMode.KEYBOARD;
+
+  /** Cache of {@link EditorInfo} instance to switch candidate views. */
+  private EditorInfo editorInfo = new EditorInfo();
+  /** Cache of {@link Skin} instance to switch candidate views. */
+  private Skin skin = Skin.getFallbackInstance();
+  /** Cache of candidate text size. */
+  private float candidateTextSize;
+  /** Cache of description text size. */
+  private float descriptionTextSize;
+  /** Cache of {@link ViewEventListener}. */
+  private Optional<ViewEventListener> viewEventListener = Optional.absent();
+  /** Cache of {@link VisibilityChangeListener}. */
+  private Optional<VisibilityChangeListener> onVisibilityChangeListener = Optional.absent();
+  /**
+   * True if extracted mode (== fullscreen mode) is activated.
+   * <p>
+   * On extracted mode, floating candidate should be disabled in order to show extracted view
+   * in the screen.
+   */
+  private boolean isExtractedMode = false;
+  private boolean allowFloatingMode = false;
+  private boolean narrowMode = false;
+
+  /**
+   * Cache of {@link CursorAnchorInfo} instance to switch candidate views.
+   * This field is null if and only-if Floating candidate view is NOT available, so we don't mark
+   * this value as {code @Nullable} since this field should NOT be used in that situation.
+   */
+  private CursorAnchorInfo cursorAnchorInfo;
+
+  private Animation numberCandidateViewInAnimation = NO_ANIMATION;
+  private Animation numberCandidateViewOutAnimation = NO_ANIMATION;
+
+  @SuppressLint("NewApi")
+  public CandidateViewManager(
+      CandidateView keyboardCandidateView, FloatingCandidateView floatingCandidateView) {
+    this.keyboardCandidateView = Preconditions.checkNotNull(keyboardCandidateView);
+    this.floatingCandidateView = Preconditions.checkNotNull(floatingCandidateView);
+    if (FloatingCandidateView.isAvailable()) {
+      cursorAnchorInfo = new CursorAnchorInfo.Builder().build();
+    }
+
+    keyboardCandidateView.setOutAnimationListener(
+        new ClearCandidateAnimationListener(keyboardCandidateView));
+  }
+
+  public void setNumberCandidateView(CandidateView numberCandidateView) {
+    this.numberCandidateView = Optional.of(numberCandidateView);
+    numberCandidateView.setSkin(skin);
+    numberCandidateView.enableFoldButton(true);
+    numberCandidateView.setInAnimation(numberCandidateViewInAnimation);
+    numberCandidateView.setOutAnimation(numberCandidateViewOutAnimation);
+    numberCandidateView.setOutAnimationListener(
+        new ClearCandidateAnimationListener(numberCandidateView));
+    if (candidateTextSize > 0 && descriptionTextSize > 0) {
+      numberCandidateView.setCandidateTextDimension(candidateTextSize, descriptionTextSize);
+    }
+    if (viewEventListener.isPresent()) {
+      numberCandidateView.setViewEventListener(viewEventListener.get());
+    }
+    numberCandidateView.setOnVisibilityChangeListener(onVisibilityChangeListener.orNull());
+  }
+
+  /**
+   * Updates the candidate views by {@code outCommand} and may invoke some animations.
+   * <p>
+   * On-keyboard candidate view may animate and the animation listener may be invoked.
+   */
+  public void update(Command outCommand) {
+    updateInternal(Preconditions.checkNotNull(outCommand), true);
+  }
+
+  private void updateWithoutAnimation(Command outCommand) {
+    updateInternal(outCommand, false);
+  }
+
+  private void updateInternal(Command outCommand, boolean withAnimation) {
+    if (candidateMode == CandidateMode.FLOATING) {
+      floatingCandidateView.setCandidates(outCommand);
+      return;
+    }
+
+    Preconditions.checkState(
+        candidateMode == CandidateMode.KEYBOARD
+        || (candidateMode == CandidateMode.NUMBER && numberCandidateView.isPresent()));
+    CandidateView candidateView = (candidateMode == CandidateMode.KEYBOARD)
+        ? keyboardCandidateView : numberCandidateView.get();
+
+    if (withAnimation) {
+      if (hasCandidates(outCommand)) {
+        candidateView.update(outCommand);
+        // Call CandidateView#update only if there are some candidates in the output.
+        // In such case the candidate view will clear its canvas.
+        startKeyboardCandidateViewInAnimation();
+      } else {
+        // We don't call update method here and clear candidates at the end of this animation,
+        // because it will clear the view's contents during the animation.
+        startKeyboardCandidateViewOutAnimation();
+      }
+    } else {
+      candidateView.update(outCommand);
+      if (hasCandidates(outCommand)) {
+        candidateView.setVisibility(View.VISIBLE);
+      } else {
+        candidateView.setVisibility(View.GONE);
+      }
+    }
+  }
+
+  public void setOnVisibilityChangeListener(Optional<VisibilityChangeListener> listener) {
+    this.onVisibilityChangeListener = Preconditions.checkNotNull(listener);
+    keyboardCandidateView.setOnVisibilityChangeListener(listener.orNull());
+    if (numberCandidateView.isPresent()) {
+      numberCandidateView.get().setOnVisibilityChangeListener(listener.orNull());
+    }
+  }
+
+  /**
+   * Enables/Disables a floating candidate view.
+   * <p>
+   * This method turned floating mode on if it is preferred.
+   * The floating candidate view is only available on Lollipop or later.
+   */
+  private void updateCandiadateWindowActivation() {
+    boolean floatingMode = narrowMode && allowFloatingMode && !isExtractedMode;
+    if (floatingMode == (candidateMode == CandidateMode.FLOATING)) {
+      return;
+    }
+
+    // Clears candidates on the current candidate window.
+    updateWithoutAnimation(EMPTY_COMMAND);
+
+    // Updates the other candidate view.
+    candidateMode = floatingMode ? CandidateMode.FLOATING : CandidateMode.KEYBOARD;
+    updateWithoutAnimation(EMPTY_COMMAND);
+    setEditorInfo(editorInfo);
+    if (FloatingCandidateView.isAvailable()) {
+      setCursorAnchorInfo(cursorAnchorInfo);
+    }
+    // In order to show extracted view correctly, make the visibility GONE when it is not activated.
+    floatingCandidateView.setVisibility(floatingMode ? View.VISIBLE : View.GONE);
+  }
+
+  public void setAllowFloatingMode(boolean allowFloatingMode) {
+    Preconditions.checkArgument(!allowFloatingMode || FloatingCandidateView.isAvailable());
+    this.allowFloatingMode = allowFloatingMode;
+    updateCandiadateWindowActivation();
+  }
+
+  public void setNarrowMode(boolean narrowMode) {
+    this.narrowMode = narrowMode;
+    keyboardCandidateView.enableFoldButton(!narrowMode);
+    updateCandiadateWindowActivation();
+  }
+
+  public void setNumberMode(boolean numberMode) {
+    CandidateMode nextMode = numberMode ? CandidateMode.NUMBER : CandidateMode.KEYBOARD;
+    if (candidateMode == nextMode) {
+      return;
+    }
+    if (nextMode == CandidateMode.NUMBER) {
+      // Hide keyboard candidate view since it is higher than symbol view.
+      updateWithoutAnimation(EMPTY_COMMAND);
+    }
+    candidateMode = nextMode;
+    // Set empty command in order to clear the candidates which have been registered into the next
+    // view. Otherwise such candidates (typically they are obsolete) are shown unexpectedly.
+    // TODO(hsumita): Revisit when Mozc server returns candidates for SWITCH_INPUT_MODE command.
+    updateWithoutAnimation(EMPTY_COMMAND);
+  }
+
+  public void setCandidateTextDimension(float candidateTextSize, float descriptionTextSize) {
+    this.candidateTextSize = candidateTextSize;
+    this.descriptionTextSize = descriptionTextSize;
+    if (numberCandidateView.isPresent()) {
+      numberCandidateView.get().setCandidateTextDimension(
+          candidateTextSize, descriptionTextSize);
+    } else {
+      keyboardCandidateView.setCandidateTextDimension(candidateTextSize, descriptionTextSize);
+    }
+  }
+
+  public void setInputFrameFoldButtonChecked(boolean isChecked) {
+    switch (candidateMode) {
+      case KEYBOARD:
+        keyboardCandidateView.setInputFrameFoldButtonChecked(isChecked);
+        break;
+      case NUMBER:
+        numberCandidateView.get().setInputFrameFoldButtonChecked(isChecked);
+        break;
+      case FLOATING:
+        throw new IllegalStateException("Fold button is not available on floating mode.");
+    }
+  }
+
+  public void setEditorInfo(EditorInfo info) {
+    this.editorInfo = Preconditions.checkNotNull(info);
+    if (candidateMode == CandidateMode.FLOATING) {
+      floatingCandidateView.setEditorInfo(info);
+    }
+  }
+
+  @TargetApi(21)
+  public void setCursorAnchorInfo(CursorAnchorInfo info) {
+    this.cursorAnchorInfo = Preconditions.checkNotNull(info);
+    if (candidateMode == CandidateMode.FLOATING) {
+      floatingCandidateView.setCursorAnchorInfo(info);
+    }
+  }
+
+  public void setSkin(Skin skin) {
+    this.skin = Preconditions.checkNotNull(skin);
+    keyboardCandidateView.setSkin(skin);
+    if (numberCandidateView.isPresent()) {
+      numberCandidateView.get().setSkin(skin);
+    }
+  }
+
+  public void setEventListener(ViewEventListener viewEventListener,
+                               KeyboardCandidateViewHeightListener hightListener) {
+    this.viewEventListener = Optional.of(viewEventListener);
+    this.keyboardCandidateViewHeightListener = Optional.of(hightListener);
+    keyboardCandidateView.setViewEventListener(viewEventListener);
+    floatingCandidateView.setViewEventListener(viewEventListener);
+    if (numberCandidateView.isPresent()) {
+      numberCandidateView.get().setViewEventListener(viewEventListener);
+    }
+  }
+
+  /**
+   * Set true if extracted mode (== fullscreen mode) is activated.
+   */
+  public void setExtractedMode(boolean isExtractedMode) {
+    this.isExtractedMode = isExtractedMode;
+    updateCandiadateWindowActivation();
+  }
+
+  public void setHardwareCompositionMode(CompositionMode mode) {
+    if (isFloatingMode()) {
+      floatingCandidateView.setCompositionMode(mode);
+    }
+  }
+
+  public void reset() {
+    keyboardCandidateView.clearAnimation();
+    keyboardCandidateView.setVisibility(View.GONE);
+    keyboardCandidateView.reset();
+    if (numberCandidateView.isPresent()) {
+      numberCandidateView.get().clearAnimation();
+      numberCandidateView.get().setVisibility(View.GONE);
+      numberCandidateView.get().reset();
+    }
+
+    candidateMode = CandidateMode.KEYBOARD;
+    floatingCandidateView.setVisibility(View.GONE);
+  }
+
+  public void resetHeightDependingComponents(
+      Resources resources, int windowHeight, int inputFrameHeight) {
+    Preconditions.checkNotNull(resources);
+
+    int keyboardCandidateViewHeight = windowHeight - inputFrameHeight;
+    long duration = resources.getInteger(R.integer.candidate_frame_transition_duration);
+    float fromAlpha = 0.0f;
+    float toAlpha = 1.0f;
+
+    keyboardCandidateView.setInAnimation(createKeyboardCandidateViewTransitionAnimation(
+        keyboardCandidateViewHeight, 0, fromAlpha, toAlpha, duration));
+    keyboardCandidateView.setOutAnimation(createKeyboardCandidateViewTransitionAnimation(
+        0, keyboardCandidateViewHeight, toAlpha, fromAlpha, duration));
+
+    int numberCandidateViewHeight = resources.getDimensionPixelSize(R.dimen.button_frame_height);
+    numberCandidateViewInAnimation = createKeyboardCandidateViewTransitionAnimation(
+        numberCandidateViewHeight, 0, fromAlpha, toAlpha, duration);
+    numberCandidateViewOutAnimation = createKeyboardCandidateViewTransitionAnimation(
+        0, numberCandidateViewHeight, toAlpha, fromAlpha, duration);
+    if (numberCandidateView.isPresent()) {
+      numberCandidateView.get().setInAnimation(numberCandidateViewInAnimation);
+      numberCandidateView.get().setOutAnimation(numberCandidateViewOutAnimation);
+    }
+  }
+
+  public boolean isKeyboardCandidateViewVisible() {
+    return keyboardCandidateView.getVisibility() == View.VISIBLE;
+  }
+
+  private static Animation createKeyboardCandidateViewTransitionAnimation(
+      int fromY, int toY, float fromAlpha, float toAlpha, long duration) {
+    AnimationSet animation = new AnimationSet(false);
+    animation.setDuration(duration);
+
+    AlphaAnimation alphaAnimation = new AlphaAnimation(fromAlpha, toAlpha);
+    alphaAnimation.setDuration(duration);
+    animation.addAnimation(alphaAnimation);
+
+    TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, fromY, toY);
+    translateAnimation.setInterpolator(new DecelerateInterpolator());
+    translateAnimation.setDuration(duration);
+    animation.addAnimation(translateAnimation);
+    return animation;
+  }
+
+  private void startKeyboardCandidateViewInAnimation() {
+    switch (candidateMode) {
+      case KEYBOARD:
+        keyboardCandidateView.startInAnimation();
+        if (keyboardCandidateViewHeightListener.isPresent()) {
+          keyboardCandidateViewHeightListener.get().onExpanded();
+        }
+        break;
+      case NUMBER:
+        numberCandidateView.get().startInAnimation();
+        break;
+      case FLOATING:
+        throw new IllegalStateException("Floating mode doesn't support in-animation.");
+    }
+  }
+
+  private void startKeyboardCandidateViewOutAnimation() {
+    switch (candidateMode) {
+      case KEYBOARD:
+        if (keyboardCandidateViewHeightListener.isPresent()) {
+          keyboardCandidateViewHeightListener.get().onCollapse();
+        }
+        keyboardCandidateView.startOutAnimation();
+        break;
+      case NUMBER:
+        numberCandidateView.get().startOutAnimation();
+        break;
+      case FLOATING:
+        throw new IllegalStateException("Floating mode doesn't support out-animation.");
+    }
+  }
+
+  private static boolean hasCandidates(Command command) {
+    return command.getOutput().getAllCandidateWords().getCandidatesCount() > 0;
+  }
+
+  @Override
+  public void trimMemory() {
+    keyboardCandidateView.trimMemory();
+    if (numberCandidateView.isPresent()) {
+      numberCandidateView.get().trimMemory();
+    }
+  }
+
+  public boolean isFloatingMode() {
+    return candidateMode == CandidateMode.FLOATING;
+  }
+}
diff --git a/src/android/src/com/google/android/inputmethod/japanese/CandidateWordView.java b/src/android/src/com/google/android/inputmethod/japanese/CandidateWordView.java
index d456b99..f9ff8be 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/CandidateWordView.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/CandidateWordView.java
@@ -42,23 +42,23 @@
 import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer;
 import org.mozc.android.inputmethod.japanese.ui.CandidateLayouter;
 import org.mozc.android.inputmethod.japanese.ui.SnapScroller;
-import org.mozc.android.inputmethod.japanese.view.SkinType;
+import org.mozc.android.inputmethod.japanese.view.CarrierEmojiRenderHelper;
+import org.mozc.android.inputmethod.japanese.view.Skin;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 
 import android.content.Context;
 import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Paint.Align;
 import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
 import android.support.v4.view.ViewCompat;
-import android.support.v4.widget.EdgeEffectCompat;
 import android.util.AttributeSet;
 import android.view.GestureDetector;
 import android.view.GestureDetector.SimpleOnGestureListener;
 import android.view.MotionEvent;
 import android.view.View;
+import android.widget.EdgeEffect;
 
 import javax.annotation.Nullable;
 
@@ -73,14 +73,16 @@
    * Handles gestures to scroll candidate list and choose a candidate.
    */
   class CandidateWordGestureDetector {
+
     class CandidateWordViewGestureListener extends SimpleOnGestureListener {
+
       @Override
       public boolean onFling(
           MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
         float velocity = orientationTrait.projectVector(velocityX, velocityY);
         // As fling is started, current action is not tapping.
         // Reset pressing state so that candidate selection is not triggered at touch up event.
-        releaseCandidate();
+        reset();
         // Fling makes scrolling.
         scroller.fling(-(int) velocity);
         invalidate();
@@ -96,7 +98,7 @@
         orientationTrait.scrollTo(CandidateWordView.this, scroller.getScrollPosition());
         // As scroll is started, current action is not tapping.
         // Reset pressing state so that candidate selection is not triggered at touch up event.
-        releaseCandidate();
+        reset();
 
         // Edge effect. Now, in production, we only support vertical scroll.
         if (oldScrollPosition + distance < 0) {
@@ -119,7 +121,7 @@
     // GestureDetector cannot handle all complex gestures which we need.
     // But we use GestureDetector for some gesture recognition
     // because implementing whole gesture detection logic by ourselves is a bit tedious.
-    final GestureDetector gestureDetector;
+    private final GestureDetector gestureDetector;
 
     /**
      * Points to an instance of currently pressed candidate word. Or {@code null} if any
@@ -146,9 +148,10 @@
                         span.getRight(), row.getTop() + row.getHeight());
     }
 
-    private void releaseCandidate() {
+    void reset() {
       pressedCandidate = null;
       pressedRowIndex = Optional.absent();
+      // NOTE: candidateRect doesn't need reset.
     }
 
     CandidateWord getPressedCandidate() {
@@ -195,6 +198,14 @@
     }
 
     boolean onTouchEvent(MotionEvent event) {
+      // Before delegation to gesture detector, handle ACTION_UP event
+      // in order to release edge effect.
+      if (event.getAction() == MotionEvent.ACTION_UP) {
+        topEdgeEffect.onRelease();
+        bottomEdgeEffect.onRelease();
+        invalidate();
+      }
+
       if (gestureDetector.onTouchEvent(event)) {
         return true;
       }
@@ -207,23 +218,25 @@
           scroller.stopScrolling();
           if (!topEdgeEffect.isFinished()) {
             topEdgeEffect.onRelease();
+            invalidate();
           }
           if (!bottomEdgeEffect.isFinished()) {
             bottomEdgeEffect.onRelease();
+            invalidate();
           }
           return true;
         case MotionEvent.ACTION_MOVE:
           if (pressedCandidate != null) {
             // Turn off highlighting if contact point gets out of the candidate.
             if (!candidateRect.contains(scrolledX, scrolledY)) {
-              releaseCandidate();
+              reset();
               invalidate();
             }
           }
           return true;
         case MotionEvent.ACTION_CANCEL:
           if (pressedCandidate != null) {
-            releaseCandidate();
+            reset();
             invalidate();
           }
           return true;
@@ -232,7 +245,7 @@
             if (candidateRect.contains(scrolledX, scrolledY) && candidateSelectListener != null) {
               candidateSelectListener.onCandidateSelected(pressedCandidate, pressedRowIndex);
             }
-            releaseCandidate();
+            reset();
             invalidate();
           }
           return true;
@@ -346,8 +359,8 @@
 
   // Finally, we only need vertical scrolling.
   // TODO(hidehiko): Remove horizontal scrolling related codes.
-  private final EdgeEffectCompat topEdgeEffect = new EdgeEffectCompat(getContext());
-  private final EdgeEffectCompat bottomEdgeEffect = new EdgeEffectCompat(getContext());
+  private final EdgeEffect topEdgeEffect = new EdgeEffect(getContext());
+  private final EdgeEffect bottomEdgeEffect = new EdgeEffect(getContext());
 
   // The Scroller which manages the status of scrolling the view.
   // Default behavior of ScrollView does not suffice our UX design
@@ -367,8 +380,10 @@
   // No padding by default.
   private int horizontalPadding = 0;
 
+  protected final CarrierEmojiRenderHelper carrierEmojiRenderHelper =
+      new CarrierEmojiRenderHelper(this);
   protected final CandidateLayoutRenderer candidateLayoutRenderer =
-      new CandidateLayoutRenderer(this);
+      new CandidateLayoutRenderer();
 
   CandidateWordGestureDetector candidateWordGestureDetector =
       new CandidateWordGestureDetector(getContext());
@@ -377,7 +392,7 @@
   private final OrientationTrait orientationTrait;
 
   protected final BackgroundDrawableFactory backgroundDrawableFactory =
-      new BackgroundDrawableFactory(getResources().getDisplayMetrics().density);
+      new BackgroundDrawableFactory(getResources());
   private DrawableType backgroundDrawableType = null;
 
   private final CandidateWindowAccessibilityDelegate accessibilityDelegate;
@@ -404,6 +419,12 @@
     ViewCompat.setAccessibilityDelegate(this, accessibilityDelegate);
   }
 
+  void reset() {
+    calculatedLayout = null;
+    currentCandidateList = null;
+    candidateWordGestureDetector.reset();
+  }
+
   void setCandidateSelectListener(CandidateSelectListener candidateSelectListener) {
     this.candidateSelectListener = candidateSelectListener;
   }
@@ -436,19 +457,18 @@
   @Override
   protected void onAttachedToWindow() {
     super.onAttachedToWindow();
-    candidateLayoutRenderer.onAttachedToWindow();
+    carrierEmojiRenderHelper.onAttachedToWindow();
   }
 
   @Override
   protected void onDetachedFromWindow() {
-    candidateLayoutRenderer.onDetachedFromWindow();
+    carrierEmojiRenderHelper.onDetachedFromWindow();
     super.onDetachedFromWindow();
   }
 
   public void setEmojiProviderType(EmojiProviderType providerType) {
     Preconditions.checkNotNull(providerType);
-
-    candidateLayoutRenderer.setEmojiProviderType(providerType);
+    carrierEmojiRenderHelper.setEmojiProviderType(providerType);
   }
 
   @Override
@@ -506,7 +526,8 @@
       CandidateWord pressedCandidate = candidateWordGestureDetector.getPressedCandidate();
       int pressedCandidateIndex = (pressedCandidate != null && pressedCandidate.hasIndex())
           ? pressedCandidate.getIndex() : -1;
-      candidateLayoutRenderer.drawCandidateLayout(canvas, calculatedLayout, pressedCandidateIndex);
+      candidateLayoutRenderer.drawCandidateLayout(
+          canvas, calculatedLayout, pressedCandidateIndex, carrierEmojiRenderHelper);
     } finally {
       canvas.restoreToCount(saveCount);
     }
@@ -525,11 +546,13 @@
           topEdgeEffect.onAbsorb(velocity.intValue());
           if (!bottomEdgeEffect.isFinished()) {
             bottomEdgeEffect.onRelease();
+            invalidate();
           }
         } else if (velocity > 0) {
           bottomEdgeEffect.onAbsorb(velocity.intValue());
           if (!topEdgeEffect.isFinished()) {
             topEdgeEffect.onRelease();
+            invalidate();
           }
         }
       }
@@ -586,7 +609,9 @@
   void update(CandidateList candidateList) {
     CandidateList previousCandidateList = currentCandidateList;
     currentCandidateList = candidateList;
-    candidateLayoutRenderer.setCandidateList(Optional.fromNullable(candidateList));
+    Optional<CandidateList> optionalCandidateList = Optional.fromNullable(candidateList);
+    candidateLayoutRenderer.setCandidateList(optionalCandidateList);
+    carrierEmojiRenderHelper.setCandidateList(optionalCandidateList);
     if (layouter != null && !equals(candidateList, previousCandidateList)) {
       updateCalculatedLayout();
     }
@@ -655,32 +680,29 @@
     return currentCandidateList;
   }
 
-  /**
-   * Utility method for creating paint instance.
-   */
-  protected static Paint createPaint(
-      boolean antiAlias, int color, Align textAlign, float textSize) {
-    Paint paint = new Paint();
-    paint.setAntiAlias(antiAlias);
-    paint.setColor(color);
-    paint.setTextAlign(textAlign);
-    paint.setTextSize(textSize);
-    return paint;
-  }
-
-  protected void setBackgroundDrawableType(DrawableType drawableType) {
+  protected void setSpanBackgroundDrawableType(DrawableType drawableType) {
     backgroundDrawableType = drawableType;
-    resetBackground();
+    resetSpanBackground();
   }
 
-  private void resetBackground() {
-    candidateLayoutRenderer.setSpanBackgroundDrawable(
-        Optional.fromNullable(backgroundDrawableFactory.getDrawable(backgroundDrawableType)));
+  private void resetSpanBackground() {
+    Drawable drawable = (backgroundDrawableType != null)
+        ? backgroundDrawableFactory.getDrawable(backgroundDrawableType) : null;
+    candidateLayoutRenderer.setSpanBackgroundDrawable(Optional.fromNullable(drawable));
   }
 
-  void setSkinType(SkinType skinType) {
-    backgroundDrawableFactory.setSkinType(skinType);
-    resetBackground();
+  /**
+   * Returns a Drawable which should be set as the view's background.
+   */
+  protected abstract Drawable getViewBackgroundDrawable(Skin skin);
+
+  @SuppressWarnings("deprecation")
+  void setSkin(Skin skin) {
+    backgroundDrawableFactory.setSkin(Preconditions.checkNotNull(skin));
+    resetSpanBackground();
+    candidateLayoutRenderer.setSkin(skin);
+    setBackgroundDrawable(
+        getViewBackgroundDrawable(skin).getConstantState().newDrawable());
   }
 
   @Override
@@ -697,4 +719,15 @@
     }
     return false;
   }
+
+  @Override
+  protected void onVisibilityChanged(View changedView, int visibility) {
+    super.onVisibilityChanged(changedView, visibility);
+    // If this view gets invisible, reset the internal state of the gesture detector.
+    // Otherwise UP event, which is sent after this view being invisible, will cause
+    // unexpected onCandidateSelected callback.
+    if (visibility != View.VISIBLE) {
+      candidateWordGestureDetector.reset();
+    }
+  }
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/ComposingTextTrackingInputConnection.java b/src/android/src/com/google/android/inputmethod/japanese/ComposingTextTrackingInputConnection.java
index 5325745..4a1e04d 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/ComposingTextTrackingInputConnection.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/ComposingTextTrackingInputConnection.java
@@ -194,4 +194,9 @@
     }
     return new ComposingTextTrackingInputConnection(baseConnection);
   }
+
+  @Override
+  public boolean requestCursorUpdates(int cursorUpdateMode) {
+    return baseConnection.requestCursorUpdates(cursorUpdateMode);
+  }
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/ConversionCandidateWordContainerView.java b/src/android/src/com/google/android/inputmethod/japanese/ConversionCandidateWordContainerView.java
index 1c88a74..6507095 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/ConversionCandidateWordContainerView.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/ConversionCandidateWordContainerView.java
@@ -78,8 +78,7 @@
   void setCandidateTextDimension(float candidateTextSize) {
     Preconditions.checkArgument(candidateTextSize > 0);
 
-    foldingIconSize = candidateTextSize
-        + getResources().getDimension(R.dimen.candidate_vertical_padding_size) * 2;
+    foldingIconSize = getResources().getDimension(R.dimen.candidate_fold_icon_width);
   }
 
   @Override
diff --git a/src/android/src/com/google/android/inputmethod/japanese/DependencyFactory.java b/src/android/src/com/google/android/inputmethod/japanese/DependencyFactory.java
index 8880954..38c25e4 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/DependencyFactory.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/DependencyFactory.java
@@ -52,6 +52,7 @@
  */
 public class DependencyFactory {
 
+
   /**
    * Dependencies.
    */
diff --git a/src/android/src/com/google/android/inputmethod/japanese/FeedbackManager.java b/src/android/src/com/google/android/inputmethod/japanese/FeedbackManager.java
index 50d5ff0..98a76e4 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/FeedbackManager.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/FeedbackManager.java
@@ -53,11 +53,35 @@
     /**
      * Fired when the input view is expanded (the candidate view is fold).
      */
-    INPUTVIEW_EXPAND(true),
+    INPUTVIEW_EXPAND(true, AudioManager.FX_KEYPRESS_STANDARD),
     /**
      * Fired when the input view is fold (the candidate view is expand).
      */
-    INPUTVIEW_FOLD(true),
+    INPUTVIEW_FOLD(true, AudioManager.FX_KEYPRESS_STANDARD),
+    /**
+     * Fired when the symbol input view is closed.
+     */
+    SYMBOL_INPUTVIEW_CLOSED(true, AudioManager.FX_KEYPRESS_STANDARD),
+    /**
+     * Fired when a minor category is selected.
+     */
+    SYMBOL_INPUTVIEW_MINOR_CATEGORY_SELECTED(true, AudioManager.FX_KEYPRESS_STANDARD),
+    /**
+     * Fired when a major category is selected.
+     */
+    SYMBOL_INPUTVIEW_MAJOR_CATEGORY_SELECTED(true, AudioManager.FX_KEYPRESS_STANDARD),
+    /**
+     * Fired when microphone button is touched.
+     */
+    MICROPHONE_BUTTON_DOWN(true, AudioManager.FX_KEYPRESS_STANDARD),
+    /**
+     * Fired when the hardware composition button in narrow frame is touched.
+     */
+    NARROW_FRAME_HARDWARE_COMPOSITION_BUTTON_DOWN(true, AudioManager.FX_KEYPRESS_STANDARD),
+    /**
+     * Fired when the widen button in narrow frame is touched.
+     */
+    NARROW_FRAME_WIDEN_BUTTON_DOWN(true, AudioManager.FX_KEYPRESS_STANDARD),
     ;
     // Constant value to indicate no sound feedback should be played.
     static final int NO_SOUND = -1;
@@ -103,7 +127,7 @@
   private boolean isHapticFeedbackEnabled;
   private long hapticFeedbackDuration = 30;  // 30ms by default.
   private boolean isSoundFeedbackEnabled;
-  private float soundFeedbackVolume = 0.1f;  // System default volume parameter.
+  private float soundFeedbackVolume = 0.4f;  // System default volume parameter.
   @VisibleForTesting final FeedbackListener feedbackListener;
 
   /**
diff --git a/src/android/src/com/google/android/inputmethod/japanese/FloatingCandidateView.java b/src/android/src/com/google/android/inputmethod/japanese/FloatingCandidateView.java
new file mode 100644
index 0000000..97048db
--- /dev/null
+++ b/src/android/src/com/google/android/inputmethod/japanese/FloatingCandidateView.java
@@ -0,0 +1,550 @@
+// Copyright 2010-2014, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package org.mozc.android.inputmethod.japanese;
+
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.Category;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Output;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Preedit.Segment;
+import org.mozc.android.inputmethod.japanese.ui.FloatingCandidateLayoutRenderer;
+import org.mozc.android.inputmethod.japanese.ui.FloatingModeIndicator;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Build;
+import android.text.InputType;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+import android.widget.PopupWindow;
+
+/**
+ * Floating candidate view for hardware keyboard.
+ */
+@TargetApi(21)
+public class FloatingCandidateView extends View {
+
+  private interface FloatingCandidateViewProxy {
+    public void draw(Canvas canvas);
+    public void viewSizeChanged(int width, int height);
+    public void setCursorAnchorInfo(CursorAnchorInfo info);
+    public void setCandidates(Command outCommand);
+    public void setEditorInfo(EditorInfo editorInfo);
+    public void setCompositionMode(CompositionMode mode);
+    public void setViewEventListener(ViewEventListener listener);
+    public void setVisibility(int visibility);
+    public Optional<Rect> getVisibleRect();
+  }
+
+  private static class FloatingCandidateViewStub implements FloatingCandidateViewProxy {
+    @Override
+    public void draw(Canvas canvas) {}
+
+    @Override
+    public void viewSizeChanged(int width, int height) {}
+
+    @Override
+    public void setCursorAnchorInfo(CursorAnchorInfo info) {}
+
+    @Override
+    public void setCandidates(Command outCommand) {}
+
+    @Override
+    public void setEditorInfo(EditorInfo editorInfo) {}
+
+    @Override
+    public void setCompositionMode(CompositionMode mode) {}
+
+    @Override
+    public void setViewEventListener(ViewEventListener listener) {}
+
+    @Override
+    public void setVisibility(int visibility) {}
+
+    @Override
+    public Optional<Rect> getVisibleRect() {
+      return Optional.absent();
+    }
+  }
+
+  @TargetApi(21)
+  private static class FloatingCandidateViewImpl implements FloatingCandidateViewProxy {
+
+    private final View parentView;
+
+    /** Layouts the floating candidate window and draws it's contents. */
+    private final FloatingCandidateLayoutRenderer layoutRenderer;
+
+    private final FloatingModeIndicator modeIndicator;
+
+    /**
+     * Pop-up window to handle touch events.
+     * <p>
+     * A touch down event on outside a touchable region (set by {@link MozcService#onComputeInsets})
+     * cannot be caught by view, and we cannot expand the touchable region since all touch down
+     * events inside the region are not delegated to a background application.
+     * To handle these touch events, we employ pop-up window.
+     * <p>
+     * This window is always invisible since we cannot control the transition behavior.
+     * (e.g. Pop-up window always move with animation)
+     */
+    private final PopupWindow touchEventReceiverWindow;
+
+    private final int windowVerticalMargin;
+    private final int windowHorizontalMargin;
+
+    /**
+     * Base position of the floating candidate window.
+     * <p>
+     * It is same as the cursor rectangle on pre-composition state, and the left-edge of the focused
+     * segment on other states.
+     */
+    private int basePositionTop;
+    private int basePositionBottom;
+    private int basePositionX;
+    private Optional<CursorAnchorInfo> cursorAnchorInfo = Optional.absent();
+    private Category candidatesCategory = Category.CONVERSION;
+    private int highlightedCharacterStart;
+    private int compositionCharacterEnd;
+    /** True if EditorInfo says suggestion should be suppressed. */
+    private boolean suppressSuggestion;
+    /**
+     * Horizontal offset of the candidate window. See also {@link FloatingCandidateLayoutRenderer}
+     */
+    private int offsetX;
+    /** Vertical offset of the candidate window. See also {@link FloatingCandidateLayoutRenderer} */
+    private int offsetY;
+    private boolean isCandidateWindowShowing;
+
+    public FloatingCandidateViewImpl(View parentView) {
+      Context context = Preconditions.checkNotNull(parentView).getContext();
+      this.parentView = parentView;
+      this.layoutRenderer = new FloatingCandidateLayoutRenderer(context.getResources());
+      this.modeIndicator = new FloatingModeIndicator(parentView);
+      this.touchEventReceiverWindow = createPopupWindow(context);
+      Resources resources = context.getResources();
+      this.windowVerticalMargin =
+          Math.round(resources.getDimension(R.dimen.floating_candidate_window_vertical_margin));
+      this.windowHorizontalMargin =
+          Math.round(resources.getDimension(R.dimen.floating_candidate_window_horizontal_margin));
+    }
+
+    public FloatingCandidateViewImpl(View parentView, PopupWindow popupWindowMock,
+                                     FloatingCandidateLayoutRenderer layoutRenderer,
+                                     FloatingModeIndicator modeIndicator) {
+      this.parentView = Preconditions.checkNotNull(parentView);
+      this.layoutRenderer = Preconditions.checkNotNull(layoutRenderer);
+      this.modeIndicator = Preconditions.checkNotNull(modeIndicator);
+      this.touchEventReceiverWindow = Preconditions.checkNotNull(popupWindowMock);
+      Resources resources = parentView.getContext().getResources();
+      this.windowVerticalMargin =
+          resources.getDimensionPixelSize(R.dimen.floating_candidate_window_vertical_margin);
+      this.windowHorizontalMargin =
+          resources.getDimensionPixelSize(R.dimen.floating_candidate_window_horizontal_margin);
+    }
+
+    private PopupWindow createPopupWindow(Context context) {
+      return new PopupWindow(new View(context) {
+        @Override
+        public boolean onTouchEvent(MotionEvent event) {
+          Optional<Rect> rect = layoutRenderer.getWindowRect();
+          if (!rect.isPresent()) {
+            return false;
+          }
+
+          MotionEvent copiedEvent = MotionEvent.obtain(event);
+          try {
+            copiedEvent.offsetLocation(rect.get().left, rect.get().top);
+            layoutRenderer.onTouchEvent(copiedEvent);
+            // TODO(hsumita): Don't invalidate the view if not necessary.
+            parentView.invalidate();
+          } finally {
+            copiedEvent.recycle();
+          }
+          return true;
+        }
+      });
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+      if (!isCandidateWindowShowing) {
+        return;
+      }
+
+      int saveId = canvas.save(Canvas.MATRIX_SAVE_FLAG);
+      try {
+        canvas.translate(offsetX, offsetY);
+        layoutRenderer.draw(canvas);
+      } finally {
+        canvas.restoreToCount(saveId);
+      }
+    }
+
+    @Override
+    public void viewSizeChanged(int width, int height) {
+      layoutRenderer.setMaxWidth(width - windowHorizontalMargin * 2);
+      updateCandidateWindowWithSize(width, height);
+    }
+
+    /** Sets {@link CursorAnchorInfo} to update the candidate window position. */
+    @Override
+    public void setCursorAnchorInfo(CursorAnchorInfo info) {
+      cursorAnchorInfo = Optional.of(info);
+      modeIndicator.setCursorAnchorInfo(info);
+      updateCandidateWindow();
+    }
+
+    /** Sets {@link Command} to update the contents of the candidate window. */
+    @Override
+    public void setCandidates(Command outCommand) {
+      Output output = Preconditions.checkNotNull(outCommand).getOutput();
+      layoutRenderer.setCandidates(outCommand);
+      modeIndicator.setCommand(outCommand);
+      highlightedCharacterStart = output.getPreedit().getHighlightedPosition();
+      int currentPreeditPosition = 0;
+      for (Segment segment : output.getPreedit().getSegmentList()) {
+        currentPreeditPosition += segment.getValueLength();
+      }
+      compositionCharacterEnd = currentPreeditPosition;
+      candidatesCategory = output.getCandidates().getCategory();
+      updateCandidateWindow();
+    }
+
+    /** Sets {@link EditorInfo} for context-aware behavior. */
+    @Override
+    public void setEditorInfo(EditorInfo editorInfo) {
+      Preconditions.checkNotNull(editorInfo);
+      boolean previusSuppressSuggestion = suppressSuggestion;
+      suppressSuggestion = shouldSuppressSuggestion(editorInfo);
+      if (previusSuppressSuggestion != suppressSuggestion) {
+        updateCandidateWindow();
+      }
+    }
+
+    @Override
+    public void setCompositionMode(CompositionMode mode) {
+      modeIndicator.setCompositionMode(mode);
+    }
+
+    /** Set view event listener to handle events invoked by the candidate window. */
+    @Override
+    public void setViewEventListener(ViewEventListener listener) {
+      layoutRenderer.setViewEventListener(Preconditions.checkNotNull(listener));
+    }
+
+    @Override
+    public void setVisibility(int visibility) {
+      if (visibility != View.VISIBLE) {
+        modeIndicator.hide();
+      }
+    }
+
+    /**
+     * Updates the candidate window.
+     * <p>
+     * All layout related states should be updated before call this method.
+     */
+    private void updateCandidateWindow() {
+      updateCandidateWindowWithSize(parentView.getWidth(), parentView.getHeight());
+    }
+
+    private int calculateWindowLeftPosition(Rect rect, int basePositionX, int viewWidth) {
+      return MozcUtil.clamp(
+          basePositionX + rect.left,
+          windowHorizontalMargin, viewWidth - rect.width() - windowHorizontalMargin);
+    }
+
+    /**
+     * Updates the candidate window with width and height.
+     * <p>
+     * All layout related states should be updated before call this method.
+     */
+    private void updateCandidateWindowWithSize(int viewWidth, int viewHeight) {
+      if (suppressSuggestion && candidatesCategory == Category.SUGGESTION) {
+        dismissCandidateWindow();
+        return;
+      }
+
+      Optional<Rect> optionalWindowRect = layoutRenderer.getWindowRect();
+      if (!optionalWindowRect.isPresent()) {
+        dismissCandidateWindow();
+        return;
+      }
+
+      Rect rect = optionalWindowRect.get();
+      updateBasePosition(rect, viewWidth);
+      int lowerAreaHeight = viewHeight - basePositionBottom - windowVerticalMargin;
+      int upperAreaHeight = basePositionTop - windowVerticalMargin;
+      int top = (lowerAreaHeight < rect.height() && lowerAreaHeight < upperAreaHeight)
+        ? MozcUtil.clamp(basePositionTop - rect.height() - windowVerticalMargin,
+                         0, viewHeight - rect.height())
+        : Math.max(0, basePositionBottom + windowVerticalMargin);
+      int left = calculateWindowLeftPosition(rect, basePositionX, viewWidth);
+
+      offsetX = left - rect.left;
+      offsetY = top - rect.top;
+
+      rect.offset(offsetX, offsetY);
+      showCandidateWindow(rect);
+    }
+
+    /** Return true if floating candidate window should be suppressed. */
+    private boolean shouldSuppressSuggestion(EditorInfo editorInfo) {
+      if ((editorInfo.inputType & EditorInfo.TYPE_MASK_CLASS) != InputType.TYPE_CLASS_TEXT) {
+        return true;
+      }
+
+      if ((editorInfo.inputType
+           & (InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE))
+           != 0) {
+        return true;
+      }
+
+      switch (editorInfo.inputType & EditorInfo.TYPE_MASK_VARIATION) {
+        case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS:
+        case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS:
+        case InputType.TYPE_TEXT_VARIATION_PASSWORD:
+        case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD:
+        case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD:
+        case InputType.TYPE_TEXT_VARIATION_URI:
+        case InputType.TYPE_TEXT_VARIATION_FILTER:
+          return true;
+        default:
+          return false;
+      }
+    }
+
+    private void resetBasePosition() {
+      basePositionTop = 0;
+      basePositionBottom = 0;
+      basePositionX = 0;
+      return;
+    }
+
+    /**
+     * Update {@code basePositionTop}, {@code basePositionBottom} and {@code basePositionX} using
+     * {@code cursorAnchorInfo}.
+     */
+    private void updateBasePosition(Rect windowRect, int viewWidth) {
+      if (!cursorAnchorInfo.isPresent()) {
+        resetBasePosition();
+        return;
+      }
+
+      CursorAnchorInfo info = cursorAnchorInfo.get();
+      int composingStartIndex = info.getComposingTextStart() + highlightedCharacterStart;
+      int composingEndIndex = info.getComposingTextStart() + compositionCharacterEnd - 1;
+      RectF firstCharacterBounds = info.getCharacterBounds(composingStartIndex);
+      float[] points;
+      if (firstCharacterBounds != null) {
+        points = new float[] {firstCharacterBounds.left, firstCharacterBounds.top,
+                              firstCharacterBounds.left, firstCharacterBounds.bottom};
+      } else if (!Float.isNaN(info.getInsertionMarkerHorizontal())) {
+        points = new float[] {info.getInsertionMarkerHorizontal(), info.getInsertionMarkerTop(),
+                              info.getInsertionMarkerHorizontal(), info.getInsertionMarkerBottom()};
+      } else {
+        resetBasePosition();
+        return;
+      }
+
+      // Adjust the bottom base position not to hide composition characters by the floating
+      // candidate window.
+      int windowLeft = calculateWindowLeftPosition(windowRect, (int) points[0], viewWidth);
+      for (int i = composingEndIndex; i > composingStartIndex; --i) {
+        RectF bounds = info.getCharacterBounds(i);
+        if (bounds == null) {
+          continue;
+        }
+        if (bounds.bottom <= points[3]) {
+          break;
+        }
+        if (bounds.right > windowLeft) {
+          points[3] = bounds.bottom;
+          break;
+        }
+      }
+
+      info.getMatrix().mapPoints(points);
+      int[] screenOffset = new int[2];
+      parentView.getLocationOnScreen(screenOffset);
+      basePositionX = Math.round(points[0]) - screenOffset[0];
+      basePositionTop = Math.round(points[1]) - screenOffset[1];
+      basePositionBottom = Math.round(points[3]) - screenOffset[1];
+    }
+
+    /**
+     * Shows the candidate window.
+     * <p>
+     * First {@code touchEventReceiverWindow} is shown (or is updated its position if it has been
+     * already shown). Then this view is invalidated. As the result {@code draw} will be called back
+     * and visible candidate window will be shown.
+    */
+    private void showCandidateWindow(Rect rect) {
+      isCandidateWindowShowing = true;
+      if (touchEventReceiverWindow.isShowing()) {
+        touchEventReceiverWindow.update(rect.left, rect.top, rect.width(), rect.height());
+      } else {
+        touchEventReceiverWindow.setWidth(rect.width());
+        touchEventReceiverWindow.setHeight(rect.height());
+        touchEventReceiverWindow.showAtLocation(
+            parentView, Gravity.NO_GRAVITY, rect.left, rect.top);
+      }
+      parentView.postInvalidate();
+    }
+
+    /**
+     * Dismisses the candidate window.
+     * <p>
+     * Does the very similar things as {@showCandidateWindow}.
+    */
+    private void dismissCandidateWindow() {
+      if (isCandidateWindowShowing) {
+        isCandidateWindowShowing = false;
+        touchEventReceiverWindow.dismiss();
+        parentView.postInvalidate();
+      }
+    }
+
+    @Override
+    public Optional<Rect> getVisibleRect() {
+      Optional<Rect> rect = layoutRenderer.getWindowRect();
+      if (touchEventReceiverWindow.isShowing() && rect.isPresent()) {
+        rect.get().offset(offsetX, offsetY);
+        return rect;
+      } else {
+        return Optional.<Rect>absent();
+      }
+    }
+  }
+
+  private final FloatingCandidateViewProxy floatingCandidateViewProxy;
+
+  public FloatingCandidateView(Context context) {
+    super(context);
+    floatingCandidateViewProxy = createFloatingCandidateViewInstance(this);
+  }
+
+  public FloatingCandidateView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    floatingCandidateViewProxy = createFloatingCandidateViewInstance(this);
+  }
+
+  @VisibleForTesting
+  FloatingCandidateView(Context context, PopupWindow popupWindowMock) {
+    super(context);
+    floatingCandidateViewProxy = new FloatingCandidateViewImpl(
+        this, popupWindowMock, new FloatingCandidateLayoutRenderer(context.getResources()),
+        new FloatingModeIndicator(this));
+  }
+
+  @VisibleForTesting
+  FloatingCandidateView(Context context, PopupWindow popupWindowMock,
+                        FloatingCandidateLayoutRenderer layoutRenderer,
+                        FloatingModeIndicator modeIndicator) {
+    super(context);
+    floatingCandidateViewProxy =
+        new FloatingCandidateViewImpl(this, popupWindowMock, layoutRenderer, modeIndicator);
+  }
+
+  private static FloatingCandidateViewProxy createFloatingCandidateViewInstance(View view) {
+    return isAvailable()
+        ? new FloatingCandidateViewImpl(view)
+        : new FloatingCandidateViewStub();
+  }
+
+  @Override
+  protected void onFinishInflate() {
+    // Use software renderer since hardware renderer doesn't support Paint#setShadowLayer() and
+    // Canvas#drawPicture() which is used by MozcDrawable.
+    this.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+  }
+
+  public static boolean isAvailable() {
+    return Build.VERSION.SDK_INT >= 21;
+  }
+
+  @Override
+  public void setVisibility(int visibility) {
+    super.setVisibility(visibility);
+    floatingCandidateViewProxy.setVisibility(visibility);
+  }
+
+  @Override
+  public void onDraw(Canvas canvas) {
+    super.onDraw(canvas);
+    floatingCandidateViewProxy.draw(canvas);
+  }
+
+  @Override
+  public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+    super.onSizeChanged(width, height, oldWidth, oldHeight);
+    floatingCandidateViewProxy.viewSizeChanged(width, height);
+  }
+
+  /** Sets {@link CursorAnchorInfo} to update the candidate window position. */
+  public void setCursorAnchorInfo(CursorAnchorInfo info) {
+    floatingCandidateViewProxy.setCursorAnchorInfo(info);
+  }
+
+  /** Sets {@link Command} to update the contents of the candidate window. */
+  public void setCandidates(Command outCommand) {
+    floatingCandidateViewProxy.setCandidates(outCommand);
+  }
+
+  /** Sets {@link EditorInfo} for context-aware behavior. */
+  public void setEditorInfo(EditorInfo editorInfo) {
+    floatingCandidateViewProxy.setEditorInfo(editorInfo);
+  }
+
+  public void setCompositionMode(CompositionMode mode) {
+    floatingCandidateViewProxy.setCompositionMode(mode);
+  }
+
+  /** Set view event listener to handle events invoked by the candidate window. */
+  public void setViewEventListener(ViewEventListener listener) {
+    floatingCandidateViewProxy.setViewEventListener(listener);
+  }
+
+  @VisibleForTesting Optional<Rect> getVisibleRect() {
+    return floatingCandidateViewProxy.getVisibleRect();
+  }
+}
diff --git a/src/android/src/com/google/android/inputmethod/japanese/InputDeviceReceiver.java b/src/android/src/com/google/android/inputmethod/japanese/InputDeviceReceiver.java
new file mode 100644
index 0000000..847692c
--- /dev/null
+++ b/src/android/src/com/google/android/inputmethod/japanese/InputDeviceReceiver.java
@@ -0,0 +1,39 @@
+// Copyright 2010-2014, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package org.mozc.android.inputmethod.japanese;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+class InputDeviceReceiver extends BroadcastReceiver {
+  @Override
+  public void onReceive(Context context, Intent intent) {}
+}
diff --git a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboard.java b/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboard.java
deleted file mode 100644
index 7b2ec9d..0000000
--- a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboard.java
+++ /dev/null
@@ -1,323 +0,0 @@
-// Copyright 2010-2014, Google Inc.
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-//     * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-//     * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-//     * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package org.mozc.android.inputmethod.japanese;
-
-import org.mozc.android.inputmethod.japanese.keyboard.Flick;
-import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction;
-import org.mozc.android.inputmethod.japanese.keyboard.Key;
-import org.mozc.android.inputmethod.japanese.keyboard.KeyEntity;
-import org.mozc.android.inputmethod.japanese.keyboard.KeyState;
-import org.mozc.android.inputmethod.japanese.keyboard.Keyboard;
-import org.mozc.android.inputmethod.japanese.keyboard.Row;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.CrossingEdgeBehavior;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpaceOnAlphanumeric;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpecialRomanjiTable;
-import org.mozc.android.inputmethod.japanese.resources.R;
-import com.google.common.base.Optional;
-import com.google.common.base.Preconditions;
-
-import android.util.SparseIntArray;
-
-import java.util.List;
-
-/**
- */
-public class JapaneseKeyboard extends Keyboard {
-  /**
-   * Each keyboard has its own specification.
-   *
-   * For example, some keyboards use a special Romanji table.
-   */
-  public static enum KeyboardSpecification {
-    // 12 keys.
-    TWELVE_KEY_TOGGLE_KANA(
-        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_KANA", 0, 1, 1),
-        R.xml.kbd_12keys_kana,
-        CompositionMode.HIRAGANA,
-        SpecialRomanjiTable.TWELVE_KEYS_TO_HIRAGANA,
-        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
-        true,
-        CrossingEdgeBehavior.DO_NOTHING),
-
-    TWELVE_KEY_TOGGLE_ALPHABET(
-        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_ALPHABET", 0, 1, 1),
-        R.xml.kbd_12keys_abc,
-        CompositionMode.HALF_ASCII,
-        SpecialRomanjiTable.TWELVE_KEYS_TO_HALFWIDTHASCII,
-        SpaceOnAlphanumeric.COMMIT,
-        false,
-        CrossingEdgeBehavior.DO_NOTHING),
-
-    TWELVE_KEY_TOGGLE_NUMBER(
-        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_NUMBER", 0, 1, 1),
-        R.xml.kbd_12keys_123,
-        CompositionMode.HALF_ASCII,
-        SpecialRomanjiTable.TWELVE_KEYS_TO_NUMBER,
-        SpaceOnAlphanumeric.COMMIT,
-        false,
-        CrossingEdgeBehavior.DO_NOTHING),
-
-    TWELVE_KEY_TOGGLE_QWERTY_ALPHABET(
-        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_QWERTY_ALPHABET", 0, 4, 0),
-        R.xml.kbd_12keys_qwerty_abc,
-        CompositionMode.HALF_ASCII,
-        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
-        SpaceOnAlphanumeric.COMMIT,
-        false,
-        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),
-
-    // Flick mode.
-    TWELVE_KEY_FLICK_KANA(
-        new KeyboardSpecificationName("TWELVE_KEY_FLICK_KANA", 0, 1, 3),
-        R.xml.kbd_12keys_flick_kana,
-        CompositionMode.HIRAGANA,
-        SpecialRomanjiTable.FLICK_TO_HIRAGANA,
-        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
-        true,
-        CrossingEdgeBehavior.DO_NOTHING),
-
-    TWELVE_KEY_FLICK_ALPHABET(
-        new KeyboardSpecificationName("TWELVE_KEY_FLICK_ALPHABET", 0, 1, 1),
-        R.xml.kbd_12keys_flick_abc,
-        CompositionMode.HALF_ASCII,
-        SpecialRomanjiTable.FLICK_TO_HALFWIDTHASCII,
-        SpaceOnAlphanumeric.COMMIT,
-        false,
-        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),
-
-    TWELVE_KEY_FLICK_NUMBER(
-        new KeyboardSpecificationName("TWELVE_KEY_FLICK_NUMBER", 0, 1, 1),
-        R.xml.kbd_12keys_flick_123,
-        CompositionMode.HALF_ASCII,
-        SpecialRomanjiTable.FLICK_TO_NUMBER,
-        SpaceOnAlphanumeric.COMMIT,
-        false,
-        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),
-
-    TWELVE_KEY_TOGGLE_FLICK_KANA(
-        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_KANA", 0, 1, 3),
-        R.xml.kbd_12keys_flick_kana,
-        CompositionMode.HIRAGANA,
-        SpecialRomanjiTable.TOGGLE_FLICK_TO_HIRAGANA,
-        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
-        true,
-        CrossingEdgeBehavior.DO_NOTHING),
-
-    TWELVE_KEY_TOGGLE_FLICK_ALPHABET(
-        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_ALPHABET", 0, 1, 1),
-        R.xml.kbd_12keys_flick_abc,
-        CompositionMode.HALF_ASCII,
-        SpecialRomanjiTable.TOGGLE_FLICK_TO_HALFWIDTHASCII,
-        SpaceOnAlphanumeric.COMMIT,
-        false,
-        CrossingEdgeBehavior.DO_NOTHING),
-
-    TWELVE_KEY_TOGGLE_FLICK_NUMBER(
-        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_ALPHABET", 0, 1, 1),
-        R.xml.kbd_12keys_flick_123,
-        CompositionMode.HALF_ASCII,
-        SpecialRomanjiTable.TOGGLE_FLICK_TO_NUMBER,
-        SpaceOnAlphanumeric.COMMIT,
-        false,
-        CrossingEdgeBehavior.DO_NOTHING),
-
-    // QWERTY keyboard.
-    QWERTY_KANA(
-        new KeyboardSpecificationName("QWERTY_KANA", 0, 3, 1),
-        R.xml.kbd_qwerty_kana,
-        CompositionMode.HIRAGANA,
-        SpecialRomanjiTable.QWERTY_MOBILE_TO_HIRAGANA,
-        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
-        false,
-        CrossingEdgeBehavior.DO_NOTHING),
-
-    QWERTY_KANA_NUMBER(
-        new KeyboardSpecificationName("QWERTY_KANA_NUMBER", 0, 2, 1),
-        R.xml.kbd_qwerty_kana_123,
-        CompositionMode.HIRAGANA,
-        SpecialRomanjiTable.QWERTY_MOBILE_TO_HIRAGANA_NUMBER,
-        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
-        false,
-        CrossingEdgeBehavior.DO_NOTHING),
-
-    QWERTY_ALPHABET(
-        new KeyboardSpecificationName("QWERTY_ALPHABET", 0, 4, 0),
-        R.xml.kbd_qwerty_abc,
-        CompositionMode.HALF_ASCII,
-        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
-        SpaceOnAlphanumeric.COMMIT,
-        false,
-        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),
-
-    QWERTY_ALPHABET_NUMBER(
-        new KeyboardSpecificationName("QWERTY_ALPHABET_NUMBER", 0, 2, 1),
-        R.xml.kbd_qwerty_abc_123,
-        CompositionMode.HALF_ASCII,
-        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
-        SpaceOnAlphanumeric.COMMIT,
-        false,
-        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),
-
-    // Godan keyboard.
-    GODAN_KANA(
-        new KeyboardSpecificationName("GODAN_KANA", 0, 1, 1),
-        R.xml.kbd_godan_kana,
-        CompositionMode.HIRAGANA,
-        SpecialRomanjiTable.GODAN_TO_HIRAGANA,
-        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
-        true,
-        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),
-
-    // HARDWARE QWERTY keyboard.
-    HARDWARE_QWERTY_KANA(
-        new KeyboardSpecificationName("HARDWARE_QWERTY_KANA", 0, 1, 0),
-        0,
-        CompositionMode.HIRAGANA,
-        SpecialRomanjiTable.DEFAULT_TABLE,
-        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
-        false,
-        CrossingEdgeBehavior.DO_NOTHING),
-
-    HARDWARE_QWERTY_ALPHABET(
-        new KeyboardSpecificationName("HARDWARE_QWERTY_ALPHABET", 0, 1, 0),
-        0,
-        CompositionMode.HALF_ASCII,
-        SpecialRomanjiTable.DEFAULT_TABLE,
-        SpaceOnAlphanumeric.COMMIT,
-        false,
-        CrossingEdgeBehavior.DO_NOTHING),
-
-    ;
-
-    private final KeyboardSpecificationName specName;
-    private final int resourceId;
-    private final CompositionMode compositionMode;
-    private final SpecialRomanjiTable specialRomanjiTable;
-    private final SpaceOnAlphanumeric spaceOnAlphanumeric;
-    private final boolean kanaModifierInsensitiveConversion;
-    private final CrossingEdgeBehavior crossingEdgeBehavior;
-
-    private KeyboardSpecification(
-        KeyboardSpecificationName specName,
-        int resourceId,
-        CompositionMode compositionMode,
-        SpecialRomanjiTable specialRomanjiTable,
-        SpaceOnAlphanumeric spaceOnAlphanumeric,
-        boolean kanaModifierInsensitiveConversion,
-        CrossingEdgeBehavior crossingEdgeBehavior) {
-      this.specName = Preconditions.checkNotNull(specName);
-      this.resourceId = resourceId;
-      this.compositionMode = Preconditions.checkNotNull(compositionMode);
-      this.specialRomanjiTable = Preconditions.checkNotNull(specialRomanjiTable);
-      this.spaceOnAlphanumeric = Preconditions.checkNotNull(spaceOnAlphanumeric);
-      this.kanaModifierInsensitiveConversion = kanaModifierInsensitiveConversion;
-      this.crossingEdgeBehavior = Preconditions.checkNotNull(crossingEdgeBehavior);
-    }
-
-    public int getXmlLayoutResourceId() {
-      return resourceId;
-    }
-
-    public CompositionMode getCompositionMode() {
-      return compositionMode;
-    }
-
-    public KeyboardSpecificationName getKeyboardSpecificationName() {
-      return specName;
-    }
-
-    public KeyboardSpecificationName getSpecName() {
-      return specName;
-    }
-
-    public SpecialRomanjiTable getSpecialRomanjiTable() {
-      return specialRomanjiTable;
-    }
-
-    public SpaceOnAlphanumeric getSpaceOnAlphanumeric() {
-      return spaceOnAlphanumeric;
-    }
-
-    public boolean isKanaModifierInsensitiveConversion() {
-      return kanaModifierInsensitiveConversion;
-    }
-
-    public CrossingEdgeBehavior getCrossingEdgeBehavior() {
-      return crossingEdgeBehavior;
-    }
-  }
-
-  private final KeyboardSpecification specification;
-  private Optional<SparseIntArray> sourceIdToKeyCode = Optional.absent();
-
-  public JapaneseKeyboard(
-      Optional<String> contentDescription,
-      List<Row> rowList, float flickThreshold, KeyboardSpecification specification) {
-    super(Preconditions.checkNotNull(contentDescription),
-          Preconditions.checkNotNull(rowList), flickThreshold);
-    this.specification = Preconditions.checkNotNull(specification);
-  }
-
-  public KeyboardSpecification getSpecification() {
-    return specification;
-  }
-
-  /**
-   * Returns keyCode from {@code souceId}.
-   *
-   * <p>If not found, {@code Integer.MIN_VALUE} is returned.
-   */
-  public int getKeyCode(int sourceId) {
-    ensureSourceIdToKeyCode();
-    return sourceIdToKeyCode.get().get(sourceId, Integer.MIN_VALUE);
-  }
-
-  private void ensureSourceIdToKeyCode() {
-    if (sourceIdToKeyCode.isPresent()) {
-      return;
-    }
-    SparseIntArray result = new SparseIntArray();
-    for (Row row : getRowList()) {
-      for (Key key : row.getKeyList()) {
-        for (KeyState keyState : key.getKeyStates()) {
-          for (Direction direction : Direction.values()) {
-            Flick flick = keyState.getFlick(direction);
-            if (flick != null) {
-              KeyEntity keyEntity = flick.getKeyEntity();
-              result.put(keyEntity.getSourceId(), keyEntity.getKeyCode());
-            }
-          }
-        }
-      }
-    }
-    sourceIdToKeyCode = Optional.of(result);
-  }
-}
diff --git a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardFactory.java b/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardFactory.java
deleted file mode 100644
index 106ea58..0000000
--- a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardFactory.java
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright 2010-2014, Google Inc.
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-//     * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-//     * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-//     * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package org.mozc.android.inputmethod.japanese;
-
-import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification;
-import org.mozc.android.inputmethod.japanese.util.LeastRecentlyUsedCacheMap;
-
-import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
-
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.Map;
-
-/**
- * Factory of the keyboard data based on xml.
- *
- */
-public class JapaneseKeyboardFactory {
-
-  /**
-   * Key for the cache map of keyboard.
-   *
-   * Currently the keyboard is depending on its specification and display's size.
-   */
-  private static class CacheKey {
-    private final KeyboardSpecification specification;
-    private final int width;
-    private final int height;
-
-    CacheKey(KeyboardSpecification specification, int width, int height) {
-      this.specification = specification;
-      this.width = width;
-      this.height = height;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-      if (obj instanceof CacheKey) {
-        CacheKey other = CacheKey.class.cast(obj);
-        return specification == other.specification &&
-               width == other.width &&
-               height == other.height;
-      }
-      return false;
-    }
-
-    @Override
-    public int hashCode() {
-      return (specification.hashCode() * 31 ^ width) * 31 ^ height;
-    }
-  }
-
-  /**
-   * The max size of cached keyboards. This is based on the max number of keyboard variation
-   * for a configuration.
-   */
-  private static final int CACHE_SIZE = 6;
-
-  private final Map<CacheKey, JapaneseKeyboard> cache =
-      new LeastRecentlyUsedCacheMap<CacheKey, JapaneseKeyboard>(CACHE_SIZE);
-
-  /**
-   * @return JapaneseKeyboard instance based on given resources and specification.
-   *         If it is already parsed, just returns cached one. Otherwise, tries to parse
-   *         corresponding xml data, then caches and returns it.
-   *         Returns {@code null} if parsing is failed.
-   * @throws NullPointerException if given {@code resources} or {@code specification} is
-   *         {@code null}.
-   */
-  public JapaneseKeyboard get(Resources resources, KeyboardSpecification specification,
-                              int keyboardWidth, int keyboardHeight) {
-    if (resources == null) {
-      throw new NullPointerException("resources is null.");
-    }
-    if (specification == null) {
-      throw new NullPointerException("specification is null.");
-    }
-
-    CacheKey cacheKey =
-        new CacheKey(specification, keyboardWidth, keyboardHeight);
-
-    // First, look up from the cache.
-    JapaneseKeyboard keyboard = cache.get(cacheKey);
-    if (keyboard == null) {
-      // If not found, parse keyboard from a xml resource file. The result will be cached in
-      // the cache map.
-      keyboard = parseKeyboard(resources, specification, keyboardWidth, keyboardHeight);
-      if (keyboard != null) {
-        cache.put(cacheKey, keyboard);
-      }
-    }
-    return keyboard;
-  }
-
-  private static JapaneseKeyboard parseKeyboard(
-      Resources resources, KeyboardSpecification specification,
-      int keyboardWidth, int keyboardHeight) {
-    JapaneseKeyboardParser parser = new JapaneseKeyboardParser(
-        resources, resources.getXml(specification.getXmlLayoutResourceId()), specification,
-        keyboardWidth, keyboardHeight);
-    try {
-      return parser.parseKeyboard();
-    } catch (NotFoundException e) {
-      MozcLog.e(e.getMessage());
-    } catch (XmlPullParserException e) {
-      MozcLog.e(e.getMessage());
-    } catch (IOException e) {
-      MozcLog.e(e.getMessage());
-    }
-
-    // Returns null if failed.
-    return null;
-  }
-
-  /**
-   * Clears cached keyboards.
-   */
-  public void clear() {
-    cache.clear();
-  }
-}
diff --git a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardParser.java b/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardParser.java
deleted file mode 100644
index 19cde71..0000000
--- a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardParser.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright 2010-2014, Google Inc.
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-//     * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-//     * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-//     * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package org.mozc.android.inputmethod.japanese;
-
-import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification;
-import org.mozc.android.inputmethod.japanese.keyboard.Keyboard;
-import org.mozc.android.inputmethod.japanese.keyboard.KeyboardParser;
-import org.mozc.android.inputmethod.japanese.keyboard.Row;
-import com.google.common.base.Optional;
-import com.google.common.base.Preconditions;
-
-import android.content.res.Resources;
-import android.content.res.XmlResourceParser;
-
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-import java.util.List;
-
-/**
- */
-public class JapaneseKeyboardParser extends KeyboardParser {
-  private final KeyboardSpecification specification;
-
-  public JapaneseKeyboardParser(
-      Resources resources, XmlResourceParser parser, KeyboardSpecification specification,
-      int keyboardWidth, int keyboardHeight) {
-    super(resources, parser, keyboardWidth, keyboardHeight);
-    if (specification == null) {
-      throw new NullPointerException("specification is null.");
-    }
-    this.specification = specification;
-  }
-
-  /**
-   * Parses a XML file and returns the keyboard instance.
-   * @return JapaneseKeyboard instance parsed from the resource.
-   * @throws XmlPullParserException is thrown if parsing is failed.
-   * @throws IOException is thrown if there is trouble to read data.
-   */
-  @Override
-  public JapaneseKeyboard parseKeyboard() throws XmlPullParserException, IOException {
-    return JapaneseKeyboard.class.cast(super.parseKeyboard());
-  }
-
-  @Override
-  protected Keyboard buildKeyboard(Optional<String> contentDescription,
-                                   List<Row> rowList, float flickThreshold) {
-    return new JapaneseKeyboard(Preconditions.checkNotNull(contentDescription),
-                                Preconditions.checkNotNull(rowList), flickThreshold,
-                                specification);
-  }
-}
diff --git a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardView.java b/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardView.java
deleted file mode 100644
index 8b0e81d..0000000
--- a/src/android/src/com/google/android/inputmethod/japanese/JapaneseKeyboardView.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2010-2014, Google Inc.
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are
-// met:
-//
-//     * Redistributions of source code must retain the above copyright
-// notice, this list of conditions and the following disclaimer.
-//     * Redistributions in binary form must reproduce the above
-// copyright notice, this list of conditions and the following disclaimer
-// in the documentation and/or other materials provided with the
-// distribution.
-//     * Neither the name of Google Inc. nor the names of its
-// contributors may be used to endorse or promote products derived from
-// this software without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-package org.mozc.android.inputmethod.japanese;
-
-import org.mozc.android.inputmethod.japanese.keyboard.KeyboardView;
-
-import android.content.Context;
-import android.util.AttributeSet;
-
-/**
- * Keyboard view for Japanese.
- *
- * The features dedicated to Japanese input are implemented in this class.
- *
- */
-public class JapaneseKeyboardView extends KeyboardView {
-  public JapaneseKeyboardView(Context context) {
-    super(context);
-  }
-
-  public JapaneseKeyboardView(Context context, AttributeSet attrs) {
-    super(context, attrs);
-  }
-
-  public JapaneseKeyboardView(Context context, AttributeSet attrs, int defStyle) {
-    super(context, attrs, defStyle);
-  }
-
-  /**
-   * Sets a Keyboard.
-   *
-   * Internally this method delegates to super.setKeyboard(), which is protected scope.
-   * TODO(hidehiko): Rename following methods to {set,get}Keyboard, as covariant
-   * return value should be supported Java 1.5 or later.
-   * @param keyboard a keyboard instance to be set
-   */
-  public void setJapaneseKeyboard(JapaneseKeyboard keyboard) {
-    super.setKeyboard(keyboard);
-  }
-
-  public JapaneseKeyboard getJapaneseKeyboard() {
-    return JapaneseKeyboard.class.cast(super.getKeyboard());
-  }
-}
diff --git a/src/android/src/com/google/android/inputmethod/japanese/KeyEventButtonTouchListener.java b/src/android/src/com/google/android/inputmethod/japanese/KeyEventButtonTouchListener.java
index 8316583..a43c9c5 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/KeyEventButtonTouchListener.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/KeyEventButtonTouchListener.java
@@ -29,6 +29,7 @@
 
 package org.mozc.android.inputmethod.japanese;
 
+import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType;
 import org.mozc.android.inputmethod.japanese.keyboard.Flick;
 import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction;
 import org.mozc.android.inputmethod.japanese.keyboard.Key;
@@ -37,8 +38,10 @@
 import org.mozc.android.inputmethod.japanese.keyboard.KeyEventContext;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyEventHandler;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyState;
+import org.mozc.android.inputmethod.japanese.keyboard.PopUp;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchAction;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
 
 import android.view.MotionEvent;
 import android.view.View;
@@ -54,6 +57,7 @@
  *
  */
 public class KeyEventButtonTouchListener implements OnTouchListener {
+
   private final int sourceId;
   private final int keyCode;
   private KeyEventHandler keyEventHandler = null;
@@ -95,9 +99,11 @@
    * {@code keyCode}.
    * This is exported as package private for testing.
    */
-  static Key createKey(View button, int sourceId, int keyCode) {
-    KeyEntity keyEntity =
-        new KeyEntity(sourceId, keyCode, KeyEntity.INVALID_KEY_CODE, 0, null, null, false, null);
+  @VisibleForTesting static Key createKey(View button, int sourceId, int keyCode) {
+    KeyEntity keyEntity = new KeyEntity(
+            sourceId, keyCode, KeyEntity.INVALID_KEY_CODE, true, 0,
+            Optional.<String>absent(), false,
+            Optional.<PopUp>absent(), 0, 0, 0, 0);
     Flick flick = new Flick(Direction.CENTER, keyEntity);
     KeyState keyState =
         new KeyState("",
@@ -105,9 +111,10 @@
                      Collections.<KeyState.MetaState>emptySet(),
                      Collections.<KeyState.MetaState>emptySet(),
                      Collections.singletonList(flick));
-    // Now, we support repetable keys only.
+    // Now, we support repeatable keys only.
     return new Key(0, 0, button.getWidth(), button.getHeight(), 0, 0,
-                   true, false, false, Stick.EVEN, Collections.singletonList(keyState));
+                   true, false, Stick.EVEN, DrawableType.TWELVEKEYS_REGULAR_KEY_BACKGROUND,
+                   Collections.singletonList(keyState));
   }
 
   private static KeyEventContext createKeyEventContext(
@@ -157,8 +164,9 @@
     if (keyEventHandler != null && keyEventContext != null) {
       keyEventContext.update(x, y, TouchAction.TOUCH_UP, timestamp);
       keyEventHandler.cancelDelayedKeyEvent(keyEventContext);
+      // TODO(hsumita): Confirm that we can put null as a touch event or not.
       keyEventHandler.sendKey(keyEventContext.getKeyCode(),
-                              Collections.singletonList(keyEventContext.getTouchEvent()));
+                              Collections.singletonList(keyEventContext.getTouchEvent().orNull()));
       keyEventHandler.sendRelease(keyEventContext.getPressedKeyCode());
     }
     this.keyEventContext = null;
diff --git a/src/android/src/com/google/android/inputmethod/japanese/KeycodeConverter.java b/src/android/src/com/google/android/inputmethod/japanese/KeycodeConverter.java
index c42e6d3..f7e3e7b 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/KeycodeConverter.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/KeycodeConverter.java
@@ -32,6 +32,8 @@
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.KeyEvent.ModifierKey;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.KeyEvent.SpecialKey;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
 
 /**
  * Converts Androids's KeyEvent to Mozc's KeyEvent.
@@ -52,7 +54,7 @@
    */
   public interface KeyEventInterface {
     int getKeyCode();
-    android.view.KeyEvent getNativeEvent();
+    Optional<android.view.KeyEvent> getNativeEvent();
   }
 
   private static final int ASCII_MIN = 32; // Space.
@@ -112,6 +114,7 @@
   }
 
   public static KeyEventInterface getKeyEventInterface(final android.view.KeyEvent keyEvent) {
+    Preconditions.checkNotNull(keyEvent);
     return new KeyEventInterface() {
 
       @Override
@@ -120,8 +123,8 @@
       }
 
       @Override
-      public android.view.KeyEvent getNativeEvent() {
-        return keyEvent;
+      public Optional<android.view.KeyEvent> getNativeEvent() {
+        return Optional.of(keyEvent);
       }
     };
   }
@@ -135,14 +138,14 @@
       }
 
       @Override
-      public android.view.KeyEvent getNativeEvent() {
-        return null;
+      public Optional<android.view.KeyEvent> getNativeEvent() {
+        return Optional.<android.view.KeyEvent>absent();
       }
     };
   }
 
   public static boolean isMetaKey(android.view.KeyEvent keyEvent) {
-    int keyCode = keyEvent.getKeyCode();
+    int keyCode = Preconditions.checkNotNull(keyEvent).getKeyCode();
     return keyCode == android.view.KeyEvent.KEYCODE_SHIFT_LEFT ||
         keyCode == android.view.KeyEvent.KEYCODE_SHIFT_RIGHT ||
         keyCode == android.view.KeyEvent.KEYCODE_CTRL_LEFT ||
diff --git a/src/android/src/com/google/android/inputmethod/japanese/LauncherActivity.java b/src/android/src/com/google/android/inputmethod/japanese/LauncherActivity.java
new file mode 100644
index 0000000..b8063cf
--- /dev/null
+++ b/src/android/src/com/google/android/inputmethod/japanese/LauncherActivity.java
@@ -0,0 +1,49 @@
+// Copyright 2010-2014, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package org.mozc.android.inputmethod.japanese;
+
+import org.mozc.android.inputmethod.japanese.preference.MozcProxyActivity;
+import org.mozc.android.inputmethod.japanese.preference.MozcProxyPreferenceActivity;
+
+import android.content.Intent;
+
+/**
+ * Activity to show launcher icon on the home screen.
+ *
+ * <p>MozcProxyPreferenceActivity is always active because it is invoked from system preference.
+ * This class might be deactivated to hide launcher icon.
+ */
+public class LauncherActivity extends MozcProxyActivity {
+
+  @Override
+  protected Intent getForwardIntent() {
+    return new Intent(this, MozcProxyPreferenceActivity.class);
+  }
+}
diff --git a/src/android/src/com/google/android/inputmethod/japanese/LauncherIconVisibilityInitializer.java b/src/android/src/com/google/android/inputmethod/japanese/LauncherIconVisibilityInitializer.java
new file mode 100644
index 0000000..c7b6bca
--- /dev/null
+++ b/src/android/src/com/google/android/inputmethod/japanese/LauncherIconVisibilityInitializer.java
@@ -0,0 +1,56 @@
+// Copyright 2010-2014, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package org.mozc.android.inputmethod.japanese;
+
+import org.mozc.android.inputmethod.japanese.util.LauncherIconManagerFactory;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * A broadcast receiver to initialize launcher icon's visibility.
+ */
+public class LauncherIconVisibilityInitializer extends BroadcastReceiver {
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    if (shouldHandle(intent)) {
+      LauncherIconManagerFactory.getDefaultInstance().updateLauncherIconVisibility(context);
+    }
+  }
+
+  private boolean shouldHandle(Intent intent) {
+    String action = intent.getAction();
+    return "android.intent.action.BOOT_COMPLETED".equals(action)
+           || "android.intent.action.MY_PACKAGE_REPLACED".equals(action)
+           || "android.intent.action.USER_INITIALIZE".equals(action);
+  }
+}
\ No newline at end of file
diff --git a/src/android/src/com/google/android/inputmethod/japanese/MozcMenuDialogListenerImpl.java b/src/android/src/com/google/android/inputmethod/japanese/MozcMenuDialogListenerImpl.java
index e5dd770..189f2fe 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/MozcMenuDialogListenerImpl.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/MozcMenuDialogListenerImpl.java
@@ -31,7 +31,6 @@
 
 import org.mozc.android.inputmethod.japanese.mushroom.MushroomUtil;
 import org.mozc.android.inputmethod.japanese.ui.MenuDialog.MenuDialogListener;
-import org.mozc.android.inputmethod.japanese.util.ImeSwitcherFactory.ImeSwitcher;
 import com.google.common.base.Preconditions;
 
 import android.content.Context;
@@ -45,12 +44,10 @@
  */
 class MozcMenuDialogListenerImpl implements MenuDialogListener {
   private final InputMethodService inputMethodService;
-  private final ImeSwitcher imeSwitcher;
   private boolean showInputMethodPicker = false;
 
-  MozcMenuDialogListenerImpl(InputMethodService inputMethodService, ImeSwitcher imeSwitcher) {
+  MozcMenuDialogListenerImpl(InputMethodService inputMethodService) {
     this.inputMethodService = Preconditions.checkNotNull(inputMethodService);
-    this.imeSwitcher = Preconditions.checkNotNull(imeSwitcher);
   }
 
   @Override
@@ -86,13 +83,6 @@
   }
 
   @Override
-  public void onLaunchVoiceInputActivitySelected(Context context) {
-    if (!imeSwitcher.switchToVoiceIme("ja")) {
-      MozcLog.e("Voice IME for ja locale is not found.");
-    }
-  }
-
-  @Override
   public void onShowMushroomSelectionDialogSelected(Context context) {
     // Reset the composing text, otherwise the composing text will be committed automatically
     // and as the result the user would see the duplicated committing.
diff --git a/src/android/src/com/google/android/inputmethod/japanese/MozcService.java b/src/android/src/com/google/android/inputmethod/japanese/MozcService.java
index 0cfa18e..17439b0 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/MozcService.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/MozcService.java
@@ -31,22 +31,21 @@
 
 import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent;
 import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackListener;
-import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface;
 import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType;
 import org.mozc.android.inputmethod.japanese.emoji.EmojiUtil;
-import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard;
 import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode;
 import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboardSpecification;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.model.SelectionTracker;
 import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage.SymbolHistoryStorage;
 import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory;
 import org.mozc.android.inputmethod.japanese.mushroom.MushroomResultProxy;
 import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference;
+import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Context.InputFieldType;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.DeletionRange;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.GenericStorageEntry.StorageType;
@@ -56,9 +55,6 @@
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Preedit;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Preedit.Segment;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Preedit.Segment.Annotation;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.CrossingEdgeBehavior;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpaceOnAlphanumeric;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpecialRomanjiTable;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.SessionCommand;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.SessionCommand.UsageStatsEvent;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoConfig.Config;
@@ -69,11 +65,13 @@
 import org.mozc.android.inputmethod.japanese.session.SessionHandlerFactory;
 import org.mozc.android.inputmethod.japanese.util.ImeSwitcherFactory;
 import org.mozc.android.inputmethod.japanese.util.ImeSwitcherFactory.ImeSwitcher;
+import org.mozc.android.inputmethod.japanese.util.LauncherIconManagerFactory;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.protobuf.ByteString;
 
+import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.Intent;
@@ -97,6 +95,7 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputBinding;
 import android.view.inputmethod.InputConnection;
@@ -209,6 +208,8 @@
 
     @Override
     public void addHistory(SymbolMajorCategory majorCategory, String value) {
+      Preconditions.checkNotNull(majorCategory);
+      Preconditions.checkNotNull(value);
       sessionExecutor.insertToStorage(
           STORAGE_TYPE_MAP.get(majorCategory),
           value,
@@ -217,8 +218,7 @@
   }
 
   // Called back from ViewManager
-  // Package private for testing.
-  class MozcEventListener implements ViewEventListener {
+  @VisibleForTesting class MozcEventListener implements ViewEventListener {
     @Override
     public void onConversionCandidateSelected(int candidateId, Optional<Integer> rowIndex) {
       sessionExecutor.submitCandidate(candidateId, rowIndex, renderResultCallback);
@@ -226,8 +226,23 @@
     }
 
     @Override
+    public void onPageUp() {
+      sessionExecutor.pageUp(renderResultCallback);
+      feedbackManager.fireFeedback(FeedbackEvent.KEY_DOWN);
+    }
+
+    @Override
+    public void onPageDown() {
+      sessionExecutor.pageDown(renderResultCallback);
+      feedbackManager.fireFeedback(FeedbackEvent.KEY_DOWN);
+    }
+
+    @Override
     public void onSymbolCandidateSelected(SymbolMajorCategory majorCategory, String candidate,
                                           boolean updateHistory) {
+      Preconditions.checkNotNull(majorCategory);
+      Preconditions.checkNotNull(candidate);
+
       // Directly commit the text.
       commitText(candidate);
 
@@ -252,8 +267,8 @@
 
     @Override
     public void onKeyEvent(
-        ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface keyEvent,
-        KeyboardSpecification keyboardSpecification, List<? extends TouchEvent> touchEventList) {
+        @Nullable ProtoCommands.KeyEvent mozcKeyEvent, @Nullable KeyEventInterface keyEvent,
+        @Nullable KeyboardSpecification keyboardSpecification, List<TouchEvent> touchEventList) {
       if (mozcKeyEvent == null && keyboardSpecification == null) {
         // We don't send a key event to Mozc native layer since {@code mozcKeyEvent} is null, and we
         // don't need to update the keyboard specification since {@code keyboardSpecification} is
@@ -275,7 +290,7 @@
     }
 
     @Override
-    public void onUndo(List<? extends TouchEvent> touchEventList) {
+    public void onUndo(List<TouchEvent> touchEventList) {
       sessionExecutor.undoOrRewind(touchEventList, renderResultCallback);
     }
 
@@ -300,57 +315,73 @@
     }
 
     @Override
-    public void onShowMenuDialog(List<? extends TouchEvent> touchEventList) {
+    public void onShowMenuDialog(List<TouchEvent> touchEventList) {
       sessionExecutor.touchEventUsageStatsEvent(touchEventList);
     }
 
     @Override
-    public void onShowSymbolInputView(List<? extends TouchEvent> touchEventList) {
-      // Send request with (only) keyboard name for logging usage stats.
-      sessionExecutor.updateRequest(
-          MozcUtil.getRequestForKeyboard(
-              SymbolInputView.SPEC_NAME,
-              Optional.<SpecialRomanjiTable>absent(),
-              Optional.<SpaceOnAlphanumeric>absent(),
-              Optional.<Boolean>absent(),
-              Optional.<CrossingEdgeBehavior>absent(),
-              getConfiguration()),
-          touchEventList);
+    public void onShowSymbolInputView(List<TouchEvent> touchEventList) {
+      changeKeyboardSpecificationAndSendKey(
+          null, null, KeyboardSpecification.SYMBOL_NUMBER, getConfiguration(),
+          Collections.<TouchEvent>emptyList());
+      viewManager.onShowSymbolInputView();
     }
 
     @Override
     public void onCloseSymbolInputView() {
-      KeyboardSpecification specification = viewManager.getJapaneseKeyboardSpecification();
-      sessionExecutor.updateRequest(
-          MozcUtil.getRequestForKeyboard(
-              specification.getKeyboardSpecificationName(),
-              Optional.of(specification.getSpecialRomanjiTable()),
-              Optional.of(specification.getSpaceOnAlphanumeric()),
-              Optional.of(specification.isKanaModifierInsensitiveConversion()),
-              Optional.of(specification.getCrossingEdgeBehavior()),
-              getConfiguration()),
-          Collections.<TouchEvent>emptyList());
-    }
-
-    @Override
-    public void onHardwareKeyboardCompositionModeChange(CompositionSwitchMode mode) {
-      CompositionMode oldMode = hardwareKeyboard.getCompositionMode();
-      hardwareKeyboard.setCompositionMode(mode);
-      CompositionMode newMode = hardwareKeyboard.getCompositionMode();
-      if (oldMode != newMode) {
-        viewManager.setHardwareKeyboardCompositionMode(newMode);
-        sendKeyWithKeyboardSpecification(
-            null, null,
-            hardwareKeyboard.getKeyboardSpecification(),
-            getConfiguration(),
+      viewManager.onCloseSymbolInputView();
+      // This callback is called in two ways: one is from touch event on symbol input view.
+      // The other is from onKeyDown event by hardware keyboard.  ViewManager.isNarrowMode()
+      // is abused to distinguish these two triggers where its true value indicates that
+      // onCloseSymbolInputView() is called on hardware keyboard event.  In the case of hardware
+      // keyboard event, keyboard specification has been already updated so we shouldn't update it.
+      if (!viewManager.isNarrowMode()) {
+        changeKeyboardSpecificationAndSendKey(
+            null, null, viewManager.getKeyboardSpecification(), getConfiguration(),
             Collections.<TouchEvent>emptyList());
       }
     }
 
     @Override
+    public void onHardwareKeyboardCompositionModeChange(CompositionSwitchMode mode) {
+      viewManager.switchHardwareKeyboardCompositionMode(mode);
+    }
+
+    @Override
     public void onActionKey() {
       // false means that the key is for Action and not ENTER.
-      sendDefaultEditorAction(false);
+      sendEditorAction(false);
+    }
+
+    @Override
+    public void onNarrowModeChanged(boolean newNarrowMode) {
+      if (!newNarrowMode) {
+        // Hardware keyboard to software keyboard transition: Submit composition.
+        sessionExecutor.submit(renderResultCallback);
+      }
+      updateImposedConfig();
+    }
+
+    @Override
+    public void onUpdateKeyboardLayoutAdjustment(
+        ViewManagerInterface.LayoutAdjustment layoutAdjustment) {
+      Preconditions.checkNotNull(layoutAdjustment);
+      Configuration configuration = getConfiguration();
+      if (sharedPreferences == null || configuration == null) {
+        return;
+      }
+      boolean isLandscapeKeyboardSettingActive =
+          PreferenceUtil.isLandscapeKeyboardSettingActive(
+              sharedPreferences, configuration.orientation);
+      String key;
+      if (isLandscapeKeyboardSettingActive) {
+        key = PreferenceUtil.PREF_LANDSCAPE_LAYOUT_ADJUSTMENT_KEY;
+      } else {
+        key = PreferenceUtil.PREF_PORTRAIT_LAYOUT_ADJUSTMENT_KEY;
+      }
+      sharedPreferences.edit()
+          .putString(key, layoutAdjustment.toString())
+          .apply();
     }
   }
 
@@ -360,16 +391,18 @@
   private class RenderResultCallback implements SessionExecutor.EvaluationCallback {
 
     @Override
-    public void onCompleted(Command command, @Nullable KeyEventInterface triggeringKeyEvent) {
-      Preconditions.checkNotNull(command);
-      if (command.getInput().getCommand().getType() !=
-          SessionCommand.CommandType.EXPAND_SUGGESTION) {
+    public void onCompleted(
+        Optional<Command> command, Optional<KeyEventInterface> triggeringKeyEvent) {
+      Preconditions.checkArgument(Preconditions.checkNotNull(command).isPresent());
+      Preconditions.checkNotNull(triggeringKeyEvent);
+      if (command.get().getInput().getCommand().getType()
+          != SessionCommand.CommandType.EXPAND_SUGGESTION) {
         // For expanding suggestions, we don't need to update our rendering result.
-        renderInputConnection(command, triggeringKeyEvent);
+        renderInputConnection(command.get(), triggeringKeyEvent.orNull());
       }
       // Transit to narrow mode if required (e.g., Typed 'a' key from h/w keyboard).
-      viewManager.maybeTransitToNarrowMode(command, triggeringKeyEvent);
-      viewManager.render(command);
+      viewManager.maybeTransitToNarrowMode(command.get(), triggeringKeyEvent.orNull());
+      viewManager.render(command.get());
     }
   }
 
@@ -380,10 +413,10 @@
   class SendKeyToApplicationCallback implements SessionExecutor.EvaluationCallback {
 
     @Override
-    public void onCompleted(@Nullable Command command,
-                            @Nullable KeyEventInterface triggeringKeyEvent) {
-      Preconditions.checkArgument(command == null);
-      sendKeyEvent(triggeringKeyEvent);
+    public void onCompleted(Optional<Command> command,
+                            Optional<KeyEventInterface> triggeringKeyEvent) {
+      Preconditions.checkArgument(!Preconditions.checkNotNull(command).isPresent());
+      sendKeyEvent(triggeringKeyEvent.orNull());
     }
   }
 
@@ -393,10 +426,11 @@
   private class SendKeyToViewCallback implements SessionExecutor.EvaluationCallback {
 
     @Override
-    public void onCompleted(@Nullable Command command, KeyEventInterface triggeringKeyEvent) {
-      Preconditions.checkArgument(command == null);
-      Preconditions.checkNotNull(triggeringKeyEvent);
-      viewManager.consumeKeyOnViewSynchronously(triggeringKeyEvent.getNativeEvent());
+    public void onCompleted(
+        Optional<Command> command, Optional<KeyEventInterface> triggeringKeyEvent) {
+      Preconditions.checkArgument(!Preconditions.checkNotNull(command).isPresent());
+      Preconditions.checkArgument(Preconditions.checkNotNull(triggeringKeyEvent).isPresent());
+      viewManager.consumeKeyOnViewSynchronously(triggeringKeyEvent.get().getNativeEvent().orNull());
     }
   }
 
@@ -417,6 +451,7 @@
   /**
    * We need to send SYNC_DATA command periodically. This class handles it.
    */
+  @SuppressLint("HandlerLeak")
   private class SendSyncDataCommandHandler extends Handler {
     /**
      * The current period of sending SYNC_DATA is 15 mins (as same as desktop version).
@@ -439,6 +474,7 @@
    * This class handles callback operation.
    * Posting and removing messages should be done in appropriate point.
    */
+  @SuppressLint("HandlerLeak")
   private class MemoryTrimmingHandler extends Handler {
 
     /**
@@ -468,16 +504,21 @@
 
   // Focused segment's attribute.
   @VisibleForTesting static final CharacterStyle SPAN_CONVERT_HIGHLIGHT =
-      new BackgroundColorSpan(0x8888FFFF);
+      new BackgroundColorSpan(0x66EF3566);
+
+  // Background color span for non-focused conversion segment.
+  // We don't create a static CharacterStyle instance since there are multiple segments at the same
+  // time. Otherwise, segments except for the last one cannot have style.
+  @VisibleForTesting static final int CONVERT_NORMAL_COLOR = 0x19EF3566;
 
   // Cursor position.
   // Note that InputConnection seems not to be able to show cursor. This is a workaround.
   @VisibleForTesting static final CharacterStyle SPAN_BEFORE_CURSOR =
-      new BackgroundColorSpan(0x88FF88FF);
+      new BackgroundColorSpan(0x664DB6AC);
 
-  // To hide a caret, we use non-transparent background for partial conversion.
-  private static final CharacterStyle SPAN_PARTIAL_SUGGESTION_COLOR =
-      new BackgroundColorSpan(0xFFFFE0E0);
+  // Background color span for partial conversion.
+  @VisibleForTesting static final CharacterStyle SPAN_PARTIAL_SUGGESTION_COLOR =
+      new BackgroundColorSpan(0x194DB6AC);
 
   // Underline.
   @VisibleForTesting static final CharacterStyle SPAN_UNDERLINE = new UnderlineSpan();
@@ -535,10 +576,6 @@
   @VisibleForTesting KeyboardSpecification currentKeyboardSpecification =
       KeyboardSpecification.TWELVE_KEY_TOGGLE_KANA;
 
-  // Non-final for testing
-  // TODO(matsuzakit): Setting this in onCreateInternal might be more consistent.
-  @VisibleForTesting HardwareKeyboard hardwareKeyboard = new HardwareKeyboard();
-
   // Current HardKeyboardHidden configuration value.
   // This is updated only when onConfigurationChanged is called and
   // Configuration.HARDKEYBOARDHIDDEN_* differs to this.
@@ -553,6 +590,15 @@
   // Held for testing.
   private ViewEventListener eventListener;
 
+  @SuppressWarnings("deprecation")
+  @SuppressLint("NewApi")
+  public MozcService() {
+    super();
+    if (Build.VERSION.SDK_INT >= 17) {
+      enableHardwareAcceleration();
+    }
+  }
+
   @Override
   public void onBindInput() {
     super.onBindInput();
@@ -566,6 +612,13 @@
   }
 
   @Override
+  public void onUpdateCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) {
+    if (viewManager != null) {
+      viewManager.setCursorAnchorInfo(cursorAnchorInfo);
+    }
+  }
+
+  @Override
   public MozcInputMethod onCreateInputMethodInterface() {
     return new MozcInputMethod();
   }
@@ -579,9 +632,10 @@
     // Callback object mainly used by views.
     MozcEventListener eventListener = new MozcEventListener();
     SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+    Preconditions.checkNotNull(sharedPreferences);
     SessionExecutor sessionExecutor =
         SessionExecutor.getInstanceInitializedIfNecessary(
-            new SessionHandlerFactory(sharedPreferences), this);
+            new SessionHandlerFactory(Optional.of(sharedPreferences)), this);
     onCreateInternal(eventListener, null, sharedPreferences, getConfiguration(),
                      sessionExecutor);
 
@@ -598,9 +652,9 @@
   }
 
   @VisibleForTesting
-  void onCreateInternal(ViewEventListener eventListener, ViewManagerInterface viewManager,
-                        SharedPreferences sharedPreferences, Configuration deviceConfiguration,
-                        SessionExecutor sessionExecutor) {
+  void onCreateInternal(ViewEventListener eventListener, @Nullable ViewManagerInterface viewManager,
+                        @Nullable SharedPreferences sharedPreferences,
+                        Configuration deviceConfiguration, SessionExecutor sessionExecutor) {
     super.onCreate();
 
     Context context = getApplicationContext();
@@ -614,8 +668,8 @@
     prepareOnce(eventListener, symbolHistoryStorage, viewManager, sharedPreferences);
     prepareEveryTime(sharedPreferences, deviceConfiguration);
 
-    if (propagatedClientSidePreference == null ||
-        propagatedClientSidePreference.getHardwareKeyMap() == null) {
+    if (propagatedClientSidePreference == null
+        || propagatedClientSidePreference.getHardwareKeyMap() == null) {
       HardwareKeyboardSpecification.maybeSetDetectedHardwareKeyMap(
           sharedPreferences, deviceConfiguration, false);
     }
@@ -630,14 +684,15 @@
    * Prepares something which should be done every time when the session is newly created.
    */
   private void prepareEveryTime(
-      SharedPreferences sharedPreferences, Configuration deviceConfiguration) {
-    boolean isLogging = sharedPreferences != null &&
-            sharedPreferences.getBoolean(PREF_TWEAK_LOGGING_PROTOCOL_BUFFERS, false);
+      @Nullable SharedPreferences sharedPreferences, Configuration deviceConfiguration) {
+    boolean isLogging = sharedPreferences != null
+        && sharedPreferences.getBoolean(PREF_TWEAK_LOGGING_PROTOCOL_BUFFERS, false);
     // Force to initialize here.
-    sessionExecutor.reset(new SessionHandlerFactory(sharedPreferences), this);
+    sessionExecutor.reset(
+        new SessionHandlerFactory(Optional.fromNullable(sharedPreferences)), this);
     sessionExecutor.setLogging(isLogging);
 
-    updateImposedConfig(getConfiguration());
+    updateImposedConfig();
     viewManager.onConfigurationChanged(getConfiguration());
     // Make sure that the server and the client have the same keyboard specification.
     // User preference's keyboard will be set after this step.
@@ -646,11 +701,12 @@
         Collections.<TouchEvent>emptyList());
     if (sharedPreferences != null) {
       propagateClientSidePreference(
-          new ClientSidePreference(sharedPreferences, deviceConfiguration.orientation));
+          new ClientSidePreference(
+              sharedPreferences, getResources(), deviceConfiguration.orientation));
       // TODO(hidehiko): here we just set the config based on preferences. When we start
       //   to support sync on Android, we need to revisit the config related design.
       sessionExecutor.setConfig(ConfigUtil.toConfig(sharedPreferences));
-      sessionExecutor.preferenceUsageStatsEvent(sharedPreferences);
+      sessionExecutor.preferenceUsageStatsEvent(sharedPreferences, getResources());
     }
 
     maybeSetNarrowMode(deviceConfiguration);
@@ -661,16 +717,17 @@
    */
   private void prepareOnce(ViewEventListener eventListener,
       SymbolHistoryStorage symbolHistoryStorage,
-      ViewManagerInterface viewManager,
-      SharedPreferences sharedPreferences) {
+      @Nullable ViewManagerInterface viewManager,
+      @Nullable SharedPreferences sharedPreferences) {
     Context context = getApplicationContext();
-    boolean omitWelcomeActivity = false;
     Optional<Intent> forwardIntent =
         ApplicationInitializerFactory.createInstance(this).initialize(
-            omitWelcomeActivity,
+            MozcUtil.isSystemApplication(context),
             MozcUtil.isDevChannel(context),
             DependencyFactory.getDependency(getApplicationContext()).isWelcomeActivityPreferrable(),
-            MozcUtil.getAbiIndependentVersionCode(context));
+            MozcUtil.getAbiIndependentVersionCode(context),
+            LauncherIconManagerFactory.getDefaultInstance(),
+            PreferenceUtil.getDefaultPreferenceManagerStatic());
     if (forwardIntent.isPresent()) {
       startActivity(forwardIntent.get());
     }
@@ -684,7 +741,7 @@
               eventListener,
               symbolHistoryStorage,
               imeSwitcher,
-              new MozcMenuDialogListenerImpl(this, imeSwitcher));
+              new MozcMenuDialogListenerImpl(this));
     }
 
     // Setup FeedbackManager.
@@ -714,7 +771,7 @@
     return inputView;
   }
 
-  void resetContext() {
+  private void resetContext() {
     if (sessionExecutor != null) {
       sessionExecutor.resetContext();
     }
@@ -740,16 +797,16 @@
 
     // Update full screen mode, because the application may be changed.
     viewManager.setFullscreenMode(
-        applicationCompatibility.isFullScreenModeSupported() &&
-        propagatedClientSidePreference != null &&
-        propagatedClientSidePreference.isFullscreenMode());
+        applicationCompatibility.isFullScreenModeSupported()
+        && propagatedClientSidePreference != null
+        && propagatedClientSidePreference.isFullscreenMode());
 
     // Some applications, e.g. gmail or maps, send onStartInput with restarting = true, when a user
     // rotates a device. In such cases, we don't want to update caret positions, nor reset
     // the context basically. However, some other applications, such as one with a webview widget
     // like a browser, send onStartInput with restarting = true, too. Unfortunately,
     // there seems no way to figure out which one causes this invocation.
-    // So, as a point of compromise, we reset the context everytime here. Also, we'll send
+    // So, as a point of compromise, we reset the context every time here. Also, we'll send
     // finishComposingText as well, in case the new attached field has already had composing text
     // (we hit such a situation on webview, too).
     // See also onConfigurationChanged for caret position handling on gmail-like applications'
@@ -778,7 +835,7 @@
    * field, commit it. Then, (regardless of whether there exists pending result,) clears
    * all remaining pending result.
    */
-  static void maybeCommitMushroomResult(EditorInfo attribute, InputConnection connection) {
+  private static void maybeCommitMushroomResult(EditorInfo attribute, InputConnection connection) {
     if (connection == null) {
       return;
     }
@@ -796,10 +853,20 @@
     }
   }
 
+  @SuppressLint("NewApi")
+  private static boolean enableCursorAnchorInfo(InputConnection connection) {
+    Preconditions.checkNotNull(connection);
+    if (Build.VERSION.SDK_INT < 21) {
+      return false;
+    }
+    return connection.requestCursorUpdates(
+        InputConnection.CURSOR_UPDATE_IMMEDIATE | InputConnection.CURSOR_UPDATE_MONITOR);
+  }
+
   /**
    * @return true if connected view is WebEditText (or the application pretends it)
    */
-  boolean isWebEditText(EditorInfo editorInfo) {
+  private boolean isWebEditText(EditorInfo editorInfo) {
     if (editorInfo == null) {
       return false;
     }
@@ -816,16 +883,26 @@
 
   @Override
   public void onStartInputView(EditorInfo attribute, boolean restarting) {
+    InputConnection inputConnection = getCurrentInputConnection();
+    if (inputConnection != null && Build.VERSION.SDK_INT >= 21) {
+      viewManager.setCursorAnchorInfoEnabled(enableCursorAnchorInfo(inputConnection));
+      updateImposedConfig();
+    }
+
     viewManager.setTextForActionButton(getTextForImeAction(attribute.imeOptions));
     viewManager.setEditorInfo(attribute);
+    // updateXxxxxButtonEnabled cannot be placed in onStartInput because
+    // the view might be created after onStartInput with *reset* status.
+    viewManager.updateGlobeButtonEnabled();
+    viewManager.updateMicrophoneButtonEnabled();
   }
 
   static InputFieldType getInputFieldType(EditorInfo attribute) {
     int inputType = attribute.inputType;
-    int inputClass = inputType & InputType.TYPE_MASK_CLASS;
-    if (MozcUtil.isPasswordField(attribute)) {
+    if (MozcUtil.isPasswordField(inputType)) {
       return InputFieldType.PASSWORD;
     }
+    int inputClass = inputType & InputType.TYPE_MASK_CLASS;
     if (inputClass == InputType.TYPE_CLASS_PHONE) {
       return InputFieldType.TEL;
     }
@@ -867,6 +944,8 @@
     return super.onGenericMotionEvent(event);
   }
 
+  @SuppressLint("DefaultLocale")
+  @VisibleForTesting
   boolean onKeyDownInternal(int keyCode, KeyEvent event, Configuration configuration) {
     if (MozcLog.isLoggable(Log.DEBUG)) {
       MozcLog.d(
@@ -893,7 +972,7 @@
       return super.onKeyDown(keyCode, event);
     }
 
-    // Push the event to the asyncronous execution queue if it should be processed
+    // Push the event to the asynchronous execution queue if it should be processed
     // directly in the view.
     if (viewManager.isKeyConsumedOnViewAsynchronously(event)) {
       sessionExecutor.sendKeyEvent(KeycodeConverter.getKeyEventInterface(event),
@@ -902,28 +981,17 @@
     }
 
     // Lazy evaluation.
-    // If hardware keybaord is not set in the preference screen,
+    // If hardware keyboard is not set in the preference screen,
     // set it based on the configuration.
-    if (propagatedClientSidePreference == null ||
-        propagatedClientSidePreference.getHardwareKeyMap() == null) {
+    if (propagatedClientSidePreference == null
+        || propagatedClientSidePreference.getHardwareKeyMap() == null) {
       HardwareKeyboardSpecification.maybeSetDetectedHardwareKeyMap(
           sharedPreferences, configuration, true);
     }
 
     // Here we decided to send the event to the server.
 
-    // Maybe update the composition mode based on the event.
-    // For example, zen/han key toggles the composition mode (hiragana <--> alphabet).
-    CompositionMode compositionMode = hardwareKeyboard.getCompositionMode();
-    hardwareKeyboard.setCompositionModeByKey(event);
-    CompositionMode currentCompositionMode = hardwareKeyboard.getCompositionMode();
-    if (currentCompositionMode != compositionMode) {
-      viewManager.setHardwareKeyboardCompositionMode(currentCompositionMode);
-    }
-    sendKeyWithKeyboardSpecification(
-        hardwareKeyboard.getMozcKeyEvent(event), hardwareKeyboard.getKeyEventInterface(event),
-        hardwareKeyboard.getKeyboardSpecification(), configuration,
-        Collections.<TouchEvent>emptyList());
+    viewManager.onHardwareKeyEvent(event);
     return true;
   }
 
@@ -973,10 +1041,15 @@
    */
   @VisibleForTesting
   void sendKeyWithKeyboardSpecification(
-      ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface event,
-      KeyboardSpecification keyboardSpecification, Configuration configuration,
-      List<? extends TouchEvent> touchEventList) {
-    if (currentKeyboardSpecification != keyboardSpecification) {
+      @Nullable ProtoCommands.KeyEvent mozcKeyEvent, @Nullable KeyEventInterface event,
+      @Nullable KeyboardSpecification keyboardSpecification, Configuration configuration,
+      List<TouchEvent> touchEventList) {
+    if (keyboardSpecification != null && currentKeyboardSpecification != keyboardSpecification) {
+      // Submit composition on the transition from software KB to hardware KB by key event.
+      if (!currentKeyboardSpecification.isHardwareKeyboard()
+          && keyboardSpecification.isHardwareKeyboard()) {
+        sessionExecutor.submit(renderResultCallback);
+      }
       changeKeyboardSpecificationAndSendKey(
           mozcKeyEvent, event, keyboardSpecification, configuration, touchEventList);
       updateStatusIcon();
@@ -991,31 +1064,26 @@
   }
 
   /**
-   * Sends Request for changing keybaord setting to mozc server and sends key.
+   * Sends Request for changing keyboard setting to mozc server and sends key.
    */
   private void changeKeyboardSpecificationAndSendKey(
-      ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface event,
+      @Nullable ProtoCommands.KeyEvent mozcKeyEvent, @Nullable KeyEventInterface event,
       KeyboardSpecification keyboardSpecification, Configuration configuration,
-      List<? extends TouchEvent> touchEventList) {
+      List<TouchEvent> touchEventList) {
     // Send Request to change composition table.
     sessionExecutor.updateRequest(
-        MozcUtil.getRequestForKeyboard(
-            keyboardSpecification.getKeyboardSpecificationName(),
-            Optional.of(keyboardSpecification.getSpecialRomanjiTable()),
-            Optional.of(keyboardSpecification.getSpaceOnAlphanumeric()),
-            Optional.of(keyboardSpecification.isKanaModifierInsensitiveConversion()),
-            Optional.of(keyboardSpecification.getCrossingEdgeBehavior()),
-            configuration),
+        MozcUtil.getRequestBuilder(getResources(), keyboardSpecification, configuration).build(),
         touchEventList);
     if (mozcKeyEvent == null) {
       // Change composition mode.
-      sessionExecutor.switchInputMode(event, keyboardSpecification.getCompositionMode(),
-                                      renderResultCallback);
+      sessionExecutor.switchInputMode(
+          Optional.fromNullable(event), keyboardSpecification.getCompositionMode(),
+          renderResultCallback);
     } else {
       // Send key with composition mode change.
       sessionExecutor.sendKey(
           ProtoCommands.KeyEvent.newBuilder(mozcKeyEvent)
-          .setMode(keyboardSpecification.getCompositionMode()).build(),
+              .setMode(keyboardSpecification.getCompositionMode()).build(),
           event, touchEventList, renderResultCallback);
     }
     currentKeyboardSpecification = keyboardSpecification;
@@ -1035,7 +1103,7 @@
   /**
    * Shows the status icon basing on the current keyboard spec.
    */
-  void showStatusIcon() {
+  private void showStatusIcon() {
     switch (currentKeyboardSpecification.getCompositionMode()) {
       case HIRAGANA:
         showStatusIcon(R.drawable.status_icon_hiragana);
@@ -1052,6 +1120,17 @@
   }
 
   @Override
+  public boolean onShowInputRequested(int flags, boolean configChange) {
+    boolean result = super.onShowInputRequested(flags, configChange);
+    boolean isHardwareKeyboardConnected =
+        getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS;
+    // Original result becomes false when a hardware keyboard is connected.
+    // This means that the window won't be shown in such situation.
+    // We want to show it even with a hardware keyboard so override the result here.
+    return result || isHardwareKeyboardConnected;
+  }
+
+  @Override
   public void onWindowShown() {
     showStatusIcon();
     // Remove memory trimming message.
@@ -1070,7 +1149,7 @@
   public void onWindowHidden() {
     // "Hiding IME's window" is very similar to "Turning off IME" for PC.
     // Thus
-    // - Commiting composing text.
+    // - Committing composing text.
     // - Removing all pending messages.
     // - Resetting Mozc server
     // are needed.
@@ -1096,6 +1175,7 @@
    * @param keyEvent Trigger event for this calling. When direct input is
    *        needed, this event is sent to InputConnection.
    */
+  @VisibleForTesting
   void renderInputConnection(Command command, @Nullable KeyEventInterface keyEvent) {
     Preconditions.checkNotNull(command);
 
@@ -1115,8 +1195,8 @@
     // case, the command is consumed by Mozc server and the application cannot get the key event.
     // To avoid such situation, we should send the key event back to application. b/13238551
     // The command itself is consumed by Mozc server, so we should NOT put a return statement here.
-    if (keyEvent != null && keyEvent.getNativeEvent() != null &&
-        KeycodeConverter.isMetaKey(keyEvent.getNativeEvent())) {
+    if (keyEvent != null && keyEvent.getNativeEvent().isPresent()
+        && KeycodeConverter.isMetaKey(keyEvent.getNativeEvent().get())) {
       sendKeyEvent(keyEvent);
     }
 
@@ -1136,7 +1216,8 @@
     }
   }
 
-  static KeyEvent createKeyEvent(KeyEvent original, long eventTime, int action, int repeatCount) {
+  private static KeyEvent createKeyEvent(
+      KeyEvent original, long eventTime, int action, int repeatCount) {
     return new KeyEvent(
         original.getDownTime(), eventTime, action, original.getKeyCode(),
         repeatCount, original.getMetaState(), original.getDeviceId(), original.getScanCode(),
@@ -1146,7 +1227,7 @@
   /**
    * Sends the {@code KeyEvent}, which is not consumed by the mozc server.
    */
-  void sendKeyEvent(KeyEventInterface keyEvent) {
+  @VisibleForTesting void sendKeyEvent(KeyEventInterface keyEvent) {
     if (keyEvent == null) {
       return;
     }
@@ -1159,24 +1240,24 @@
     }
 
     // Following code is to fallback to target activity.
-    KeyEvent nativeKeyEvent = keyEvent.getNativeEvent();
+    Optional<KeyEvent> nativeKeyEvent = keyEvent.getNativeEvent();
     InputConnection inputConnection = getCurrentInputConnection();
 
-    if (nativeKeyEvent != null && inputConnection != null) {
+    if (nativeKeyEvent.isPresent() && inputConnection != null) {
       // Meta keys are from this.onKeyDown/Up so fallback each time.
-      if (KeycodeConverter.isMetaKey(nativeKeyEvent)) {
+      if (KeycodeConverter.isMetaKey(nativeKeyEvent.get())) {
         inputConnection.sendKeyEvent(createKeyEvent(
-            nativeKeyEvent, MozcUtil.getUptimeMillis(),
-            nativeKeyEvent.getAction(), nativeKeyEvent.getRepeatCount()));
+            nativeKeyEvent.get(), MozcUtil.getUptimeMillis(),
+            nativeKeyEvent.get().getAction(), nativeKeyEvent.get().getRepeatCount()));
         return;
       }
 
       // Other keys are from this.onKeyDown so create dummy Down/Up events.
       inputConnection.sendKeyEvent(createKeyEvent(
-          nativeKeyEvent, MozcUtil.getUptimeMillis(), KeyEvent.ACTION_DOWN, 0));
+          nativeKeyEvent.get(), MozcUtil.getUptimeMillis(), KeyEvent.ACTION_DOWN, 0));
 
       inputConnection.sendKeyEvent(createKeyEvent(
-          nativeKeyEvent, MozcUtil.getUptimeMillis(), KeyEvent.ACTION_UP, 0));
+          nativeKeyEvent.get(), MozcUtil.getUptimeMillis(), KeyEvent.ACTION_UP, 0));
       return;
     }
 
@@ -1206,13 +1287,33 @@
     if (keyCode != KeyEvent.KEYCODE_ENTER || !isInputViewShown()) {
       return false;
     }
-
-    // Fall back to EditorAction. Note that the keyCode is ENTER here, so set the fromEnterKey
-    // argument true.
-    return sendDefaultEditorAction(true);
+    return sendEditorAction(true);
   }
 
-  static void maybeDeleteSurroundingText(Output output, InputConnection inputConnection) {
+  /**
+   * Sends editor action to {@code InputConnection}.
+   * <p>
+   * The difference from {@link InputMethodService#sendDefaultEditorAction(boolean)} is
+   * that if custom action label is specified {@code EditorInfo#actionId} is sent instead.
+   */
+  private boolean sendEditorAction(boolean fromEnterKey) {
+    // If custom action label is specified (=non-null), special action id is also specified.
+    // If there is no IME_FLAG_NO_ENTER_ACTION option, we should send the id to the InputConnection.
+    EditorInfo editorInfo = getCurrentInputEditorInfo();
+    if (editorInfo != null
+        && (editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0
+        && editorInfo.actionLabel != null) {
+      InputConnection inputConnection = getCurrentInputConnection();
+      if (inputConnection != null) {
+        inputConnection.performEditorAction(editorInfo.actionId);
+        return true;
+      }
+    }
+    // No custom action label is specified. Fall back to default EditorAction.
+    return sendDefaultEditorAction(fromEnterKey);
+  }
+
+  private static void maybeDeleteSurroundingText(Output output, InputConnection inputConnection) {
     if (!output.hasDeletionRange()) {
       return;
     }
@@ -1232,7 +1333,7 @@
     }
   }
 
-  static void maybeCommitText(Output output, InputConnection inputConnection) {
+  private static void maybeCommitText(Output output, InputConnection inputConnection) {
     if (!output.hasResult()) {
       return;
     }
@@ -1245,8 +1346,8 @@
 
     int position = MozcUtil.CURSOR_POSITION_TAIL;
     if (output.getResult().hasCursorOffset()) {
-      if (output.getResult().getCursorOffset() ==
-          -outputText.codePointCount(0, outputText.length())) {
+      if (output.getResult().getCursorOffset()
+          == -outputText.codePointCount(0, outputText.length())) {
         position = MozcUtil.CURSOR_POSITION_HEAD;
       } else {
         MozcLog.e("Unsupported position: " + output.getResult().toString());
@@ -1258,7 +1359,7 @@
     }
   }
 
-  void setComposingText(Command command, InputConnection inputConnection) {
+  private void setComposingText(Command command, InputConnection inputConnection) {
     Preconditions.checkNotNull(command);
     Preconditions.checkNotNull(inputConnection);
 
@@ -1274,8 +1375,8 @@
       // To avoid from this issue, we don't clear the composing text if the input
       // is SWITCH_INPUT_MODE.
       Input input = command.getInput();
-      if (input.getType() != Input.CommandType.SEND_COMMAND ||
-          input.getCommand().getType() != SessionCommand.CommandType.SWITCH_INPUT_MODE) {
+      if (input.getType() != Input.CommandType.SEND_COMMAND
+          || input.getCommand().getType() != SessionCommand.CommandType.SWITCH_INPUT_MODE) {
         if (!inputConnection.setComposingText("", 0)) {
           MozcLog.e("Failed to set composing text.");
         }
@@ -1289,14 +1390,6 @@
     SpannableStringBuilder builder = new SpannableStringBuilder();
     for (Segment segment : preedit.getSegmentList()) {
       builder.append(segment.getValue());
-      if (segment.hasAnnotation() && segment.getAnnotation() == Annotation.HIGHLIGHT) {
-        // Highlight for the focused conversion part.
-        builder.setSpan(
-            SPAN_CONVERT_HIGHLIGHT,
-            builder.length() - segment.getValue().length(),
-            builder.length(),
-            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-      }
     }
 
     // Set underline for all the preedit text.
@@ -1304,18 +1397,30 @@
 
     // Draw cursor if in composition mode.
     int cursor = preedit.getCursor();
-    if (!(output.hasAllCandidateWords() &&
-          output.getAllCandidateWords().hasCategory() &&
-          output.getAllCandidateWords().getCategory() == ProtoCandidates.Category.CONVERSION)) {
+    int spanFlags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING;
+    if (output.hasAllCandidateWords()
+        && output.getAllCandidateWords().hasCategory()
+        && output.getAllCandidateWords().getCategory() == ProtoCandidates.Category.CONVERSION) {
+      int offsetInString = 0;
+      for (Segment segment : preedit.getSegmentList()) {
+        int length = segment.getValue().length();
+        builder.setSpan(
+            segment.hasAnnotation() && segment.getAnnotation() == Annotation.HIGHLIGHT
+                ? SPAN_CONVERT_HIGHLIGHT
+                : CharacterStyle.class.cast(new BackgroundColorSpan(CONVERT_NORMAL_COLOR)),
+            offsetInString, offsetInString + length, spanFlags);
+        offsetInString += length;
+      }
+    } else {
       // We cannot show system cursor inside preedit here.
       // Instead we change text style before the preedit's cursor.
+      int cursorOffsetInString = builder.toString().offsetByCodePoints(0, cursor);
       if (cursor != builder.length()) {
-        // This condition is workaround not to show unexpected background color for EditText.
-        builder.setSpan(SPAN_PARTIAL_SUGGESTION_COLOR, cursor, builder.length(),
-                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        builder.setSpan(SPAN_PARTIAL_SUGGESTION_COLOR, cursorOffsetInString, builder.length(),
+                        spanFlags);
       }
       if (cursor > 0) {
-        builder.setSpan(SPAN_BEFORE_CURSOR, 0, cursor, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        builder.setSpan(SPAN_BEFORE_CURSOR, 0, cursorOffsetInString, spanFlags);
       }
     }
 
@@ -1327,7 +1432,7 @@
     }
   }
 
-  void maybeSetSelection(Output output, InputConnection inputConnection) {
+  private void maybeSetSelection(Output output, InputConnection inputConnection) {
     if (!output.hasPreedit()) {
       return;
     }
@@ -1382,68 +1487,71 @@
       return;
     }
     ClientSidePreference oldPreference = propagatedClientSidePreference;
-    if (oldPreference == null ||
-        oldPreference.isHapticFeedbackEnabled() != newPreference.isHapticFeedbackEnabled()) {
+    if (oldPreference == null
+        || oldPreference.isHapticFeedbackEnabled() != newPreference.isHapticFeedbackEnabled()) {
       feedbackManager.setHapticFeedbackEnabled(newPreference.isHapticFeedbackEnabled());
     }
-    if (oldPreference == null ||
-        oldPreference.getHapticFeedbackDuration() != newPreference.getHapticFeedbackDuration()) {
+    if (oldPreference == null
+        || oldPreference.getHapticFeedbackDuration() != newPreference.getHapticFeedbackDuration()) {
       feedbackManager.setHapticFeedbackDuration(newPreference.getHapticFeedbackDuration());
     }
-    if (oldPreference == null ||
-        oldPreference.isSoundFeedbackEnabled() != newPreference.isSoundFeedbackEnabled()) {
+    if (oldPreference == null
+        || oldPreference.isSoundFeedbackEnabled() != newPreference.isSoundFeedbackEnabled()) {
       feedbackManager.setSoundFeedbackEnabled(newPreference.isSoundFeedbackEnabled());
     }
-    if (oldPreference == null ||
-        oldPreference.getSoundFeedbackVolume() != newPreference.getSoundFeedbackVolume()) {
-      // The default value is 0.1f. In order to set the 50 to the default value, divide the
-      // preference value by 500f heuristically.
-      feedbackManager.setSoundFeedbackVolume(newPreference.getSoundFeedbackVolume() / 500f);
+    if (oldPreference == null
+        || oldPreference.getSoundFeedbackVolume() != newPreference.getSoundFeedbackVolume()) {
+      // The default value is 0.4f. In order to set the 50 to the default value, divide the
+      // preference value by 125f heuristically.
+      feedbackManager.setSoundFeedbackVolume(newPreference.getSoundFeedbackVolume() / 125f);
     }
-    if (oldPreference == null ||
-        oldPreference.isPopupFeedbackEnabled() != newPreference.isPopupFeedbackEnabled()) {
+    if (oldPreference == null
+        || oldPreference.isPopupFeedbackEnabled() != newPreference.isPopupFeedbackEnabled()) {
       viewManager.setPopupEnabled(newPreference.isPopupFeedbackEnabled());
     }
-    if (oldPreference == null ||
-        oldPreference.getKeyboardLayout() != newPreference.getKeyboardLayout()) {
+    if (oldPreference == null
+        || oldPreference.getKeyboardLayout() != newPreference.getKeyboardLayout()) {
       viewManager.setKeyboardLayout(newPreference.getKeyboardLayout());
     }
-    if (oldPreference == null ||
-        oldPreference.getInputStyle() != newPreference.getInputStyle()) {
+    if (oldPreference == null
+        || oldPreference.getInputStyle() != newPreference.getInputStyle()) {
       viewManager.setInputStyle(newPreference.getInputStyle());
     }
-    if (oldPreference == null ||
-        oldPreference.isQwertyLayoutForAlphabet() != newPreference.isQwertyLayoutForAlphabet()) {
+    if (oldPreference == null
+        || oldPreference.isQwertyLayoutForAlphabet() != newPreference.isQwertyLayoutForAlphabet()) {
       viewManager.setQwertyLayoutForAlphabet(newPreference.isQwertyLayoutForAlphabet());
     }
-    if (oldPreference == null ||
-        oldPreference.isFullscreenMode() != newPreference.isFullscreenMode()) {
+    if (oldPreference == null
+        || oldPreference.isFullscreenMode() != newPreference.isFullscreenMode()) {
       viewManager.setFullscreenMode(
-          applicationCompatibility.isFullScreenModeSupported() &&
-          newPreference.isFullscreenMode());
+          applicationCompatibility.isFullScreenModeSupported() && newPreference.isFullscreenMode());
     }
-    if (oldPreference == null ||
-        oldPreference.getFlickSensitivity() != newPreference.getFlickSensitivity()) {
+    if (oldPreference == null
+        || oldPreference.getFlickSensitivity() != newPreference.getFlickSensitivity()) {
       viewManager.setFlickSensitivity(newPreference.getFlickSensitivity());
     }
-    if (oldPreference == null ||
-        oldPreference.getEmojiProviderType() != newPreference.getEmojiProviderType()) {
+    if (oldPreference == null
+        || oldPreference.getEmojiProviderType() != newPreference.getEmojiProviderType()) {
       viewManager.setEmojiProviderType(newPreference.getEmojiProviderType());
     }
-    if (oldPreference == null ||
-        oldPreference.getHardwareKeyMap() != newPreference.getHardwareKeyMap()) {
-      hardwareKeyboard.setHardwareKeyMap(newPreference.getHardwareKeyMap());
+    if (oldPreference == null
+        || oldPreference.getHardwareKeyMap() != newPreference.getHardwareKeyMap()) {
+      viewManager.setHardwareKeyMap(newPreference.getHardwareKeyMap());
     }
-    if (oldPreference == null ||
-        oldPreference.getSkinType() != newPreference.getSkinType()) {
-      viewManager.setSkinType(newPreference.getSkinType());
+    if (oldPreference == null
+        || oldPreference.getSkinType() != newPreference.getSkinType()) {
+      viewManager.setSkin(newPreference.getSkinType().getSkin(getResources()));
     }
-    if (oldPreference == null ||
-        oldPreference.getLayoutAdjustment() != newPreference.getLayoutAdjustment()) {
-      viewManager.setLayoutAdjustment(getResources(), newPreference.getLayoutAdjustment());
+    if (oldPreference == null
+        || oldPreference.isMicrophoneButtonEnabled() != newPreference.isMicrophoneButtonEnabled()) {
+      viewManager.setMicrophoneButtonEnabledByPreference(newPreference.isMicrophoneButtonEnabled());
     }
-    if (oldPreference == null ||
-        oldPreference.getKeyboardHeightRatio() != newPreference.getKeyboardHeightRatio()) {
+    if (oldPreference == null
+        || oldPreference.getLayoutAdjustment() != newPreference.getLayoutAdjustment()) {
+      viewManager.setLayoutAdjustment(newPreference.getLayoutAdjustment());
+    }
+    if (oldPreference == null
+        || oldPreference.getKeyboardHeightRatio() != newPreference.getKeyboardHeightRatio()) {
       viewManager.setKeyboardHeightRatio(newPreference.getKeyboardHeightRatio());
     }
 
@@ -1455,18 +1563,17 @@
    *
    * Some config items should be mobile ones.
    * For example, "selection shortcut" should be disabled on software keyboard
-   * regardless of stored config.
-   * Imposed config should be based on device configuration
-   * but currently we ignore device config because we currently do not support
-   * hardware keyboard.
-   *
-   * @param deviceConfig the current device configuration
+   * regardless of stored config if there is no hardware keyboard.
    */
-  private void updateImposedConfig(Configuration deviceConfig) {
+  private void updateImposedConfig() {
+    // TODO(hsumita): Respect Config.SelectionShortcut.
+    SelectionShortcut shortcutMode = (viewManager != null && viewManager.isFloatingCandidateMode())
+        ? SelectionShortcut.SHORTCUT_123456789 : SelectionShortcut.NO_SHORTCUT;
+
     // TODO(matsuzakit): deviceConfig should be used to set following config items.
     sessionExecutor.setImposedConfig(Config.newBuilder()
         .setSessionKeymap(SessionKeymap.MOBILE)
-        .setSelectionShortcut(SelectionShortcut.NO_SHORTCUT)
+        .setSelectionShortcut(shortcutMode)
         .setUseEmojiConversion(true)
         .build());
   }
@@ -1487,20 +1594,21 @@
         return;
       }
       propagateClientSidePreference(
-          new ClientSidePreference(sharedPreferences, getConfiguration().orientation));
+          new ClientSidePreference(
+              sharedPreferences, getResources(), getConfiguration().orientation));
       sessionExecutor.setConfig(ConfigUtil.toConfig(sharedPreferences));
-      sessionExecutor.preferenceUsageStatsEvent(sharedPreferences);
+      sessionExecutor.preferenceUsageStatsEvent(sharedPreferences, getResources());
     }
   }
 
-  void maybeSetNarrowMode(Configuration configuration) {
+  @VisibleForTesting void maybeSetNarrowMode(Configuration configuration) {
     // If given hardKeyboardHidden is equal to current one, skip updating narrow mode.
     // In other words, only hardKeyboardHidden flag changes narrow mode automatically.
     // This behavior is beneficial for a user who want to change narrow/full mode manually
-    // because this method keeps current narrow mode unless hardwarekeyboard connection is changed.
+    // because this method keeps current narrow mode unless hardware keyboard connection is changed.
     if (viewManager != null && configuration.hardKeyboardHidden != currentHardKeyboardHidden) {
       currentHardKeyboardHidden = configuration.hardKeyboardHidden;
-      switch(currentHardKeyboardHidden) {
+      switch (currentHardKeyboardHidden) {
         case Configuration.HARDKEYBOARDHIDDEN_NO:
           if (!viewManager.isNarrowMode()) {
             viewManager.hideSubInputView();
@@ -1518,7 +1626,7 @@
     }
   }
 
-  void onConfigurationChangedInternal(Configuration newConfig) {
+  @VisibleForTesting void onConfigurationChangedInternal(Configuration newConfig) {
     InputConnection inputConnection = getCurrentInputConnection();
     if (inputConnection != null) {
       if (inputBound) {
@@ -1544,12 +1652,16 @@
     resetContext();
     selectionTracker.onConfigurationChanged();
 
+    sessionExecutor.updateRequest(
+        MozcUtil.getRequestBuilder(getResources(), currentKeyboardSpecification, newConfig).build(),
+        Collections.<TouchEvent>emptyList());
+
     // NOTE : This method is not called at the time when the service is started.
-    // Based on newConfig, imposed config and client side prefereces should be sent
+    // Based on newConfig, client side preferences should be sent
     // because they change based on device config.
-    updateImposedConfig(newConfig);
     propagateClientSidePreference(new ClientSidePreference(
-        PreferenceManager.getDefaultSharedPreferences(this), newConfig.orientation));
+        Preconditions.checkNotNull(PreferenceManager.getDefaultSharedPreferences(this)),
+        getResources(), newConfig.orientation));
     maybeSetNarrowMode(newConfig);
     viewManager.onConfigurationChanged(newConfig);
   }
@@ -1562,6 +1674,7 @@
     super.onConfigurationChanged(newConfig);
   }
 
+  @VisibleForTesting
   void onUpdateSelectionInternal(int oldSelStart, int oldSelEnd,
                                  int newSelStart, int newSelEnd,
                                  int candidatesStart, int candidatesEnd) {
@@ -1643,11 +1756,6 @@
     return eventListener;
   }
 
-  /**
-   * attachBaseContext is defined with protected visibility in
-   * {@link android.content.ContextWrapper} but this is required for testing.
-   * Here just makes it public.
-   */
   @Override
   @VisibleForTesting
   public void attachBaseContext(Context base) {
diff --git a/src/android/src/com/google/android/inputmethod/japanese/MozcUtil.java b/src/android/src/com/google/android/inputmethod/japanese/MozcUtil.java
index a65ec44..761250a 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/MozcUtil.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/MozcUtil.java
@@ -29,10 +29,8 @@
 
 package org.mozc.android.inputmethod.japanese;
 
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.CrossingEdgeBehavior;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpaceOnAlphanumeric;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpecialRomanjiTable;
 import org.mozc.android.inputmethod.japanese.util.ResourcesWrapper;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
@@ -182,6 +180,9 @@
   private static Optional<Boolean> isDevChannel = Optional.<Boolean>absent();
   private static Optional<Boolean> isMozcEnabled = Optional.<Boolean>absent();
   private static Optional<Boolean> isMozcDefaultIme = Optional.<Boolean>absent();
+  private static Optional<Boolean> isSystemApplication = Optional.<Boolean>absent();
+  private static Optional<Boolean> isTouchUI = Optional.<Boolean>absent();
+  private static Optional<Boolean> isUpdatedSystemApplication = Optional.<Boolean>absent();
   private static Optional<Integer> versionCode = Optional.<Integer>absent();
   private static Optional<Long> uptimeMillis = Optional.<Long>absent();
 
@@ -250,6 +251,38 @@
     return isDevChannelVersionName(getVersionName(context));
   }
 
+  public static final boolean isSystemApplication(Context context) {
+    Preconditions.checkNotNull(context);
+    if (isSystemApplication.isPresent()) {
+      return isSystemApplication.get();
+    }
+    return checkApplicationFlag(context, ApplicationInfo.FLAG_SYSTEM);
+  }
+
+  /**
+   * For testing purpose.
+   * @param isSystemApplication Optional.absent() if default behavior is preferable
+   */
+  public static final void setSystemApplication(Optional<Boolean> isSystemApplication) {
+    MozcUtil.isSystemApplication = Preconditions.checkNotNull(isSystemApplication);
+  }
+
+  public static final boolean isUpdatedSystemApplication(Context context) {
+    Preconditions.checkNotNull(context);
+    if (isUpdatedSystemApplication.isPresent()) {
+      return isUpdatedSystemApplication.get();
+    }
+    return checkApplicationFlag(context, ApplicationInfo.FLAG_UPDATED_SYSTEM_APP);
+  }
+
+  /**
+   * For testing purpose.
+   * @param isUpdatedSystemApplication Optional.absent() if default behavior is preferable
+   */
+  public static final void setUpdatedSystemApplication(
+      Optional<Boolean> isUpdatedSystemApplication) {
+    MozcUtil.isUpdatedSystemApplication = Preconditions.checkNotNull(isUpdatedSystemApplication);
+  }
 
   /**
    * Gets version name.
@@ -437,6 +470,26 @@
     return Optional.absent();
   }
 
+  /**
+   * Returns true is touch UI should be shown.
+   */
+  public static boolean isTouchUI(Context context) {
+    Preconditions.checkNotNull(context);
+    if (isTouchUI.isPresent()) {
+      return isTouchUI.get();
+    }
+    return context.getResources().getConfiguration().touchscreen
+               != Configuration.TOUCHSCREEN_NOTOUCH;
+  }
+
+  /**
+   * For testing purpose.
+   *
+   * @param isTouchUI Optional.absent() if default behavior is preferable
+   */
+  public static final void setTouchUI(Optional<Boolean> isTouchUI) {
+    MozcUtil.isTouchUI = Preconditions.checkNotNull(isTouchUI);
+  }
 
   public static long getUptimeMillis() {
     if (uptimeMillis.isPresent()) {
@@ -557,69 +610,102 @@
     window.setAttributes(layoutParams);
   }
 
-  public static Request getRequestForKeyboard(
-      KeyboardSpecificationName specName,
-      Optional<SpecialRomanjiTable> romanjiTable,
-      Optional<SpaceOnAlphanumeric> spaceOnAlphanumeric,
-      Optional<Boolean> isKanaModifierInsensitiveConversion,
-      Optional<CrossingEdgeBehavior> crossingEdgeBehavior,
-      Configuration configuration) {
-    Preconditions.checkNotNull(specName);
-    Preconditions.checkNotNull(romanjiTable);
-    Preconditions.checkNotNull(spaceOnAlphanumeric);
-    Preconditions.checkNotNull(isKanaModifierInsensitiveConversion);
-    Preconditions.checkNotNull(crossingEdgeBehavior);
+  private static Request.Builder getRequestBuilderInternal(
+      KeyboardSpecification specification, Configuration configuration) {
+    return Request.newBuilder()
+        .setKeyboardName(
+            specification.getKeyboardSpecificationName().formattedKeyboardName(configuration))
+        .setSpecialRomanjiTable(specification.getSpecialRomanjiTable())
+        .setSpaceOnAlphanumeric(specification.getSpaceOnAlphanumeric())
+        .setKanaModifierInsensitiveConversion(
+            specification.isKanaModifierInsensitiveConversion())
+        .setCrossingEdgeBehavior(specification.getCrossingEdgeBehavior());
+  }
+
+  private static void setHardwareKeyboardRequest(Request.Builder builder, Resources resources) {
+    builder.setMixedConversion(false)
+        .setZeroQuerySuggestion(false)
+        .setUpdateInputModeFromSurroundingText(true)
+        .setAutoPartialSuggestion(false)
+        .setCandidatePageSize(resources.getInteger(R.integer.floating_candidate_candidate_num));
+  }
+
+  public static void setSoftwareKeyboardRequest(Request.Builder builder) {
+    builder.setMixedConversion(true)
+        .setZeroQuerySuggestion(true)
+        .setUpdateInputModeFromSurroundingText(false)
+        .setAutoPartialSuggestion(true);
+  }
+
+  public static Request.Builder getRequestBuilder(
+      Resources resources, KeyboardSpecification specification, Configuration configuration) {
+    Preconditions.checkNotNull(resources);
+    Preconditions.checkNotNull(specification);
     Preconditions.checkNotNull(configuration);
-    Request.Builder builder = Request.newBuilder();
-    builder.setKeyboardName(specName.formattedKeyboardName(configuration));
-    if (romanjiTable.isPresent()) {
-      builder.setSpecialRomanjiTable(romanjiTable.get());
+    Request.Builder builder = getRequestBuilderInternal(specification, configuration);
+    if (specification.isHardwareKeyboard()) {
+      setHardwareKeyboardRequest(builder, resources);
+    } else {
+      setSoftwareKeyboardRequest(builder);
     }
-    if (spaceOnAlphanumeric.isPresent()) {
-      builder.setSpaceOnAlphanumeric(spaceOnAlphanumeric.get());
-    }
-    if (isKanaModifierInsensitiveConversion.isPresent()) {
-      builder.setKanaModifierInsensitiveConversion(
-          isKanaModifierInsensitiveConversion.get().booleanValue());
-    }
-    if (crossingEdgeBehavior.isPresent()) {
-      builder.setCrossingEdgeBehavior(crossingEdgeBehavior.get());
-    }
-    return builder.build();
+    return builder;
   }
 
   @SuppressLint("InlinedApi")
-  public static boolean isPasswordField(EditorInfo editorInfo) {
-    Preconditions.checkNotNull(editorInfo);
-    int inputType = editorInfo.inputType;
+  public static boolean isPasswordField(int inputType) {
     int inputClass = inputType & InputType.TYPE_MASK_CLASS;
     int inputVariation = inputType & InputType.TYPE_MASK_VARIATION;
-    return inputClass == InputType.TYPE_CLASS_TEXT &&
-        (inputVariation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ||
-        inputVariation == InputType.TYPE_TEXT_VARIATION_PASSWORD ||
-        inputVariation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD);
+    return inputClass == InputType.TYPE_CLASS_TEXT
+        && (inputVariation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
+            || inputVariation == InputType.TYPE_TEXT_VARIATION_PASSWORD
+            || inputVariation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD);
   }
 
   /**
-   * Returns true if the editor accepts microphone input.
+   * Returns true if voice input is preferred by EditorInfo.inputType.
    *
-   * <p>Some editors sends a special record in privateImeOptions. In such situation transition to
-   * Voice IME should be disabled.
+   * <p>Caller should check that voice input is allowed or not by isVoiceInputAllowed().
    */
-  public static boolean isVoiceInputAllowed(EditorInfo editorInfo) {
+  public static boolean isVoiceInputPreferred(EditorInfo editorInfo) {
     Preconditions.checkNotNull(editorInfo);
-    if (editorInfo.privateImeOptions == null) {
-      return true;
+
+    // Check privateImeOptions to ensure the text field supports voice input.
+    if (editorInfo.privateImeOptions != null) {
+      for (String option : editorInfo.privateImeOptions.split(",")) {
+        if (option.equals(IME_OPTION_NO_MICROPHONE)
+            || option.equals(IME_OPTION_NO_MICROPHONE_COMPAT)) {
+          return false;
+        }
+      }
     }
-    for (String option : editorInfo.privateImeOptions.split(",")) {
-      if (option.equals(IME_OPTION_NO_MICROPHONE) ||
-          option.equals(IME_OPTION_NO_MICROPHONE_COMPAT)) {
-        return false;
+
+    int inputType = editorInfo.inputType;
+    if (isNumberKeyboardPreferred(inputType) || isPasswordField(inputType)) {
+      return false;
+    }
+    if ((inputType & EditorInfo.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
+      switch (inputType & EditorInfo.TYPE_MASK_VARIATION) {
+        case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS:
+        case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS:
+        case InputType.TYPE_TEXT_VARIATION_URI:
+          return false;
+        default:
+          break;
       }
     }
     return true;
   }
 
+  /** Returns true if number keyboard is preferred by EditorInfo.inputType. */
+  public static boolean isNumberKeyboardPreferred(int inputType) {
+    int typeClass = inputType & InputType.TYPE_MASK_CLASS;
+    // As of API Level 21, following condition equals to "typeClass != InputType.TYPE_CLASS_TEXT".
+    // However type-class might be added in future so safer expression is employed here.
+    return typeClass == InputType.TYPE_CLASS_DATETIME
+        || typeClass == InputType.TYPE_CLASS_NUMBER
+        || typeClass == InputType.TYPE_CLASS_PHONE;
+  }
+
   public static String utf8CStyleByteStringToString(ByteString value) {
     Preconditions.checkNotNull(value);
     // Find '\0' terminator. (if value doesn't contain '\0', the size should be as same as
diff --git a/src/android/src/com/google/android/inputmethod/japanese/MozcView.java b/src/android/src/com/google/android/inputmethod/japanese/MozcView.java
index 5bce6c6..e59f119 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/MozcView.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/MozcView.java
@@ -29,28 +29,33 @@
 
 package org.mozc.android.inputmethod.japanese;
 
+import org.mozc.android.inputmethod.japanese.CandidateViewManager.KeyboardCandidateViewHeightListener;
 import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent;
 import org.mozc.android.inputmethod.japanese.LayoutParamsAnimator.InterpolationListener;
 import org.mozc.android.inputmethod.japanese.ViewManagerInterface.LayoutAdjustment;
 import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType;
-import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode;
 import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyEventHandler;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard;
+import org.mozc.android.inputmethod.japanese.keyboard.KeyboardView;
 import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage;
+import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
-import org.mozc.android.inputmethod.japanese.resources.R;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Output;
 import org.mozc.android.inputmethod.japanese.ui.SideFrameStubProxy;
-import org.mozc.android.inputmethod.japanese.view.MozcDrawableFactory;
-import org.mozc.android.inputmethod.japanese.view.RoundRectKeyDrawable;
-import org.mozc.android.inputmethod.japanese.view.SkinType;
+import org.mozc.android.inputmethod.japanese.view.MozcImageView;
+import org.mozc.android.inputmethod.japanese.view.Skin;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.inputmethodservice.InputMethodService.Insets;
 import android.os.Handler;
@@ -63,48 +68,50 @@
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
-import android.view.animation.AnimationSet;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.view.animation.TranslateAnimation;
+import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
 import android.widget.CompoundButton;
 import android.widget.FrameLayout;
-import android.widget.ImageView;
 import android.widget.LinearLayout;
 
 import java.util.Collections;
 import java.util.EnumSet;
 
+import javax.annotation.Nullable;
+
 /**
  * Root {@code View} of the MechaMozc.
  * It is expected that instance methods are used after inflation is done.
  *
  */
-public class MozcView extends LinearLayout implements MemoryManageable {
+public class MozcView extends FrameLayout implements MemoryManageable {
 
+  @VisibleForTesting
   static class DimensionPixelSize {
     final int imeWindowPartialWidth;
     final int imeWindowRegionInsetThreshold;
     final int narrowFrameHeight;
     final int narrowImeWindowHeight;
     final int sideFrameWidth;
-    final int translucentBorderHeight;
+    final int buttonFrameHeight;
     public DimensionPixelSize(Resources resources) {
       imeWindowPartialWidth = resources.getDimensionPixelSize(R.dimen.ime_window_partial_width);
       imeWindowRegionInsetThreshold = resources.getDimensionPixelSize(
           R.dimen.ime_window_region_inset_threshold);
       narrowFrameHeight = resources.getDimensionPixelSize(R.dimen.narrow_frame_height);
       narrowImeWindowHeight = resources.getDimensionPixelSize(R.dimen.narrow_ime_window_height);
-      translucentBorderHeight = resources.getDimensionPixelSize(
-          R.dimen.translucent_border_height);
       sideFrameWidth = resources.getDimensionPixelSize(R.dimen.side_frame_width);
+      buttonFrameHeight = resources.getDimensionPixelSize(R.dimen.button_frame_height);
     }
   }
 
+  @VisibleForTesting
   static class HeightLinearInterpolationListener implements InterpolationListener {
-    @VisibleForTesting final int fromHeight;
-    @VisibleForTesting final int toHeight;
+    final int fromHeight;
+    final int toHeight;
 
     public HeightLinearInterpolationListener(int fromHeight, int toHeight) {
       this.fromHeight = fromHeight;
@@ -121,20 +128,20 @@
 
   // TODO(hidehiko): Refactor CandidateViewListener along with View structure refactoring.
   class InputFrameFoldButtonClickListener implements OnClickListener {
-    private final ViewEventListener eventListener;
     private final View keyboardView;
+    private final int originalHeight;
     private final long foldDuration;
     private final Interpolator foldKeyboardViewInterpolator;
     private final long expandDuration;
     private final Interpolator expandKeyboardViewInterpolator;
     private final LayoutParamsAnimator layoutParamsAnimator;
     InputFrameFoldButtonClickListener(
-        ViewEventListener eventListener, View keyboardView,
+        View keyboardView, int originalHeight,
         long foldDuration, Interpolator foldKeyboardViewInterpolator,
         long expandDuration, Interpolator expandKeyboardViewInterpolator,
         LayoutParamsAnimator layoutParamsAnimator) {
-      this.eventListener = eventListener;
       this.keyboardView = keyboardView;
+      this.originalHeight = originalHeight;
       this.foldDuration = foldDuration;
       this.foldKeyboardViewInterpolator = foldKeyboardViewInterpolator;
       this.expandDuration = expandDuration;
@@ -144,30 +151,45 @@
 
     @Override
     public void onClick(View v) {
-      if (keyboardView.getHeight() == getInputFrameHeight()) {
-        eventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_FOLD);
+      if (keyboardView.getHeight() == originalHeight) {
+        if (viewEventListener != null) {
+          viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_FOLD);
+        }
         layoutParamsAnimator.startAnimation(
             keyboardView,
             new HeightLinearInterpolationListener(keyboardView.getHeight(), 0),
             foldKeyboardViewInterpolator, foldDuration, 0);
         CompoundButton.class.cast(v).setChecked(true);
       } else {
-        eventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_EXPAND);
+        if (viewEventListener != null) {
+          viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_EXPAND);
+        }
         layoutParamsAnimator.startAnimation(
             keyboardView,
-            new HeightLinearInterpolationListener(keyboardView.getHeight(), getInputFrameHeight()),
+            new HeightLinearInterpolationListener(keyboardView.getHeight(), originalHeight),
             expandKeyboardViewInterpolator, expandDuration, 0);
         CompoundButton.class.cast(v).setChecked(false);
       }
     }
   }
 
-  // TODO(hidehiko): Move hard coded parameters to dimens.xml or skin.
-  private static final float NARROW_MODE_BUTTON_CORNOR_RADIUS = 3.5f;  // in dip.
-  private static final float NARROW_MODE_BUTTON_LEFT_OFFSET = 2.0f;
-  private static final float NARROW_MODE_BUTTON_TOP_OFFSET = 1.0f;
-  private static final float NARROW_MODE_BUTTON_RIGHT_OFFSET = 2.0f;
-  private static final float NARROW_MODE_BUTTON_BOTTOM_OFFSET = 3.0f;
+  /** Manages background view height. */
+  @VisibleForTesting
+  class SoftwareKeyboardHeightListener implements KeyboardCandidateViewHeightListener {
+    @Override
+    public void onExpanded() {
+      if (!isNarrowMode()) {
+        changeBottomBackgroundHeight(imeWindowHeight);
+      }
+    }
+
+    @Override
+    public void onCollapse() {
+      if (!isNarrowMode() && getSymbolInputView().getVisibility() != VISIBLE) {
+        resetBottomBackgroundHeight();
+      }
+    }
+  }
 
   @VisibleForTesting
   final InOutAnimatedFrameLayout.VisibilityChangeListener onVisibilityChangeListener =
@@ -181,23 +203,22 @@
   private final DimensionPixelSize dimensionPixelSize = new DimensionPixelSize(getResources());
   private final SideFrameStubProxy leftFrameStubProxy = new SideFrameStubProxy();
   private final SideFrameStubProxy rightFrameStubProxy = new SideFrameStubProxy();
-  private final MozcDrawableFactory mozcDrawableFactory = new MozcDrawableFactory(getResources());
 
+  @VisibleForTesting ViewEventListener viewEventListener;
   @VisibleForTesting boolean fullscreenMode = false;
-  boolean narrowMode = false;
-  private SkinType skinType = SkinType.ORANGE_LIGHTGRAY;
+  @VisibleForTesting boolean narrowMode = false;
+  private boolean buttonFrameVisible = true;
+  private Skin skin = Skin.getFallbackInstance();
   @VisibleForTesting LayoutAdjustment layoutAdjustment = LayoutAdjustment.FILL;
   private int inputFrameHeight = 0;
   @VisibleForTesting int imeWindowHeight = 0;
-  @VisibleForTesting Animation candidateViewInAnimation;
-  @VisibleForTesting Animation candidateViewOutAnimation;
+  @VisibleForTesting int symbolInputViewHeight = 0;
   @VisibleForTesting Animation symbolInputViewInAnimation;
   @VisibleForTesting Animation symbolInputViewOutAnimation;
-  @VisibleForTesting Animation dropShadowCandidateViewInAnimation;
-  @VisibleForTesting Animation dropShadowCandidateViewOutAnimation;
-  @VisibleForTesting Animation dropShadowSymbolInputViewInAnimation;
-  @VisibleForTesting Animation dropShadowSymbolInputViewOutAnimation;
-  @VisibleForTesting boolean isDropShadowExpanded = false;
+  @VisibleForTesting SoftwareKeyboardHeightListener softwareKeyboardHeightListener =
+      new SoftwareKeyboardHeightListener();
+  @VisibleForTesting CandidateViewManager candidateViewManager;
+  @VisibleForTesting boolean allowFloatingCandidateMode;
 
   public MozcView(Context context) {
     super(context);
@@ -207,82 +228,29 @@
     super(context, attrSet);
   }
 
-  private static Drawable createButtonBackgroundDrawable(float density) {
-    return BackgroundDrawableFactory.createPressableDrawable(
-        new RoundRectKeyDrawable(
-            (int) (NARROW_MODE_BUTTON_LEFT_OFFSET * density),
-            (int) (NARROW_MODE_BUTTON_TOP_OFFSET * density),
-            (int) (NARROW_MODE_BUTTON_RIGHT_OFFSET * density),
-            (int) (NARROW_MODE_BUTTON_BOTTOM_OFFSET * density),
-            (int) (NARROW_MODE_BUTTON_CORNOR_RADIUS * density),
-            0xFFE9E4E4, 0xFFB2ADAD, 0, 0xFF1E1E1E),
-        new RoundRectKeyDrawable(
-            (int) (NARROW_MODE_BUTTON_LEFT_OFFSET * density),
-            (int) (NARROW_MODE_BUTTON_TOP_OFFSET * density),
-            (int) (NARROW_MODE_BUTTON_RIGHT_OFFSET * density),
-            (int) (NARROW_MODE_BUTTON_BOTTOM_OFFSET * density),
-            (int) (NARROW_MODE_BUTTON_CORNOR_RADIUS * density),
-            0xFF858087, 0xFF67645F, 0, 0xFF1E1E1E));
-  }
-
-  @SuppressWarnings("deprecation")
-  private void setupImageButton(ImageView view, int resourceID) {
-    float density = getResources().getDisplayMetrics().density;
-    view.setImageDrawable(mozcDrawableFactory.getDrawable(resourceID).orNull());
-    view.setBackgroundDrawable(createButtonBackgroundDrawable(density));
-    view.setPadding(0, 0, 0, 0);
-  }
-
   private static Animation createAlphaAnimation(float fromAlpha, float toAlpha, long duration) {
     AlphaAnimation animation = new AlphaAnimation(fromAlpha, toAlpha);
     animation.setDuration(duration);
     return animation;
   }
 
-  private static Animation createCandidateViewTransitionAnimation(int fromY, int toY,
-                                                                  float fromAlpha, float toAlpha,
-                                                                  long duration) {
-    AnimationSet animation = new AnimationSet(false);
-    animation.setDuration(duration);
-
-    AlphaAnimation alphaAnimation = new AlphaAnimation(fromAlpha, toAlpha);
-    alphaAnimation.setDuration(duration);
-    animation.addAnimation(alphaAnimation);
-
-    TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, fromY, toY);
-    translateAnimation.setInterpolator(new DecelerateInterpolator());
-    translateAnimation.setDuration(duration);
-    animation.addAnimation(translateAnimation);
-    return animation;
-  }
-
   @Override
   public void onFinishInflate() {
     setKeyboardHeightRatio(100);
 
-    setupImageButton(getWidenButton(), R.raw.hardware__function__close);
-    setupImageButton(getHardwareCompositionButton(), R.raw.qwerty__function__kana__icon);
-
     leftFrameStubProxy.initialize(this,
-                                  R.id.stub_left_frame, R.id.dropshadow_left_short_top,
-                                  R.id.dropshadow_left_long_top, R.id.left_adjust_button,
-                                  R.raw.adjust_arrow_left, 1.0f, R.id.left_dropshadow_short,
-                                  R.id.left_dropshadow_long);
+                                  R.id.stub_left_frame, R.id.left_adjust_button,
+                                  R.raw.adjust_arrow_left);
     rightFrameStubProxy.initialize(this,
-                                   R.id.stub_right_frame, R.id.dropshadow_right_short_top,
-                                   R.id.dropshadow_right_long_top, R.id.right_adjust_button,
-                                   R.raw.adjust_arrow_right, 0.0f, R.id.right_dropshadow_short,
-                                   R.id.right_dropshadow_long);
+                                   R.id.stub_right_frame, R.id.right_adjust_button,
+                                   R.raw.adjust_arrow_right);
+
+    candidateViewManager = new CandidateViewManager(
+        getKeyboardCandidateView(),
+        FloatingCandidateView.class.cast(findViewById(R.id.floating_candidate_view)));
   }
 
-  public void setEventListener(final ViewEventListener viewEventListener,
-                               OnClickListener widenButtonClickListener,
-                               OnClickListener leftAdjustButtonClickListener,
-                               OnClickListener rightAdjustButtonClickListener) {
-    checkInflated();
-
-    // Propagate the given listener into the child views.
-    // Set CandidateViewListener as well here, because it uses viewEventListener.
+  private InputFrameFoldButtonClickListener createFoldButtonListener(View view, int height) {
     Resources resources = getResources();
     int foldOvershootDurationRate =
         resources.getInteger(R.integer.input_frame_fold_overshoot_duration_rate);
@@ -292,50 +260,60 @@
         resources.getInteger(R.integer.input_frame_expand_overshoot_duration_rate);
     int expandOvershootRate =
         resources.getInteger(R.integer.input_frame_expand_overshoot_rate);
-    getCandidateView().setViewEventListener(
-        viewEventListener,
-        new InputFrameFoldButtonClickListener(
-            viewEventListener, getKeyboardFrame(),
-            resources.getInteger(R.integer.input_frame_fold_duration),
-            SequentialInterpolator.newBuilder()
-                .add(new DecelerateInterpolator(),
-                    foldOvershootDurationRate, -foldOvershootRate / 1e6f)
-                .add(new AccelerateInterpolator(), 1e6f - foldOvershootDurationRate, 1)
-                .build(),
-            resources.getInteger(R.integer.input_frame_expand_duration),
-            SequentialInterpolator.newBuilder()
-                .add(new DecelerateInterpolator(),
-                    expandOvershootDurationRate, 1 + expandOvershootRate / 1e6f)
-                .add(new AccelerateDecelerateInterpolator(), 1e6f - expandOvershootDurationRate, 1)
-                .build(),
-        new LayoutParamsAnimator(new Handler(Looper.myLooper()))));
 
-    getSymbolInputView().setViewEventListener(
+    return new InputFrameFoldButtonClickListener(
+        view, height, resources.getInteger(R.integer.input_frame_fold_duration),
+        SequentialInterpolator.newBuilder()
+            .add(new DecelerateInterpolator(),
+                foldOvershootDurationRate, -foldOvershootRate / 1e6f)
+            .add(new AccelerateInterpolator(), 1e6f - foldOvershootDurationRate, 1)
+            .build(),
+        resources.getInteger(R.integer.input_frame_expand_duration),
+        SequentialInterpolator.newBuilder()
+            .add(new DecelerateInterpolator(),
+                expandOvershootDurationRate, 1 + expandOvershootRate / 1e6f)
+            .add(new AccelerateDecelerateInterpolator(), 1e6f - expandOvershootDurationRate, 1)
+            .build(),
+        new LayoutParamsAnimator(new Handler(Looper.myLooper())));
+  }
+
+  public void setEventListener(final ViewEventListener viewEventListener,
+                               OnClickListener widenButtonClickListener,
+                               OnClickListener leftAdjustButtonClickListener,
+                               OnClickListener rightAdjustButtonClickListener,
+                               OnClickListener microphoneButtonClickListener) {
+    Preconditions.checkNotNull(viewEventListener);
+    Preconditions.checkNotNull(widenButtonClickListener);
+    Preconditions.checkNotNull(leftAdjustButtonClickListener);
+    Preconditions.checkNotNull(rightAdjustButtonClickListener);
+    Preconditions.checkNotNull(microphoneButtonClickListener);
+
+    checkInflated();
+
+    this.viewEventListener = viewEventListener;
+
+    // Propagate the given listener into the child views.
+    // Set CandidateViewListener as well here, because it uses viewEventListener.
+    candidateViewManager.setEventListener(viewEventListener, softwareKeyboardHeightListener);
+
+    getSymbolInputView().setEventListener(
         viewEventListener,
-        /**
-         * Click handler of the close button.
-         */
+        /** Click handler of the close button. */
         new OnClickListener() {
           @Override
           public void onClick(View v) {
             if (viewEventListener != null) {
-              viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_FOLD);
+              viewEventListener.onFireFeedbackEvent(FeedbackEvent.SYMBOL_INPUTVIEW_CLOSED);
             }
-            startSymbolInputViewOutAnimation();
+            hideSymbolInputView();
           }
-        });
+        },
+        microphoneButtonClickListener);
 
-    getHardwareCompositionButton().setOnClickListener(new OnClickListener() {
-      @Override
-      public void onClick(View v) {
-        viewEventListener.onHardwareKeyboardCompositionModeChange(CompositionSwitchMode.TOGGLE);
-      }
-    });
-
-    getWidenButton().setOnClickListener(widenButtonClickListener);
-
+    getNarrowFrame().setEventListener(viewEventListener, widenButtonClickListener);
     leftFrameStubProxy.setButtonOnClickListener(leftAdjustButtonClickListener);
     rightFrameStubProxy.setButtonOnClickListener(rightAdjustButtonClickListener);
+    getMicrophoneButton().setOnClickListener(microphoneButtonClickListener);
   }
 
   public void setKeyEventHandler(KeyEventHandler keyEventHandler) {
@@ -347,14 +325,17 @@
   }
 
   // TODO(hidehiko): Probably we'd like to remove this method when we decide to move MVC model.
-  public JapaneseKeyboard getJapaneseKeyboard() {
+  @Nullable public Keyboard getKeyboard() {
     checkInflated();
-    return getKeyboardView().getJapaneseKeyboard();
+    return getKeyboardView().getKeyboard().orNull();
   }
 
-  public void setJapaneseKeyboard(JapaneseKeyboard keyboard) {
+  public void setKeyboard(Keyboard keyboard) {
     checkInflated();
-    getKeyboardView().setJapaneseKeyboard(keyboard);
+    getKeyboardView().setKeyboard(keyboard);
+    CompositionMode compositionMode = keyboard.getSpecification().getCompositionMode();
+    getNarrowFrame().setHardwareCompositionButtonImage(compositionMode);
+    candidateViewManager.setHardwareCompositionMode(compositionMode);
   }
 
   public void setEmojiEnabled(boolean unicodeEmojiEnabled, boolean carrierEmojiEnabled) {
@@ -371,6 +352,7 @@
   public void setEditorInfo(EditorInfo editorInfo) {
     checkInflated();
     getKeyboardView().setEditorInfo(editorInfo);
+    candidateViewManager.setEditorInfo(editorInfo);
   }
 
   public void setFlickSensitivity(int flickSensitivity) {
@@ -393,6 +375,7 @@
   public void setPopupEnabled(boolean popupEnabled) {
     checkInflated();
     getKeyboardView().setPopupEnabled(popupEnabled);
+    getSymbolInputView().setPopupEnabled(popupEnabled);
   }
 
   public boolean isPopupEnabled() {
@@ -400,16 +383,28 @@
     return getKeyboardView().isPopupEnabled();
   }
 
-  public void setSkinType(SkinType skinType) {
+  @SuppressWarnings("deprecation")
+  public void setSkin(Skin skin) {
+    Preconditions.checkNotNull(skin);
     checkInflated();
-    this.skinType = skinType;
-    getKeyboardView().setSkinType(skinType);
-    getSymbolInputView().setSkinType(skinType);
-    getCandidateView().setSkinType(skinType);
+    this.skin = skin;
+    getKeyboardView().setSkin(skin);
+    getSymbolInputView().setSkin(skin);
+    candidateViewManager.setSkin(skin);
+    getMicrophoneButton().setBackgroundDrawable(BackgroundDrawableFactory.createPressableDrawable(
+        new ColorDrawable(skin.buttonFrameButtonPressedColor), Optional.<Drawable>absent()));
+    getMicrophoneButton().setSkin(skin);
+    leftFrameStubProxy.setSkin(skin);
+    rightFrameStubProxy.setSkin(skin);
+    getButtonFrame().setBackgroundDrawable(
+        skin.buttonFrameBackgroundDrawable.getConstantState().newDrawable());
+    getNarrowFrame().setSkin(skin);
+    getKeyboardFrameSeparator().setBackgroundDrawable(
+        skin.keyboardFrameSeparatorBackgroundDrawable.getConstantState().newDrawable());
   }
 
-  public SkinType getSkinType() {
-    return skinType;
+  public Skin getSkin() {
+    return skin;
   }
 
   /**
@@ -417,6 +412,7 @@
    * or do nothing otherwise.
    * Exposed as a package private method for testing purpose.
    */
+  @VisibleForTesting
   void checkInflated() {
     if (getChildCount() == 0) {
       throw new IllegalStateException("It is necessary to inflate mozc_view.xml");
@@ -425,22 +421,36 @@
 
   public void setCommand(Command outCommand) {
     checkInflated();
+    candidateViewManager.update(outCommand);
+    updateMetaStatesBasedOnOutput(outCommand.getOutput());
+  }
 
-    CandidateView candidateView = getCandidateView();
-    if (outCommand.getOutput().getAllCandidateWords().getCandidatesCount() > 0) {
-      // Call CandidateView#update only if there are some candidates in the output.
-      // In such case the candidate view will clear its canvas.
-      candidateView.update(outCommand);
-      startCandidateViewInAnimation();
+  // Update COMPOSING metastate.
+  @VisibleForTesting
+  void updateMetaStatesBasedOnOutput(Output output) {
+    Preconditions.checkNotNull(output);
 
+    boolean hasPreedit = output.hasPreedit()
+        && output.getPreedit().getSegmentCount() > 0;
+    if (hasPreedit) {
+      getKeyboardView().updateMetaStates(EnumSet.of(MetaState.COMPOSING),
+                                         Collections.<MetaState>emptySet());
     } else {
-      // We don't call update method here, because it will clear the view's contents during the
-      // animation.
-      // TODO(hidehiko): Clear the candidates when the animation is finished.
-      startCandidateViewOutAnimation();
+      getKeyboardView().updateMetaStates(Collections.<MetaState>emptySet(),
+                                         EnumSet.of(MetaState.COMPOSING));
     }
   }
 
+  @TargetApi(21)
+  public void setCursorAnchorInfo(CursorAnchorInfo info) {
+    candidateViewManager.setCursorAnchorInfo(info);
+  }
+
+  public void setCursorAnchorInfoEnabled(boolean enabled) {
+    allowFloatingCandidateMode = enabled;
+    candidateViewManager.setAllowFloatingMode(enabled);
+  }
+
   public void reset() {
     checkInflated();
 
@@ -449,24 +459,21 @@
     resetKeyboardViewState();
 
     // Reset candidate view.
-    CandidateView candidateView = getCandidateView();
-    candidateView.clearAnimation();
-    candidateView.setVisibility(View.GONE);
-    candidateView.reset();
+    candidateViewManager.reset();
 
     // Reset symbol input view visibility. Set Visibility directly (without animation).
     SymbolInputView symbolInputView = getSymbolInputView();
     symbolInputView.clearAnimation();
     symbolInputView.setVisibility(View.GONE);
 
-    // Reset *all* metastates.
+    // Reset *all* metastates (and set NO_GLOBE as default value).
     // Expecting metastates will be set next initialization.
-    getKeyboardView().updateMetaStates(Collections.<MetaState>emptySet(),
+    getKeyboardView().updateMetaStates(EnumSet.of(MetaState.NO_GLOBE),
                                        EnumSet.allOf(MetaState.class));
 
     resetFullscreenMode();
     setLayoutAdjustmentAndNarrowMode(layoutAdjustment, narrowMode);
-    collapseDropShadowAndBackground();
+    resetBottomBackgroundHeight();
     updateBackgroundColor();
   }
 
@@ -477,29 +484,38 @@
       return;
     }
 
-    View keyboardFrame = getKeyboardFrame();
+    SymbolInputView symbolInputView = getSymbolInputView();
+    View keyboardFrame;
+    int keyboardFrameHeight;
+    if (symbolInputView.isInflated() && symbolInputView.getVisibility() == View.VISIBLE) {
+      keyboardFrame = getNumberKeyboardFrame();
+      keyboardFrameHeight = symbolInputView.getNumberKeyboardHeight();
+    } else {
+      keyboardFrame = getKeyboardFrame();
+      keyboardFrameHeight = getInputFrameHeight();
+    }
+
     keyboardFrame.setVisibility(View.VISIBLE);
 
     // The height may be changed so reset it here.
     ViewGroup.LayoutParams layoutParams = keyboardFrame.getLayoutParams();
-    int keyboardFrameHeight = getInputFrameHeight();
     if (layoutParams.height != keyboardFrameHeight) {
       layoutParams.height = keyboardFrameHeight;
       keyboardFrame.setLayoutParams(layoutParams);
 
       // Also reset the state of the folding button, which is "conceptually" a part of
       // the keyboard.
-      getCandidateView().setInputFrameFoldButtonChecked(false);
+      candidateViewManager.setInputFrameFoldButtonChecked(false);
     }
   }
 
   public void resetKeyboardViewState() {
     checkInflated();
-
     getKeyboardView().resetState();
   }
 
-  public boolean showSymbolInputView() {
+  public boolean showSymbolInputView(Optional<SymbolMajorCategory> category) {
+    Preconditions.checkNotNull(category);
     checkInflated();
 
     SymbolInputView view = getSymbolInputView();
@@ -509,10 +525,17 @@
 
     if (!view.isInflated()) {
       view.inflateSelf();
+      CandidateView numberCandidateView =
+          CandidateView.class.cast(view.findViewById(R.id.candidate_view_in_symbol_view));
+      numberCandidateView.setInputFrameFoldButtonOnClickListener(createFoldButtonListener(
+          getNumberKeyboardFrame(), view.getNumberKeyboardHeight()));
+      candidateViewManager.setNumberCandidateView(numberCandidateView);
     }
 
-    view.reset();
+    view.resetToMajorCategory(category);
     startSymbolInputViewInAnimation();
+    candidateViewManager.setNumberMode(true);
+
     return true;
   }
 
@@ -524,41 +547,51 @@
       return false;
     }
 
+    candidateViewManager.setNumberMode(false);
     startSymbolInputViewOutAnimation();
     return true;
   }
 
+  private int getButtonFrameHeightIfVisible() {
+    return buttonFrameVisible ? dimensionPixelSize.buttonFrameHeight : 0;
+  }
+
   /**
    * Decides input frame height in not fullscreen mode.
    */
-  public int getVisibleViewHeight() {
+  @VisibleForTesting
+  int getVisibleViewHeight() {
     checkInflated();
 
+    boolean isSymbolInputViewVisible = getSymbolInputView().getVisibility() == View.VISIBLE;
     // Means only software keyboard or narrow frame
-    boolean isDefaultView = getCandidateView().getVisibility() != View.VISIBLE
-        && getSymbolInputView().getVisibility() != View.VISIBLE;
+    boolean isDefaultView =
+        !candidateViewManager.isKeyboardCandidateViewVisible() && !isSymbolInputViewVisible;
 
     if (narrowMode) {
       if (isDefaultView) {
         return dimensionPixelSize.narrowFrameHeight;
       } else {
-        return dimensionPixelSize.narrowImeWindowHeight
-            - dimensionPixelSize.translucentBorderHeight;
+        return dimensionPixelSize.narrowImeWindowHeight;
       }
     } else {
       if (isDefaultView) {
-        return getInputFrameHeight();
+        return getInputFrameHeight() + getButtonFrameHeightIfVisible();
       } else {
-        return imeWindowHeight - dimensionPixelSize.translucentBorderHeight;
+        if (isSymbolInputViewVisible) {
+          return symbolInputViewHeight;
+        } else {
+          return imeWindowHeight;
+        }
       }
     }
   }
 
+  @VisibleForTesting
   void updateInputFrameHeight() {
     // input_frame's height depends on fullscreen mode, narrow mode and Candidate/Symbol views.
     if (fullscreenMode) {
-      setLayoutHeight(getBottomFrame(), getVisibleViewHeight()
-          + dimensionPixelSize.translucentBorderHeight);
+      setLayoutHeight(getBottomFrame(), getVisibleViewHeight());
       setLayoutHeight(getKeyboardFrame(), getInputFrameHeight());
     } else {
       if (narrowMode) {
@@ -570,6 +603,7 @@
     }
   }
 
+  @VisibleForTesting
   int getSideAdjustedWidth() {
     return dimensionPixelSize.imeWindowPartialWidth + dimensionPixelSize.sideFrameWidth;
   }
@@ -582,27 +616,29 @@
     return fullscreenMode;
   }
 
+  @VisibleForTesting
   void resetFullscreenMode() {
     if (fullscreenMode) {
       // In fullscreen mode, InputMethodService shows extract view which height is 0 and
       // weight is 0. So our MozcView height should be fixed.
       // If CandidateView or SymbolInputView appears, MozcView height is enlarged to fix them.
-      setLayoutHeight(getOverlayView(), 0);
+      getOverlayView().setVisibility(View.GONE);
       setLayoutHeight(getTextInputFrame(), LayoutParams.WRAP_CONTENT);
-      getCandidateView().setOnVisibilityChangeListener(onVisibilityChangeListener);
+      candidateViewManager.setOnVisibilityChangeListener(Optional.of(onVisibilityChangeListener));
       getSymbolInputView().setOnVisibilityChangeListener(onVisibilityChangeListener);
     } else {
-      setLayoutHeight(getOverlayView(), LayoutParams.MATCH_PARENT);
+      getOverlayView().setVisibility(View.VISIBLE);
       setLayoutHeight(getTextInputFrame(), LayoutParams.MATCH_PARENT);
-      getCandidateView().setOnVisibilityChangeListener(null);
+      candidateViewManager.setOnVisibilityChangeListener(
+          Optional.<InOutAnimatedFrameLayout.VisibilityChangeListener>absent());
       getSymbolInputView().setOnVisibilityChangeListener(null);
     }
-
+    candidateViewManager.setExtractedMode(fullscreenMode);
     updateInputFrameHeight();
     updateBackgroundColor();
   }
 
-  static void setLayoutHeight(View view, int height) {
+  private static void setLayoutHeight(View view, int height) {
     ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
     layoutParams.height = height;
     view.setLayoutParams(layoutParams);
@@ -612,17 +648,8 @@
     return narrowMode;
   }
 
-  public void setHardwareCompositionButtonImage(CompositionMode compositionMode) {
-    switch (compositionMode) {
-      case HIRAGANA:
-        getHardwareCompositionButton().setImageDrawable(
-            mozcDrawableFactory.getDrawable(R.raw.qwerty__function__kana__icon).orNull());
-        break;
-      default:
-        getHardwareCompositionButton().setImageDrawable(
-            mozcDrawableFactory.getDrawable(R.raw.qwerty__function__alphabet__icon).orNull());
-        break;
-    }
+  public boolean isFloatingCandidateMode() {
+    return candidateViewManager.isFloatingMode();
   }
 
   public Rect getKeyboardSize() {
@@ -678,17 +705,18 @@
     float descriptionTextSize = layoutAdjustment == LayoutAdjustment.FILL
         ? resources.getDimension(R.dimen.candidate_description_text_size)
         : resources.getDimension(R.dimen.candidate_description_text_size_aligned_layout);
-    getCandidateView().setCandidateTextDimension(candidateTextSize, descriptionTextSize);
+    candidateViewManager.setCandidateTextDimension(candidateTextSize, descriptionTextSize);
     getSymbolInputView().setCandidateTextDimension(candidateTextSize, descriptionTextSize);
-    getConversionCandidateWordContainerView().setCandidateTextDimension(candidateTextSize);
 
     // In narrow mode, hide software keyboard and show narrow status bar.
-    getCandidateView().setNarrowMode(narrowMode);
+    candidateViewManager.setNarrowMode(narrowMode);
     if (narrowMode) {
       getKeyboardFrame().setVisibility(GONE);
+      getButtonFrame().setVisibility(GONE);
       getNarrowFrame().setVisibility(VISIBLE);
     } else {
       getKeyboardFrame().setVisibility(VISIBLE);
+      getButtonFrame().setVisibility(buttonFrameVisible ? VISIBLE : GONE);
       getNarrowFrame().setVisibility(GONE);
       resetKeyboardFrameVisibility();
     }
@@ -709,12 +737,13 @@
     getForegroundFrame().startAnimation(translateAnimation);
   }
 
+  @VisibleForTesting
   void updateBackgroundColor() {
     // If fullscreenMode, background should not show original window.
     // If narrowMode, it is always full-width.
     // If isFloatingMode, background should be transparent.
-    int resourceId = (fullscreenMode || (!narrowMode && !isFloatingMode())) ?
-        R.color.input_frame_background : 0;
+    int resourceId = (fullscreenMode || (!narrowMode && !isFloatingMode()))
+        ? R.color.input_frame_background : 0;
     getBottomBackground().setBackgroundResource(resourceId);
   }
 
@@ -748,62 +777,29 @@
     return;
   }
 
-  void expandDropShadowAndBackground() {
-    leftFrameStubProxy.flipDropShadowVisibility(INVISIBLE);
-    rightFrameStubProxy.flipDropShadowVisibility(INVISIBLE);
-    getDropShadowTop().setVisibility(VISIBLE);
-    getResources();
-    setLayoutHeight(getBottomBackground(), imeWindowHeight
-        - (fullscreenMode ? 0 : dimensionPixelSize.translucentBorderHeight));
-    isDropShadowExpanded = true;
-  }
-
-  void collapseDropShadowAndBackground() {
-    leftFrameStubProxy.flipDropShadowVisibility(VISIBLE);
-    rightFrameStubProxy.flipDropShadowVisibility(VISIBLE);
-    getDropShadowTop().setVisibility(fullscreenMode ? VISIBLE : INVISIBLE);
-    getResources();
-    setLayoutHeight(getBottomBackground(), getInputFrameHeight()
-        + (fullscreenMode ? dimensionPixelSize.translucentBorderHeight : 0));
-    isDropShadowExpanded = false;
-  }
-
-  void startDropShadowAnimation(Animation mainAnimation, Animation subAnimation) {
-    leftFrameStubProxy.startDropShadowAnimation(subAnimation, mainAnimation);
-    rightFrameStubProxy.startDropShadowAnimation(subAnimation, mainAnimation);
-    getDropShadowTop().startAnimation(mainAnimation);
-  }
-
-  void startCandidateViewInAnimation() {
-    getCandidateView().startInAnimation();
-    if (!isDropShadowExpanded) {
-      expandDropShadowAndBackground();
-      startDropShadowAnimation(candidateViewInAnimation, dropShadowCandidateViewInAnimation);
+  @VisibleForTesting
+  void changeBottomBackgroundHeight(int targetHeight) {
+    if (getBottomBackground().getHeight() != targetHeight) {
+      setLayoutHeight(getBottomBackground(), targetHeight);
     }
   }
 
-  void startCandidateViewOutAnimation() {
-    getCandidateView().startOutAnimation();
-    if (getSymbolInputView().getVisibility() != VISIBLE && isDropShadowExpanded) {
-      collapseDropShadowAndBackground();
-      startDropShadowAnimation(candidateViewOutAnimation, dropShadowCandidateViewOutAnimation);
-    }
+  @VisibleForTesting
+  void resetBottomBackgroundHeight() {
+    setLayoutHeight(getBottomBackground(), getInputFrameHeight() + getButtonFrameHeightIfVisible());
   }
 
+  @VisibleForTesting
   void startSymbolInputViewInAnimation() {
     getSymbolInputView().startInAnimation();
-    if (!isDropShadowExpanded) {
-      expandDropShadowAndBackground();
-      startDropShadowAnimation(symbolInputViewInAnimation, dropShadowSymbolInputViewInAnimation);
-    }
+    changeBottomBackgroundHeight(symbolInputViewHeight);
   }
 
+  @VisibleForTesting
   void startSymbolInputViewOutAnimation() {
     getSymbolInputView().startOutAnimation();
-    if (getCandidateView().getVisibility() != VISIBLE && isDropShadowExpanded) {
-      collapseDropShadowAndBackground();
-      startDropShadowAnimation(symbolInputViewOutAnimation,
-                               dropShadowSymbolInputViewOutAnimation);
+    if (!candidateViewManager.isKeyboardCandidateViewVisible()) {
+      resetBottomBackgroundHeight();
     }
   }
 
@@ -811,52 +807,34 @@
    * Reset components depending inputFrameHeight or imeWindowHeight.
    * This should be called when inputFrameHeight and/or imeWindowHeight are updated.
    */
+  @VisibleForTesting
   void resetHeightDependingComponents() {
-    // Create In/Out animation which dropshadows share between CandidateView and SymbolInputView.
-    {
-      CandidateView candidateView = getCandidateView();
-      int windowHeight = imeWindowHeight;
-      int inputFrameHeight = getInputFrameHeight();
-      int candidateViewHeight = windowHeight - inputFrameHeight;
-      long duration = getResources().getInteger(R.integer.candidate_frame_transition_duration);
-      float fromAlpha = 0.0f;
-      float toAlpha = 1.0f;
+    getKeyboardCandidateView().setInputFrameFoldButtonOnClickListener(
+        createFoldButtonListener(getKeyboardFrame(), getInputFrameHeight()));
 
-      candidateViewInAnimation = createCandidateViewTransitionAnimation(
-          candidateViewHeight, 0, fromAlpha, toAlpha, duration);
-      candidateView.setInAnimation(candidateViewInAnimation);
-      dropShadowCandidateViewInAnimation = createAlphaAnimation(
-          1.0f - fromAlpha, 1.0f - toAlpha, duration);
-
-      candidateViewOutAnimation = createCandidateViewTransitionAnimation(
-          0, candidateViewHeight, toAlpha, fromAlpha, duration);
-      candidateView.setOutAnimation(candidateViewOutAnimation);
-      dropShadowCandidateViewOutAnimation = createAlphaAnimation(
-          1.0f - toAlpha, 1.0f - fromAlpha, duration);
+    if (candidateViewManager != null) {
+      candidateViewManager.resetHeightDependingComponents(
+          getResources(), imeWindowHeight, inputFrameHeight);
     }
 
     SymbolInputView symbolInputView = getSymbolInputView();
     {
-      long duration = getResources().getInteger(R.integer.symbol_input_transition_duration_in);
+      long duration = getResources().getInteger(R.integer.symbol_input_transition_duration);
       float fromAlpha = 0.3f;
       float toAlpha = 1.0f;
 
       symbolInputViewInAnimation = createAlphaAnimation(fromAlpha, toAlpha, duration);
       symbolInputView.setInAnimation(symbolInputViewInAnimation);
-      dropShadowSymbolInputViewInAnimation = createAlphaAnimation(
-          1.0f - fromAlpha, 1.0f - toAlpha, duration);
-
       symbolInputViewOutAnimation = createAlphaAnimation(toAlpha, fromAlpha, duration);
       symbolInputView.setOutAnimation(symbolInputViewOutAnimation);
-      dropShadowSymbolInputViewOutAnimation = createAlphaAnimation(
-          1.0f - toAlpha, 1.0f - fromAlpha, duration);
     }
 
-    // Reset drop shadow height.
-    int shortHeight = getInputFrameHeight() + dimensionPixelSize.translucentBorderHeight;
-    int longHeight = imeWindowHeight;
-    leftFrameStubProxy.setDropShadowHeight(shortHeight, longHeight);
-    rightFrameStubProxy.setDropShadowHeight(shortHeight, longHeight);
+    if (symbolInputView.isInflated()) {
+      CandidateView numberCandidateView = CandidateView.class.cast(
+          symbolInputView.findViewById(R.id.candidate_view_in_symbol_view));
+      numberCandidateView.setInputFrameFoldButtonOnClickListener(createFoldButtonListener(
+          getNumberKeyboardFrame(), symbolInputView.getNumberKeyboardHeight()));
+    }
 
     // Reset side adjust buttons height.
     leftFrameStubProxy.resetAdjustButtonBottomMargin(getInputFrameHeight());
@@ -872,85 +850,112 @@
 
     Resources resources = getResources();
     float heightScale = keyboardHeightRatio * 0.01f;
-    int originalImeWindowHeight = resources.getDimensionPixelSize(R.dimen.ime_window_height);
-    int originalInputFrameHeight = resources.getDimensionPixelSize(R.dimen.input_frame_height);
-    imeWindowHeight = Math.round(originalImeWindowHeight * heightScale);
+    float originalImeWindowHeight = resources.getDimension(R.dimen.ime_window_height);
+    float originalInputFrameHeight = resources.getDimension(R.dimen.input_frame_height);
     inputFrameHeight = Math.round(originalInputFrameHeight * heightScale);
-    // TODO(yoichio): Update SymbolInputView height scale.
-    // getSymbolInputView().setHeightScale(heightScale);
+    int minImeWindowHeight = inputFrameHeight + dimensionPixelSize.buttonFrameHeight;
+    imeWindowHeight =
+        Math.max(Math.round(originalImeWindowHeight * heightScale), minImeWindowHeight);
+    symbolInputViewHeight = Math.min(imeWindowHeight, minImeWindowHeight);
 
     updateInputFrameHeight();
+    getSymbolInputView().setVerticalDimension(symbolInputViewHeight, heightScale);
     resetHeightDependingComponents();
   }
 
-  public int getInputFrameHeight() {
+  @VisibleForTesting
+  int getInputFrameHeight() {
     return inputFrameHeight;
   }
 
-  // Getters of child views.
-  // TODO(hidehiko): Remove (or hide) following methods, in order to split the dependencies to
-  //   those child views from other components.
-  public CandidateView getCandidateView() {
-    return CandidateView.class.cast(findViewById(R.id.candidate_view));
-  }
-
-  public ConversionCandidateWordContainerView getConversionCandidateWordContainerView() {
-    return ConversionCandidateWordContainerView.class.cast(
-        findViewById(R.id.conversion_candidate_word_container_view));
-  }
-
-  public View getKeyboardFrame() {
+  @VisibleForTesting
+  View getKeyboardFrame() {
     return findViewById(R.id.keyboard_frame);
   }
 
-  public JapaneseKeyboardView getKeyboardView() {
-    return JapaneseKeyboardView.class.cast(findViewById(R.id.keyboard_view));
+  View getKeyboardFrameSeparator() {
+    return findViewById(R.id.keyboard_frame_separator);
   }
 
-  public SymbolInputView getSymbolInputView() {
+  private View getNumberKeyboardFrame() {
+    return findViewById(R.id.number_keyboard_frame);
+  }
+
+  @VisibleForTesting
+  KeyboardView getKeyboardView() {
+    return KeyboardView.class.cast(findViewById(R.id.keyboard_view));
+  }
+
+  private CandidateView getKeyboardCandidateView() {
+    return CandidateView.class.cast(findViewById(R.id.candidate_view));
+  }
+
+  @VisibleForTesting
+  SymbolInputView getSymbolInputView() {
     return SymbolInputView.class.cast(findViewById(R.id.symbol_input_view));
   }
 
+  @VisibleForTesting
   View getOverlayView() {
     return findViewById(R.id.overlay_view);
   }
 
+  @VisibleForTesting
   LinearLayout getTextInputFrame() {
     return LinearLayout.class.cast(findViewById(R.id.textinput_frame));
   }
 
-  FrameLayout getNarrowFrame() {
-    return FrameLayout.class.cast(findViewById(R.id.narrow_frame));
+  @VisibleForTesting
+  NarrowFrameView getNarrowFrame() {
+    return NarrowFrameView.class.cast(findViewById(R.id.narrow_frame));
   }
 
-  ImageView getHardwareCompositionButton() {
-    return ImageView.class.cast(findViewById(R.id.hardware_composition_button));
-  }
-
-  ImageView getWidenButton() {
-    return ImageView.class.cast(findViewById(R.id.widen_button));
-  }
-
+  @VisibleForTesting
   View getForegroundFrame() {
     return findViewById(R.id.foreground_frame);
   }
 
-  View getDropShadowTop() {
-    return findViewById(R.id.dropshadow_top);
-  }
-
+  @VisibleForTesting
   View getBottomFrame() {
     return findViewById(R.id.bottom_frame);
   }
 
+  @VisibleForTesting
   View getBottomBackground() {
     return findViewById(R.id.bottom_background);
   }
 
+  @VisibleForTesting
+  View getButtonFrame() {
+    return findViewById(R.id.button_frame);
+  }
+
+  @VisibleForTesting
+  MozcImageView getMicrophoneButton() {
+    return MozcImageView.class.cast(findViewById(R.id.microphone_button));
+  }
+
   @Override
   public void trimMemory() {
     getKeyboardView().trimMemory();
-    getCandidateView().trimMemory();
     getSymbolInputView().trimMemory();
+    candidateViewManager.trimMemory();
+  }
+
+  void setGlobeButtonEnabled(boolean globeButtonEnabled) {
+    getKeyboardView().setGlobeButtonEnabled(globeButtonEnabled);
+  }
+
+  void setMicrophoneButtonEnabled(boolean microphoneButtonEnabled) {
+    if (narrowMode) {
+      buttonFrameVisible = false;
+    } else {
+      buttonFrameVisible = microphoneButtonEnabled;
+      int visibility = buttonFrameVisible ? View.VISIBLE : View.GONE;
+      getButtonFrame().setVisibility(visibility);
+      getMicrophoneButton().setVisibility(visibility);
+      getSymbolInputView().setMicrophoneButtonEnabled(microphoneButtonEnabled);
+      resetBottomBackgroundHeight();
+    }
   }
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/NarrowFrameView.java b/src/android/src/com/google/android/inputmethod/japanese/NarrowFrameView.java
new file mode 100644
index 0000000..8434aa4
--- /dev/null
+++ b/src/android/src/com/google/android/inputmethod/japanese/NarrowFrameView.java
@@ -0,0 +1,170 @@
+// Copyright 2010-2014, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package org.mozc.android.inputmethod.japanese;
+
+import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent;
+import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode;
+import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
+import org.mozc.android.inputmethod.japanese.view.MozcImageView;
+import org.mozc.android.inputmethod.japanese.view.RoundRectKeyDrawable;
+import org.mozc.android.inputmethod.japanese.view.Skin;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+
+/**
+ * Narrow frame view.
+ */
+public class NarrowFrameView extends LinearLayout {
+
+  // TODO(hsumita): Move hard coded parameters to dimens.xml or skin.
+  private static final float BUTTON_CORNOR_RADIUS = 3.5f;  // in dip.
+  private static final float BUTTON_LEFT_OFFSET = 2.0f;
+  private static final float BUTTON_TOP_OFFSET = 2.0f;
+  private static final float BUTTON_RIGHT_OFFSET = 2.0f;
+  private static final float BUTTON_BOTTOM_OFFSET = 2.0f;
+
+  private Skin skin = Skin.getFallbackInstance();
+  private CompositionMode hardwareKeyboardCompositionMode = CompositionMode.HIRAGANA;
+
+  public NarrowFrameView(Context context) {
+    super(context);
+  }
+
+  public NarrowFrameView(Context context, AttributeSet attrSet) {
+    super(context, attrSet);
+  }
+
+  @Override
+  public void onFinishInflate() {
+    // Disable h/w acceleration to use a PictureDrawable.
+    getHardwareCompositionButton().setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+    getWidenButton().setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+
+    setSkin(skin);
+  }
+
+  private static Drawable createButtonBackgroundDrawable(float density, Skin skin) {
+    return BackgroundDrawableFactory.createPressableDrawable(
+        new RoundRectKeyDrawable(
+            (int) (BUTTON_LEFT_OFFSET * density),
+            (int) (BUTTON_TOP_OFFSET * density),
+            (int) (BUTTON_RIGHT_OFFSET * density),
+            (int) (BUTTON_BOTTOM_OFFSET * density),
+            (int) (BUTTON_CORNOR_RADIUS * density),
+            skin.twelvekeysLayoutPressedFunctionKeyTopColor,
+            skin.twelvekeysLayoutPressedFunctionKeyBottomColor,
+            skin.twelvekeysLayoutPressedFunctionKeyHighlightColor,
+            skin.twelvekeysLayoutPressedFunctionKeyShadowColor),
+        Optional.<Drawable>of(new RoundRectKeyDrawable(
+            (int) (BUTTON_LEFT_OFFSET * density),
+            (int) (BUTTON_TOP_OFFSET * density),
+            (int) (BUTTON_RIGHT_OFFSET * density),
+            (int) (BUTTON_BOTTOM_OFFSET * density),
+            (int) (BUTTON_CORNOR_RADIUS * density),
+            skin.twelvekeysLayoutReleasedFunctionKeyTopColor,
+            skin.twelvekeysLayoutReleasedFunctionKeyBottomColor,
+            skin.twelvekeysLayoutReleasedFunctionKeyHighlightColor,
+            skin.twelvekeysLayoutReleasedFunctionKeyShadowColor)));
+  }
+
+  @SuppressWarnings("deprecation")
+  private void setupImageButton(MozcImageView view, int resourceID) {
+    float density = getResources().getDisplayMetrics().density;
+    view.setImageDrawable(skin.getDrawable(getResources(), resourceID));
+    view.setBackgroundDrawable(createButtonBackgroundDrawable(density, skin));
+  }
+
+  private void updateImageButton() {
+    setupImageButton(getWidenButton(), R.raw.hardware__function__close);
+    MozcImageView hardwareCompositionButton = getHardwareCompositionButton();
+    if (hardwareKeyboardCompositionMode == CompositionMode.HIRAGANA) {
+      setupImageButton(hardwareCompositionButton, R.raw.qwerty__function__kana__icon);
+      hardwareCompositionButton.setContentDescription(
+          getResources().getString(R.string.cd_key_chartype_to_abc));
+    } else {
+      setupImageButton(hardwareCompositionButton, R.raw.qwerty__function__alphabet__icon);
+      hardwareCompositionButton.setContentDescription(
+          getResources().getString(R.string.cd_key_chartype_to_kana));
+    }
+  }
+
+  @SuppressWarnings("deprecation")
+  public void setSkin(Skin skin) {
+    this.skin = Preconditions.checkNotNull(skin);
+    setBackgroundDrawable(skin.narrowFrameBackgroundDrawable);
+    getNarrowFrameSeparator().setBackgroundDrawable(
+        skin.keyboardFrameSeparatorBackgroundDrawable.getConstantState().newDrawable());
+    updateImageButton();
+  }
+
+  public void setEventListener(
+      final ViewEventListener viewEventListener, OnClickListener widenButtonClickListener) {
+    Preconditions.checkNotNull(viewEventListener);
+    Preconditions.checkNotNull(widenButtonClickListener);
+
+    getHardwareCompositionButton().setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        viewEventListener.onFireFeedbackEvent(
+            FeedbackEvent.NARROW_FRAME_HARDWARE_COMPOSITION_BUTTON_DOWN);
+        viewEventListener.onHardwareKeyboardCompositionModeChange(CompositionSwitchMode.TOGGLE);
+      }
+    });
+    getWidenButton().setOnClickListener(widenButtonClickListener);
+  }
+
+  public void setHardwareCompositionButtonImage(CompositionMode compositionMode) {
+    this.hardwareKeyboardCompositionMode = Preconditions.checkNotNull(compositionMode);
+    updateImageButton();
+  }
+
+  @VisibleForTesting
+  MozcImageView getHardwareCompositionButton() {
+    return MozcImageView.class.cast(findViewById(R.id.hardware_composition_button));
+  }
+
+  @VisibleForTesting
+  MozcImageView getWidenButton() {
+    return MozcImageView.class.cast(findViewById(R.id.widen_button));
+  }
+
+  @VisibleForTesting
+  View getNarrowFrameSeparator() {
+    return findViewById(R.id.narrow_frame_separator);
+  }
+}
diff --git a/src/android/src/com/google/android/inputmethod/japanese/PrimaryKeyCodeConverter.java b/src/android/src/com/google/android/inputmethod/japanese/PrimaryKeyCodeConverter.java
index aeb8f8f..ed51f35 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/PrimaryKeyCodeConverter.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/PrimaryKeyCodeConverter.java
@@ -30,11 +30,13 @@
 package org.mozc.android.inputmethod.japanese;
 
 import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard;
 import org.mozc.android.inputmethod.japanese.keyboard.ProbableKeyEventGuesser;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.KeyEvent.ProbableKeyEvent;
 import org.mozc.android.inputmethod.japanese.resources.R;
+import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 
 import android.content.Context;
@@ -44,8 +46,6 @@
 
 import java.util.List;
 
-import javax.annotation.Nullable;
-
 /**
  * Converter from primary key code (‚Čí code point) to Mozc's key event.
  *
@@ -60,10 +60,7 @@
   public final int keyCodeBackspace;
   public final int keyCodeEnter;
   public final int keyCodeChartypeToKana;
-  public final int keyCodeChartypeTo123;
   public final int keyCodeChartypeToAbc;
-  public final int keyCodeChartypeToKana123;
-  public final int keyCodeChartypeToAbc123;
   public final int keyCodeSymbol;
   public final int keyCodeUndo;
   public final int keyCodeCapslock;
@@ -90,10 +87,7 @@
     keyCodeBackspace = res.getInteger(R.integer.key_backspace);
     keyCodeEnter = res.getInteger(R.integer.key_enter);
     keyCodeChartypeToKana = res.getInteger(R.integer.key_chartype_to_kana);
-    keyCodeChartypeTo123 = res.getInteger(R.integer.key_chartype_to_123);
     keyCodeChartypeToAbc = res.getInteger(R.integer.key_chartype_to_abc);
-    keyCodeChartypeToKana123 = res.getInteger(R.integer.key_chartype_to_kana_123);
-    keyCodeChartypeToAbc123 = res.getInteger(R.integer.key_chartype_to_abc_123);
     keyCodeSymbol = res.getInteger(R.integer.key_symbol);
     keyCodeUndo = res.getInteger(R.integer.key_undo);
     keyCodeCapslock = res.getInteger(R.integer.key_capslock);
@@ -103,63 +97,63 @@
     this.guesser = Preconditions.checkNotNull(guesser);
   }
 
-  public void setJapaneseKeyboard(@Nullable JapaneseKeyboard japaneseKeyboard) {
-    guesser.setJapaneseKeyboard(japaneseKeyboard);
+  public void setKeyboard(Keyboard keyboard) {
+    guesser.setKeyboard(Preconditions.checkNotNull(keyboard));
   }
 
   public void setConfiguration(Configuration newConfig) {
-    guesser.setConfiguration(Preconditions.checkNotNull(newConfig));
+    guesser.setConfiguration(Optional.of(newConfig));
   }
 
-  public ProtoCommands.KeyEvent createMozcKeyEvent(
-      int primaryCode, List<? extends TouchEvent> touchEventList) {
+  public Optional<ProtoCommands.KeyEvent> createMozcKeyEvent(
+      int primaryCode, List<TouchEvent> touchEventList) {
+    Preconditions.checkNotNull(touchEventList);
+
     // Space
     if (primaryCode == ' ') {
-      return KeycodeConverter.SPECIALKEY_SPACE;
+      return Optional.of(KeycodeConverter.SPECIALKEY_SPACE);
     }
 
     // Enter
     if (primaryCode == keyCodeEnter) {
-      return KeycodeConverter.SPECIALKEY_VIRTUAL_ENTER;
+      return Optional.of(KeycodeConverter.SPECIALKEY_VIRTUAL_ENTER);
     }
 
     // Backspace
     if (primaryCode == keyCodeBackspace) {
-      return KeycodeConverter.SPECIALKEY_BACKSPACE;
+      return Optional.of(KeycodeConverter.SPECIALKEY_BACKSPACE);
     }
 
     // Up arrow.
     if (primaryCode == keyCodeUp) {
-      return KeycodeConverter.SPECIALKEY_UP;
+      return Optional.of(KeycodeConverter.SPECIALKEY_UP);
     }
 
     // Left arrow.
     if (primaryCode == keyCodeLeft) {
-      return KeycodeConverter.SPECIALKEY_VIRTUAL_LEFT;
+      return Optional.of(KeycodeConverter.SPECIALKEY_VIRTUAL_LEFT);
     }
 
     // Right arrow.
     if (primaryCode == keyCodeRight) {
-      return KeycodeConverter.SPECIALKEY_VIRTUAL_RIGHT;
+      return Optional.of(KeycodeConverter.SPECIALKEY_VIRTUAL_RIGHT);
     }
 
     // Down arrow.
     if (primaryCode == keyCodeDown) {
-      return KeycodeConverter.SPECIALKEY_DOWN;
+      return Optional.of(KeycodeConverter.SPECIALKEY_DOWN);
     }
 
     if (primaryCode > 0) {
       ProtoCommands.KeyEvent.Builder builder =
           ProtoCommands.KeyEvent.newBuilder().setKeyCode(primaryCode);
-      if (guesser != null && touchEventList != null) {
+      if (!touchEventList.isEmpty()) {
         List<ProbableKeyEvent> probableKeyEvents = guesser.getProbableKeyEvents(touchEventList);
-        if (probableKeyEvents != null) {
-          builder.addAllProbableKeyEvent(probableKeyEvents);
-        }
+        builder.addAllProbableKeyEvent(probableKeyEvents);
       }
-      return builder.build();
+      return Optional.of(builder.build());
     }
-    return null;
+    return Optional.<ProtoCommands.KeyEvent>absent();
   }
 
   public KeyEventInterface getPrimaryCodeKeyEvent(int primaryCode) {
@@ -268,8 +262,8 @@
     }
 
     @Override
-    public KeyEvent getNativeEvent() {
-      return null;
+    public Optional<KeyEvent> getNativeEvent() {
+      return Optional.<KeyEvent>absent();
     }
   }
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/SymbolInputView.java b/src/android/src/com/google/android/inputmethod/japanese/SymbolInputView.java
index 5dbc0bb..5265cdf 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/SymbolInputView.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/SymbolInputView.java
@@ -34,21 +34,27 @@
 import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory;
 import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyEventHandler;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
+import org.mozc.android.inputmethod.japanese.keyboard.KeyboardActionListener;
+import org.mozc.android.inputmethod.japanese.keyboard.KeyboardFactory;
+import org.mozc.android.inputmethod.japanese.keyboard.KeyboardView;
 import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage;
 import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory;
 import org.mozc.android.inputmethod.japanese.model.SymbolMinorCategory;
 import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateList;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord;
-import org.mozc.android.inputmethod.japanese.resources.R;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent;
 import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer.DescriptionLayoutPolicy;
 import org.mozc.android.inputmethod.japanese.ui.CandidateLayoutRenderer.ValueScalingPolicy;
 import org.mozc.android.inputmethod.japanese.ui.ScrollGuideView;
 import org.mozc.android.inputmethod.japanese.ui.SpanFactory;
 import org.mozc.android.inputmethod.japanese.ui.SymbolCandidateLayouter;
-import org.mozc.android.inputmethod.japanese.view.MozcDrawableFactory;
+import org.mozc.android.inputmethod.japanese.view.MozcImageButton;
+import org.mozc.android.inputmethod.japanese.view.MozcImageView;
 import org.mozc.android.inputmethod.japanese.view.RoundRectKeyDrawable;
-import org.mozc.android.inputmethod.japanese.view.SkinType;
+import org.mozc.android.inputmethod.japanese.view.Skin;
 import org.mozc.android.inputmethod.japanese.view.SymbolMajorCategoryButtonDrawableFactory;
 import org.mozc.android.inputmethod.japanese.view.TabSelectedBackgroundDrawable;
 import com.google.common.annotations.VisibleForTesting;
@@ -62,36 +68,31 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Typeface;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
 import android.graphics.drawable.LayerDrawable;
 import android.os.IBinder;
+import android.os.Looper;
 import android.preference.PreferenceManager;
 import android.support.v4.view.PagerAdapter;
 import android.support.v4.view.ViewPager;
 import android.support.v4.view.ViewPager.OnPageChangeListener;
 import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.GestureDetector;
-import android.view.GestureDetector.OnGestureListener;
-import android.view.Gravity;
 import android.view.InflateException;
 import android.view.LayoutInflater;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.animation.Animation;
-import android.widget.ImageButton;
 import android.widget.ImageView;
+import android.widget.LinearLayout;
 import android.widget.TabHost;
 import android.widget.TabHost.OnTabChangeListener;
 import android.widget.TabHost.TabSpec;
 import android.widget.TabWidget;
 import android.widget.TextView;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -113,15 +114,18 @@
 
   /**
    * Adapter for symbol candidate selection.
-   * Exposed as package private for testing.
    */
   // TODO(hidehiko): make this class static.
-  class SymbolCandidateSelectListener implements CandidateSelectListener {
+  @VisibleForTesting class SymbolCandidateSelectListener implements CandidateSelectListener {
     @Override
     public void onCandidateSelected(CandidateWord candidateWord, Optional<Integer> row) {
-      if (viewEventListener != null) {
+      Preconditions.checkNotNull(candidateWord);
+      // When current major category is NUMBER, CandidateView.ConversionCandidateSelectListener
+      // should handle candidate selection event.
+      Preconditions.checkState(currentMajorCategory != SymbolMajorCategory.NUMBER);
+      if (viewEventListener.isPresent()) {
         // If we are on password field, history shouldn't be updated to protect privacy.
-        viewEventListener.onSymbolCandidateSelected(
+        viewEventListener.get().onSymbolCandidateSelected(
             currentMajorCategory, candidateWord.getValue(), !isPasswordField);
       }
     }
@@ -130,20 +134,18 @@
   /**
    * Click handler of major category buttons.
    */
-  class MajorCategoryButtonClickListener implements OnClickListener {
+  @VisibleForTesting class MajorCategoryButtonClickListener implements OnClickListener {
     private final SymbolMajorCategory majorCategory;
 
     MajorCategoryButtonClickListener(SymbolMajorCategory majorCategory) {
-      if (majorCategory == null) {
-        throw new NullPointerException("majorCategory should not be null.");
-      }
-      this.majorCategory = majorCategory;
+      this.majorCategory = Preconditions.checkNotNull(majorCategory);
     }
 
     @Override
     public void onClick(View majorCategorySelectorButton) {
-      if (viewEventListener != null) {
-        viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_EXPAND);
+      if (viewEventListener.isPresent()) {
+        viewEventListener.get().onFireFeedbackEvent(
+            FeedbackEvent.SYMBOL_INPUTVIEW_MAJOR_CATEGORY_SELECTED);
       }
 
       if (emojiEnabled
@@ -152,17 +154,17 @@
         // Ask the user which emoji provider s/he'd like to use.
         // If the user cancels the dialog, do nothing.
         maybeInitializeEmojiProviderDialog(getContext());
-        if (emojiProviderDialog != null) {
+        if (emojiProviderDialog.isPresent()) {
           IBinder token = getWindowToken();
           if (token != null) {
-            MozcUtil.setWindowToken(token, emojiProviderDialog);
+            MozcUtil.setWindowToken(token, emojiProviderDialog.get());
           } else {
             MozcLog.w("Unknown window token.");
           }
 
           // If a user selects a provider, the dialog handler will set major category
           // to EMOJI automatically. If s/he cancels, nothing will be happened.
-          emojiProviderDialog.show();
+          emojiProviderDialog.get().show();
           return;
         }
       }
@@ -171,65 +173,77 @@
     }
   }
 
-  // Manages the relationship between.
-  static class SymbolTabWidgetViewPagerAdapter extends PagerAdapter
+  private static class SymbolTabWidgetViewPagerAdapter extends PagerAdapter
       implements OnTabChangeListener, OnPageChangeListener {
 
     private static final int HISTORY_INDEX = 0;
 
     private final Context context;
     private final SymbolCandidateStorage symbolCandidateStorage;
-    private final ViewEventListener viewEventListener;
+    private final Optional<ViewEventListener> viewEventListener;
     private final CandidateSelectListener candidateSelectListener;
     private final SymbolMajorCategory majorCategory;
-    private final SkinType skinType;
+    private Skin skin;
     private final EmojiProviderType emojiProviderType;
     private final TabHost tabHost;
     private final ViewPager viewPager;
     private final float candidateTextSize;
     private final float descriptionTextSize;
 
-    private View historyViewCache = null;
+    private Optional<View> historyViewCache = Optional.absent();
     private int scrollState = ViewPager.SCROLL_STATE_IDLE;
+    private boolean feedbackEnabled = true;
 
     SymbolTabWidgetViewPagerAdapter(
         Context context, SymbolCandidateStorage symbolCandidateStorage,
-        ViewEventListener viewEventListener, CandidateSelectListener candidateSelectListener,
-        SymbolMajorCategory majorCategory,
-        SkinType skinType, EmojiProviderType emojiProviderType,
-        TabHost tabHost, ViewPager viewPager,
+        Optional<ViewEventListener> viewEventListener,
+        CandidateSelectListener candidateSelectListener, SymbolMajorCategory majorCategory,
+        Skin skin, EmojiProviderType emojiProviderType, TabHost tabHost, ViewPager viewPager,
         float candidateTextSize, float descriptionTextSize) {
-      Preconditions.checkNotNull(emojiProviderType);
+      this.context = Preconditions.checkNotNull(context);
+      this.symbolCandidateStorage = Preconditions.checkNotNull(symbolCandidateStorage);
+      this.viewEventListener = Preconditions.checkNotNull(viewEventListener);
+      this.candidateSelectListener = Preconditions.checkNotNull(candidateSelectListener);
+      this.majorCategory = Preconditions.checkNotNull(majorCategory);
+      this.skin = Preconditions.checkNotNull(skin);
+      this.emojiProviderType = Preconditions.checkNotNull(emojiProviderType);
+      this.tabHost = Preconditions.checkNotNull(tabHost);
+      this.viewPager = Preconditions.checkNotNull(viewPager);
+      this.candidateTextSize = Preconditions.checkNotNull(candidateTextSize);
+      this.descriptionTextSize = Preconditions.checkNotNull(descriptionTextSize);
+    }
 
-      this.context = context;
-      this.symbolCandidateStorage = symbolCandidateStorage;
-      this.viewEventListener = viewEventListener;
-      this.candidateSelectListener = candidateSelectListener;
-      this.majorCategory = majorCategory;
-      this.skinType = skinType;
-      this.emojiProviderType = emojiProviderType;
-      this.tabHost = tabHost;
-      this.viewPager = viewPager;
-      this.candidateTextSize = candidateTextSize;
-      this.descriptionTextSize = descriptionTextSize;
+    public void setSkin(Skin skin) {
+      Preconditions.checkNotNull(skin);
+      this.skin = skin;
+    }
+
+    public void setFeedbackEnabled(boolean enabled) {
+      feedbackEnabled = enabled;
     }
 
     private void maybeResetHistoryView() {
-      if (viewPager.getCurrentItem() != HISTORY_INDEX && historyViewCache != null) {
+      if (viewPager.getCurrentItem() != HISTORY_INDEX && historyViewCache.isPresent()) {
         resetHistoryView();
       }
     }
 
     private void resetHistoryView() {
+      if (!historyViewCache.isPresent()) {
+        return;
+      }
       CandidateList candidateList =
           symbolCandidateStorage.getCandidateList(majorCategory.minorCategories.get(0));
+      View noHistoryView = historyViewCache.get().findViewById(R.id.symbol_input_no_history);
       if (candidateList.getCandidatesCount() == 0) {
-        historyViewCache.findViewById(R.id.symbol_input_no_history).setVisibility(View.VISIBLE);
+        noHistoryView.setVisibility(View.VISIBLE);
+        TextView.class.cast(historyViewCache.get().findViewById(R.id.symbol_input_no_history_text))
+            .setTextColor(skin.candidateValueTextColor);
       } else {
-        historyViewCache.findViewById(R.id.symbol_input_no_history).setVisibility(View.GONE);
+        noHistoryView.setVisibility(View.GONE);
       }
-      SymbolCandidateView.class.cast(
-          historyViewCache.findViewById(R.id.symbol_input_candidate_view)).update(candidateList);
+      SymbolCandidateView.class.cast(historyViewCache.get().findViewById(
+          R.id.symbol_input_candidate_view)).update(candidateList);
     }
 
     @Override
@@ -250,10 +264,6 @@
       tabHost.setOnTabChangedListener(null);
       tabHost.setCurrentTab(position);
       tabHost.setOnTabChangedListener(this);
-
-      if (viewEventListener != null) {
-        viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_EXPAND);
-      }
     }
 
     @Override
@@ -265,8 +275,9 @@
       }
       viewPager.setCurrentItem(position, false);
 
-      if (viewEventListener != null) {
-        viewEventListener.onFireFeedbackEvent(FeedbackEvent.INPUTVIEW_EXPAND);
+      if (feedbackEnabled && viewEventListener.isPresent()) {
+        viewEventListener.get().onFireFeedbackEvent(
+            FeedbackEvent.SYMBOL_INPUTVIEW_MINOR_CATEGORY_SELECTED);
       }
     }
 
@@ -293,13 +304,21 @@
       symbolCandidateView.setCandidateSelectListener(candidateSelectListener);
       symbolCandidateView.setMinColumnWidth(
           context.getResources().getDimension(majorCategory.minColumnWidthResourceId));
-      symbolCandidateView.setSkinType(skinType);
+      symbolCandidateView.setSkin(skin);
       symbolCandidateView.setEmojiProviderType(emojiProviderType);
-      symbolCandidateView.setCandidateTextDimension(candidateTextSize, descriptionTextSize);
+      if (majorCategory.layoutPolicy == DescriptionLayoutPolicy.GONE) {
+        // As it's guaranteed for descriptions not to be shown,
+        // show values using additional space where is reserved for descriptions.
+        // This makes Emoji bigger.
+        symbolCandidateView.setCandidateTextDimension(candidateTextSize + descriptionTextSize, 0);
+      } else {
+        symbolCandidateView.setCandidateTextDimension(candidateTextSize, descriptionTextSize);
+      }
+      symbolCandidateView.setDescriptionLayoutPolicy(majorCategory.layoutPolicy);
 
       // Set candidate contents.
       if (position == HISTORY_INDEX) {
-        historyViewCache = view;
+        historyViewCache = Optional.of(view);
         resetHistoryView();
       } else {
         symbolCandidateView.update(symbolCandidateStorage.getCandidateList(
@@ -309,7 +328,7 @@
 
       ScrollGuideView scrollGuideView =
           ScrollGuideView.class.cast(view.findViewById(R.id.symbol_input_scroll_guide_view));
-      scrollGuideView.setSkinType(skinType);
+      scrollGuideView.setSkin(skin);
 
       // Connect guide and candidate view.
       scrollGuideView.setScroller(symbolCandidateView.scroller);
@@ -322,81 +341,20 @@
     @Override
     public void destroyItem(ViewGroup collection, int position, Object view) {
       if (position == HISTORY_INDEX) {
-        historyViewCache = null;
+        historyViewCache = Optional.absent();
       }
       collection.removeView(View.class.cast(view));
     }
   }
 
   /**
-   * The text view for the minor category tab.
-   * The most thing is as same as base TextView, but if the text is too long to fit in
-   * the view, this view automatically scales the text horizontally. (If there is enough
-   * space, doesn't widen.)
-   */
-  private static class TabTextView extends TextView {
-
-    /** Cached Paint instance to measure the text. */
-    private final Paint paint = new Paint();
-
-    TabTextView(Context context) {
-      super(context);
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-      super.onLayout(changed, left, top, right, bottom);
-      resetTextXSize();
-    }
-
-    private void resetTextXSize() {
-      // Reset the paint instance.
-      Paint paint = this.paint;
-      paint.reset();
-      paint.setAntiAlias(true);
-      paint.setTextSize(getTextSize());
-      paint.setTypeface(getTypeface());
-
-      // Measure the width for each line.
-      CharSequence text = getText().toString();
-      float maxTextWidth = 0;
-      int beginIndex = 0;
-      for (int i = beginIndex; i < text.length(); ++i) {
-        if (text.charAt(i) == '\n') {
-          // Split the line.
-          float textWidth = paint.measureText(text, beginIndex, i);
-          if (textWidth > maxTextWidth) {
-            maxTextWidth = textWidth;
-          }
-          // Exclude '\n'.
-          beginIndex = i + 1;
-        }
-      }
-      {
-        // Last line.
-        float textWidth = paint.measureText(text, beginIndex, text.length());
-        if (textWidth > maxTextWidth) {
-          maxTextWidth = textWidth;
-        }
-      }
-
-      // Calculate scale factory. Note that 0.98f is the heuristic value,
-      // in order to avoid wrapping lines by sub px calculations just in case.
-      float scaleX = (getWidth() - getPaddingLeft() - getPaddingRight()) * 0.98f / maxTextWidth;
-
-      // Cap the scaleX by 1f not to widen.
-      setTextScaleX(Math.min(scaleX, 1f));
-    }
-  }
-
-  /**
    * An event listener for the menu dialog window.
    */
   private class EmojiProviderDialogListener implements DialogInterface.OnClickListener {
     private final Context context;
 
     EmojiProviderDialogListener(Context context) {
-      this.context = context;
+      this.context = Preconditions.checkNotNull(context);
     }
 
     @Override
@@ -422,11 +380,10 @@
    * The differences from CandidateView.CandidateWordViewForConversion are
    * 1) this class scrolls horizontally 2) the layout algorithm is simpler.
    */
-  static class SymbolCandidateView extends CandidateWordView {
+  private static class SymbolCandidateView extends CandidateWordView {
     private static final String DESCRIPTION_DELIMITER = "\n";
 
-    private View scrollGuideView = null;
-    private GestureDetector gestureDetector = null;
+    private Optional<View> scrollGuideView = Optional.absent();
 
     public SymbolCandidateView(Context context) {
       super(context, Orientation.VERTICAL);
@@ -442,7 +399,7 @@
 
     // Shared instance initializer.
     {
-      setBackgroundDrawableType(DrawableType.SYMBOL_CANDIDATE_BACKGROUND);
+      setSpanBackgroundDrawableType(DrawableType.SYMBOL_CANDIDATE_BACKGROUND);
       Resources resources = getResources();
       scroller.setDecayRate(
           resources.getInteger(R.integer.symbol_input_scroller_velocity_decay_rate) / 1000000f);
@@ -452,25 +409,27 @@
     }
 
     void setCandidateTextDimension(float textSize, float descriptionTextSize) {
-      Preconditions.checkArgument(textSize > 0);
-      Preconditions.checkArgument(descriptionTextSize > 0);
+      Preconditions.checkArgument(textSize >= 0);
+      Preconditions.checkArgument(descriptionTextSize >= 0);
 
       Resources resources = getResources();
 
       float valueHorizontalPadding =
-          resources.getDimension(R.dimen.candidate_horizontal_padding_size);
+          resources.getDimension(R.dimen.symbol_candidate_horizontal_padding_size);
       float descriptionHorizontalPadding =
           resources.getDimension(R.dimen.symbol_description_right_padding);
       float descriptionVerticalPadding =
           resources.getDimension(R.dimen.symbol_description_bottom_padding);
+      float separatorWidth = resources.getDimensionPixelSize(R.dimen.candidate_separator_width);
 
+      carrierEmojiRenderHelper.setCandidateTextSize(textSize);
       candidateLayoutRenderer.setValueTextSize(textSize);
       candidateLayoutRenderer.setValueHorizontalPadding(valueHorizontalPadding);
       candidateLayoutRenderer.setValueScalingPolicy(ValueScalingPolicy.UNIFORM);
       candidateLayoutRenderer.setDescriptionTextSize(descriptionTextSize);
       candidateLayoutRenderer.setDescriptionHorizontalPadding(descriptionHorizontalPadding);
       candidateLayoutRenderer.setDescriptionVerticalPadding(descriptionVerticalPadding);
-      candidateLayoutRenderer.setDescriptionLayoutPolicy(DescriptionLayoutPolicy.OVERLAY);
+      candidateLayoutRenderer.setSeparatorWidth(separatorWidth);
 
       SpanFactory spanFactory = new SpanFactory();
       spanFactory.setValueTextSize(textSize);
@@ -492,32 +451,31 @@
       updateLayouter();
     }
 
-    void setOnGestureListener(OnGestureListener gestureListener) {
-      if (gestureListener == null) {
-        gestureDetector = null;
-      } else {
-        gestureDetector = new GestureDetector(getContext(), gestureListener);
-      }
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-      if (gestureDetector != null && gestureDetector.onTouchEvent(event)) {
-        return true;
-      }
-      return super.onTouchEvent(event);
-    }
-
     @Override
     protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);
-      if (scrollGuideView != null) {
-        scrollGuideView.invalidate();
+      if (scrollGuideView.isPresent()) {
+        scrollGuideView.get().invalidate();
       }
     }
 
     void setScrollIndicator(View scrollGuideView) {
-      this.scrollGuideView = scrollGuideView;
+      this.scrollGuideView = Optional.of(scrollGuideView);
+    }
+
+    @Override
+    protected Drawable getViewBackgroundDrawable(Skin skin) {
+      return Preconditions.checkNotNull(skin).symbolCandidateViewBackgroundDrawable;
+    }
+
+    @Override
+    public void setSkin(Skin skin) {
+      super.setSkin(skin);
+      candidateLayoutRenderer.setSeparatorColor(skin.symbolCandidateBackgroundSeparatorColor);
+    }
+
+    public void setDescriptionLayoutPolicy(DescriptionLayoutPolicy policy) {
+      candidateLayoutRenderer.setDescriptionLayoutPolicy(Preconditions.checkNotNull(policy));
     }
   }
 
@@ -539,59 +497,46 @@
    */
   static final KeyboardSpecificationName SPEC_NAME =
       new KeyboardSpecificationName("SYMBOL_INPUT_VIEW", 0, 1, 0);
-  // Source ID of the delete button for logging usage stats.
+  // Source ID of the delete/enter button for logging usage stats.
   private static final int DELETE_BUTTON_SOURCE_ID = 1;
-
-  // TODO(hidehiko): move these parameters to skin instance.
-  private static final float BUTTON_CORNOR_RADIUS = 3.5f;  // in dip.
-  private static final float BUTTON_LEFT_OFFSET = 2.0f;
-  private static final float BUTTON_TOP_OFFSET = 2.0f;
-  private static final float BUTTON_RIGHT_OFFSET = 2.0f;
-  private static final float BUTTON_BOTTOM_OFFSET = 2.0f;
-
-  private static final int MAJOR_CATEGORY_TOP_COLOR = 0xFFF5F5F5;
-  private static final int MAJOR_CATEGORY_BOTTOM_COLOR = 0xFFD2D2D2;
-  private static final int MAJOR_CATEGORY_PRESSED_TOP_COLOR = 0xFFAAAAAA;
-  private static final int MAJOR_CATEGORY_PRESSED_BOTTOM_COLOR = 0xFF828282;
-  private static final int MAJOR_CATEGORY_SHADOW_COLOR = 0x57000000;
-
-  // TODO(hidehiko): This parameter is not fixed yet. Needs to revisit again.
-  private static final float SYMBOL_VIEW_MINOR_CATEGORY_TAB_SELECTED_HEIGHT = 6f;
+  private static final int ENTER_BUTTON_SOURCE_ID = 2;
 
   private static final int NUM_TABS = 6;
 
-  private SymbolCandidateStorage symbolCandidateStorage;
+  private Optional<Integer> viewHeight = Optional.absent();
+  private Optional<Integer> numberKeyboardHeight = Optional.absent();
+  private Optional<Float> keyboardHeightScale = Optional.absent();
 
-  @VisibleForTesting SymbolMajorCategory currentMajorCategory;
+  private Optional<SymbolCandidateStorage> symbolCandidateStorage = Optional.absent();
+
+  @VisibleForTesting SymbolMajorCategory currentMajorCategory = SymbolMajorCategory.NUMBER;
   @VisibleForTesting boolean emojiEnabled;
   private boolean isPasswordField;
   @VisibleForTesting EmojiProviderType emojiProviderType = EmojiProviderType.NONE;
 
   @VisibleForTesting SharedPreferences sharedPreferences;
-  @VisibleForTesting AlertDialog emojiProviderDialog;
+  @VisibleForTesting Optional<AlertDialog> emojiProviderDialog = Optional.absent();
 
-  private ViewEventListener viewEventListener;
+  private Optional<ViewEventListener> viewEventListener = Optional.absent();
   private final KeyEventButtonTouchListener deleteKeyEventButtonTouchListener =
       createDeleteKeyEventButtonTouchListener(getResources());
-  private OnClickListener closeButtonClickListener = null;
+  private final KeyEventButtonTouchListener enterKeyEventButtonTouchListener =
+      createEnterKeyEventButtonTouchListener(getResources());
+  private Optional<OnClickListener> closeButtonClickListener = Optional.absent();
+  private Optional<OnClickListener> microphoneButtonClickListener = Optional.absent();
   private final SymbolCandidateSelectListener symbolCandidateSelectListener =
       new SymbolCandidateSelectListener();
 
-  private SkinType skinType = SkinType.ORANGE_LIGHTGRAY;
-  private final MozcDrawableFactory mozcDrawableFactory = new MozcDrawableFactory(getResources());
+  private Skin skin = Skin.getFallbackInstance();
   private final SymbolMajorCategoryButtonDrawableFactory majorCategoryButtonDrawableFactory =
-      new SymbolMajorCategoryButtonDrawableFactory(
-          mozcDrawableFactory,
-          MAJOR_CATEGORY_TOP_COLOR,
-          MAJOR_CATEGORY_BOTTOM_COLOR,
-          MAJOR_CATEGORY_PRESSED_TOP_COLOR,
-          MAJOR_CATEGORY_PRESSED_BOTTOM_COLOR,
-          MAJOR_CATEGORY_SHADOW_COLOR,
-          BUTTON_CORNOR_RADIUS * getResources().getDisplayMetrics().density);
+      new SymbolMajorCategoryButtonDrawableFactory(getResources());
   // Candidate text size in dip.
   private float candidateTextSize;
   // Description text size in dip.
   private float desciptionTextSize;
+  private Optional<KeyEventHandler> keyEventHandler = Optional.absent();
+  private boolean isMicrophoneButtonEnabled;
+  private boolean popupEnabled;
 
   public SymbolInputView(Context context, AttributeSet attrs, int defStyle) {
     super(context, attrs, defStyle);
@@ -607,27 +552,28 @@
 
   {
     setOutAnimationListener(new OutAnimationAdapter());
-    sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
+    sharedPreferences =
+        Preconditions.checkNotNull(PreferenceManager.getDefaultSharedPreferences(getContext()));
   }
 
   private static KeyEventButtonTouchListener createDeleteKeyEventButtonTouchListener(
       Resources resources) {
-    // Use 8 as default value of backspace key code (only for testing).
-    // This code is introduced just for testing purpose due to AndroidMock's limitation.
-    // When we move to EasyMock, we can remove this method.
-    int ucharBackspace = resources == null ? 8 : resources.getInteger(R.integer.uchar_backspace);
-    return new KeyEventButtonTouchListener(DELETE_BUTTON_SOURCE_ID, ucharBackspace);
+    return new KeyEventButtonTouchListener(
+        DELETE_BUTTON_SOURCE_ID, resources.getInteger(R.integer.uchar_backspace));
   }
 
+  private static KeyEventButtonTouchListener createEnterKeyEventButtonTouchListener(
+      Resources resources) {
+    return new KeyEventButtonTouchListener(
+        ENTER_BUTTON_SOURCE_ID, resources.getInteger(R.integer.uchar_linefeed));
+  }
 
   boolean isInflated() {
     return getChildCount() > 0;
   }
 
   void inflateSelf() {
-    if (isInflated()) {
-      throw new IllegalStateException("The symbol input view is already inflated.");
-    }
+    Preconditions.checkState(!isInflated(), "The symbol input view is already inflated.");
 
     // Hack: Because we wrap the real context to inject "retrying" for Drawable loading,
     // LayoutInflater.from(getContext()).getContext() may be different from getContext().
@@ -649,17 +595,112 @@
    * So, instead, we define another onFinishInflate method and invoke this manually.
    */
   protected void onFinishInflateSelf() {
-    initializeMajorCategoryButtons();
+    if (viewHeight.isPresent() && keyboardHeightScale.isPresent()) {
+      setVerticalDimension(viewHeight.get(), keyboardHeightScale.get());
+    }
+
     initializeMinorCategoryTab();
     initializeCloseButton();
     initializeDeleteButton();
+    initializeEnterButton();
+    initializeMicrophoneButton();
 
-    resetMajorCategoryBackground();
-    resetTabBackground();
+    // Set TouchListener that does nothing. Without this hack, state_pressed event
+    // will be propagated to close / enter key and the drawable will be changed to
+    // state_pressed one unexpectedly. Note that those keys are NOT children of this view.
+    // Setting ClickListener to the key seems to suppress this unexpected highlight, too,
+    // but we want to keep the current TouchListener for the enter key.
+    OnTouchListener doNothingOnTouchListener = new OnTouchListener() {
+      @Override
+      public boolean onTouch(View button, android.view.MotionEvent event) {
+        return true;
+      }
+    };
+    for (int id : new int[] {R.id.button_frame_in_symbol_view,
+                             R.id.symbol_view_backspace_separator,
+                             R.id.symbol_major_category,
+                             R.id.symbol_separator_1,
+                             R.id.symbol_separator_2,
+                             R.id.symbol_separator_3,
+                             R.id.symbol_view_close_button_separator,
+                             R.id.symbol_view_enter_button_separator}) {
+      findViewById(id).setOnTouchListener(doNothingOnTouchListener);
+    }
+
+    KeyboardView keyboardView = KeyboardView.class.cast(findViewById(R.id.number_keyboard));
+    keyboardView.setPopupEnabled(popupEnabled);
+    keyboardView.setKeyEventHandler(new KeyEventHandler(
+        Looper.getMainLooper(),
+        new KeyboardActionListener() {
+          @Override
+          public void onRelease(int keycode) {
+          }
+
+          @Override
+          public void onPress(int keycode) {
+            if (viewEventListener.isPresent()) {
+              viewEventListener.get().onFireFeedbackEvent(FeedbackEvent.KEY_DOWN);
+            }
+          }
+
+          @Override
+          public void onKey(int primaryCode, List<TouchEvent> touchEventList) {
+            if (keyEventHandler.isPresent()) {
+              keyEventHandler.get().sendKey(primaryCode, touchEventList);
+            }
+          }
+
+          @Override
+          public void onCancel() {
+          }
+        },
+        getResources().getInteger(R.integer.config_repeat_key_delay),
+        getResources().getInteger(R.integer.config_repeat_key_interval),
+        getResources().getInteger(R.integer.config_long_press_key_delay)));
+
     enableEmoji(emojiEnabled);
+
+    // Disable h/w acceleration to use a PictureDrawable.
+    for (SymbolMajorCategory majorCategory : SymbolMajorCategory.values()) {
+      getMajorCategoryButton(majorCategory).setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+    }
+
+    updateSkinAwareDrawable();
     reset();
   }
 
+  private static void setLayoutHeight(View view, int height) {
+    ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+    layoutParams.height = height;
+    view.setLayoutParams(layoutParams);
+  }
+
+  public void setVerticalDimension(int symbolInputViewHeight, float keyboardHeightScale) {
+    this.viewHeight = Optional.of(symbolInputViewHeight);
+    this.keyboardHeightScale = Optional.of(keyboardHeightScale);
+
+    Resources resources = getResources();
+    float originalMajorCategoryHeight =
+        resources.getDimension(R.dimen.symbol_view_major_category_height);
+    int majorCategoryHeight = Math.round(originalMajorCategoryHeight * keyboardHeightScale);
+    this.numberKeyboardHeight = Optional.of(
+        symbolInputViewHeight - majorCategoryHeight
+        - resources.getDimensionPixelSize(R.dimen.button_frame_height));
+
+    if (!isInflated()) {
+      return;
+    }
+
+    setLayoutHeight(this, symbolInputViewHeight);
+    setLayoutHeight(getMajorCategoryFrame(), majorCategoryHeight);
+    setLayoutHeight(findViewById(R.id.number_keyboard), numberKeyboardHeight.get());
+    setLayoutHeight(findViewById(R.id.number_keyboard_frame), LayoutParams.WRAP_CONTENT);
+  }
+
+  public int getNumberKeyboardHeight() {
+    return numberKeyboardHeight.get();
+  }
+
   private void resetCandidateViewPager() {
     if (!isInflated()) {
       return;
@@ -667,52 +708,59 @@
 
     ViewPager candidateViewPager = getCandidateViewPager();
     TabHost tabHost = getTabHost();
+    Preconditions.checkState(symbolCandidateStorage.isPresent());
 
     SymbolTabWidgetViewPagerAdapter adapter = new SymbolTabWidgetViewPagerAdapter(
         getContext(),
-        symbolCandidateStorage, viewEventListener, symbolCandidateSelectListener,
-        currentMajorCategory, skinType, emojiProviderType, tabHost, candidateViewPager,
+        symbolCandidateStorage.get(), viewEventListener, symbolCandidateSelectListener,
+        currentMajorCategory, skin, emojiProviderType, tabHost, candidateViewPager,
         candidateTextSize, desciptionTextSize);
     candidateViewPager.setAdapter(adapter);
     candidateViewPager.setOnPageChangeListener(adapter);
     tabHost.setOnTabChangedListener(adapter);
   }
 
-  private void resetMajorCategoryBackground() {
-    View view = findViewById(R.id.symbol_major_category);
+  @SuppressWarnings("deprecation")
+  private void updateMajorCategoryBackgroundSkin() {
+    View view = getMajorCategoryFrame();
     if (view != null) {
-      if (skinType == null) {
-        view.setBackgroundColor(Color.BLACK);
-      } else {
-        view.setBackgroundResource(skinType.windowBackgroundResourceId);
-      }
+      view.setBackgroundDrawable(
+          skin.symbolMajorCategoryBackgroundDrawable.getConstantState().newDrawable());
     }
   }
 
-  private void setMozcDrawable(ImageView imageView, int resourceId) {
-    Optional<Drawable> drawable = mozcDrawableFactory.getDrawable(resourceId);
-    if (drawable.isPresent()) {
-      imageView.setImageDrawable(drawable.get());
+  @SuppressWarnings("deprecation")
+  private void updateMinorCategoryBackgroundSkin() {
+    View view = getMinorCategoryFrame();
+    if (view != null) {
+      view.setBackgroundDrawable(
+          skin.buttonFrameBackgroundDrawable.getConstantState().newDrawable());
     }
   }
 
+  @SuppressWarnings("deprecation")
+  private void updateNumberKeyboardSkin() {
+    getNumberKeyboardView().setSkin(skin);
+    findViewById(R.id.number_frame).setBackgroundDrawable(
+        skin.windowBackgroundDrawable.getConstantState().newDrawable());
+    findViewById(R.id.button_frame_in_symbol_view).setBackgroundDrawable(
+        skin.buttonFrameBackgroundDrawable.getConstantState().newDrawable());
+  }
+
   /**
    * Sets click event handlers to each major category button.
    * It is necessary that the inflation has been done before this method invocation.
    */
   @SuppressWarnings("deprecation")
-  private void initializeMajorCategoryButtons() {
+  private void updateMajorCategoryButtonsSkin() {
+    Resources resources = getResources();
     for (SymbolMajorCategory majorCategory : SymbolMajorCategory.values()) {
-      ImageView view = ImageView.class.cast(findViewById(majorCategory.buttonResourceId));
-      if (view == null) {
-        throw new IllegalStateException(
-            "The view corresponding to " + majorCategory.name() + " is not found.");
-      }
+      MozcImageButton view = getMajorCategoryButton(majorCategory);
+      Preconditions.checkState(
+          view != null, "The view corresponding to " + majorCategory.name() + " is not found.");
       view.setOnClickListener(new MajorCategoryButtonClickListener(majorCategory));
-      setMozcDrawable(view, majorCategory.buttonImageResourceId);
-
       switch (majorCategory) {
-        case SYMBOL:
+        case NUMBER:
           view.setBackgroundDrawable(
               majorCategoryButtonDrawableFactory.createLeftButtonDrawable());
           break;
@@ -725,16 +773,16 @@
               majorCategoryButtonDrawableFactory.createCenterButtonDrawable());
           break;
       }
+      view.setImageDrawable(skin.getDrawable(resources, majorCategory.buttonImageResourceId));
+      // Update the padding since setBackgroundDrawable() overwrites it.
+      view.setMaxImageHeight(
+          resources.getDimensionPixelSize(majorCategory.maxImageHeightResourceId));
     }
   }
 
   private void initializeMinorCategoryTab() {
-    TabHost tabhost = TabHost.class.cast(findViewById(android.R.id.tabhost));
+    TabHost tabhost = getTabHost();
     tabhost.setup();
-
-    float textSize = getResources().getDimension(R.dimen.symbol_view_minor_category_text_size);
-    int textColor = getResources().getColor(android.R.color.black);
-
     // Create NUM_TABS (= 6) tabs.
     // Note that we may want to change the number of tabs, however due to the limitation of
     // the current TabHost implementation, it is difficult. Fortunately, all major categories
@@ -742,12 +790,9 @@
     for (int i = 0; i < NUM_TABS; ++i) {
       // The tab's id is the index of the tab.
       TabSpec tab = tabhost.newTabSpec(String.valueOf(i));
-      TextView textView = new TabTextView(getContext());
-      textView.setTypeface(Typeface.DEFAULT_BOLD);
-      textView.setTextColor(textColor);
-      textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
-      textView.setGravity(Gravity.CENTER);
-      tab.setIndicator(textView);
+      MozcImageView view = new MozcImageView(getContext());
+      view.setSoundEffectsEnabled(false);
+      tab.setIndicator(view);
       // Set dummy view for the content. The actual content will be managed by ViewPager.
       tab.setContent(R.id.symbol_input_dummy);
       tabhost.addTab(tab);
@@ -759,79 +804,84 @@
   }
 
   @SuppressWarnings("deprecation")
-  private void resetTabBackground() {
+  private void updateTabBackgroundSkin() {
     if (!isInflated()) {
       return;
     }
-
-    float density = getResources().getDisplayMetrics().density;
-
-    TabWidget tabWidget = TabWidget.class.cast(findViewById(android.R.id.tabs));
+    getTabHost().setBackgroundDrawable(
+        skin.windowBackgroundDrawable.getConstantState().newDrawable());
+    TabWidget tabWidget = getTabWidget();
     for (int i = 0; i < tabWidget.getTabCount(); ++i) {
       View view = tabWidget.getChildTabViewAt(i);
-      view.setBackgroundDrawable(createTabBackgroundDrawable(skinType, density));
+      view.setBackgroundDrawable(createTabBackgroundDrawable(skin));
     }
   }
 
-  private static Drawable createTabBackgroundDrawable(SkinType skinType, float density) {
+  private static Drawable createTabBackgroundDrawable(Skin skin) {
+    Preconditions.checkNotNull(skin);
     return new LayerDrawable(new Drawable[] {
         BackgroundDrawableFactory.createSelectableDrawable(
             new TabSelectedBackgroundDrawable(
-                (int) (SYMBOL_VIEW_MINOR_CATEGORY_TAB_SELECTED_HEIGHT * density),
-                skinType.symbolMinorCategoryTabSelectedColor),
-            null),
-        BackgroundDrawableFactory.createPressableDrawable(
-            new ColorDrawable(skinType.symbolMinorCategoryTabPressedColor), null),
+                Math.round(skin.symbolMinorIndicatorHeightDimension),
+                skin.symbolMinorCategoryTabSelectedColor),
+            Optional.<Drawable>absent()),
+        createMinorButtonBackgroundDrawable(skin)
     });
   }
 
-  private void resetTabText() {
+  private void resetTabImageForMinorCategory() {
     if (!isInflated()) {
       return;
     }
-
-    TabWidget tabWidget = TabWidget.class.cast(findViewById(android.R.id.tabs));
+    TabWidget tabWidget = getTabWidget();
     List<SymbolMinorCategory> minorCategoryList = currentMajorCategory.minorCategories;
-    for (int i = 0; i < tabWidget.getChildCount(); ++i) {
-      TextView textView = TextView.class.cast(tabWidget.getChildTabViewAt(i));
-      textView.setText(minorCategoryList.get(i).textResourceId);
+    int definedTabSize = Math.min(minorCategoryList.size(), tabWidget.getChildCount());
+    for (int i = 0; i < definedTabSize; ++i) {
+      MozcImageView view = MozcImageView.class.cast(tabWidget.getChildTabViewAt(i));
+      SymbolMinorCategory symbolMinorCategory = minorCategoryList.get(i);
+      view.setRawId(symbolMinorCategory.drawableResourceId);
+      if (symbolMinorCategory.maxImageHeightResourceId != SymbolMinorCategory.INVALID_RESOURCE_ID) {
+        view.setMaxImageHeight(
+            getResources().getDimensionPixelSize(symbolMinorCategory.maxImageHeightResourceId));
+      }
+      if (symbolMinorCategory.contentDescriptionResourceId
+          != SymbolMinorCategory.INVALID_RESOURCE_ID) {
+        view.setContentDescription(
+            getResources().getString(symbolMinorCategory.contentDescriptionResourceId));
+      }
     }
   }
 
-  private static Drawable createButtonBackgroundDrawable(SkinType skinType, float density) {
+  private static Drawable createMajorButtonBackgroundDrawable(Skin skin) {
+    int padding = Math.round(skin.symbolMajorButtonPaddingDimension);
+    int round = Math.round(skin.symbolMajorButtonRoundDimension);
     return BackgroundDrawableFactory.createPressableDrawable(
         new RoundRectKeyDrawable(
-            (int) (BUTTON_LEFT_OFFSET * density),
-            (int) (BUTTON_TOP_OFFSET * density),
-            (int) (BUTTON_RIGHT_OFFSET * density),
-            (int) (BUTTON_BOTTOM_OFFSET * density),
-            (int) (BUTTON_CORNOR_RADIUS * density),
-            skinType.symbolPressedFunctionKeyTopColor,
-            skinType.symbolPressedFunctionKeyBottomColor,
-            skinType.symbolPressedFunctionKeyHighlightColor,
-            skinType.symbolPressedFunctionKeyShadowColor),
-        new RoundRectKeyDrawable(
-            (int) (BUTTON_LEFT_OFFSET * density),
-            (int) (BUTTON_TOP_OFFSET * density),
-            (int) (BUTTON_RIGHT_OFFSET * density),
-            (int) (BUTTON_BOTTOM_OFFSET * density),
-            (int) (BUTTON_CORNOR_RADIUS * density),
-            skinType.symbolReleasedFunctionKeyTopColor,
-            skinType.symbolReleasedFunctionKeyBottomColor,
-            skinType.symbolReleasedFunctionKeyHighlightColor,
-            skinType.symbolReleasedFunctionKeyShadowColor));
+            padding, padding, padding, padding, round,
+            skin.symbolPressedFunctionKeyTopColor,
+            skin.symbolPressedFunctionKeyBottomColor,
+            skin.symbolPressedFunctionKeyHighlightColor,
+            skin.symbolPressedFunctionKeyShadowColor),
+        Optional.<Drawable>of(new RoundRectKeyDrawable(
+            padding, padding, padding, padding, round,
+            skin.symbolReleasedFunctionKeyTopColor,
+            skin.symbolReleasedFunctionKeyBottomColor,
+            skin.symbolReleasedFunctionKeyHighlightColor,
+            skin.symbolReleasedFunctionKeyShadowColor)));
+  }
+
+  private static Drawable createMinorButtonBackgroundDrawable(Skin skin) {
+    return BackgroundDrawableFactory.createPressableDrawable(
+        new ColorDrawable(skin.symbolMinorCategoryTabPressedColor),
+        Optional.<Drawable>absent());
   }
 
   @SuppressWarnings("deprecation")
   private void initializeCloseButton() {
     ImageView closeButton = ImageView.class.cast(findViewById(R.id.symbol_view_close_button));
-    if (closeButtonClickListener != null) {
-      closeButton.setOnClickListener(closeButtonClickListener);
+    if (closeButtonClickListener.isPresent()) {
+      closeButton.setOnClickListener(closeButtonClickListener.get());
     }
-    closeButton.setBackgroundDrawable(
-        createButtonBackgroundDrawable(skinType, getResources().getDisplayMetrics().density));
-    setMozcDrawable(closeButton, R.raw.symbol__function__close);
-    closeButton.setPadding(2, 2, 2, 2);
   }
 
   /**
@@ -840,37 +890,77 @@
    */
   @SuppressWarnings("deprecation")
   private void initializeDeleteButton() {
-    ImageView deleteButton = ImageView.class.cast(findViewById(R.id.symbol_view_delete_button));
+    MozcImageView deleteButton =
+        MozcImageView.class.cast(findViewById(R.id.symbol_view_delete_button));
     deleteButton.setOnTouchListener(deleteKeyEventButtonTouchListener);
-    deleteButton.setBackgroundDrawable(
-        createButtonBackgroundDrawable(skinType, getResources().getDisplayMetrics().density));
-    setMozcDrawable(deleteButton, R.raw.symbol__function__delete);
-    deleteButton.setPadding(0, 0, 0, 0);
   }
 
-  TabHost getTabHost() {
+  /** c.f., {@code initializeDeleteButton}. */
+  @SuppressWarnings("deprecation")
+  private void initializeEnterButton() {
+    ImageView enterButton = ImageView.class.cast(findViewById(R.id.symbol_view_enter_button));
+    enterButton.setOnTouchListener(enterKeyEventButtonTouchListener);
+  }
+
+  private void initializeMicrophoneButton() {
+    MozcImageView microphoneButton = getMicrophoneButton();
+    if (microphoneButtonClickListener.isPresent()) {
+      microphoneButton.setOnClickListener(microphoneButtonClickListener.get());
+    }
+    microphoneButton.setVisibility(isMicrophoneButtonEnabled ? VISIBLE : GONE);
+  }
+
+  @SuppressWarnings("deprecation")
+  private void updateSeparatorsSkin() {
+    Resources resources = getResources();
+    int minorPaddingSize = (int) resources.getFraction(
+        R.fraction.symbol_separator_padding_fraction,
+        resources.getDimensionPixelSize(R.dimen.button_frame_height), 0);
+    findViewById(R.id.symbol_view_backspace_separator).setBackgroundDrawable(
+        new InsetDrawable(new ColorDrawable(skin.symbolSeparatorColor),
+                          0, minorPaddingSize, 0, minorPaddingSize));
+    int majorPaddingSize = (int) resources.getFraction(
+        R.fraction.symbol_separator_padding_fraction,
+        resources.getDimensionPixelSize(R.dimen.symbol_view_major_category_height), 0);
+    InsetDrawable separator = new InsetDrawable(
+        new ColorDrawable(skin.symbolSeparatorColor), 0, majorPaddingSize, 0, majorPaddingSize);
+    for (int id : new int[] {R.id.symbol_view_close_button_separator,
+                             R.id.symbol_view_enter_button_separator}) {
+      findViewById(id).setBackgroundDrawable(separator.getConstantState().newDrawable());
+    }
+
+    for (int id : new int[] {R.id.symbol_separator_1,
+                             R.id.symbol_separator_3}) {
+      findViewById(id).setBackgroundDrawable(
+          skin.keyboardFrameSeparatorBackgroundDrawable.getConstantState().newDrawable());
+    }
+    findViewById(R.id.symbol_separator_2).setBackgroundDrawable(
+        skin.symbolSeparatorAboveMajorCategoryBackgroundDrawable
+            .getConstantState().newDrawable());
+  }
+
+  @VisibleForTesting TabHost getTabHost() {
     return TabHost.class.cast(findViewById(android.R.id.tabhost));
   }
 
-  ViewPager getCandidateViewPager() {
+  private ViewPager getCandidateViewPager() {
     return ViewPager.class.cast(findViewById(R.id.symbol_input_candidate_view_pager));
   }
 
-  ImageButton getMajorCategoryButton(SymbolMajorCategory majorCategory) {
-    if (majorCategory == null) {
-      throw new NullPointerException("majorCategory shouldn't be null.");
-    }
-    return ImageButton.class.cast(findViewById(majorCategory.buttonResourceId));
+  @VisibleForTesting MozcImageButton getMajorCategoryButton(SymbolMajorCategory majorCategory) {
+    Preconditions.checkNotNull(majorCategory);
+    return MozcImageButton.class.cast(findViewById(majorCategory.buttonResourceId));
   }
 
-  View getEmojiDisabledMessageView() {
+  @VisibleForTesting View getEmojiDisabledMessageView() {
     return findViewById(R.id.symbol_emoji_disabled_message_view);
   }
 
   public void setEmojiEnabled(boolean unicodeEmojiEnabled, boolean carrierEmojiEnabled) {
     this.emojiEnabled = unicodeEmojiEnabled || carrierEmojiEnabled;
     enableEmoji(this.emojiEnabled);
-    symbolCandidateStorage.setEmojiEnabled(unicodeEmojiEnabled, carrierEmojiEnabled);
+    Preconditions.checkState(symbolCandidateStorage.isPresent());
+    symbolCandidateStorage.get().setEmojiEnabled(unicodeEmojiEnabled, carrierEmojiEnabled);
   }
 
   public void setPasswordField(boolean isPasswordField) {
@@ -883,36 +973,47 @@
       return;
     }
 
-    ImageButton imageButton = getMajorCategoryButton(SymbolMajorCategory.EMOJI);
+    MozcImageButton imageButton = getMajorCategoryButton(SymbolMajorCategory.EMOJI);
     imageButton.setBackgroundDrawable(
         majorCategoryButtonDrawableFactory.createRightButtonDrawable(enableEmoji));
+    // Update the padding since setBackgroundDrawable() overwrites it.
+    imageButton.setMaxImageHeight(getResources().getDimensionPixelSize(
+        SymbolMajorCategory.EMOJI.maxImageHeightResourceId));
   }
 
-  /**
-   * Resets the status.
-   */
-  void reset() {
-    // the current minor category is also updated in setMajorCategory.
-    setMajorCategory(SymbolMajorCategory.SYMBOL);
+  void resetToMajorCategory(Optional<SymbolMajorCategory> category) {
+    Preconditions.checkNotNull(category);
+    setMajorCategory(category.or(currentMajorCategory));
     deleteKeyEventButtonTouchListener.reset();
+    enterKeyEventButtonTouchListener.reset();
+  }
+
+  @VisibleForTesting void reset() {
+    // the current minor category is also updated in setMajorCategory.
+    resetToMajorCategory(Optional.of(SymbolMajorCategory.NUMBER));
   }
 
   @Override
   public void setVisibility(int visibility) {
     int previousVisibility = getVisibility();
     super.setVisibility(visibility);
-    if (viewEventListener != null
-        && previousVisibility == View.VISIBLE && visibility != View.VISIBLE) {
-      viewEventListener.onCloseSymbolInputView();
+    if (viewEventListener.isPresent()) {
+      if (previousVisibility == View.VISIBLE && visibility != View.VISIBLE) {
+        viewEventListener.get().onCloseSymbolInputView();
+      } else if (previousVisibility != View.VISIBLE && visibility == View.VISIBLE) {
+        viewEventListener.get().onShowSymbolInputView(Collections.<TouchEvent>emptyList());
+      }
     }
   }
 
   void setSymbolCandidateStorage(SymbolCandidateStorage symbolCandidateStorage) {
-    this.symbolCandidateStorage = symbolCandidateStorage;
+    this.symbolCandidateStorage = Optional.of(symbolCandidateStorage);
   }
 
   void setKeyEventHandler(KeyEventHandler keyEventHandler) {
+    this.keyEventHandler = Optional.of(keyEventHandler);
     deleteKeyEventButtonTouchListener.setKeyEventHandler(keyEventHandler);
+    enterKeyEventButtonTouchListener.setKeyEventHandler(keyEventHandler);
   }
 
   void setCandidateTextDimension(float candidateTextSize, float descriptionTextSize) {
@@ -923,12 +1024,19 @@
     this.desciptionTextSize = descriptionTextSize;
   }
 
+  void setPopupEnabled(boolean popupEnabled) {
+    this.popupEnabled = popupEnabled;
+    if (!isInflated()) {
+      return;
+    }
+    getNumberKeyboardView().setPopupEnabled(popupEnabled);
+  }
+
   /**
    * Initializes EmojiProvider selection dialog, if necessary.
-   * Exposed as protected for testing purpose.
    */
-  protected void maybeInitializeEmojiProviderDialog(Context context) {
-    if (emojiProviderDialog != null) {
+  @VisibleForTesting void maybeInitializeEmojiProviderDialog(Context context) {
+    if (emojiProviderDialog.isPresent()) {
       return;
     }
 
@@ -938,7 +1046,7 @@
           .setTitle(R.string.pref_emoji_provider_type_title)
           .setItems(R.array.pref_emoji_provider_type_entries, listener)
           .create();
-      this.emojiProviderDialog = dialog;
+      this.emojiProviderDialog = Optional.of(dialog);
     } catch (InflateException e) {
       // Ignore the exception.
     }
@@ -950,32 +1058,66 @@
    * The view is updated.
    * The active minor category is also updated.
    *
+   * This method submit a preedit text except for a {@link SymbolMajorCategory#NUMBER} major
+   * category since this class commit a candidate directly.
+   *
    * @param newCategory the major category to show.
    */
-  protected void setMajorCategory(SymbolMajorCategory newCategory) {
-    if (newCategory == null) {
-      throw new NullPointerException("newCategory must be non-null.");
+  @VisibleForTesting void setMajorCategory(SymbolMajorCategory newCategory) {
+    Preconditions.checkNotNull(newCategory);
+
+    {
+      SymbolCandidateView symbolCandidateView =
+          SymbolCandidateView.class.cast(findViewById(R.id.symbol_input_candidate_view));
+      if (symbolCandidateView != null) {
+        symbolCandidateView.reset();
+      }
     }
+
+    if (newCategory != SymbolMajorCategory.NUMBER && viewEventListener.isPresent()) {
+      viewEventListener.get().onSubmitPreedit();
+    }
+
+    if (newCategory == SymbolMajorCategory.NUMBER) {
+      CandidateView candidateView =
+          CandidateView.class.cast(findViewById(R.id.candidate_view_in_symbol_view));
+      candidateView.clearAnimation();
+      candidateView.setVisibility(View.GONE);
+      candidateView.reset();
+    }
+
     currentMajorCategory = newCategory;
 
-    // Reset the minor category to the default value.
-    resetTabText();
-    resetCandidateViewPager();
-    SymbolMinorCategory minorCategory = currentMajorCategory.getDefaultMinorCategory();
-    if (symbolCandidateStorage.getCandidateList(minorCategory).getCandidatesCount() == 0) {
-      minorCategory = currentMajorCategory.getMinorCategoryByRelativeIndex(minorCategory, 1);
+    if (currentMajorCategory == SymbolMajorCategory.NUMBER) {
+      findViewById(android.R.id.tabhost).setVisibility(View.GONE);
+      findViewById(R.id.number_frame).setVisibility(View.VISIBLE);
+      setNumberKeyboard();
+    } else {
+      findViewById(android.R.id.tabhost).setVisibility(View.VISIBLE);
+      findViewById(R.id.number_frame).setVisibility(View.GONE);
+      updateMinorCategory();
     }
-    int index = newCategory.minorCategories.indexOf(minorCategory);
-    getCandidateViewPager().setCurrentItem(index);
-    getTabHost().setCurrentTab(index);
+
+    // Hide overlapping separator
+    if (currentMajorCategory == SymbolMajorCategory.NUMBER) {
+      findViewById(R.id.symbol_view_close_button_separator).setVisibility(View.INVISIBLE);
+    } else {
+      findViewById(R.id.symbol_view_close_button_separator).setVisibility(View.VISIBLE);
+    }
+
+    if (currentMajorCategory == SymbolMajorCategory.EMOJI) {
+      findViewById(R.id.symbol_view_enter_button_separator).setVisibility(View.INVISIBLE);
+    } else {
+      findViewById(R.id.symbol_view_enter_button_separator).setVisibility(View.VISIBLE);
+    }
 
     // Update visibility relating attributes.
     for (SymbolMajorCategory majorCategory : SymbolMajorCategory.values()) {
       // Update major category selector button's look and feel.
-      ImageButton button = getMajorCategoryButton(majorCategory);
+      MozcImageButton button = getMajorCategoryButton(majorCategory);
       if (button != null) {
-        button.setSelected(majorCategory == newCategory);
-        button.setEnabled(majorCategory != newCategory);
+        button.setSelected(majorCategory == currentMajorCategory);
+        button.setEnabled(majorCategory != currentMajorCategory);
       }
     }
 
@@ -983,15 +1125,62 @@
     if (emojiDisabledMessageView != null) {
       // Show messages about emoji-disabling, if necessary.
       emojiDisabledMessageView.setVisibility(
-          newCategory == SymbolMajorCategory.EMOJI && !emojiEnabled ? View.VISIBLE : View.GONE);
+          currentMajorCategory == SymbolMajorCategory.EMOJI
+          && !emojiEnabled ? View.VISIBLE : View.GONE);
     }
   }
 
+  private void setNumberKeyboard() {
+    final KeyboardSpecification spec = KeyboardSpecification.SYMBOL_NUMBER;
+    final KeyboardFactory factory = new KeyboardFactory();
+
+    getNumberKeyboardView().addOnLayoutChangeListener(new OnLayoutChangeListener() {
+      @Override
+      public void onLayoutChange(
+          View view, int left, int top, int right, int bottom,
+          int oldLeft, int oldTop, int oldRight, int oldBottom) {
+        if (right - left == 0 || bottom - top == 0) {
+          return;
+        }
+        KeyboardView keyboardView = KeyboardView.class.cast(view);
+        Keyboard keyboard = factory.get(getResources(), spec, right - left, bottom - top);
+        keyboardView.setKeyboard(keyboard);
+        keyboardView.invalidate();
+      }
+    });
+  }
+
+  private void updateMinorCategory() {
+    // Reset the minor category to the default value.
+    resetTabImageForMinorCategory();
+    resetCandidateViewPager();
+    SymbolMinorCategory minorCategory = currentMajorCategory.getDefaultMinorCategory();
+    Preconditions.checkState(symbolCandidateStorage.isPresent());
+    if (symbolCandidateStorage.get().getCandidateList(minorCategory).getCandidatesCount() == 0) {
+      minorCategory = currentMajorCategory.getMinorCategoryByRelativeIndex(minorCategory, 1);
+    }
+    int index = currentMajorCategory.minorCategories.indexOf(minorCategory);
+    getCandidateViewPager().setCurrentItem(index);
+
+    // Disable feedback before setting the current tab programatically.
+    // Background: TabHost.setCurrentTab calls back onTabChanged, in which feedback event is fired.
+    // However, we don't have ways to distinguish if onTabChanged is called through user click
+    // event or by the call of setCurrentTab.  If we don't disable feedback here, the click sound
+    // effect is fired twice; one is from the onClick event on major category tab and the other is
+    // by the call of setCurrentTab here.  See b/17119766.
+    SymbolTabWidgetViewPagerAdapter adapter =
+        SymbolTabWidgetViewPagerAdapter.class.cast(getCandidateViewPager().getAdapter());
+    adapter.setFeedbackEnabled(false);
+    getTabHost().setCurrentTab(index);
+    adapter.setFeedbackEnabled(true);
+  }
+
   void setEmojiProviderType(EmojiProviderType emojiProviderType) {
     Preconditions.checkNotNull(emojiProviderType);
 
+    Preconditions.checkState(symbolCandidateStorage.isPresent());
     this.emojiProviderType = emojiProviderType;
-    this.symbolCandidateStorage.setEmojiProviderType(emojiProviderType);
+    this.symbolCandidateStorage.get().setEmojiProviderType(emojiProviderType);
     if (!isInflated()) {
       return;
     }
@@ -999,29 +1188,99 @@
     resetCandidateViewPager();
   }
 
-  void setViewEventListener(ViewEventListener listener, OnClickListener closeButtonClickListener) {
-    if (listener == null) {
-      throw new NullPointerException("lister must be non-null.");
-    }
-    viewEventListener = listener;
-    this.closeButtonClickListener = closeButtonClickListener;
+  void setEventListener(
+      ViewEventListener viewEventListener, OnClickListener closeButtonClickListener,
+      OnClickListener microphoneButtonClickListener) {
+    this.viewEventListener = Optional.of(viewEventListener);
+    this.closeButtonClickListener = Optional.of(closeButtonClickListener);
+    this.microphoneButtonClickListener = Optional.of(microphoneButtonClickListener);
   }
 
-  void setSkinType(SkinType skinType) {
-    if (this.skinType == skinType) {
+  void setMicrophoneButtonEnabled(boolean enabled) {
+    isMicrophoneButtonEnabled = enabled;
+    if (isInflated()) {
+      getMicrophoneButton().setVisibility(enabled ? VISIBLE : GONE);
+    }
+  }
+
+  void setSkin(Skin skin) {
+    Preconditions.checkNotNull(skin);
+    if (this.skin.equals(skin)) {
       return;
     }
-
-    this.skinType = skinType;
-    mozcDrawableFactory.setSkinType(skinType);
+    this.skin = skin;
+    majorCategoryButtonDrawableFactory.setSkin(skin);
     if (!isInflated()) {
       return;
     }
+    updateSkinAwareDrawable();
+  }
 
-    // Reset the minor category tab, candidate view and major category buttons.
-    resetTabBackground();
-    resetCandidateViewPager();
-    resetMajorCategoryBackground();
+  @SuppressWarnings("deprecation")
+  private void updateSkinAwareDrawable() {
+    updateTabBackgroundSkin();
+    resetTabImageForMinorCategory();
+
+    SymbolTabWidgetViewPagerAdapter adapter =
+        SymbolTabWidgetViewPagerAdapter.class.cast(getCandidateViewPager().getAdapter());
+    if (adapter != null) {
+      adapter.setSkin(skin);
+    }
+    updateMajorCategoryBackgroundSkin();
+    updateMajorCategoryButtonsSkin();
+    updateMinorCategoryBackgroundSkin();
+    updateNumberKeyboardSkin();
+    updateSeparatorsSkin();
+    getMicrophoneButton().setSkin(skin);
+
+    TabWidget tabWidget = TabWidget.class.cast(findViewById(android.R.id.tabs));
+    for (int i = 0; i < tabWidget.getChildCount(); ++i) {
+      MozcImageView.class.cast(tabWidget.getChildTabViewAt(i)).setSkin(skin);
+    }
+
+    // Note delete button shouldn't be applied createMajorButtonBackgroundDrawable as background
+    // as it should show different background (same as minor categories).
+    for (int id : new int[] {R.id.symbol_view_close_button,
+                             R.id.symbol_view_enter_button}) {
+      MozcImageView view = MozcImageView.class.cast(findViewById(id));
+      view.setSkin(skin);
+      view.setBackgroundDrawable(createMajorButtonBackgroundDrawable(skin));
+    }
+    MozcImageView deleteKeyView =
+        MozcImageView.class.cast(findViewById(R.id.symbol_view_delete_button));
+    deleteKeyView.setSkin(skin);
+    deleteKeyView.setBackgroundDrawable(createMinorButtonBackgroundDrawable(skin));
+  }
+
+  private KeyboardView getNumberKeyboardView() {
+    return KeyboardView.class.cast(findViewById(R.id.number_keyboard));
+  }
+
+  private LinearLayout getMajorCategoryFrame() {
+    return LinearLayout.class.cast(findViewById(R.id.symbol_major_category));
+  }
+
+  private LinearLayout getMinorCategoryFrame() {
+    return LinearLayout.class.cast(findViewById(R.id.symbol_minor_category));
+  }
+
+  private TabWidget getTabWidget() {
+    return TabWidget.class.cast(findViewById(android.R.id.tabs));
+  }
+
+  private MozcImageView getMicrophoneButton() {
+    return MozcImageView.class.cast(findViewById(R.id.microphone_button));
+  }
+
+  @Override
+  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+    super.onSizeChanged(w, h, oldw, oldh);
+    // The boundary of Drawable instance which has been set as background
+    // is not updated automatically.
+    // Update the boundary below.
+    if (isInflated()) {
+      updateSkinAwareDrawable();
+    }
   }
 
   @Override
diff --git a/src/android/src/com/google/android/inputmethod/japanese/ViewEventDelegator.java b/src/android/src/com/google/android/inputmethod/japanese/ViewEventDelegator.java
index c98b2f0..36cfcd1 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/ViewEventDelegator.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/ViewEventDelegator.java
@@ -30,9 +30,9 @@
 package org.mozc.android.inputmethod.japanese;
 
 import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent;
-import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface;
 import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent;
@@ -41,6 +41,8 @@
 
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 /**
  * This class delegates all method calls to a ViewEventListener, passed to the constructor.
  * Typical usage is to hook/override some of listener's methods to change their behavior.
@@ -61,6 +63,7 @@
  */
 @SuppressWarnings("javadoc")
 public abstract class ViewEventDelegator implements ViewEventListener {
+
   private final ViewEventListener delegated;
 
   public ViewEventDelegator(ViewEventListener delegated) {
@@ -68,14 +71,15 @@
   }
 
   @Override
-  public void onKeyEvent(ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface keyEvent,
-                         KeyboardSpecification keyboardSpecification,
-                         List<? extends TouchEvent> touchEventList) {
+  public void onKeyEvent(@Nullable ProtoCommands.KeyEvent mozcKeyEvent,
+                         @Nullable KeyEventInterface keyEvent,
+                         @Nullable KeyboardSpecification keyboardSpecification,
+                         List<TouchEvent> touchEventList) {
     delegated.onKeyEvent(mozcKeyEvent, keyEvent, keyboardSpecification, touchEventList);
   }
 
   @Override
-  public void onUndo(List<? extends TouchEvent> touchEventList) {
+  public void onUndo(List<TouchEvent> touchEventList) {
     delegated.onUndo(touchEventList);
   }
 
@@ -85,6 +89,16 @@
   }
 
   @Override
+  public void onPageUp() {
+    delegated.onPageUp();
+  }
+
+  @Override
+  public void onPageDown() {
+    delegated.onPageDown();
+  }
+
+  @Override
   public void onSymbolCandidateSelected(SymbolMajorCategory majorCategory, String candidate,
                                         boolean updateHistory) {
     delegated.onSymbolCandidateSelected(majorCategory, candidate, updateHistory);
@@ -106,12 +120,12 @@
   }
 
   @Override
-  public void onShowMenuDialog(List<? extends TouchEvent> touchEventList) {
+  public void onShowMenuDialog(List<TouchEvent> touchEventList) {
     delegated.onShowMenuDialog(touchEventList);
   }
 
   @Override
-  public void onShowSymbolInputView(List<? extends TouchEvent> touchEventList) {
+  public void onShowSymbolInputView(List<TouchEvent> touchEventList) {
     delegated.onShowSymbolInputView(touchEventList);
   }
 
@@ -129,4 +143,15 @@
   public void onActionKey() {
     delegated.onActionKey();
   }
+
+  @Override
+  public void onNarrowModeChanged(boolean newNarrowMode) {
+    delegated.onNarrowModeChanged(newNarrowMode);
+  }
+
+  @Override
+  public void onUpdateKeyboardLayoutAdjustment(
+      ViewManagerInterface.LayoutAdjustment layoutAdjustment) {
+    delegated.onUpdateKeyboardLayoutAdjustment(layoutAdjustment);
+  }
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/ViewEventListener.java b/src/android/src/com/google/android/inputmethod/japanese/ViewEventListener.java
index e43e4a6..c7cf441 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/ViewEventListener.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/ViewEventListener.java
@@ -30,9 +30,9 @@
 package org.mozc.android.inputmethod.japanese;
 
 import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent;
-import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface;
 import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent;
@@ -40,6 +40,8 @@
 
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 /**
  * Callback object for view evnets.
  *
@@ -54,16 +56,17 @@
    * @param touchEventList {@code TouchEvent} instances related to this key event for logging
    *        usage stats.
    */
-  public void onKeyEvent(ProtoCommands.KeyEvent mozcKeyEvent, KeyEventInterface keyEvent,
-                         KeyboardSpecification keyboardSpecification,
-                         List<? extends TouchEvent> touchEventList);
+  public void onKeyEvent(@Nullable ProtoCommands.KeyEvent mozcKeyEvent,
+                         @Nullable KeyEventInterface keyEvent,
+                         @Nullable KeyboardSpecification keyboardSpecification,
+                         List<TouchEvent> touchEventList);
 
   /**
    * Called when Undo is fired (by soft keyboard).
    * @param touchEventList {@code TouchEvent} instances related to this undo for logging
    *        usage stats.
    */
-  public void onUndo(List<? extends TouchEvent> touchEventList);
+  public void onUndo(List<TouchEvent> touchEventList);
 
   /**
    * Called when a conversion candidate is selected.
@@ -73,6 +76,12 @@
    */
   public void onConversionCandidateSelected(int candidateId, Optional<Integer> rowIndex);
 
+  /** Called when page down button is tapped. */
+  public void onPageUp();
+
+  /** Called when page down button is tapped. */
+  public void onPageDown();
+
   /**
    * Called when a candidate on symbol input view is selected.
    */
@@ -102,7 +111,7 @@
    *        for logging usage stats.
    */
   // TODO(matsuzakit): Rename. onFlushTouchEventStats ?
-  public void onShowMenuDialog(List<? extends TouchEvent> touchEventList);
+  public void onShowMenuDialog(List<TouchEvent> touchEventList);
 
   /**
    * Called when the symbol input view is shown.
@@ -110,7 +119,7 @@
    * @param touchEventList {@code TouchEvent} instances which is related to this event
    *        for logging usage stats.
    */
-  public void onShowSymbolInputView(List<? extends TouchEvent> touchEventList);
+  public void onShowSymbolInputView(List<TouchEvent> touchEventList);
 
   /**
    * Called when the symbol input view is closed.
@@ -127,4 +136,15 @@
    * Called when the key for editor action is pressed.
    */
   public void onActionKey();
+
+  /** Called when the narrow mode of the view is changed. */
+  public void onNarrowModeChanged(boolean newNarrowMode);
+
+  /**
+   * Called when the keyboard layout preference should be updated.
+   * <p>
+   * The visible keyboard will also be updated as the result through a callback object.
+   */
+  public void onUpdateKeyboardLayoutAdjustment(
+      ViewManagerInterface.LayoutAdjustment layoutAdjustment);
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/ViewManager.java b/src/android/src/com/google/android/inputmethod/japanese/ViewManager.java
index ea0f2b8..a3927cc 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/ViewManager.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/ViewManager.java
@@ -30,18 +30,24 @@
 package org.mozc.android.inputmethod.japanese;
 
 import org.mozc.android.inputmethod.japanese.FeedbackManager.FeedbackEvent;
-import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface;
 import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType;
 import org.mozc.android.inputmethod.japanese.emoji.EmojiUtil;
+import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard;
+import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyEntity;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyEventHandler;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyboardActionListener;
+import org.mozc.android.inputmethod.japanese.keyboard.KeyboardFactory;
 import org.mozc.android.inputmethod.japanese.keyboard.ProbableKeyEventGuesser;
 import org.mozc.android.inputmethod.japanese.model.JapaneseSoftwareKeyboardModel;
 import org.mozc.android.inputmethod.japanese.model.JapaneseSoftwareKeyboardModel.KeyboardMode;
 import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage;
 import org.mozc.android.inputmethod.japanese.model.SymbolCandidateStorage.SymbolHistoryStorage;
+import org.mozc.android.inputmethod.japanese.model.SymbolMajorCategory;
+import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.HardwareKeyMap;
 import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.InputStyle;
 import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.KeyboardLayout;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands;
@@ -52,11 +58,12 @@
 import org.mozc.android.inputmethod.japanese.ui.MenuDialog;
 import org.mozc.android.inputmethod.japanese.ui.MenuDialog.MenuDialogListener;
 import org.mozc.android.inputmethod.japanese.util.ImeSwitcherFactory.ImeSwitcher;
-import org.mozc.android.inputmethod.japanese.view.SkinType;
+import org.mozc.android.inputmethod.japanese.view.Skin;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -65,6 +72,7 @@
 import android.os.Build;
 import android.os.IBinder;
 import android.os.Looper;
+import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -72,6 +80,7 @@
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.view.Window;
+import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
 
 import java.util.Collections;
@@ -89,6 +98,7 @@
    * An small wrapper to inject keyboard view resizing when a user selects a candidate.
    */
   class ViewManagerEventListener extends ViewEventDelegator {
+
     ViewManagerEventListener(ViewEventListener delegated) {
       super(delegated);
     }
@@ -105,37 +115,32 @@
 
   /**
    * Converts S/W Keyboard's keycode to KeyEvent instance.
-   * Exposed as protected for testing purpose.
    */
-  protected void onKey(int primaryCode, List<? extends TouchEvent> touchEventList) {
-    if (primaryCode == keycodeCapslock ||
-        primaryCode == keycodeAlt) {
+  void onKey(int primaryCode, List<TouchEvent> touchEventList) {
+    if (primaryCode == keycodeCapslock || primaryCode == keycodeAlt) {
       // Ignore those key events because they are handled by KeyboardView,
       // but send touchEventList for logging usage stats.
-      if (eventListener != null) {
-        eventListener.onKeyEvent(null, null, null, touchEventList);
-      }
+      eventListener.onKeyEvent(null, null, null, touchEventList);
       return;
     }
 
     // Keyboard switch event.
-    if (primaryCode == keycodeChartypeToKana ||
-        primaryCode == keycodeChartypeTo123 ||
-        primaryCode == keycodeChartypeToAbc ||
-        primaryCode == keycodeChartypeToKana123 ||
-        primaryCode == keycodeChartypeToAbc123) {
+    if (primaryCode == keycodeChartypeToKana
+        || primaryCode == keycodeChartypeToAbc
+        || primaryCode == keycodeChartypeToAbc123) {
       if (primaryCode == keycodeChartypeToKana) {
         japaneseSoftwareKeyboardModel.setKeyboardMode(KeyboardMode.KANA);
       } else if (primaryCode == keycodeChartypeToAbc) {
         japaneseSoftwareKeyboardModel.setKeyboardMode(KeyboardMode.ALPHABET);
-      } else if (primaryCode == keycodeChartypeTo123 ||
-                 primaryCode == keycodeChartypeToKana123) {
-        japaneseSoftwareKeyboardModel.setKeyboardMode(KeyboardMode.KANA_NUMBER);
       } else if (primaryCode == keycodeChartypeToAbc123) {
         japaneseSoftwareKeyboardModel.setKeyboardMode(KeyboardMode.ALPHABET_NUMBER);
       }
-      setJapaneseKeyboard(
-          japaneseSoftwareKeyboardModel.getKeyboardSpecification(), touchEventList);
+      propagateSoftwareKeyboardChange(touchEventList);
+      return;
+    }
+
+    if (primaryCode == keycodeGlobe) {
+      imeSwitcher.switchToNextInputMethod(false);
       return;
     }
 
@@ -144,9 +149,7 @@
       if (mozcView != null) {
         mozcView.resetKeyboardViewState();
       }
-      if (eventListener != null) {
-        eventListener.onShowMenuDialog(touchEventList);
-      }
+      eventListener.onShowMenuDialog(touchEventList);
       if (primaryCode == keycodeMenuDialog) {
         showMenuDialog();
       } else if (primaryCode == keycodeImePickerDialog) {
@@ -156,34 +159,30 @@
     }
 
     if (primaryCode == keycodeSymbol) {
-      if (eventListener != null) {
-        eventListener.onSubmitPreedit();
-      }
       if (mozcView != null) {
-        mozcView.resetKeyboardViewState();
-        mozcView.showSymbolInputView();
-        if (eventListener != null) {
-          eventListener.onShowSymbolInputView(touchEventList);
-        }
+        mozcView.showSymbolInputView(Optional.<SymbolMajorCategory>absent());
+      }
+      return;
+    }
+
+    if (primaryCode == keycodeSymbolEmoji) {
+      if (mozcView != null) {
+        mozcView.showSymbolInputView(Optional.of(SymbolMajorCategory.EMOJI));
       }
       return;
     }
 
     if (primaryCode == keycodeUndo) {
-      if (eventListener != null) {
-        eventListener.onUndo(touchEventList);
-      }
+      eventListener.onUndo(touchEventList);
       return;
     }
 
-    ProtoCommands.KeyEvent mozcKeyEvent =
+    Optional<ProtoCommands.KeyEvent> mozcKeyEvent =
         primaryKeyCodeConverter.createMozcKeyEvent(primaryCode, touchEventList);
-    if (eventListener != null) {
-      eventListener.onKeyEvent(mozcKeyEvent,
-                               primaryKeyCodeConverter.getPrimaryCodeKeyEvent(primaryCode),
-                               japaneseSoftwareKeyboardModel.getKeyboardSpecification(),
-                               touchEventList);
-    }
+    eventListener.onKeyEvent(mozcKeyEvent.orNull(),
+                             primaryKeyCodeConverter.getPrimaryCodeKeyEvent(primaryCode),
+                             getActiveSoftwareKeyboardModel().getKeyboardSpecification(),
+                             touchEventList);
   }
 
   /**
@@ -196,13 +195,13 @@
     }
 
     @Override
-    public void onKey(int primaryCode, List<? extends TouchEvent> touchEventList) {
+    public void onKey(int primaryCode, List<TouchEvent> touchEventList) {
       ViewManager.this.onKey(primaryCode, touchEventList);
     }
 
     @Override
     public void onPress(int primaryCode) {
-      if (eventListener != null && primaryCode != KeyEntity.INVALID_KEY_CODE) {
+      if (primaryCode != KeyEntity.INVALID_KEY_CODE) {
         eventListener.onFireFeedbackEvent(FeedbackEvent.KEY_DOWN);
       }
     }
@@ -212,6 +211,92 @@
     }
   }
 
+  @VisibleForTesting class ViewLayerEventHandler {
+    private static final int NEXUS_KEYBOARD_VENDOR_ID = 0x0D62;
+    private static final int NEXUS_KEYBOARD_PRODUCT_ID = 0x160B;
+    private boolean isEmojiKeyDownAvailable = false;
+    private boolean isEmojiInvoking = false;
+    private int pressedKeyNum = 0;
+    @VisibleForTesting boolean disableDeviceCheck = false;
+
+    @SuppressLint("NewApi")
+    private boolean hasPhysicalEmojiKey(KeyEvent event) {
+      InputDevice device = InputDevice.getDevice(event.getDeviceId());
+      return disableDeviceCheck
+          || (Build.VERSION.SDK_INT >= 19
+              && device != null
+              && device.getVendorId() == NEXUS_KEYBOARD_VENDOR_ID
+              && device.getProductId() == NEXUS_KEYBOARD_PRODUCT_ID);
+    }
+
+    private boolean isEmojiKey(KeyEvent event) {
+      if (!hasPhysicalEmojiKey(event)) {
+        return false;
+      }
+      if (event.getKeyCode() != KeyEvent.KEYCODE_ALT_LEFT
+          && event.getKeyCode() != KeyEvent.KEYCODE_ALT_RIGHT) {
+        return false;
+      }
+      if (event.getAction() == KeyEvent.ACTION_UP) {
+        return event.hasNoModifiers();
+      } else {
+        return event.hasModifiers(KeyEvent.META_ALT_ON);
+      }
+    }
+
+    public boolean evaluateKeyEvent(KeyEvent event) {
+      Preconditions.checkNotNull(event);
+      if (event.getAction() == KeyEvent.ACTION_DOWN) {
+        ++pressedKeyNum;
+      } else if (event.getAction() == KeyEvent.ACTION_UP) {
+        pressedKeyNum = Math.max(0, pressedKeyNum - 1);
+      } else {
+        return false;
+      }
+
+      if (isEmojiKey(event)) {
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {
+          isEmojiKeyDownAvailable = true;
+          isEmojiInvoking = false;
+        } else if (isEmojiKeyDownAvailable && pressedKeyNum == 0) {
+          isEmojiKeyDownAvailable = false;
+          isEmojiInvoking = true;
+        }
+      } else {
+        isEmojiKeyDownAvailable = false;
+        isEmojiInvoking = false;
+      }
+      return isEmojiInvoking;
+    }
+
+    public void invoke() {
+      if (!isEmojiInvoking) {
+        return;
+      }
+      isEmojiInvoking = false;
+      if (mozcView != null) {
+        if (isSymbolInputViewVisible) {
+          mozcView.hideSymbolInputView();
+          if (!isNarrowMode()) {
+            setNarrowMode(true);
+          }
+        } else {
+          isSymbolInputViewShownByEmojiKey = true;
+          if (isNarrowMode()) {
+            setNarrowMode(false);
+          }
+          mozcView.showSymbolInputView(Optional.of(SymbolMajorCategory.EMOJI));
+        }
+      }
+    }
+
+    public void reset() {
+      isEmojiKeyDownAvailable = false;
+      isEmojiInvoking = false;
+      pressedKeyNum = 0;
+    }
+  }
+
   // Registered by the user (typically MozcService)
   @VisibleForTesting final ViewEventListener eventListener;
 
@@ -225,38 +310,83 @@
   // IME switcher instance to detect that voice input is available or not.
   private final ImeSwitcher imeSwitcher;
 
-  // Called back by keyboards.
+  /** Key event handler to handle events on Mozc server. */
   private final KeyEventHandler keyEventHandler;
 
-  // Model to represent the current software keyboard state.
-  @VisibleForTesting final JapaneseSoftwareKeyboardModel japaneseSoftwareKeyboardModel =
+  /** Key event handler to handle events on view layer. */
+  @VisibleForTesting final ViewLayerEventHandler viewLayerKeyEventHandler =
+      new ViewLayerEventHandler();
+
+  /**
+   * Model to represent the current software keyboard state.
+   * All the setter methods don't affect symbolNumberSoftwareKeyboardModel but
+   * japaneseSoftwareKeyboardModel.
+   */
+  private final JapaneseSoftwareKeyboardModel japaneseSoftwareKeyboardModel =
+      new JapaneseSoftwareKeyboardModel();
+  /**
+   * Model to represent the number software keyboard state.
+   * Its keyboard mode is set in the constructor to KeyboardMode.SYMBOL_NUMBER and will never be
+   * changed.
+   */
+  private final JapaneseSoftwareKeyboardModel symbolNumberSoftwareKeyboardModel =
       new JapaneseSoftwareKeyboardModel();
 
-  // The factory of parsed keyboard data.
-  private final JapaneseKeyboardFactory japaneseKeyboardFactory = new JapaneseKeyboardFactory();
+  @VisibleForTesting final HardwareKeyboard hardwareKeyboard;
+
+  /** True if symbol input view is visible. */
+  private boolean isSymbolInputViewVisible;
+
+  /** True if symbol input view is shown by the Emoji key on physical keyboard. */
+  private boolean isSymbolInputViewShownByEmojiKey;
+
+  /** The factory of parsed keyboard data. */
+  private final KeyboardFactory keyboardFactory = new KeyboardFactory();
 
   private final SymbolCandidateStorage symbolCandidateStorage;
 
-  // Current fullscreen mode
+  /** Current fullscreen mode */
   private boolean fullscreenMode = false;
 
-  // Current narrow mode
+  /** Current narrow mode */
   private boolean narrowMode = false;
 
-  // Current popup enabled state.
+  /** Current popup enabled state. */
   private boolean popupEnabled = true;
 
-  // Current voice input allowed state.
-  private boolean voiceInputAllowed = false;
+  /** Current Globe button enabled state. */
+  private boolean globeButtonEnabled = false;
+
+  /** True if CursorAnchorInfo is enabled. */
+  private boolean cursorAnchroInfoEnabled = false;
+
+  /** True if hardware keyboard exists. */
+  private boolean hardwareKeyboardExist = false;
+
+  /**
+   * True if voice input is eligible.
+   * <p>
+   * This conditions is calculated based on following conditions.
+   * <ul>
+   * <li>VoiceIME's status: If VoiceIME is not available, this flag becomes false.
+   * <li>EditorInfo: If current editor does not want to use voice input, this flag becomes false.
+   *   <ul>
+   *   <li>Voice input might be explicitly forbidden by the editor.
+   *   <li>Voice input should be useless for the number input editors.
+   *   <li>Voice input should be useless for password field.
+   *   <ul>
+   * </ul>
+   */
+  private boolean isVoiceInputEligible = false;
+
+  private boolean isVoiceInputEnabledByPreference = true;
 
   private int flickSensitivity = 0;
 
-  private CompositionMode hardwareCompositionMode = CompositionMode.HIRAGANA;
-
   @VisibleForTesting EmojiProviderType emojiProviderType = EmojiProviderType.NONE;
 
   /** Current skin type. */
-  private SkinType skinType = SkinType.ORANGE_LIGHTGRAY;
+  private Skin skin = Skin.getFallbackInstance();
 
   private LayoutAdjustment layoutAdjustment = LayoutAdjustment.FILL;
 
@@ -269,47 +399,51 @@
   // but such name like "KEYCODE_LEFT" makes Lint unhappy
   // because they are not "static final".
   private final int keycodeChartypeToKana;
-  private final int keycodeChartypeTo123;
   private final int keycodeChartypeToAbc;
-  private final int keycodeChartypeToKana123;
   private final int keycodeChartypeToAbc123;
+  private final int keycodeGlobe;
   private final int keycodeSymbol;
+  private final int keycodeSymbolEmoji;
   private final int keycodeUndo;
   private final int keycodeCapslock;
   private final int keycodeAlt;
   private final int keycodeMenuDialog;
   private final int keycodeImePickerDialog;
 
-  // Handles software keyboard event and sends it to the service.
+  /** Handles software keyboard event and sends it to the service. */
   private final KeyboardActionAdapter keyboardActionListener;
 
   private final PrimaryKeyCodeConverter primaryKeyCodeConverter;
 
-  public ViewManager(Context context, final ViewEventListener listener,
+  public ViewManager(Context context, ViewEventListener listener,
                      SymbolHistoryStorage symbolHistoryStorage, ImeSwitcher imeSwitcher,
                      MenuDialogListener menuDialogListener) {
     this(context, listener, symbolHistoryStorage, imeSwitcher, menuDialogListener,
-         new ProbableKeyEventGuesser(context.getAssets()));
+         new ProbableKeyEventGuesser(context.getAssets()), new HardwareKeyboard());
   }
 
   @VisibleForTesting
   ViewManager(Context context, ViewEventListener listener,
               SymbolHistoryStorage symbolHistoryStorage, ImeSwitcher imeSwitcher,
-              @Nullable MenuDialogListener menuDialogListener, ProbableKeyEventGuesser guesser) {
+              @Nullable MenuDialogListener menuDialogListener, ProbableKeyEventGuesser guesser,
+              HardwareKeyboard hardwareKeyboard) {
     Preconditions.checkNotNull(context);
     Preconditions.checkNotNull(listener);
     Preconditions.checkNotNull(imeSwitcher);
+    Preconditions.checkNotNull(hardwareKeyboard);
 
     primaryKeyCodeConverter = new PrimaryKeyCodeConverter(context, guesser);
 
+    symbolNumberSoftwareKeyboardModel.setKeyboardMode(KeyboardMode.SYMBOL_NUMBER);
+
     // Prefetch keycodes from resource
     Resources res = context.getResources();
     keycodeChartypeToKana = res.getInteger(R.integer.key_chartype_to_kana);
-    keycodeChartypeTo123 = res.getInteger(R.integer.key_chartype_to_123);
     keycodeChartypeToAbc = res.getInteger(R.integer.key_chartype_to_abc);
-    keycodeChartypeToKana123 = res.getInteger(R.integer.key_chartype_to_kana_123);
     keycodeChartypeToAbc123 = res.getInteger(R.integer.key_chartype_to_abc_123);
+    keycodeGlobe = res.getInteger(R.integer.key_globe);
     keycodeSymbol = res.getInteger(R.integer.key_symbol);
+    keycodeSymbolEmoji = res.getInteger(R.integer.key_symbol_emoji);
     keycodeUndo = res.getInteger(R.integer.key_undo);
     keycodeCapslock = res.getInteger(R.integer.key_capslock);
     keycodeAlt = res.getInteger(R.integer.key_alt);
@@ -330,6 +464,7 @@
     this.imeSwitcher = imeSwitcher;
     this.menuDialogListener = menuDialogListener;
     this.symbolCandidateStorage = new SymbolCandidateStorage(symbolHistoryStorage);
+    this.hardwareKeyboard = hardwareKeyboard;
   }
 
   /**
@@ -356,35 +491,46 @@
     // until all the updates done in this method are finished. Just in case.
     mozcView.setVisibility(View.GONE);
     mozcView.setKeyboardHeightRatio(keyboardHeightRatio);
+    mozcView.setCursorAnchorInfoEnabled(cursorAnchroInfoEnabled);
+    OnClickListener widenButtonClickListener = new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        eventListener.onFireFeedbackEvent(FeedbackEvent.NARROW_FRAME_WIDEN_BUTTON_DOWN);
+        setNarrowMode(!narrowMode);
+      }
+    };
+    OnClickListener leftAdjustButtonClickListener = new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        eventListener.onUpdateKeyboardLayoutAdjustment(LayoutAdjustment.LEFT);
+      }
+    };
+    OnClickListener rightAdjustButtonClickListener = new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        eventListener.onUpdateKeyboardLayoutAdjustment(LayoutAdjustment.RIGHT);
+      }
+    };
+
+    OnClickListener microphoneButtonClickListener = new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        eventListener.onFireFeedbackEvent(FeedbackEvent.MICROPHONE_BUTTON_DOWN);
+        imeSwitcher.switchToVoiceIme("ja-jp");
+      }
+    };
     mozcView.setEventListener(
         eventListener,
-        new OnClickListener() {
-          @Override
-          public void onClick(View v) {
-            setNarrowMode(!narrowMode);
-          }
-        },
+        widenButtonClickListener,
         // User pushes these buttons to move position in order to see hidden text in editing rather
         // than to change his/her favorite position. So we should not apply it to preferences.
-        new OnClickListener() {
-          @Override
-          public void onClick(View v) {
-            setLayoutAdjustment(v.getContext().getResources(), LayoutAdjustment.LEFT);
-            mozcView.startLayoutAdjustmentAnimation();
-          }
-        },
-        new OnClickListener() {
-          @Override
-          public void onClick(View v) {
-            setLayoutAdjustment(v.getContext().getResources(), LayoutAdjustment.RIGHT);
-            mozcView.startLayoutAdjustmentAnimation();
-          }
-        });
+        leftAdjustButtonClickListener,
+        rightAdjustButtonClickListener,
+        microphoneButtonClickListener);
 
     mozcView.setKeyEventHandler(keyEventHandler);
 
-    setJapaneseKeyboard(japaneseSoftwareKeyboardModel.getKeyboardSpecification(),
-                        Collections.<TouchEvent>emptyList());
+    propagateSoftwareKeyboardChange(Collections.<TouchEvent>emptyList());
     mozcView.setFullscreenMode(fullscreenMode);
     mozcView.setLayoutAdjustmentAndNarrowMode(layoutAdjustment, narrowMode);
     // At the moment, it is necessary to set the storage to the view, *before* setting emoji
@@ -392,10 +538,9 @@
     // TODO(hidehiko): Remove the restriction.
     mozcView.setSymbolCandidateStorage(symbolCandidateStorage);
     mozcView.setEmojiProviderType(emojiProviderType);
-    mozcView.setHardwareCompositionButtonImage(hardwareCompositionMode);
     mozcView.setPopupEnabled(popupEnabled);
     mozcView.setFlickSensitivity(flickSensitivity);
-    mozcView.setSkinType(skinType);
+    mozcView.setSkin(skin);
 
     // Clear the menu dialog.
     menuDialog = null;
@@ -412,9 +557,7 @@
       return;
     }
 
-    boolean voiceInputEnabled = voiceInputAllowed && imeSwitcher.isVoiceImeAvailable();
-    menuDialog = new MenuDialog(
-        mozcView.getContext(), Optional.fromNullable(menuDialogListener), voiceInputEnabled);
+    menuDialog = new MenuDialog(mozcView.getContext(), Optional.fromNullable(menuDialogListener));
     IBinder windowToken = mozcView.getWindowToken();
     if (windowToken == null) {
       MozcLog.w("Unknown window token");
@@ -454,8 +597,8 @@
     if (mozcView == null) {
       return;
     }
-    if (outCommand.getOutput().getAllCandidateWords().getCandidatesCount() == 0 &&
-        !outCommand.getInput().getRequestSuggestion()) {
+    if (outCommand.getOutput().getAllCandidateWords().getCandidatesCount() == 0
+        && !outCommand.getInput().getRequestSuggestion()) {
       // The server doesn't return the suggestion result, because there is following
       // key sequence, which will trigger the suggest and the new suggestion will overwrite
       // the current suggest. In order to avoid chattering the candidate window,
@@ -475,26 +618,32 @@
    * @return the current keyboard specification.
    */
   @Override
-  public KeyboardSpecification getJapaneseKeyboardSpecification() {
-    return japaneseSoftwareKeyboardModel.getKeyboardSpecification();
+  public KeyboardSpecification getKeyboardSpecification() {
+    return getActiveSoftwareKeyboardModel().getKeyboardSpecification();
   }
 
-  /**
-   * Set {@code EditorInfo} instance to the current view.
-   */
+  /** Set {@code EditorInfo} instance to the current view. */
   @Override
   public void setEditorInfo(EditorInfo attribute) {
-    mozcView.setEmojiEnabled(
-        EmojiUtil.isUnicodeEmojiAvailable(Build.VERSION.SDK_INT),
-        EmojiUtil.isCarrierEmojiAllowed(attribute));
-    mozcView.setPasswordField(MozcUtil.isPasswordField(attribute));
-    mozcView.setEditorInfo(attribute);
-    voiceInputAllowed = MozcUtil.isVoiceInputAllowed(attribute);
+    if (mozcView != null) {
+      mozcView.setEmojiEnabled(
+          EmojiUtil.isUnicodeEmojiAvailable(Build.VERSION.SDK_INT),
+          EmojiUtil.isCarrierEmojiAllowed(attribute));
+      mozcView.setPasswordField(MozcUtil.isPasswordField(attribute.inputType));
+      mozcView.setEditorInfo(attribute);
+    }
+    isVoiceInputEligible = MozcUtil.isVoiceInputPreferred(attribute);
 
     japaneseSoftwareKeyboardModel.setInputType(attribute.inputType);
-    setJapaneseKeyboard(
-        japaneseSoftwareKeyboardModel.getKeyboardSpecification(),
-        Collections.<TouchEvent>emptyList());
+    // TODO(hsumita): Set input type on Hardware keyboard, too. Otherwise, Hiragana input can be
+    //                enabled unexpectedly. (e.g. Number text field.)
+    propagateSoftwareKeyboardChange(Collections.<TouchEvent>emptyList());
+  }
+
+  private boolean shouldVoiceImeBeEnabled() {
+    // Disable voice IME if hardware keyboard exists to avoid a framework bug.
+    return isVoiceInputEligible && isVoiceInputEnabledByPreference && !hardwareKeyboardExist
+        && imeSwitcher.isVoiceImeAvailable();
   }
 
   @Override
@@ -509,6 +658,12 @@
     }
     MozcView mozcView = this.mozcView;
 
+    if (isSymbolInputViewShownByEmojiKey) {
+      setNarrowMode(true);
+      mozcView.hideSymbolInputView();
+      return true;
+    }
+
     // Try to hide a sub view from front to back.
     if (mozcView.hideSymbolInputView()) {
       return true;
@@ -519,22 +674,66 @@
 
   /**
    * Creates and sets a keyboard represented by the resource id to the input frame.
-   *
+   * <p>
    * Note that this method requires inputFrameView is not null, and its first child is
    * the JapaneseKeyboardView.
-   * @param specification Keyboard specification for the next
    */
-  private void setJapaneseKeyboard(
-      KeyboardSpecification specification, List<? extends TouchEvent> touchEventList) {
-    eventListener.onKeyEvent(null, null, specification, touchEventList);
-    if (mozcView != null) {
-      Rect size = mozcView.getKeyboardSize();
-      JapaneseKeyboard japaneseKeyboard =
-          japaneseKeyboardFactory.get(mozcView.getResources(), specification,
-                                      size.width(), size.height());
-      mozcView.setJapaneseKeyboard(japaneseKeyboard);
-      primaryKeyCodeConverter.setJapaneseKeyboard(japaneseKeyboard);
+  private void updateKeyboardView() {
+    if (mozcView == null) {
+      return;
     }
+    Rect size = mozcView.getKeyboardSize();
+    Keyboard keyboard = keyboardFactory.get(
+        mozcView.getResources(), japaneseSoftwareKeyboardModel.getKeyboardSpecification(),
+        size.width(), size.height());
+    mozcView.setKeyboard(keyboard);
+    primaryKeyCodeConverter.setKeyboard(keyboard);
+  }
+
+  /**
+   * Propagates the change of S/W keyboard to the view layer and the H/W keyboard configuration.
+   */
+  private void propagateSoftwareKeyboardChange(List<TouchEvent> touchEventList) {
+    KeyboardSpecification specification = japaneseSoftwareKeyboardModel.getKeyboardSpecification();
+
+    // TODO(team): The purpose of the following call of onKeyEvent() is to tell the change of
+    // software keyboard specification to Mozc server through the event listener registered by
+    // MozcService. Obviously, calling onKeyEvent() for this purpose is abuse and should be fixed.
+    eventListener.onKeyEvent(null, null, specification, touchEventList);
+
+    // Update H/W keyboard specification to keep a consistency with S/W keyboard.
+    hardwareKeyboard.setCompositionMode(
+        specification.getCompositionMode() == CompositionMode.HIRAGANA
+        ? CompositionSwitchMode.KANA : CompositionSwitchMode.ALPHABET);
+
+    updateKeyboardView();
+  }
+
+  private void propagateHardwareKeyboardChange() {
+    propagateHardwareKeyboardChangeAndSendKey(null);
+  }
+
+  /**
+   * Propagates the change of S/W keyboard to the view layer and the H/W keyboard configuration, and
+   * the send key event to Mozc server.
+   */
+  private void propagateHardwareKeyboardChangeAndSendKey(@Nullable KeyEvent event) {
+    KeyboardSpecification specification = hardwareKeyboard.getKeyboardSpecification();
+
+    if (event == null) {
+      eventListener.onKeyEvent(null, null, specification, Collections.<TouchEvent>emptyList());
+    } else {
+      eventListener.onKeyEvent(
+          hardwareKeyboard.getMozcKeyEvent(event), hardwareKeyboard.getKeyEventInterface(event),
+          specification, Collections.<TouchEvent>emptyList());
+    }
+
+    // Update S/W keyboard specification to keep a consistency with H/W keyboard.
+    japaneseSoftwareKeyboardModel.setKeyboardMode(
+        specification.getCompositionMode() == CompositionMode.HIRAGANA
+        ? KeyboardMode.KANA : KeyboardMode.ALPHABET);
+
+    updateKeyboardView();
   }
 
   /**
@@ -548,13 +747,11 @@
 
     if (japaneseSoftwareKeyboardModel.getKeyboardLayout() != keyboardLayout) {
       // If changed, clear the keyboard cache.
-      japaneseKeyboardFactory.clear();
+      keyboardFactory.clear();
     }
 
     japaneseSoftwareKeyboardModel.setKeyboardLayout(keyboardLayout);
-    setJapaneseKeyboard(
-        japaneseSoftwareKeyboardModel.getKeyboardSpecification(),
-        Collections.<TouchEvent>emptyList());
+    propagateSoftwareKeyboardChange(Collections.<TouchEvent>emptyList());
   }
 
   /**
@@ -569,26 +766,22 @@
 
     if (japaneseSoftwareKeyboardModel.getInputStyle() != inputStyle) {
       // If changed, clear the keyboard cache.
-      japaneseKeyboardFactory.clear();
+      keyboardFactory.clear();
     }
 
     japaneseSoftwareKeyboardModel.setInputStyle(inputStyle);
-    setJapaneseKeyboard(
-        japaneseSoftwareKeyboardModel.getKeyboardSpecification(),
-        Collections.<TouchEvent>emptyList());
+    propagateSoftwareKeyboardChange(Collections.<TouchEvent>emptyList());
   }
 
   @Override
   public void setQwertyLayoutForAlphabet(boolean qwertyLayoutForAlphabet) {
     if (japaneseSoftwareKeyboardModel.isQwertyLayoutForAlphabet() != qwertyLayoutForAlphabet) {
       // If changed, clear the keyboard cache.
-      japaneseKeyboardFactory.clear();
+      keyboardFactory.clear();
     }
 
     japaneseSoftwareKeyboardModel.setQwertyLayoutForAlphabet(qwertyLayoutForAlphabet);
-    setJapaneseKeyboard(
-        japaneseSoftwareKeyboardModel.getKeyboardSpecification(),
-        Collections.<TouchEvent>emptyList());
+    propagateSoftwareKeyboardChange(Collections.<TouchEvent>emptyList());
   }
 
   @Override
@@ -623,13 +816,41 @@
   }
 
   /**
-   * @param isNarrowMode Whether mozc view shows in narrow mode or normal.
+   * Updates whether Globe button should be enabled or not based on
+   * {@code InputMethodManager#shouldOfferSwitchingToNextInputMethod(IBinder)}
    */
   @Override
-  public void setNarrowMode(boolean isNarrowMode) {
-    this.narrowMode = isNarrowMode;
+  public void updateGlobeButtonEnabled() {
+    this.globeButtonEnabled = imeSwitcher.shouldOfferSwitchingToNextInputMethod();
     if (mozcView != null) {
-      mozcView.setLayoutAdjustmentAndNarrowMode(layoutAdjustment, isNarrowMode);
+      mozcView.setGlobeButtonEnabled(globeButtonEnabled);
+    }
+  }
+
+  /**
+   * Updates whether Microphone button should be enabled or not based on
+   * availability of voice input method.
+   */
+  @Override
+  public void updateMicrophoneButtonEnabled() {
+    if (mozcView != null) {
+      mozcView.setMicrophoneButtonEnabled(shouldVoiceImeBeEnabled());
+    }
+  }
+
+  /**
+   * @param newNarrowMode Whether mozc view shows in narrow mode or normal.
+   */
+  @Override
+  public void setNarrowMode(boolean newNarrowMode) {
+    boolean previousNarrowMode = this.narrowMode;
+    this.narrowMode = newNarrowMode;
+    if (mozcView != null) {
+      mozcView.setLayoutAdjustmentAndNarrowMode(layoutAdjustment, newNarrowMode);
+    }
+    updateMicrophoneButtonEnabled();
+    if (previousNarrowMode != newNarrowMode) {
+      eventListener.onNarrowModeChanged(newNarrowMode);
     }
   }
 
@@ -646,20 +867,19 @@
   @Override
   public void maybeTransitToNarrowMode(Command command, KeyEventInterface keyEventInterface) {
     Preconditions.checkNotNull(command);
-    // Surely we don't anthing when on narrow mode already.
+    // Surely we don't anything when on narrow mode already.
     if (isNarrowMode()) {
       return;
     }
     // Do nothing for the input from software keyboard.
-    if (keyEventInterface == null || keyEventInterface.getNativeEvent() == null) {
+    if (keyEventInterface == null || !keyEventInterface.getNativeEvent().isPresent()) {
       return;
     }
-    // Do nothing if the key event doesn't have printable character without modifier.
-    if (!command.getInput().hasKey()
-        || !command.getInput().getKey().hasKeyCode()
-        || command.getInput().getKey().hasModifiers()) {
+    // Do nothing if input doesn't have a key. (e.g. pure modifier key)
+    if (!command.getInput().hasKey()) {
       return;
     }
+
     // Passed all the check. Transit to narrow mode.
     hideSubInputView();
     setNarrowMode(true);
@@ -671,6 +891,11 @@
   }
 
   @Override
+  public boolean isFloatingCandidateMode() {
+    return mozcView != null && mozcView.isFloatingCandidateMode();
+  }
+
+  @Override
   public void setPopupEnabled(boolean popupEnabled) {
     this.popupEnabled = popupEnabled;
     if (mozcView != null) {
@@ -679,28 +904,53 @@
   }
 
   @Override
-  public void setHardwareKeyboardCompositionMode(CompositionMode compositionMode) {
-    hardwareCompositionMode = compositionMode;
-    if (mozcView != null) {
-      mozcView.setHardwareCompositionButtonImage(compositionMode);
+  public void switchHardwareKeyboardCompositionMode(CompositionSwitchMode mode) {
+    Preconditions.checkNotNull(mode);
+
+    CompositionMode oldMode = hardwareKeyboard.getCompositionMode();
+    hardwareKeyboard.setCompositionMode(mode);
+    CompositionMode newMode = hardwareKeyboard.getCompositionMode();
+    if (oldMode != newMode) {
+      propagateHardwareKeyboardChange();
     }
   }
 
   @Override
-  public void setSkinType(SkinType skinType) {
-    this.skinType = skinType;
+  public void setHardwareKeyMap(HardwareKeyMap hardwareKeyMap) {
+    hardwareKeyboard.setHardwareKeyMap(Preconditions.checkNotNull(hardwareKeyMap));
+  }
+
+  @Override
+  public void setSkin(Skin skin) {
+    this.skin = Preconditions.checkNotNull(skin);
     if (mozcView != null) {
-      mozcView.setSkinType(skinType);
+      mozcView.setSkin(skin);
     }
   }
 
   @Override
-  public void setLayoutAdjustment(Resources resources, LayoutAdjustment layoutAdjustment) {
-    this.layoutAdjustment = layoutAdjustment;
+  public void setMicrophoneButtonEnabledByPreference(boolean microphoneButtonEnabled) {
+    this.isVoiceInputEnabledByPreference = microphoneButtonEnabled;
+    updateMicrophoneButtonEnabled();
+  }
 
+  /**
+   * Set layout adjustment and show animation if required.
+   * <p>
+   * Note that this method does *NOT* update SharedPreference.
+   * If you want to update it, use ViewEventListener#onUpdateKeyboardLayoutAdjustment(),
+   * which updates SharedPreference and indirectly calls this method.
+   */
+  @Override
+  public void setLayoutAdjustment(LayoutAdjustment layoutAdjustment) {
+    Preconditions.checkNotNull(layoutAdjustment);
     if (mozcView != null) {
       mozcView.setLayoutAdjustmentAndNarrowMode(layoutAdjustment, narrowMode);
+      if (this.layoutAdjustment != layoutAdjustment) {
+        mozcView.startLayoutAdjustmentAnimation();
+      }
     }
+    this.layoutAdjustment = layoutAdjustment;
   }
 
   @Override
@@ -724,6 +974,8 @@
       mozcView.reset();
     }
 
+    viewLayerKeyEventHandler.reset();
+
     // Reset menu dialog.
     maybeDismissMenuDialog();
   }
@@ -771,16 +1023,33 @@
   @Override
   public void onConfigurationChanged(Configuration newConfig) {
     primaryKeyCodeConverter.setConfiguration(newConfig);
+    hardwareKeyboardExist = newConfig.keyboard != Configuration.KEYBOARD_NOKEYS;
   }
 
   @Override
   public boolean isKeyConsumedOnViewAsynchronously(KeyEvent event) {
-    return false;
+    return viewLayerKeyEventHandler.evaluateKeyEvent(Preconditions.checkNotNull(event));
   }
 
   @Override
   public void consumeKeyOnViewSynchronously(KeyEvent event) {
-    throw new IllegalArgumentException("ViewManager doesn't consume any key event.");
+    viewLayerKeyEventHandler.invoke();
+  }
+
+  @Override
+  public void onHardwareKeyEvent(KeyEvent event) {
+    // Maybe update the composition mode based on the event.
+    // For example, zen/han key toggles the composition mode (hiragana <--> alphabet).
+    CompositionMode compositionMode = hardwareKeyboard.getCompositionMode();
+    hardwareKeyboard.setCompositionModeByKey(event);
+    CompositionMode currentCompositionMode = hardwareKeyboard.getCompositionMode();
+    if (compositionMode != currentCompositionMode) {
+      propagateHardwareKeyboardChangeAndSendKey(event);
+    } else {
+      eventListener.onKeyEvent(
+          hardwareKeyboard.getMozcKeyEvent(event), hardwareKeyboard.getKeyEventInterface(event),
+          hardwareKeyboard.getKeyboardSpecification(), Collections.<TouchEvent>emptyList());
+    }
   }
 
   @Override
@@ -799,10 +1068,18 @@
     return eventListener;
   }
 
+  /**
+   * Returns active (shown) JapaneseSoftwareKeyboardModel.
+   * If symbol picker is shown, symbol-number keyboard's is returned.
+   */
   @VisibleForTesting
   @Override
-  public JapaneseSoftwareKeyboardModel getJapaneseSoftwareKeyboardModel() {
-    return japaneseSoftwareKeyboardModel;
+  public JapaneseSoftwareKeyboardModel getActiveSoftwareKeyboardModel() {
+    if (isSymbolInputViewVisible) {
+      return symbolNumberSoftwareKeyboardModel;
+    } else {
+      return japaneseSoftwareKeyboardModel;
+    }
   }
 
   @VisibleForTesting
@@ -825,8 +1102,14 @@
 
   @VisibleForTesting
   @Override
-  public SkinType getSkinType() {
-    return skinType;
+  public Skin getSkin() {
+    return skin;
+  }
+
+  @VisibleForTesting
+  @Override
+  public boolean isMicrophoneButtonEnabledByPreference() {
+    return isVoiceInputEnabledByPreference;
   }
 
   @VisibleForTesting
@@ -841,6 +1124,12 @@
     return keyboardHeightRatio;
   }
 
+  @VisibleForTesting
+  @Override
+  public HardwareKeyMap getHardwareKeyMap() {
+    return hardwareKeyboard.getHardwareKeyMap();
+  }
+
   @Override
   public void trimMemory() {
     if (mozcView != null) {
@@ -852,4 +1141,31 @@
   public KeyboardActionListener getKeyboardActionListener() {
     return keyboardActionListener;
   }
+
+  @Override
+  public void setCursorAnchorInfo(CursorAnchorInfo cursorAnchorInfo) {
+    if (mozcView != null) {
+      mozcView.setCursorAnchorInfo(cursorAnchorInfo);
+    }
+  }
+
+  @Override
+  public void setCursorAnchorInfoEnabled(boolean enabled) {
+    this.cursorAnchroInfoEnabled = enabled;
+    if (mozcView != null) {
+      mozcView.setCursorAnchorInfoEnabled(enabled);
+    }
+  }
+
+  @Override
+  public void onShowSymbolInputView() {
+    isSymbolInputViewVisible = true;
+    mozcView.resetKeyboardViewState();
+  }
+
+  @Override
+  public void onCloseSymbolInputView() {
+    isSymbolInputViewVisible = false;
+    isSymbolInputViewShownByEmojiKey = false;
+  }
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/ViewManagerInterface.java b/src/android/src/com/google/android/inputmethod/japanese/ViewManagerInterface.java
index bac7918..efc12b4 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/ViewManagerInterface.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/ViewManagerInterface.java
@@ -29,26 +29,27 @@
 
 package org.mozc.android.inputmethod.japanese;
 
-import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface;
 import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType;
+import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyboardActionListener;
 import org.mozc.android.inputmethod.japanese.model.JapaneseSoftwareKeyboardModel;
+import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.HardwareKeyMap;
 import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.InputStyle;
 import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.KeyboardLayout;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Command;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
-import org.mozc.android.inputmethod.japanese.view.SkinType;
+import org.mozc.android.inputmethod.japanese.view.Skin;
 import com.google.common.annotations.VisibleForTesting;
 
 import android.content.Context;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.inputmethodservice.InputMethodService;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.Window;
+import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
 
 /**
@@ -98,6 +99,8 @@
    */
   public void consumeKeyOnViewSynchronously(KeyEvent event);
 
+  public void onHardwareKeyEvent(KeyEvent keyEvent);
+
   /**
    * @return whether the view should consume the generic motion event or not.
    */
@@ -113,7 +116,7 @@
   /**
    * @return the current keyboard specification.
    */
-  public KeyboardSpecification getJapaneseKeyboardSpecification();
+  public KeyboardSpecification getKeyboardSpecification();
 
   /**
    * Set {@code EditorInfo} instance to the current view.
@@ -163,18 +166,28 @@
 
   public boolean isNarrowMode();
 
+  public boolean isFloatingCandidateMode();
+
   public void setPopupEnabled(boolean popupEnabled);
 
-  public void setHardwareKeyboardCompositionMode(CompositionMode compositionMode);
+  public void switchHardwareKeyboardCompositionMode(CompositionSwitchMode mode);
 
-  public void setSkinType(SkinType skinType);
+  public void setHardwareKeyMap(HardwareKeyMap hardwareKeyMap);
 
-  public void setLayoutAdjustment(Resources resources, LayoutAdjustment layoutAdjustment);
+  public void setSkin(Skin skin);
+
+  public void setMicrophoneButtonEnabledByPreference(boolean microphoneButtonEnabled);
+
+  public void setLayoutAdjustment(LayoutAdjustment layoutAdjustment);
 
   public void setKeyboardHeightRatio(int keyboardHeightRatio);
 
   public void onConfigurationChanged(Configuration newConfig);
 
+  public void setCursorAnchorInfo(CursorAnchorInfo info);
+
+  public void setCursorAnchorInfoEnabled(boolean enabled);
+
   /**
    * Reset the status of the current input view.
    */
@@ -183,11 +196,14 @@
   public void computeInsets(
       Context context, InputMethodService.Insets outInsets, Window window);
 
+  public void onShowSymbolInputView();
+  public void onCloseSymbolInputView();
+
   @VisibleForTesting
   public ViewEventListener getEventListener();
 
   @VisibleForTesting
-  public JapaneseSoftwareKeyboardModel getJapaneseSoftwareKeyboardModel();
+  public JapaneseSoftwareKeyboardModel getActiveSoftwareKeyboardModel();
 
   @VisibleForTesting
   public boolean isPopupEnabled();
@@ -199,7 +215,10 @@
   public EmojiProviderType getEmojiProviderType();
 
   @VisibleForTesting
-  public SkinType getSkinType();
+  public Skin getSkin();
+
+  @VisibleForTesting
+  public boolean isMicrophoneButtonEnabledByPreference();
 
   @VisibleForTesting
   public LayoutAdjustment getLayoutAdjustment();
@@ -207,9 +226,16 @@
   @VisibleForTesting
   public int getKeyboardHeightRatio();
 
+  @VisibleForTesting
+  public HardwareKeyMap getHardwareKeyMap();
+
   /**
    * Used for testing to inject key events.
    */
   @VisibleForTesting
   public KeyboardActionListener getKeyboardActionListener();
+
+  void updateGlobeButtonEnabled();
+
+  void updateMicrophoneButtonEnabled();
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/accessibility/CandidateWindowAccessibilityNodeProvider.java b/src/android/src/com/google/android/inputmethod/japanese/accessibility/CandidateWindowAccessibilityNodeProvider.java
index cad8979..c4463fc 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/accessibility/CandidateWindowAccessibilityNodeProvider.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/accessibility/CandidateWindowAccessibilityNodeProvider.java
@@ -38,6 +38,7 @@
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -113,6 +114,7 @@
     return Optional.fromNullable(virtualViewIdToRow.get().get(virtualViewId));
   }
 
+  @SuppressLint("InlinedApi")
   private Optional<AccessibilityNodeInfoCompat> createNodeInfoForId(int virtualViewId) {
     Optional<Row> optionalRow = getRow(virtualViewId);
     if (!optionalRow.isPresent()) {
@@ -209,6 +211,9 @@
    * @param y vertical location in screen coordinate (pixel)
    */
   Optional<CandidateWord> getCandidateWord(int x, int y) {
+    if (!layout.isPresent()) {
+      return Optional.absent();
+    }
     for (Row row : layout.get().getRowList()) {
       if (y < row.getTop() || y >= row.getTop() + row.getHeight()) {
         continue;
diff --git a/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityDelegate.java b/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityDelegate.java
index 5d174d3..e4949a2 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityDelegate.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityDelegate.java
@@ -32,11 +32,9 @@
 import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction;
 import org.mozc.android.inputmethod.japanese.keyboard.Key;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyEntity;
-import org.mozc.android.inputmethod.japanese.keyboard.KeyEventHandler;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyState;
 import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState;
 import org.mozc.android.inputmethod.japanese.keyboard.Keyboard;
-import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent;
 import org.mozc.android.inputmethod.japanese.resources.R;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
@@ -71,8 +69,6 @@
   private final View view;
   private final KeyboardAccessibilityNodeProvider nodeProvider;
   private Optional<Key> lastHoverKey = Optional.absent();
-  // Handler which is called back when key-input should be simulated.
-  private Optional<KeyEventHandler> keyEventHandler = Optional.absent();
   // Handler for long-press callback.
   // Contains 0 or 1 delayed message.
   private final Handler handler;
@@ -84,6 +80,15 @@
   // In such case touch-up shouldn't send any key events.
   // Reset to false when new touch sequence is started.
   private boolean consumedByLongpress = false;
+  private final TouchEventEmulator emulator;
+
+  /**
+   * Emulator interface for touch events (Key input and long press).
+   */
+  public interface TouchEventEmulator {
+    public void emulateKeyInput(Key key);
+    public void emulateLongPress(Key key);
+  }
 
   private class LongTapHandler implements Handler.Callback {
     @Override
@@ -96,20 +101,23 @@
   }
 
 
-  public KeyboardAccessibilityDelegate(View view) {
+  public KeyboardAccessibilityDelegate(View view, TouchEventEmulator emulator) {
     this(view, new KeyboardAccessibilityNodeProvider(view),
          view.getContext().getResources().getInteger(
-             R.integer.config_long_press_key_delay_accessibility));
+             R.integer.config_long_press_key_delay_accessibility),
+         emulator);
   }
 
   @VisibleForTesting
   KeyboardAccessibilityDelegate(View view,
                                 KeyboardAccessibilityNodeProvider nodeProvider,
-                                int longpressDelay) {
+                                int longpressDelay,
+                                TouchEventEmulator emulator) {
     this.view = Preconditions.checkNotNull(view);
     this.nodeProvider = Preconditions.checkNotNull(nodeProvider);
     this.handler = new Handler(new LongTapHandler());
     this.longpressDelay = longpressDelay;
+    this.emulator = Preconditions.checkNotNull(emulator);
   }
 
   private Context getContext() {
@@ -210,13 +218,11 @@
     if (!keyState.isPresent()) {
       return;
     }
-    int keyCode = keyState.get().getFlick(Direction.CENTER).getKeyEntity().getKeyCode();
-    if (keyCode == KeyEntity.INVALID_KEY_CODE
-        || !keyEventHandler.isPresent()
-        || consumedByLongpress) {
+    int keyCode = keyState.get().getFlick(Direction.CENTER).get().getKeyEntity().getKeyCode();
+    if (keyCode == KeyEntity.INVALID_KEY_CODE || consumedByLongpress) {
       return;
     }
-    keyEventHandler.get().sendKey(keyCode, Collections.<TouchEvent>emptyList());
+    emulator.emulateKeyInput(key);
   }
 
   private void simulateLongPress(Key key) {
@@ -225,14 +231,12 @@
     if (!keyState.isPresent()) {
       return;
     }
-    int longPressKeyCode = keyState.get().getFlick(Direction.CENTER)
-                                   .getKeyEntity().getLongPressKeyCode();
-    if (longPressKeyCode == KeyEntity.INVALID_KEY_CODE
-        || !keyEventHandler.isPresent()
-        || consumedByLongpress) {
+    int longPressKeyCode =
+        keyState.get().getFlick(Direction.CENTER).get().getKeyEntity().getLongPressKeyCode();
+    if (longPressKeyCode == KeyEntity.INVALID_KEY_CODE || consumedByLongpress) {
       return;
     }
-    keyEventHandler.get().sendKey(longPressKeyCode, Collections.<TouchEvent>emptyList());
+    emulator.emulateLongPress(key);
     consumedByLongpress = true;
   }
 
@@ -355,8 +359,4 @@
     // Don't speak if the IME is connected to a password field.
     return isPasswordField;
   }
-
-  public void setKeyEventHandler(Optional<KeyEventHandler> keyEventHandler) {
-    this.keyEventHandler = Preconditions.checkNotNull(keyEventHandler);
-  }
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityNodeProvider.java b/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityNodeProvider.java
index 570e598..ab698dd 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityNodeProvider.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/accessibility/KeyboardAccessibilityNodeProvider.java
@@ -223,7 +223,8 @@
     if (key.isSpacer()) {
       return UNDEFINED;
     }
-    return getKeyState(key, metaState).getFlick(Direction.CENTER).getKeyEntity().getSourceId();
+    return getKeyState(key, metaState).getFlick(
+        Direction.CENTER).get().getKeyEntity().getSourceId();
   }
 
   private Optional<Integer> getKeyCode(Key key) {
@@ -232,7 +233,7 @@
       return Optional.absent();
     }
     return Optional.of(getKeyState(key, metaState).getFlick(
-        Direction.CENTER).getKeyEntity().getKeyCode());
+        Direction.CENTER).get().getKeyEntity().getKeyCode());
   }
 
   /**
diff --git a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboard.java b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboard.java
index 4fb8e5a..f0146f2 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboard.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboard.java
@@ -29,9 +29,9 @@
 
 package org.mozc.android.inputmethod.japanese.hardwarekeyboard;
 
-import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface;
 import org.mozc.android.inputmethod.japanese.MozcLog;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.HardwareKeyMap;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
diff --git a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboardSpecification.java b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboardSpecification.java
index 11e400c..7b4b23a 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboardSpecification.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/HardwareKeyboardSpecification.java
@@ -29,11 +29,11 @@
 
 package org.mozc.android.inputmethod.japanese.hardwarekeyboard;
 
-import org.mozc.android.inputmethod.japanese.JapaneseKeyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.KeycodeConverter.KeyEventInterface;
 import org.mozc.android.inputmethod.japanese.MozcLog;
 import org.mozc.android.inputmethod.japanese.hardwarekeyboard.HardwareKeyboard.CompositionSwitchMode;
 import org.mozc.android.inputmethod.japanese.hardwarekeyboard.KeyEventMapperFactory.KeyEventMapper;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.HardwareKeyMap;
 import org.mozc.android.inputmethod.japanese.preference.PreferenceUtil;
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands;
@@ -98,15 +98,7 @@
   JAPANESE109A(HardwareKeyMap.JAPANESE109A,
                KeyEventMapperFactory.JAPANESE_KEYBOARD_MAPPER,
                KeyboardSpecification.HARDWARE_QWERTY_KANA,
-               KeyboardSpecification.HARDWARE_QWERTY_ALPHABET),
-
-  /**
-   * Represents Japanese Mobile "12" Keyboard
-   */
-  TWELVEKEY(HardwareKeyMap.TWELVEKEY,
-            KeyEventMapperFactory.TWELVEKEY_KEYBOARD_MAPPER,
-            KeyboardSpecification.TWELVE_KEY_TOGGLE_KANA,
-            KeyboardSpecification.TWELVE_KEY_TOGGLE_ALPHABET);
+               KeyboardSpecification.HARDWARE_QWERTY_ALPHABET);
 
   /**
    * Returns true if the given {@code codepoint} is printable.
@@ -214,10 +206,8 @@
     HardwareKeyMap detectedKeyMap = null;
     switch(configuration.keyboard) {
       case Configuration.KEYBOARD_12KEY:
-        detectedKeyMap = HardwareKeyMap.TWELVEKEY;
-        break;
       case Configuration.KEYBOARD_QWERTY:
-        detectedKeyMap = HardwareKeyMap.JAPANESE109A;
+        detectedKeyMap = HardwareKeyMap.DEFAULT;
         break;
       case Configuration.KEYBOARD_NOKEYS:
       case Configuration.KEYBOARD_UNDEFINED:
@@ -477,8 +467,8 @@
       }
 
       @Override
-      public android.view.KeyEvent getNativeEvent() {
-        return keyEvent;
+      public Optional<android.view.KeyEvent> getNativeEvent() {
+        return Optional.of(keyEvent);
       }
     };
   }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/KeyEventMapperFactory.java b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/KeyEventMapperFactory.java
index 57bd6f5..71768d8 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/KeyEventMapperFactory.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/hardwarekeyboard/KeyEventMapperFactory.java
@@ -53,7 +53,6 @@
   }
 
   public static final KeyEventMapper DEFAULT_KEYBOARD_MAPPER = new DefaultKeyboardMapper();
-  public static final KeyEventMapper TWELVEKEY_KEYBOARD_MAPPER = new TwelvekeyKeyboardMapper();
   public static final KeyEventMapper JAPANESE_KEYBOARD_MAPPER = new JapaneseKeyboardMapper();
 
   /**
@@ -69,25 +68,6 @@
   }
 
   /**
-   * Similar to DefaultKeyboardMapper but additional conversion for POUND and CENTER keys.
-   */
-  private static class TwelvekeyKeyboardMapper implements KeyEventMapper {
-    static void doKeyCharacterMapping(CompactKeyEvent keyEvent) {
-      int originalKeyCode = keyEvent.getKeyCode();
-      if (originalKeyCode == KeyEvent.KEYCODE_POUND
-          || originalKeyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
-        keyEvent.setKeyCode(KeyEvent.KEYCODE_ENTER);
-      }
-    }
-    @Override
-    public void applyMapping(CompactKeyEvent keyEvent) {
-      keyEvent.setKeyCode(doKeyLayoutMappingForOldAndroids(keyEvent.getKeyCode(),
-                                                           keyEvent.getScanCode()));
-      doKeyCharacterMapping(keyEvent);
-    }
-  }
-
-  /**
    * Key-layout and key-character mapping for Japanese keyboard.
    */
   @SuppressLint("InlinedApi")
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/BackgroundDrawableFactory.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/BackgroundDrawableFactory.java
index 9414ce6..9074138 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/BackgroundDrawableFactory.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/BackgroundDrawableFactory.java
@@ -29,19 +29,25 @@
 
 package org.mozc.android.inputmethod.japanese.keyboard;
 
+import org.mozc.android.inputmethod.japanese.resources.R;
 import org.mozc.android.inputmethod.japanese.view.CandidateBackgroundDrawable;
 import org.mozc.android.inputmethod.japanese.view.CandidateBackgroundFocusedDrawable;
 import org.mozc.android.inputmethod.japanese.view.CenterCircularHighlightDrawable;
-import org.mozc.android.inputmethod.japanese.view.LightIconDrawable;
-import org.mozc.android.inputmethod.japanese.view.PopUpFrameWindowDrawable;
+import org.mozc.android.inputmethod.japanese.view.DummyDrawable;
+import org.mozc.android.inputmethod.japanese.view.QwertySpaceKeyDrawable;
 import org.mozc.android.inputmethod.japanese.view.RectKeyDrawable;
 import org.mozc.android.inputmethod.japanese.view.RoundRectKeyDrawable;
-import org.mozc.android.inputmethod.japanese.view.SkinType;
+import org.mozc.android.inputmethod.japanese.view.Skin;
 import org.mozc.android.inputmethod.japanese.view.ThreeDotsIconDrawable;
 import org.mozc.android.inputmethod.japanese.view.TriangularHighlightDrawable;
 import org.mozc.android.inputmethod.japanese.view.TriangularHighlightDrawable.HighlightDirection;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
 
+import android.content.res.Resources;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
 import android.graphics.drawable.LayerDrawable;
 import android.graphics.drawable.StateListDrawable;
 
@@ -83,8 +89,14 @@
     QWERTY_REGULAR_KEY_BACKGROUND,
     QWERTY_FUNCTION_KEY_BACKGROUND,
     QWERTY_FUNCTION_KEY_BACKGROUND_WITH_THREEDOTS,
-    QWERTY_FUNCTION_KEY_LIGHT_ON_BACKGROUND,
-    QWERTY_FUNCTION_KEY_LIGHT_OFF_BACKGROUND,
+    QWERTY_FUNCTION_KEY_SPACE_WITH_THREEDOTS,
+
+    // Separator on keyboard.
+    KEYBOARD_SEPARATOR_TOP,
+    KEYBOARD_SEPARATOR_CENTER,
+    KEYBOARD_SEPARATOR_BOTTOM,
+
+    TRNASPARENT,
 
     // Highlight for flicking.
     TWELVEKEYS_CENTER_FLICK,
@@ -104,366 +116,327 @@
 
   // According to the original design mock, the pressed key background contains some padding.
   // Here are hardcoded parameters.
-  private static final float TWELVEKEYS_LEFT_OFFSET = 0.5f;
-  private static final float TWELVEKEYS_TOP_OFFSET = 0;
-  private static final float TWELVEKEYS_RIGHT_OFFSET = 1.0f;
-  private static final float TWELVEKEYS_BOTTOM_OFFSET = 1.0f;
+  public static final float POPUP_WINDOW_PADDING = 7.0f;
+
   private static final float TWELVEKEYS_THREEDOTS_BOTTOM_OFFSET = 3.0f;
   private static final float TWELVEKEYS_THREEDOTS_RIGHT_OFFSET = 5.0f;
   private static final float TWELVEKEYS_THREEDOTS_WIDTH = 2.0f;
   private static final float TWELVEKEYS_THREEDOTS_SPAN = 2.0f;
 
-  private static final float QWERTY_LEFT_OFFSET = 2.0f;
-  private static final float QWERTY_TOP_OFFSET = 1.0f;
-  private static final float QWERTY_RIGHT_OFFSET = 2.0f;
-  private static final float QWERTY_BOTTOM_OFFSET = 3.0f;
   private static final float QWERTY_THREEDOTS_BOTTOM_OFFSET = 3.0f;
   private static final float QWERTY_THREEDOTS_RIGHT_OFFSET = 5.0f;
   private static final float QWERTY_THREEDOTS_WIDTH = 2.0f;
   private static final float QWERTY_THREEDOTS_SPAN = 2.0f;
-  private static final float QWERTY_LIGHT_TOP_OFFSET = 2.0f;
-  private static final float QWERTY_LIGHT_RIGHT_OFFSET = 2.0f;
-  private static final float QWERTY_LIGHT_RADIUS = 2.25f;
-
-  private static final float POPUP_WINDOW_PADDING = 7.0f;
-  private static final float POPUP_FRAME_BORDER_WIDTH = 5.0f;
-  private static final float POPUP_SHADOW_SIZE = 1.5f;
 
   private static final float CANDIDATE_BACKGROUND_PADDING = 0.0f;
   private static final float SYMBOL_CANDIDATE_BACKGROUND_PADDING = 0.0f;
 
+  private final Resources resources;
   private final float density;
   private final Map<DrawableType, Drawable> drawableMap =
       new EnumMap<DrawableType, Drawable>(DrawableType.class);
-  private SkinType skinType = SkinType.ORANGE_LIGHTGRAY;
+  private Skin skin = Skin.getFallbackInstance();
 
-  public BackgroundDrawableFactory(float density) {
-    this.density = density;
+  public BackgroundDrawableFactory(Resources resources) {
+    this.resources = Preconditions.checkNotNull(resources);
+    density = resources.getDisplayMetrics().density;
   }
 
   public Drawable getDrawable(DrawableType drawableType) {
-    if (drawableType == null) {
-      return null;
-    }
-
-    Drawable result = drawableMap.get(drawableType);
+    Drawable result = drawableMap.get(Preconditions.checkNotNull(drawableType));
     if (result == null) {
       result = createBackgroundDrawable(drawableType);
       drawableMap.put(drawableType, result);
     }
-
     return result;
   }
 
-  public void setSkinType(SkinType skinType) {
-    if (this.skinType == skinType) {
+  public void setSkin(Skin skin) {
+    Preconditions.checkNotNull(skin);
+    if (this.skin.equals(skin)) {
       return;
     }
-    this.skinType = skinType;
+    this.skin = skin;
     drawableMap.clear();
   }
 
   private Drawable createBackgroundDrawable(DrawableType drawableType) {
-    switch (drawableType) {
+    switch (Preconditions.checkNotNull(drawableType)) {
       case TWELVEKEYS_REGULAR_KEY_BACKGROUND:
         return createPressableDrawable(
             new RectKeyDrawable(
-                (int) (TWELVEKEYS_LEFT_OFFSET * density),
-                (int) (TWELVEKEYS_TOP_OFFSET * density),
-                (int) (TWELVEKEYS_RIGHT_OFFSET * density),
-                (int) (TWELVEKEYS_BOTTOM_OFFSET * density),
-                skinType.twelvekeysLayoutPressedKeyTopColor,
-                skinType.twelvekeysLayoutPressedKeyBottomColor,
-                skinType.twelvekeysLayoutPressedKeyHighlightColor,
-                skinType.twelvekeysLayoutPressedKeyLightShadeColor,
-                skinType.twelvekeysLayoutPressedKeyDarkShadeColor,
-                skinType.twelvekeysLayoutPressedKeyShadowColor),
-            new RectKeyDrawable(
-                (int) (TWELVEKEYS_LEFT_OFFSET * density),
-                (int) (TWELVEKEYS_TOP_OFFSET * density),
-                (int) (TWELVEKEYS_RIGHT_OFFSET * density),
-                (int) (TWELVEKEYS_BOTTOM_OFFSET * density),
-                skinType.twelvekeysLayoutReleasedKeyTopColor,
-                skinType.twelvekeysLayoutReleasedKeyBottomColor,
-                skinType.twelvekeysLayoutReleasedKeyHighlightColor,
-                skinType.twelvekeysLayoutReleasedKeyLightShadeColor,
-                skinType.twelvekeysLayoutReleasedKeyDarkShadeColor,
-                skinType.twelvekeysLayoutReleasedKeyShadowColor));
+                (int) (skin.twelvekeysLeftOffsetDimension),
+                (int) (skin.twelvekeysTopOffsetDimension),
+                (int) (skin.twelvekeysRightOffsetDimension),
+                (int) (skin.twelvekeysBottomOffsetDimension),
+                skin.twelvekeysLayoutPressedKeyTopColor,
+                skin.twelvekeysLayoutPressedKeyBottomColor,
+                skin.twelvekeysLayoutPressedKeyHighlightColor,
+                skin.twelvekeysLayoutPressedKeyLightShadeColor,
+                skin.twelvekeysLayoutPressedKeyDarkShadeColor,
+                skin.twelvekeysLayoutPressedKeyShadowColor),
+            Optional.<Drawable>of(new RectKeyDrawable(
+                (int) (skin.twelvekeysLeftOffsetDimension),
+                (int) (skin.twelvekeysTopOffsetDimension),
+                (int) (skin.twelvekeysRightOffsetDimension),
+                (int) (skin.twelvekeysBottomOffsetDimension),
+                skin.twelvekeysLayoutReleasedKeyTopColor,
+                skin.twelvekeysLayoutReleasedKeyBottomColor,
+                skin.twelvekeysLayoutReleasedKeyHighlightColor,
+                skin.twelvekeysLayoutReleasedKeyLightShadeColor,
+                skin.twelvekeysLayoutReleasedKeyDarkShadeColor,
+                skin.twelvekeysLayoutReleasedKeyShadowColor)));
 
       case TWELVEKEYS_FUNCTION_KEY_BACKGROUND:
         return createPressableDrawable(
             new RectKeyDrawable(
-                (int) (TWELVEKEYS_LEFT_OFFSET * density),
-                (int) (TWELVEKEYS_TOP_OFFSET * density),
-                (int) (TWELVEKEYS_RIGHT_OFFSET * density),
-                (int) (TWELVEKEYS_BOTTOM_OFFSET * density),
-                skinType.twelvekeysLayoutPressedFunctionKeyTopColor,
-                skinType.twelvekeysLayoutPressedFunctionKeyBottomColor,
-                skinType.twelvekeysLayoutPressedFunctionKeyHighlightColor,
-                skinType.twelvekeysLayoutPressedFunctionKeyLightShadeColor,
-                skinType.twelvekeysLayoutPressedFunctionKeyDarkShadeColor,
-                skinType.twelvekeysLayoutPressedFunctionKeyShadowColor),
-            new RectKeyDrawable(
-                (int) (TWELVEKEYS_LEFT_OFFSET * density),
-                (int) (TWELVEKEYS_TOP_OFFSET * density),
-                (int) (TWELVEKEYS_RIGHT_OFFSET * density),
-                (int) (TWELVEKEYS_BOTTOM_OFFSET * density),
-                skinType.twelvekeysLayoutReleasedFunctionKeyTopColor,
-                skinType.twelvekeysLayoutReleasedFunctionKeyBottomColor,
-                skinType.twelvekeysLayoutReleasedFunctionKeyHighlightColor,
-                skinType.twelvekeysLayoutReleasedFunctionKeyLightShadeColor,
-                skinType.twelvekeysLayoutReleasedFunctionKeyDarkShadeColor,
-                skinType.twelvekeysLayoutReleasedFunctionKeyShadowColor));
+                (int) (skin.twelvekeysLeftOffsetDimension),
+                (int) (skin.twelvekeysTopOffsetDimension),
+                (int) (skin.twelvekeysRightOffsetDimension),
+                (int) (skin.twelvekeysBottomOffsetDimension),
+                skin.twelvekeysLayoutPressedFunctionKeyTopColor,
+                skin.twelvekeysLayoutPressedFunctionKeyBottomColor,
+                skin.twelvekeysLayoutPressedFunctionKeyHighlightColor,
+                skin.twelvekeysLayoutPressedFunctionKeyLightShadeColor,
+                skin.twelvekeysLayoutPressedFunctionKeyDarkShadeColor,
+                skin.twelvekeysLayoutPressedFunctionKeyShadowColor),
+            Optional.<Drawable>of(new RectKeyDrawable(
+                (int) (skin.twelvekeysLeftOffsetDimension),
+                (int) (skin.twelvekeysTopOffsetDimension),
+                (int) (skin.twelvekeysRightOffsetDimension),
+                (int) (skin.twelvekeysBottomOffsetDimension),
+                skin.twelvekeysLayoutReleasedFunctionKeyTopColor,
+                skin.twelvekeysLayoutReleasedFunctionKeyBottomColor,
+                skin.twelvekeysLayoutReleasedFunctionKeyHighlightColor,
+                skin.twelvekeysLayoutReleasedFunctionKeyLightShadeColor,
+                skin.twelvekeysLayoutReleasedFunctionKeyDarkShadeColor,
+                skin.twelvekeysLayoutReleasedFunctionKeyShadowColor)));
 
       case TWELVEKEYS_FUNCTION_KEY_BACKGROUND_WITH_THREEDOTS:
         return new LayerDrawable(new Drawable[] {
             createPressableDrawable(
                 new RectKeyDrawable(
-                    (int) (TWELVEKEYS_LEFT_OFFSET * density),
-                    (int) (TWELVEKEYS_TOP_OFFSET * density),
-                    (int) (TWELVEKEYS_RIGHT_OFFSET * density),
-                    (int) (TWELVEKEYS_BOTTOM_OFFSET * density),
-                    skinType.twelvekeysLayoutPressedFunctionKeyTopColor,
-                    skinType.twelvekeysLayoutPressedFunctionKeyBottomColor,
-                    skinType.twelvekeysLayoutPressedFunctionKeyHighlightColor,
-                    skinType.twelvekeysLayoutPressedFunctionKeyLightShadeColor,
-                    skinType.twelvekeysLayoutPressedFunctionKeyDarkShadeColor,
-                    skinType.twelvekeysLayoutPressedFunctionKeyShadowColor),
-                new RectKeyDrawable(
-                    (int) (TWELVEKEYS_LEFT_OFFSET * density),
-                    (int) (TWELVEKEYS_TOP_OFFSET * density),
-                    (int) (TWELVEKEYS_RIGHT_OFFSET * density),
-                    (int) (TWELVEKEYS_BOTTOM_OFFSET * density),
-                    skinType.twelvekeysLayoutReleasedFunctionKeyTopColor,
-                    skinType.twelvekeysLayoutReleasedFunctionKeyBottomColor,
-                    skinType.twelvekeysLayoutReleasedFunctionKeyHighlightColor,
-                    skinType.twelvekeysLayoutReleasedFunctionKeyLightShadeColor,
-                    skinType.twelvekeysLayoutReleasedFunctionKeyDarkShadeColor,
-                    skinType.twelvekeysLayoutReleasedFunctionKeyShadowColor)),
+                    (int) (skin.twelvekeysLeftOffsetDimension),
+                    (int) (skin.twelvekeysTopOffsetDimension),
+                    (int) (skin.twelvekeysRightOffsetDimension),
+                    (int) (skin.twelvekeysBottomOffsetDimension),
+                    skin.twelvekeysLayoutPressedFunctionKeyTopColor,
+                    skin.twelvekeysLayoutPressedFunctionKeyBottomColor,
+                    skin.twelvekeysLayoutPressedFunctionKeyHighlightColor,
+                    skin.twelvekeysLayoutPressedFunctionKeyLightShadeColor,
+                    skin.twelvekeysLayoutPressedFunctionKeyDarkShadeColor,
+                    skin.twelvekeysLayoutPressedFunctionKeyShadowColor),
+                Optional.<Drawable>of(new RectKeyDrawable(
+                    (int) (skin.twelvekeysLeftOffsetDimension),
+                    (int) (skin.twelvekeysTopOffsetDimension),
+                    (int) (skin.twelvekeysRightOffsetDimension),
+                    (int) (skin.twelvekeysBottomOffsetDimension),
+                    skin.twelvekeysLayoutReleasedFunctionKeyTopColor,
+                    skin.twelvekeysLayoutReleasedFunctionKeyBottomColor,
+                    skin.twelvekeysLayoutReleasedFunctionKeyHighlightColor,
+                    skin.twelvekeysLayoutReleasedFunctionKeyLightShadeColor,
+                    skin.twelvekeysLayoutReleasedFunctionKeyDarkShadeColor,
+                    skin.twelvekeysLayoutReleasedFunctionKeyShadowColor))),
             new ThreeDotsIconDrawable(
-                (int) ((TWELVEKEYS_BOTTOM_OFFSET + TWELVEKEYS_THREEDOTS_BOTTOM_OFFSET) * density),
-                (int) ((TWELVEKEYS_RIGHT_OFFSET + TWELVEKEYS_THREEDOTS_RIGHT_OFFSET) * density),
-                skinType.threeDotsColor,
+                (int) (skin.twelvekeysBottomOffsetDimension
+                    + TWELVEKEYS_THREEDOTS_BOTTOM_OFFSET * density),
+                (int) (skin.twelvekeysRightOffsetDimension
+                    + TWELVEKEYS_THREEDOTS_RIGHT_OFFSET * density),
+                skin.threeDotsColor,
                 (int) (TWELVEKEYS_THREEDOTS_WIDTH * density),
                 (int) (TWELVEKEYS_THREEDOTS_SPAN * density))});
 
       case QWERTY_REGULAR_KEY_BACKGROUND:
         return createPressableDrawable(
             new RoundRectKeyDrawable(
-                (int) (QWERTY_LEFT_OFFSET * density),
-                (int) (QWERTY_TOP_OFFSET * density),
-                (int) (QWERTY_RIGHT_OFFSET * density),
-                (int) (QWERTY_BOTTOM_OFFSET * density),
-                (int) (skinType.qwertyKeyRoundRadius * density),
-                skinType.qwertyLayoutPressedKeyTopColor,
-                skinType.qwertyLayoutPressedKeyBottomColor,
-                skinType.qwertyLayoutPressedKeyHighlightColor,
-                skinType.qwertyLayoutPressedKeyShadowColor),
-            new RoundRectKeyDrawable(
-                (int) (QWERTY_LEFT_OFFSET * density),
-                (int) (QWERTY_TOP_OFFSET * density),
-                (int) (QWERTY_RIGHT_OFFSET * density),
-                (int) (QWERTY_BOTTOM_OFFSET * density),
-                (int) (skinType.qwertyKeyRoundRadius * density),
-                skinType.qwertyLayoutReleasedKeyTopColor,
-                skinType.qwertyLayoutReleasedKeyBottomColor,
-                skinType.qwertyLayoutReleasedKeyHighlightColor,
-                skinType.qwertyLayoutReleasedKeyShadowColor));
+                (int) (skin.qwertyLeftOffsetDimension),
+                (int) (skin.qwertyTopOffsetDimension),
+                (int) (skin.qwertyRightOffsetDimension),
+                (int) (skin.qwertyBottomOffsetDimension),
+                (int) (skin.qwertyRoundRadiusDimension),
+                skin.qwertyLayoutPressedKeyTopColor,
+                skin.qwertyLayoutPressedKeyBottomColor,
+                skin.qwertyLayoutPressedKeyHighlightColor,
+                skin.qwertyLayoutPressedKeyShadowColor),
+            Optional.<Drawable>of(new RoundRectKeyDrawable(
+                (int) (skin.qwertyLeftOffsetDimension),
+                (int) (skin.qwertyTopOffsetDimension),
+                (int) (skin.qwertyRightOffsetDimension),
+                (int) (skin.qwertyBottomOffsetDimension),
+                (int) (skin.qwertyRoundRadiusDimension),
+                skin.qwertyLayoutReleasedKeyTopColor,
+                skin.qwertyLayoutReleasedKeyBottomColor,
+                skin.qwertyLayoutReleasedKeyHighlightColor,
+                skin.qwertyLayoutReleasedKeyShadowColor)));
 
       case QWERTY_FUNCTION_KEY_BACKGROUND:
         return createPressableDrawable(
             new RoundRectKeyDrawable(
-                (int) (QWERTY_LEFT_OFFSET * density),
-                (int) (QWERTY_TOP_OFFSET * density),
-                (int) (QWERTY_RIGHT_OFFSET * density),
-                (int) (QWERTY_BOTTOM_OFFSET * density),
-                (int) (skinType.qwertyKeyRoundRadius * density),
-                skinType.qwertyLayoutPressedFunctionKeyTopColor,
-                skinType.qwertyLayoutPressedFunctionKeyBottomColor,
-                skinType.qwertyLayoutPressedFunctionKeyHighlightColor,
-                skinType.qwertyLayoutPressedFunctionKeyShadowColor),
-            new RoundRectKeyDrawable(
-                (int) (QWERTY_LEFT_OFFSET * density),
-                (int) (QWERTY_TOP_OFFSET * density),
-                (int) (QWERTY_RIGHT_OFFSET * density),
-                (int) (QWERTY_BOTTOM_OFFSET * density),
-                (int) (skinType.qwertyKeyRoundRadius * density),
-                skinType.qwertyLayoutReleasedFunctionKeyTopColor,
-                skinType.qwertyLayoutReleasedFunctionKeyBottomColor,
-                skinType.qwertyLayoutReleasedFunctionKeyHighlightColor,
-                skinType.qwertyLayoutReleasedFunctionKeyShadowColor));
+                (int) (skin.qwertyLeftOffsetDimension),
+                (int) (skin.qwertyTopOffsetDimension),
+                (int) (skin.qwertyRightOffsetDimension),
+                (int) (skin.qwertyBottomOffsetDimension),
+                (int) (skin.qwertyRoundRadiusDimension),
+                skin.qwertyLayoutPressedFunctionKeyTopColor,
+                skin.qwertyLayoutPressedFunctionKeyBottomColor,
+                skin.qwertyLayoutPressedFunctionKeyHighlightColor,
+                skin.qwertyLayoutPressedFunctionKeyShadowColor),
+            Optional.<Drawable>of(new RoundRectKeyDrawable(
+                (int) (skin.qwertyLeftOffsetDimension),
+                (int) (skin.qwertyTopOffsetDimension),
+                (int) (skin.qwertyRightOffsetDimension),
+                (int) (skin.qwertyBottomOffsetDimension),
+                (int) (skin.qwertyRoundRadiusDimension),
+                skin.qwertyLayoutReleasedFunctionKeyTopColor,
+                skin.qwertyLayoutReleasedFunctionKeyBottomColor,
+                skin.qwertyLayoutReleasedFunctionKeyHighlightColor,
+                skin.qwertyLayoutReleasedFunctionKeyShadowColor)));
 
       case QWERTY_FUNCTION_KEY_BACKGROUND_WITH_THREEDOTS:
         return new LayerDrawable(new Drawable[] {
             createPressableDrawable(
                 new RoundRectKeyDrawable(
-                    (int) (QWERTY_LEFT_OFFSET * density),
-                    (int) (QWERTY_TOP_OFFSET * density),
-                    (int) (QWERTY_RIGHT_OFFSET * density),
-                    (int) (QWERTY_BOTTOM_OFFSET * density),
-                    (int) (skinType.qwertyKeyRoundRadius * density),
-                    skinType.qwertyLayoutPressedFunctionKeyTopColor,
-                    skinType.qwertyLayoutPressedFunctionKeyBottomColor,
-                    skinType.qwertyLayoutPressedFunctionKeyHighlightColor,
-                    skinType.qwertyLayoutPressedFunctionKeyShadowColor),
-                new RoundRectKeyDrawable(
-                    (int) (QWERTY_LEFT_OFFSET * density),
-                    (int) (QWERTY_TOP_OFFSET * density),
-                    (int) (QWERTY_RIGHT_OFFSET * density),
-                    (int) (QWERTY_BOTTOM_OFFSET * density),
-                    (int) (skinType.qwertyKeyRoundRadius * density),
-                    skinType.qwertyLayoutReleasedFunctionKeyTopColor,
-                    skinType.qwertyLayoutReleasedFunctionKeyBottomColor,
-                    skinType.qwertyLayoutReleasedFunctionKeyHighlightColor,
-                    skinType.qwertyLayoutReleasedFunctionKeyShadowColor)),
+                    (int) (skin.qwertyLeftOffsetDimension),
+                    (int) (skin.qwertyTopOffsetDimension),
+                    (int) (skin.qwertyRightOffsetDimension),
+                    (int) (skin.qwertyBottomOffsetDimension),
+                    (int) (skin.qwertyRoundRadiusDimension),
+                    skin.qwertyLayoutPressedFunctionKeyTopColor,
+                    skin.qwertyLayoutPressedFunctionKeyBottomColor,
+                    skin.qwertyLayoutPressedFunctionKeyHighlightColor,
+                    skin.qwertyLayoutPressedFunctionKeyShadowColor),
+                Optional.<Drawable>of(new RoundRectKeyDrawable(
+                    (int) (skin.qwertyLeftOffsetDimension),
+                    (int) (skin.qwertyTopOffsetDimension),
+                    (int) (skin.qwertyRightOffsetDimension),
+                    (int) (skin.qwertyBottomOffsetDimension),
+                    (int) (skin.qwertyRoundRadiusDimension),
+                    skin.qwertyLayoutReleasedFunctionKeyTopColor,
+                    skin.qwertyLayoutReleasedFunctionKeyBottomColor,
+                    skin.qwertyLayoutReleasedFunctionKeyHighlightColor,
+                    skin.qwertyLayoutReleasedFunctionKeyShadowColor))),
             new ThreeDotsIconDrawable(
-                (int) ((QWERTY_BOTTOM_OFFSET + QWERTY_THREEDOTS_BOTTOM_OFFSET) * density),
-                (int) ((QWERTY_RIGHT_OFFSET + QWERTY_THREEDOTS_RIGHT_OFFSET) * density),
-                skinType.threeDotsColor,
+                (int) (skin.qwertyBottomOffsetDimension
+                    + (QWERTY_THREEDOTS_BOTTOM_OFFSET * density)),
+                (int) (skin.qwertyRightOffsetDimension
+                    + (QWERTY_THREEDOTS_RIGHT_OFFSET * density)),
+                skin.threeDotsColor,
                 (int) (QWERTY_THREEDOTS_WIDTH * density),
                 (int) (QWERTY_THREEDOTS_SPAN * density))});
 
-      case QWERTY_FUNCTION_KEY_LIGHT_ON_BACKGROUND:
+      case QWERTY_FUNCTION_KEY_SPACE_WITH_THREEDOTS:
         return new LayerDrawable(new Drawable[] {
             createPressableDrawable(
-                new RoundRectKeyDrawable(
-                    (int) (QWERTY_LEFT_OFFSET * density),
-                    (int) (QWERTY_TOP_OFFSET * density),
-                    (int) (QWERTY_RIGHT_OFFSET * density),
-                    (int) (QWERTY_BOTTOM_OFFSET * density),
-                    (int) (skinType.qwertyKeyRoundRadius * density),
-                    skinType.qwertyLayoutPressedFunctionKeyTopColor,
-                    skinType.qwertyLayoutPressedFunctionKeyBottomColor,
-                    skinType.qwertyLayoutPressedFunctionKeyHighlightColor,
-                    skinType.qwertyLayoutPressedFunctionKeyShadowColor),
-                new RoundRectKeyDrawable(
-                    (int) (QWERTY_LEFT_OFFSET * density),
-                    (int) (QWERTY_TOP_OFFSET * density),
-                    (int) (QWERTY_RIGHT_OFFSET * density),
-                    (int) (QWERTY_BOTTOM_OFFSET * density),
-                    (int) (skinType.qwertyKeyRoundRadius * density),
-                    skinType.qwertyLayoutReleasedFunctionKeyTopColor,
-                    skinType.qwertyLayoutReleasedFunctionKeyBottomColor,
-                    skinType.qwertyLayoutReleasedFunctionKeyHighlightColor,
-                    skinType.qwertyLayoutReleasedFunctionKeyShadowColor)),
-            new LightIconDrawable(
-                (int) ((QWERTY_TOP_OFFSET + QWERTY_LIGHT_TOP_OFFSET + QWERTY_LIGHT_RADIUS)
-                          * density),
-                (int) ((QWERTY_RIGHT_OFFSET + QWERTY_LIGHT_RIGHT_OFFSET + QWERTY_LIGHT_RADIUS)
-                          * density),
-                skinType.qwertyLightOnSignLightColor,
-                skinType.qwertyLightOnSignDarkColor,
-                skinType.qwertyLightOnSignShadeColor,
-                (int) (QWERTY_LIGHT_RADIUS * density))
-            });
+                new QwertySpaceKeyDrawable(
+                    (int) (skin.qwertySpaceKeyHeightDimension),
+                    (int) (skin.qwertySpaceKeyHorizontalOffsetDimension),
+                    (int) (skin.qwertyTopOffsetDimension),
+                    (int) (skin.qwertySpaceKeyHorizontalOffsetDimension),
+                    (int) (skin.qwertyBottomOffsetDimension),
+                    (int) (skin.qwertySpaceKeyRoundRadiusDimension),
+                    skin.qwertyLayoutPressedSpaceKeyTopColor,
+                    skin.qwertyLayoutPressedSpaceKeyBottomColor,
+                    skin.qwertyLayoutPressedSpaceKeyHighlightColor,
+                    skin.qwertyLayoutPressedSpaceKeyShadowColor),
+                Optional.<Drawable>of(new QwertySpaceKeyDrawable(
+                    (int) (skin.qwertySpaceKeyHeightDimension),
+                    (int) (skin.qwertySpaceKeyHorizontalOffsetDimension),
+                    (int) (skin.qwertyTopOffsetDimension),
+                    (int) (skin.qwertySpaceKeyHorizontalOffsetDimension),
+                    (int) (skin.qwertyBottomOffsetDimension),
+                    (int) (skin.qwertySpaceKeyRoundRadiusDimension),
+                    skin.qwertyLayoutReleasedSpaceKeyTopColor,
+                    skin.qwertyLayoutReleasedSpaceKeyBottomColor,
+                    skin.qwertyLayoutReleasedSpaceKeyHighlightColor,
+                    skin.qwertyLayoutReleasedSpaceKeyShadowColor))),
+            new ThreeDotsIconDrawable(
+                (int) (skin.qwertyBottomOffsetDimension
+                    + (QWERTY_THREEDOTS_BOTTOM_OFFSET * density)),
+                (int) (skin.qwertySpaceKeyHorizontalOffsetDimension
+                    + (QWERTY_THREEDOTS_RIGHT_OFFSET * density)),
+                skin.threeDotsColor,
+                (int) (QWERTY_THREEDOTS_WIDTH * density),
+                (int) (QWERTY_THREEDOTS_SPAN * density))});
 
-      case QWERTY_FUNCTION_KEY_LIGHT_OFF_BACKGROUND:
-        return new LayerDrawable(new Drawable[] {
-            createPressableDrawable(
-                new RoundRectKeyDrawable(
-                    (int) (QWERTY_LEFT_OFFSET * density),
-                    (int) (QWERTY_TOP_OFFSET * density),
-                    (int) (QWERTY_RIGHT_OFFSET * density),
-                    (int) (QWERTY_BOTTOM_OFFSET * density),
-                    (int) (skinType.qwertyKeyRoundRadius * density),
-                    skinType.qwertyLayoutPressedFunctionKeyTopColor,
-                    skinType.qwertyLayoutPressedFunctionKeyBottomColor,
-                    skinType.qwertyLayoutPressedFunctionKeyHighlightColor,
-                    skinType.qwertyLayoutPressedFunctionKeyShadowColor),
-                new RoundRectKeyDrawable(
-                    (int) (QWERTY_LEFT_OFFSET * density),
-                    (int) (QWERTY_TOP_OFFSET * density),
-                    (int) (QWERTY_RIGHT_OFFSET * density),
-                    (int) (QWERTY_BOTTOM_OFFSET * density),
-                    (int) (skinType.qwertyKeyRoundRadius * density),
-                    skinType.qwertyLayoutReleasedFunctionKeyTopColor,
-                    skinType.qwertyLayoutReleasedFunctionKeyBottomColor,
-                    skinType.qwertyLayoutReleasedFunctionKeyHighlightColor,
-                    skinType.qwertyLayoutReleasedFunctionKeyShadowColor)),
-            new LightIconDrawable(
-                (int) ((QWERTY_TOP_OFFSET + QWERTY_LIGHT_TOP_OFFSET + QWERTY_LIGHT_RADIUS)
-                          * density),
-                (int) ((QWERTY_RIGHT_OFFSET + QWERTY_LIGHT_RIGHT_OFFSET + QWERTY_LIGHT_RADIUS)
-                          * density),
-                skinType.qwertyLightOffSignLightColor,
-                skinType.qwertyLightOffSignDarkColor,
-                skinType.qwertyLightOffSignShadeColor,
-                (int) (QWERTY_LIGHT_RADIUS * density))
-            });
+      case KEYBOARD_SEPARATOR_TOP:
+        return new InsetDrawable(
+            new ColorDrawable(skin.keyboardSeparatorColor), 0,
+                              resources.getDimensionPixelSize(R.dimen.keyboard_separator_padding),
+                              0, 0);
+
+      case KEYBOARD_SEPARATOR_CENTER:
+        return new ColorDrawable(skin.keyboardSeparatorColor);
+
+      case KEYBOARD_SEPARATOR_BOTTOM:
+        return new InsetDrawable(
+            new ColorDrawable(skin.keyboardSeparatorColor), 0, 0, 0,
+            resources.getDimensionPixelSize(R.dimen.keyboard_separator_padding));
+
+      case TRNASPARENT:
+        return DummyDrawable.getInstance();
 
       case TWELVEKEYS_CENTER_FLICK:
         return new CenterCircularHighlightDrawable(
-            (int) (TWELVEKEYS_LEFT_OFFSET * density),
-            (int) (TWELVEKEYS_TOP_OFFSET * density),
-            (int) (TWELVEKEYS_RIGHT_OFFSET * density),
-            (int) (TWELVEKEYS_BOTTOM_OFFSET * density),
-            skinType.flickBaseColor,
-            skinType.flickShadeColor);
+            (int) (skin.twelvekeysLeftOffsetDimension),
+            (int) (skin.twelvekeysTopOffsetDimension),
+            (int) (skin.twelvekeysRightOffsetDimension),
+            (int) (skin.twelvekeysBottomOffsetDimension),
+            skin.flickBaseColor,
+            skin.flickShadeColor);
 
       case TWELVEKEYS_LEFT_FLICK:
         return new TriangularHighlightDrawable(
-            (int) (TWELVEKEYS_LEFT_OFFSET * density),
-            (int) (TWELVEKEYS_TOP_OFFSET * density),
-            (int) (TWELVEKEYS_RIGHT_OFFSET * density),
-            (int) (TWELVEKEYS_BOTTOM_OFFSET * density),
-            skinType.flickBaseColor,
-            skinType.flickShadeColor,
+            (int) (skin.twelvekeysLeftOffsetDimension),
+            (int) (skin.twelvekeysTopOffsetDimension),
+            (int) (skin.twelvekeysRightOffsetDimension),
+            (int) (skin.twelvekeysBottomOffsetDimension),
+            skin.flickBaseColor,
+            skin.flickShadeColor,
             HighlightDirection.LEFT);
 
       case TWELVEKEYS_UP_FLICK:
         return new TriangularHighlightDrawable(
-            (int) (TWELVEKEYS_LEFT_OFFSET * density),
-            (int) (TWELVEKEYS_TOP_OFFSET * density),
-            (int) (TWELVEKEYS_RIGHT_OFFSET * density),
-            (int) (TWELVEKEYS_BOTTOM_OFFSET * density),
-            skinType.flickBaseColor,
-            skinType.flickShadeColor,
+            (int) (skin.twelvekeysLeftOffsetDimension),
+            (int) (skin.twelvekeysTopOffsetDimension),
+            (int) (skin.twelvekeysRightOffsetDimension),
+            (int) (skin.twelvekeysBottomOffsetDimension),
+            skin.flickBaseColor,
+            skin.flickShadeColor,
             HighlightDirection.UP);
 
       case TWELVEKEYS_RIGHT_FLICK:
         return new TriangularHighlightDrawable(
-            (int) (TWELVEKEYS_LEFT_OFFSET * density),
-            (int) (TWELVEKEYS_TOP_OFFSET * density),
-            (int) (TWELVEKEYS_RIGHT_OFFSET * density),
-            (int) (TWELVEKEYS_BOTTOM_OFFSET * density),
-            skinType.flickBaseColor,
-            skinType.flickShadeColor,
+            (int) (skin.twelvekeysLeftOffsetDimension),
+            (int) (skin.twelvekeysTopOffsetDimension),
+            (int) (skin.twelvekeysRightOffsetDimension),
+            (int) (skin.twelvekeysBottomOffsetDimension),
+            skin.flickBaseColor,
+            skin.flickShadeColor,
             HighlightDirection.RIGHT);
 
       case TWELVEKEYS_DOWN_FLICK:
         return new TriangularHighlightDrawable(
-            (int) (TWELVEKEYS_LEFT_OFFSET * density),
-            (int) (TWELVEKEYS_TOP_OFFSET * density),
-            (int) (TWELVEKEYS_RIGHT_OFFSET * density),
-            (int) (TWELVEKEYS_BOTTOM_OFFSET * density),
-            skinType.flickBaseColor,
-            skinType.flickShadeColor,
+            (int) (skin.twelvekeysLeftOffsetDimension),
+            (int) (skin.twelvekeysTopOffsetDimension),
+            (int) (skin.twelvekeysRightOffsetDimension),
+            (int) (skin.twelvekeysBottomOffsetDimension),
+            skin.flickBaseColor,
+            skin.flickShadeColor,
             HighlightDirection.DOWN);
 
       case POPUP_BACKGROUND_WINDOW:
-        // TODO(hidehiko): add a flag to control popup style in SkinType.
-        return skinType == SkinType.ORANGE_LIGHTGRAY
-            ? new PopUpFrameWindowDrawable(
-                  (int) (POPUP_WINDOW_PADDING * density),
-                  (int) (POPUP_WINDOW_PADDING * density),
-                  (int) (POPUP_WINDOW_PADDING * density),
-                  (int) (POPUP_WINDOW_PADDING * density),
-                  (int) (POPUP_FRAME_BORDER_WIDTH * density),
-                  POPUP_SHADOW_SIZE * density,
-                  skinType.popupFrameWindowTopColor,
-                  skinType.popupFrameWindowBottomColor,
-                  skinType.popupFrameWindowBorderColor,
-                  skinType.popupFrameWindowInnerPaneColor)
-            : new RoundRectKeyDrawable(
-                  (int) (POPUP_WINDOW_PADDING * density),
-                  (int) (POPUP_WINDOW_PADDING * density),
-                  (int) (POPUP_WINDOW_PADDING * density),
-                  (int) (POPUP_WINDOW_PADDING * density),
-                  (int) (skinType.qwertyKeyRoundRadius * density),
-                  skinType.popupFrameWindowTopColor,
-                  skinType.popupFrameWindowBottomColor,
-                  0,  // No highlight.
-                  skinType.popupFrameWindowShadowColor);
+        return
+            new RoundRectKeyDrawable(
+                (int) (POPUP_WINDOW_PADDING * density),
+                (int) (POPUP_WINDOW_PADDING * density),
+                (int) (POPUP_WINDOW_PADDING * density),
+                (int) (POPUP_WINDOW_PADDING * density),
+                (int) (skin.qwertyRoundRadiusDimension),
+                skin.popupFrameWindowTopColor,
+                skin.popupFrameWindowBottomColor,
+                0,  // No highlight.
+                skin.popupFrameWindowShadowColor);
+
       case CANDIDATE_BACKGROUND:
         return createFocusableDrawable(
             new CandidateBackgroundFocusedDrawable(
@@ -471,18 +444,19 @@
                 (int) (CANDIDATE_BACKGROUND_PADDING * density),
                 (int) (CANDIDATE_BACKGROUND_PADDING * density),
                 (int) (CANDIDATE_BACKGROUND_PADDING * density),
-                skinType.candidateBackgroundFocusedTopColor,
-                skinType.candidateBackgroundFocusedBottomColor,
-                skinType.candidateBackgroundFocusedShadowColor),
-            new CandidateBackgroundDrawable(
+                skin.candidateBackgroundFocusedTopColor,
+                skin.candidateBackgroundFocusedBottomColor,
+                skin.candidateBackgroundFocusedShadowColor),
+            Optional.<Drawable>of(new CandidateBackgroundDrawable(
                 (int) (CANDIDATE_BACKGROUND_PADDING * density),
                 (int) (CANDIDATE_BACKGROUND_PADDING * density),
                 (int) (CANDIDATE_BACKGROUND_PADDING * density),
                 (int) (CANDIDATE_BACKGROUND_PADDING * density),
-                skinType.candidateBackgroundTopColor,
-                skinType.candidateBackgroundBottomColor,
-                skinType.candidateBackgroundHighlightColor,
-                skinType.candidateBackgroundBorderColor));
+                skin.candidateBackgroundTopColor,
+                skin.candidateBackgroundBottomColor,
+                skin.candidateBackgroundHighlightColor,
+                skin.candidateBackgroundBorderColor)));
+
       case SYMBOL_CANDIDATE_BACKGROUND:
         return createFocusableDrawable(
             new CandidateBackgroundFocusedDrawable(
@@ -490,18 +464,18 @@
                 (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density),
                 (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density),
                 (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density),
-                skinType.candidateBackgroundFocusedTopColor,
-                skinType.candidateBackgroundFocusedBottomColor,
-                skinType.candidateBackgroundFocusedShadowColor),
-            new CandidateBackgroundDrawable(
+                skin.candidateBackgroundFocusedTopColor,
+                skin.candidateBackgroundFocusedBottomColor,
+                skin.candidateBackgroundFocusedShadowColor),
+            Optional.<Drawable>of(new CandidateBackgroundDrawable(
                 (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density),
                 (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density),
                 (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density),
                 (int) (SYMBOL_CANDIDATE_BACKGROUND_PADDING * density),
-                skinType.symbolCandidateBackgroundTopColor,
-                skinType.symbolCandidateBackgroundBottomColor,
-                skinType.symbolCandidateBackgroundHighlightColor,
-                skinType.symbolCandidateBackgroundBorderColor));
+                skin.symbolCandidateBackgroundTopColor,
+                skin.symbolCandidateBackgroundBottomColor,
+                skin.symbolCandidateBackgroundHighlightColor,
+                skin.symbolCandidateBackgroundBorderColor)));
     }
 
     throw new IllegalArgumentException("Unknown drawable type: " + drawableType);
@@ -509,29 +483,32 @@
 
   // TODO(hidehiko): Move this method to the somewhere in view package.
   public static Drawable createPressableDrawable(
-      Drawable pressedDrawable, Drawable releasedDrawable) {
+      Drawable pressedDrawable, Optional<Drawable> releasedDrawable) {
     return createTwoStateDrawable(
-        android.R.attr.state_pressed, pressedDrawable, releasedDrawable);
+        android.R.attr.state_pressed, Preconditions.checkNotNull(pressedDrawable),
+        Preconditions.checkNotNull(releasedDrawable));
   }
 
   public static Drawable createFocusableDrawable(
-      Drawable focusedDrawable, Drawable unfocusedDrawable) {
+      Drawable focusedDrawable, Optional<Drawable> unfocusedDrawable) {
     return createTwoStateDrawable(
-        android.R.attr.state_focused, focusedDrawable, unfocusedDrawable);
+        android.R.attr.state_focused, Preconditions.checkNotNull(focusedDrawable),
+        Preconditions.checkNotNull(unfocusedDrawable));
   }
 
   public static Drawable createSelectableDrawable(
-      Drawable selectedDrawable, Drawable unselectedDrawable) {
+      Drawable selectedDrawable, Optional<Drawable> unselectedDrawable) {
     return createTwoStateDrawable(
-        android.R.attr.state_selected, selectedDrawable, unselectedDrawable);
+        android.R.attr.state_selected, Preconditions.checkNotNull(selectedDrawable),
+        Preconditions.checkNotNull(unselectedDrawable));
   }
 
   private static Drawable createTwoStateDrawable(
-      int state, Drawable enabledDrawable, Drawable disabledDrawable) {
+      int state, Drawable enabledDrawable, Optional<Drawable> disabledDrawable) {
     StateListDrawable drawable = new StateListDrawable();
     drawable.addState(new int[] { state }, enabledDrawable);
-    if (disabledDrawable != null) {
-      drawable.addState(EMPTY_STATE, disabledDrawable);
+    if (disabledDrawable.isPresent()) {
+      drawable.addState(EMPTY_STATE, disabledDrawable.get());
     }
     return drawable;
   }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Flick.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Flick.java
index 3ca9dab..3fe183b 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Flick.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Flick.java
@@ -30,6 +30,7 @@
 package org.mozc.android.inputmethod.japanese.keyboard;
 
 import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
 
 /**
  * A class corresponding to {@code &lt;Flick&gt;} element in xml resource files.
@@ -61,15 +62,10 @@
 
   private final Flick.Direction direction;
   private final KeyEntity keyEntity;
+
   public Flick(Direction direction, KeyEntity keyEntity) {
-    if (direction == null) {
-      throw new NullPointerException("direction shouldn't be null.");
-    }
-    if (keyEntity == null) {
-      throw new NullPointerException("keyEntity shouldn't be null.");
-    }
-    this.direction = direction;
-    this.keyEntity = keyEntity;
+    this.direction = Preconditions.checkNotNull(direction);
+    this.keyEntity = Preconditions.checkNotNull(keyEntity);
   }
 
   public Direction getDirection() {
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Key.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Key.java
index d60590e..8e51588 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Key.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Key.java
@@ -29,6 +29,7 @@
 
 package org.mozc.android.inputmethod.japanese.keyboard;
 
+import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType;
 import com.google.common.base.Objects;
 import com.google.common.base.Objects.ToStringHelper;
 import com.google.common.base.Optional;
@@ -57,7 +58,6 @@
  *   <li> {@code edgeFlags}: flags whether the key should stick to keyboard's boundary.
  *   <li> {@code isRepeatable}: whether the key long press cause a repeated key tapping.
  *   <li> {@code isModifier}: whether the key is modifier (e.g. shift key).
- *   <li> {@code isSticky}: whether the key behaves sticky (e.g. caps lock).
  * </ul>
  *
  * The {@code &lt;Key&gt;} element can have (at most) two {@code &lt;KeyState&gt;} elements.
@@ -86,16 +86,24 @@
   private final int edgeFlags;
   private final boolean isRepeatable;
   private final boolean isModifier;
-  private final boolean isSticky;
   private final Stick stick;
   // Default KeyState.
   // Absent if this is a spacer.
   private final Optional<KeyState> defaultKeyState;
+  private final DrawableType keyBackgroundDrawableType;
+
   private final List<KeyState> keyStateList;
 
   public Key(int x, int y, int width, int height, int horizontalGap,
-             int edgeFlags, boolean isRepeatable, boolean isModifier, boolean isSticky,
-             Stick stick, List<? extends KeyState> keyStateList) {
+             int edgeFlags, boolean isRepeatable, boolean isModifier,
+             Stick stick, DrawableType keyBackgroundDrawableType,
+             List<? extends KeyState> keyStateList) {
+    Preconditions.checkNotNull(stick);
+    Preconditions.checkNotNull(keyBackgroundDrawableType);
+    Preconditions.checkNotNull(keyStateList);
+    Preconditions.checkArgument(width >= 0);
+    Preconditions.checkArgument(height >= 0);
+
     this.x = x;
     this.y = y;
     this.width = width;
@@ -104,28 +112,29 @@
     this.edgeFlags = edgeFlags;
     this.isRepeatable = isRepeatable;
     this.isModifier = isModifier;
-    this.isSticky = isSticky;
     this.stick = stick;
+    this.keyBackgroundDrawableType = keyBackgroundDrawableType;
 
     List<KeyState> tmpKeyStateList = null;  // Lazy creation.
     Optional<KeyState> defaultKeyState = Optional.absent();
     for (KeyState keyState : keyStateList) {
       Set<KeyState.MetaState> metaStateSet = keyState.getMetaStateSet();
-      if (metaStateSet.isEmpty()) {
+      if (metaStateSet.isEmpty() || metaStateSet.contains(KeyState.MetaState.FALLBACK)) {
         if (defaultKeyState.isPresent()) {
           throw new IllegalArgumentException("Found duplicate default meta state");
         }
         defaultKeyState = Optional.of(keyState);
-        continue;
+        if (metaStateSet.size() <= 1) {  // metaStateSet contains only FALLBACK
+          continue;
+        }
       }
       if (tmpKeyStateList == null) {
         tmpKeyStateList = new ArrayList<KeyState>();
       }
       tmpKeyStateList.add(keyState);
     }
-    if (!defaultKeyState.isPresent() && tmpKeyStateList != null) {
-      throw new IllegalArgumentException("Default KeyState is mandatory for non-spacer.");
-    }
+    Preconditions.checkArgument(defaultKeyState.isPresent() || tmpKeyStateList == null,
+                                "Default KeyState is mandatory for non-spacer.");
     this.defaultKeyState = defaultKeyState;
     this.keyStateList = tmpKeyStateList == null
         ? Collections.<KeyState>emptyList()
@@ -164,14 +173,14 @@
     return isModifier;
   }
 
-  public boolean isSticky() {
-    return isSticky;
-  }
-
   public Stick getStick() {
     return stick;
   }
 
+  public DrawableType getKeyBackgroundDrawableType() {
+    return keyBackgroundDrawableType;
+  }
+
   /**
    * Returns {@code KeyState} at least one of which the metaState is in given {@code metaStates}.
    * <p>
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEntity.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEntity.java
index 9da4629..5184f09 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEntity.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEntity.java
@@ -29,8 +29,9 @@
 
 package org.mozc.android.inputmethod.japanese.keyboard;
 
-import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType;
 import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
 
 /**
  * A class corresponding to a {@code &lt;KeyEntity&gt;} element in xml resource files.
@@ -39,45 +40,43 @@
  *
  */
 public class KeyEntity {
+
   /** INVALID_KEY_CODE represents the key has no key code to call the Mozc server back. */
   public static final int INVALID_KEY_CODE = Integer.MIN_VALUE;
 
   private final int sourceId;
   private final int keyCode;
   private final int longPressKeyCode;
+  private final boolean longPressTimeoutTrigger;
   private final int keyIconResourceId;
   // If |keyIconResourceId| is empty and |keyCharacter| is set,
   // use this character for rendering key top.
   // TODO(team): Implement rendering method.
-  private final String keyCharacter;
-  // TODO(hidehiko): Move this field to Key.
-  private final DrawableType keyBackgroundDrawableType;
+  private final Optional<String> keyCharacter;
   private final boolean flickHighlightEnabled;
-  private final PopUp popUp;
-
-  // Constructor for compatibility.
-  // TODO(team): Remove this constructor after changing all callers including tests.
-  public KeyEntity(int sourceId, int keyCode, int longPressKeyCode,
-                   int keyIconResourceId,
-                   DrawableType keyBackgroundDrawableType,
-                   boolean flickHighlightEnabled, PopUp popUp) {
-    this(
-        sourceId, keyCode, longPressKeyCode, keyIconResourceId, null, keyBackgroundDrawableType,
-        flickHighlightEnabled, popUp);
-  }
+  private final Optional<PopUp> popUp;
+  private final int horizontalPadding;
+  private final int verticalPadding;
+  private final int iconWidth;
+  private final int iconHeight;
 
   public KeyEntity(int sourceId, int keyCode, int longPressKeyCode,
-                   int keyIconResourceId, String keyCharacter,
-                   DrawableType keyBackgroundDrawableType,
-                   boolean flickHighlightEnabled, PopUp popUp) {
+                   boolean longPressTimeoutTrigger, int keyIconResourceId,
+                   Optional<String> keyCharacter, boolean flickHighlightEnabled,
+                   Optional<PopUp> popUp, int horizontalPadding, int verticalPadding,
+                   int iconWidth, int iconHeight) {
     this.sourceId = sourceId;
     this.keyCode = keyCode;
     this.longPressKeyCode = longPressKeyCode;
+    this.longPressTimeoutTrigger = longPressTimeoutTrigger;
     this.keyIconResourceId = keyIconResourceId;
-    this.keyCharacter = keyCharacter;
-    this.keyBackgroundDrawableType = keyBackgroundDrawableType;
+    this.keyCharacter = Preconditions.checkNotNull(keyCharacter);
     this.flickHighlightEnabled = flickHighlightEnabled;
-    this.popUp = popUp;
+    this.popUp = Preconditions.checkNotNull(popUp);
+    this.horizontalPadding = horizontalPadding;
+    this.verticalPadding = verticalPadding;
+    this.iconWidth = iconWidth;
+    this.iconHeight = iconHeight;
   }
 
   public int getSourceId() {
@@ -92,34 +91,55 @@
     return longPressKeyCode;
   }
 
+  public boolean isLongPressTimeoutTrigger() {
+    return longPressTimeoutTrigger;
+  }
+
   public int getKeyIconResourceId() {
     return keyIconResourceId;
   }
 
-  public String getKeyCharacter() {
+  public Optional<String> getKeyCharacter() {
     return keyCharacter;
   }
 
-  public DrawableType getKeyBackgroundDrawableType() {
-    return keyBackgroundDrawableType;
-  }
-
   public boolean isFlickHighlightEnabled() {
     return flickHighlightEnabled;
   }
 
-  public PopUp getPopUp() {
+  public Optional<PopUp> getPopUp() {
     return popUp;
   }
 
+  public int getHorizontalPadding() {
+    return horizontalPadding;
+  }
+
+  public int getVerticalPadding() {
+    return verticalPadding;
+  }
+
+  public int getIconWidth() {
+    return iconWidth;
+  }
+
+  public int getIconHeight() {
+    return iconHeight;
+  }
+
   @Override
   public String toString() {
     return Objects.toStringHelper(this)
                   .add("sourceId", sourceId)
                   .add("keyCode", keyCode)
                   .add("longPressKeyCode", longPressKeyCode)
+                  .add("longPressTimeoutTrigger", longPressTimeoutTrigger)
                   .add("keyIconResourceId", keyIconResourceId)
                   .add("keyCharacter", keyCharacter)
+                  .add("horizontalPadding", horizontalPadding)
+                  .add("verticalPadding", verticalPadding)
+                  .add("iconWidth", iconWidth)
+                  .add("iconHeight", iconHeight)
                   .toString();
   }
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventContext.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventContext.java
index a8947d9..02c3b6c 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventContext.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventContext.java
@@ -39,8 +39,6 @@
 
 import java.util.Set;
 
-import javax.annotation.Nullable;
-
 /**
  * This class represents user's one action, e.g., the sequence of:
  * press -> move -> move -> ... -> move -> release.
@@ -49,8 +47,8 @@
  * E.g. for user's two finger strokes, two instances will be instantiated.
  *
  */
-// TODO(matsuzakit): Get rid of @Nullable.
 public class KeyEventContext {
+
   final Key key;
   final int pointerId;
   private final float pressedX;
@@ -62,7 +60,7 @@
 
   // TODO(hidehiko): Move logging code to an upper layer, e.g., MozcService or ViewManager etc.
   //   after refactoring the architecture.
-  private TouchAction lastAction = null;
+  private Optional<TouchAction> lastAction = Optional.absent();
   private float lastX;
   private float lastY;
   private long lastTimestamp;
@@ -70,7 +68,9 @@
   private final int keyboardHeight;
 
   // This variable will be updated in the callback of long press key event (if necessary).
-  boolean longPressSent = false;
+  boolean pastLongPressSentTimeout = false;
+
+  Optional<Runnable> longPressCallback = Optional.absent();
 
   public KeyEventContext(Key key, int pointerId, float pressedX, float pressedY,
                          int keyboardWidth, int keyboardHeight,
@@ -95,9 +95,8 @@
 
   /**
    * Returns true iff the point ({@code x}, {@code y}) is contained by the {@code key}'s region.
-   * This is package private for testing purpose.
    */
-  static boolean isContained(float x, float y, Key key) {
+  @VisibleForTesting static boolean isContained(float x, float y, Key key) {
     float relativeX = x - key.getX();
     float relativeY = y - key.getY();
     return 0 <= relativeX && relativeX < key.getWidth() &&
@@ -117,75 +116,82 @@
       return false;
     }
     KeyState keyState = optionalKeyState.get();
-    return keyState.getFlick(Flick.Direction.LEFT) != null ||
-           keyState.getFlick(Flick.Direction.UP) != null ||
-           keyState.getFlick(Flick.Direction.RIGHT) != null ||
-           keyState.getFlick(Flick.Direction.DOWN) != null;
+    return keyState.getFlick(Flick.Direction.LEFT).isPresent() ||
+           keyState.getFlick(Flick.Direction.UP).isPresent() ||
+           keyState.getFlick(Flick.Direction.RIGHT).isPresent() ||
+           keyState.getFlick(Flick.Direction.DOWN).isPresent();
   }
 
   /**
    * Returns the key entity corresponding to {@code metaState} and {@code direction}.
    */
-  @Nullable
-  public static KeyEntity getKeyEntity(Key key, Set<MetaState> metaState,
-                                       @Nullable Flick.Direction direction) {
+  public static Optional<KeyEntity> getKeyEntity(Key key, Set<MetaState> metaState,
+                                                 Optional<Flick.Direction> direction) {
     Preconditions.checkNotNull(key);
     Preconditions.checkNotNull(metaState);
+    Preconditions.checkNotNull(direction);
 
     if (key.isSpacer()) {
-      return null;
+      return Optional.absent();
     }
     // Key is not spacer for at least one KeyState is available.
-    return getKeyEntityInternal(key.getKeyState(metaState).get(), direction).orNull();
+    return getKeyEntityInternal(key.getKeyState(metaState).get(), direction);
   }
 
   private Optional<KeyEntity> getKeyEntity(Flick.Direction direction) {
     return keyState.isPresent()
-        ? getKeyEntityInternal(keyState.get(), direction)
+        ? getKeyEntityInternal(keyState.get(), Optional.of(direction))
         : Optional.<KeyEntity>absent();
   }
 
   private static Optional<KeyEntity> getKeyEntityInternal(KeyState keyState,
-                                                          @Nullable Flick.Direction direction) {
+                                                          Optional<Flick.Direction> direction) {
     Preconditions.checkNotNull(keyState);
+    Preconditions.checkNotNull(direction);
 
-    if (direction == null) {
+    if (!direction.isPresent()) {
       return Optional.absent();
     }
 
-    Flick flick = keyState.getFlick(direction);
-    return flick == null ? Optional.<KeyEntity>absent() : Optional.of(flick.getKeyEntity());
+    Optional<Flick> flick = keyState.getFlick(direction.get());
+    return flick.isPresent()
+        ? Optional.of(flick.get().getKeyEntity())
+        : Optional.<KeyEntity>absent();
   }
 
   /**
    * Returns the key code to be sent via {@link KeyboardActionListener#onKey(int, java.util.List)}.
+   * <p>
+   * If {@code keyEntyty} doesn't trigger longpress by timeout (isLongPressTimeoutTrigger is false),
+   * the result depends on the timestamp of touch-down event.
    */
   public int getKeyCode() {
-    if (longPressSent) {
+    Optional<KeyEntity> keyEntity = getKeyEntity(flickDirection);
+    if (!keyEntity.isPresent()
+        || (pastLongPressSentTimeout && keyEntity.get().isLongPressTimeoutTrigger())) {
       // If the long-press-key event is already sent, just return INVALID_KEY_CODE.
       return KeyEntity.INVALID_KEY_CODE;
     }
-
-    Optional<KeyEntity> keyEntity = getKeyEntity(flickDirection);
-    return keyEntity.isPresent()
-        ? keyEntity.get().getKeyCode()
-        : KeyEntity.INVALID_KEY_CODE;
+    return !keyEntity.get().isLongPressTimeoutTrigger()
+           && keyEntity.get().getLongPressKeyCode() != KeyEntity.INVALID_KEY_CODE
+           && pastLongPressSentTimeout
+           ? keyEntity.get().getLongPressKeyCode() : keyEntity.get().getKeyCode();
   }
 
   Set<MetaState> getNextMetaStates(Set<MetaState> originalMetaStates) {
+    Preconditions.checkNotNull(originalMetaStates);
     if (!key.isModifier() || key.isSpacer()) {
       // Non-modifier key shouldn't change meta state.
       return originalMetaStates;
     }
-    Set<MetaState> result = keyState.get().getNextMetaStates(originalMetaStates);
-    return result;
+    return keyState.get().getNextMetaStates(originalMetaStates);
   }
 
   /**
    * Returns the key code to be sent for long press event.
    */
   int getLongPressKeyCode() {
-    if (longPressSent) {
+    if (pastLongPressSentTimeout) {
       // If the long-press-key event is already sent, just return INVALID_KEY_CODE.
       return KeyEntity.INVALID_KEY_CODE;
     }
@@ -197,6 +203,11 @@
         : KeyEntity.INVALID_KEY_CODE;
   }
 
+  boolean isLongPressTimeoutTrigger() {
+    Optional<KeyEntity> keyEntity = getKeyEntity(Flick.Direction.CENTER);
+    return !keyEntity.isPresent() || keyEntity.get().isLongPressTimeoutTrigger();
+  }
+
   /**
    * Returns the key code to be send via {@link KeyboardActionListener#onPress(int)} and
    * {@link KeyboardActionListener#onRelease(int)}.
@@ -212,20 +223,20 @@
    * Returns true if this key event sequence represents toggling meta state.
    */
   boolean isMetaStateToggleEvent() {
-    return !longPressSent && key.isModifier() && flickDirection == Flick.Direction.CENTER;
+    return !pastLongPressSentTimeout && key.isModifier()
+        && flickDirection == Flick.Direction.CENTER;
   }
 
   /**
    * Returns the pop up data for the current state.
    */
-  @Nullable
-  PopUp getCurrentPopUp() {
-    if (longPressSent) {
-      return null;
+  Optional<PopUp> getCurrentPopUp() {
+    if (pastLongPressSentTimeout) {
+      return Optional.absent();
     }
 
     Optional<KeyEntity> keyEntity = getKeyEntity(flickDirection);
-    return keyEntity.isPresent() ? keyEntity.get().getPopUp() : null;
+    return keyEntity.isPresent() ? keyEntity.get().getPopUp() : Optional.<PopUp>absent();
   }
 
   /**
@@ -234,8 +245,9 @@
    * @return {@code true} if the internal state is actually updated.
    */
   public boolean update(float x, float y, TouchAction touchAction, long timestamp) {
+    lastAction = Optional.of(touchAction);
+
     Flick.Direction originalDirection = flickDirection;
-    lastAction = touchAction;
     lastX = x;
     lastY = y;
     lastTimestamp = timestamp;
@@ -257,28 +269,41 @@
         flickDirection = deltaX > 0 ? Flick.Direction.RIGHT : Flick.Direction.LEFT;
       }
     }
-    return flickDirection != originalDirection;
+
+    if (flickDirection == originalDirection) {
+      return false;
+    } else {
+      // If flickDirection has been updated, reset pastLongPressSentTimeout flag
+      // so that long-press even can be sent again.
+      // This happens when
+      // [Hold 'q' key]
+      // -> [Popup '1' is shown as the result of long-press]
+      // -> [Flick outside to dismiss the popup]
+      // -> [Flick again to the center position and hold]
+      // -> [Popup '1' is shown again as the result of long-press]
+      pastLongPressSentTimeout = false;
+      return true;
+    }
   }
 
   /**
    * @return {@code TouchEvent} instance which includes the stroke related to this context.
    */
-  @Nullable
-  public TouchEvent getTouchEvent() {
+  public Optional<TouchEvent> getTouchEvent() {
     Optional<KeyEntity> keyEntity = getKeyEntity(flickDirection);
     if (!keyEntity.isPresent()) {
-      return null;
+      return Optional.absent();
     }
 
     TouchEvent.Builder builder = TouchEvent.newBuilder()
         .setSourceId(keyEntity.get().getSourceId());
     builder.addStroke(createTouchPosition(
         TouchAction.TOUCH_DOWN, pressedX, pressedY, keyboardWidth, keyboardHeight, 0));
-    if (lastAction != null) {
+    if (lastAction.isPresent()) {
       builder.addStroke(createTouchPosition(
-          lastAction, lastX, lastY, keyboardWidth, keyboardHeight, lastTimestamp));
+          lastAction.get(), lastX, lastY, keyboardWidth, keyboardHeight, lastTimestamp));
     }
-    return builder.build();
+    return Optional.of(builder.build());
   }
 
   public static TouchPosition createTouchPosition(
@@ -290,4 +315,8 @@
         .setTimestamp(timestamp)
         .build();
   }
+
+  public void setLongPressCallback(Runnable longPressCallback) {
+    this.longPressCallback = Optional.of(longPressCallback);
+  }
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventHandler.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventHandler.java
index 7d5c9fb..3ab306f 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventHandler.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyEventHandler.java
@@ -31,6 +31,7 @@
 
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 
 import android.os.Handler;
 import android.os.Looper;
@@ -44,13 +45,13 @@
  *
  */
 public class KeyEventHandler implements Handler.Callback {
+
   // A dummy argument which is passed to the callback's message.
   private static final int DUMMY_ARG = 0;
 
   // Keys to figure out what message is sent in the callback.
-  // Package private for testing purpose.
-  static final int REPEAT_KEY = 1;
-  static final int LONG_PRESS_KEY = 2;
+  @VisibleForTesting static final int REPEAT_KEY = 1;
+  @VisibleForTesting static final int LONG_PRESS_KEY = 2;
 
   @VisibleForTesting final Handler handler;
   private final KeyboardActionListener keyboardActionListener;
@@ -67,14 +68,8 @@
   public KeyEventHandler(
       Looper looper, KeyboardActionListener keyboardActionListener,
       int repeatKeyDelay, int repeatKeyInterval, int longPressKeyDelay) {
-    if (looper == null) {
-      throw new NullPointerException("looper is null.");
-    }
-    if (keyboardActionListener == null) {
-      throw new NullPointerException("keyboardActionListener is null.");
-    }
-    this.handler = new Handler(looper, this);
-    this.keyboardActionListener = keyboardActionListener;
+    this.handler = new Handler(Preconditions.checkNotNull(looper), this);
+    this.keyboardActionListener = Preconditions.checkNotNull(keyboardActionListener);
     this.repeatKeyDelay = repeatKeyDelay;
     this.repeatKeyInterval = repeatKeyInterval;
     this.longPressKeyDelay = longPressKeyDelay;
@@ -82,25 +77,51 @@
 
   @Override
   public boolean handleMessage(Message message) {
-    int keyCode = message.arg1;
     KeyEventContext context = KeyEventContext.class.cast(message.obj);
-    keyboardActionListener.onKey(keyCode, Collections.singletonList(context.getTouchEvent()));
-
     switch (message.what) {
       case REPEAT_KEY: {
-        Message newMessage = handler.obtainMessage(REPEAT_KEY, keyCode, DUMMY_ARG, context);
-        handler.sendMessageDelayed(newMessage, repeatKeyInterval);
+        handleMessageRepeatKey(context);
         break;
       }
       case LONG_PRESS_KEY:
-        // Set a flag that means long-press-key-event has been sent.
-        context.longPressSent = true;
+        handleMessageLongPress(context);
         break;
     }
 
     return true;
   }
 
+  private void handleMessageRepeatKey(KeyEventContext context) {
+    int keyCode = context.getPressedKeyCode();
+    // TODO(hsumita): confirm that we can put null as a touch event or not.
+    keyboardActionListener.onKey(
+        keyCode, Collections.singletonList(context.getTouchEvent().orNull()));
+    Message newMessage = handler.obtainMessage(REPEAT_KEY, keyCode, DUMMY_ARG, context);
+    handler.sendMessageDelayed(newMessage, repeatKeyInterval);
+  }
+
+  /**
+   * Does the things which should be done when long-press operation is done.
+   * <p>
+   * This is public because this is called from KeyboardView directory in order to implement
+   * accessibility feature.
+   */
+  public void handleMessageLongPress(KeyEventContext context) {
+    int keyCode = context.getLongPressKeyCode();
+    if (context.isLongPressTimeoutTrigger()) {
+      // TODO(hsumita): confirm that we can put null as a touch event or not.
+      keyboardActionListener.onKey(
+          keyCode, Collections.singletonList(context.getTouchEvent().orNull()));
+    }
+    // Callback a function if present then flip the flag for long-press timeout.
+    // If isLongPressTimeoutTrigger is true, key-code for long-press has already been sent.
+    // If false, touch-up event for the context will send long-press key code.
+    if (context.longPressCallback.isPresent()) {
+      context.longPressCallback.get().run();
+    }
+    context.pastLongPressSentTimeout = true;
+  }
+
   public void sendPress(int keyCode) {
     keyboardActionListener.onPress(keyCode);
   }
@@ -109,7 +130,7 @@
     keyboardActionListener.onRelease(keyCode);
   }
 
-  public void sendKey(int keyCode, List<? extends TouchEvent> touchEventList) {
+  public void sendKey(int keyCode, List<TouchEvent> touchEventList) {
     keyboardActionListener.onKey(keyCode, touchEventList);
   }
 
@@ -117,9 +138,8 @@
     keyboardActionListener.onCancel();
   }
 
-  private void startDelayedKeyEventInternal(
-      int what, int keyCode, KeyEventContext context, int delay) {
-    Message message = handler.obtainMessage(what, keyCode, DUMMY_ARG, context);
+  private void startDelayedKeyEventInternal(int what, KeyEventContext context, int delay) {
+    Message message = handler.obtainMessage(what, DUMMY_ARG, DUMMY_ARG, context);
     handler.sendMessageDelayed(message, delay);
   }
 
@@ -128,16 +148,16 @@
    * based on the given {@code context}.
    */
   public void maybeStartDelayedKeyEvent(KeyEventContext context) {
-    Key key = context.key;
+    Key key = Preconditions.checkNotNull(context).key;
     if (key.isRepeatable()) {
       int keyCode = context.getPressedKeyCode();
       if (keyCode != KeyEntity.INVALID_KEY_CODE) {
-        startDelayedKeyEventInternal(REPEAT_KEY, keyCode, context, repeatKeyDelay);
+        startDelayedKeyEventInternal(REPEAT_KEY, context, repeatKeyDelay);
       }
     } else {
       int longPressKeyCode = context.getLongPressKeyCode();
       if (longPressKeyCode != KeyEntity.INVALID_KEY_CODE) {
-        startDelayedKeyEventInternal(LONG_PRESS_KEY, longPressKeyCode, context, longPressKeyDelay);
+        startDelayedKeyEventInternal(LONG_PRESS_KEY, context, longPressKeyDelay);
       }
     }
   }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyState.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyState.java
index bcc55e9..81e488b 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyState.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyState.java
@@ -30,11 +30,13 @@
 package org.mozc.android.inputmethod.japanese.keyboard;
 
 import com.google.common.base.Objects;
+import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Sets;
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.EnumMap;
 import java.util.EnumSet;
 import java.util.Map;
@@ -51,6 +53,7 @@
  *
  */
 public class KeyState {
+
   public enum MetaState {
     SHIFT(1, true),
     CAPS_LOCK(2, false),
@@ -76,13 +79,22 @@
     // TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS, TYPE_TEXT_VARIATION_EMAIL_ADDRESS
     VARIATION_EMAIL_ADDRESS(2048, false),
 
-    // Set if Globe button is enabled by preference.
-    // TODO(matsuzakit): Implement me.
+    // Set if Globe button should be offered.
+    // c.f., SubtypeImeSwitcher#shouldOfferSwitchingToNextInputMethod()
     GLOBE(4096, false),
+    // !GLOBE
+    NO_GLOBE(8192, false),
 
     // Set if there is composition string.
-    // TODO(matsuzakit): Implement me. This is mandatory when implementing ACTION_*.
-    COMPOSING(8192, false),
+    COMPOSING(16384, false),
+    // Set if the KeyboardView is handling at least one touch event.
+    HANDLING_TOUCH_EVENT(32768, false),
+
+    // DO NOT USE. Theoretically this is package private entry.
+    // "fallback" flag is useful when defining .xml file with logical-OR operator
+    // (e.g. "fallback|composing").
+    // This entry is used just for it.
+    FALLBACK(1073741824, false),
     ;
 
     private final int bitFlag;
@@ -116,11 +128,17 @@
     // MetaStates for text variations.
     public static final Set<MetaState> VARIATION_EXCLUSIVE_GROUP =
         Sets.immutableEnumSet(MetaState.VARIATION_URI, MetaState.VARIATION_EMAIL_ADDRESS);
+    // MetaStates for Globe icon.
+    public static final Set<MetaState> GLOBE_EXCLUSIVE_OR_GROUP =
+        Sets.immutableEnumSet(MetaState.GLOBE, MetaState.NO_GLOBE);
     @SuppressWarnings("unchecked")
     private static final Collection<Set<MetaState>> EXCLUSIVE_GROUP =
         Arrays.<Set<MetaState>>asList(CHAR_TYPE_EXCLUSIVE_GROUP,
                                       ACTION_EXCLUSIVE_GROUP,
-                                      VARIATION_EXCLUSIVE_GROUP);
+                                      VARIATION_EXCLUSIVE_GROUP,
+                                      GLOBE_EXCLUSIVE_OR_GROUP);
+    private static final Collection<Set<MetaState>> OR_GROUP =
+        Collections.singleton(GLOBE_EXCLUSIVE_OR_GROUP);
 
     /**
      * Checks if {@code testee} is valid set.
@@ -129,6 +147,8 @@
      * Do not call from chokepoint.
      */
     public static boolean isValidSet(Set<MetaState> testee) {
+      Preconditions.checkNotNull(testee);
+
       // Set#retainAll can make the implementation simpler, but it requires instantiation of
       // (Enum)Set for each iteration.
       for (Set<MetaState> exclusiveGroup : EXCLUSIVE_GROUP) {
@@ -142,6 +162,11 @@
           }
         }
       }
+      for (Set<MetaState> orGroup : OR_GROUP) {
+        if (orGroup.isEmpty()) {
+          return false;
+        }
+      }
       return true;
     }
   }
@@ -160,14 +185,13 @@
     this.contentDescription = Preconditions.checkNotNull(contentDescription);
     Preconditions.checkNotNull(metaStates);
     this.metaState = Sets.newEnumSet(metaStates, MetaState.class);
-    this.nextAddMetaStates = nextAddMetaStates;
-    this.nextRemoveMetaStates = nextRemoveMetaStates;
+    this.nextAddMetaStates = Preconditions.checkNotNull(nextAddMetaStates);
+    this.nextRemoveMetaStates = Preconditions.checkNotNull(nextRemoveMetaStates);
     this.flickMap = new EnumMap<Flick.Direction, Flick>(Flick.Direction.class);
-    for (Flick flick : flickCollection) {
-      if (this.flickMap.put(flick.getDirection(), flick) != null) {
-        throw new IllegalArgumentException(
-            "Duplicate flick direction is found: " + flick.getDirection());
-      }
+    for (Flick flick : Preconditions.checkNotNull(flickCollection)) {
+      Preconditions.checkArgument(
+          this.flickMap.put(flick.getDirection(), flick) == null,
+          "Duplicate flick direction is found: " + flick.getDirection());
     }
   }
 
@@ -188,12 +212,12 @@
    * The result is "valid" in the light of {@code MetaState#isValidSet(Set)}.
    */
   public Set<MetaState> getNextMetaStates(Set<MetaState> originalMetaStates) {
-    return Sets.union(Sets.difference(originalMetaStates,
+    return Sets.union(Sets.difference(Preconditions.checkNotNull(originalMetaStates),
                                       nextRemoveMetaStates), nextAddMetaStates).immutableCopy();
   }
 
-  public Flick getFlick(Flick.Direction direction) {
-    return flickMap.get(direction);
+  public Optional<Flick> getFlick(Flick.Direction direction) {
+    return Optional.fromNullable(flickMap.get(direction));
   }
 
   @Override
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Keyboard.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Keyboard.java
index 50c8910..b12e1d9 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/Keyboard.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/Keyboard.java
@@ -29,9 +29,18 @@
 
 package org.mozc.android.inputmethod.japanese.keyboard;
 
+import org.mozc.android.inputmethod.japanese.KeyboardSpecificationName;
+import org.mozc.android.inputmethod.japanese.keyboard.Flick.Direction;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.CompositionMode;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.CrossingEdgeBehavior;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpaceOnAlphanumeric;
+import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Request.SpecialRomanjiTable;
+import org.mozc.android.inputmethod.japanese.resources.R;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 
+import android.util.SparseIntArray;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -42,6 +51,231 @@
  */
 public class Keyboard {
 
+  /**
+   * Each keyboard has its own specification.
+   *
+   * For example, some keyboards use a special Romanji table.
+   */
+  public static enum KeyboardSpecification {
+    // 12 keys.
+    TWELVE_KEY_TOGGLE_KANA(
+        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_KANA", 0, 2, 0),
+        R.xml.kbd_12keys_kana,
+        false,
+        CompositionMode.HIRAGANA,
+        SpecialRomanjiTable.TWELVE_KEYS_TO_HIRAGANA,
+        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
+        true,
+        CrossingEdgeBehavior.DO_NOTHING),
+
+    TWELVE_KEY_TOGGLE_ALPHABET(
+        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_ALPHABET", 0, 2, 0),
+        R.xml.kbd_12keys_abc,
+        false,
+        CompositionMode.HALF_ASCII,
+        SpecialRomanjiTable.TWELVE_KEYS_TO_HALFWIDTHASCII,
+        SpaceOnAlphanumeric.COMMIT,
+        false,
+        CrossingEdgeBehavior.DO_NOTHING),
+
+    TWELVE_KEY_TOGGLE_QWERTY_ALPHABET(
+        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_QWERTY_ALPHABET", 0, 5, 0),
+        R.xml.kbd_qwerty_abc,
+        false,
+        CompositionMode.HALF_ASCII,
+        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
+        SpaceOnAlphanumeric.COMMIT,
+        false,
+        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),
+
+    // Flick mode.
+    TWELVE_KEY_FLICK_KANA(
+        new KeyboardSpecificationName("TWELVE_KEY_FLICK_KANA", 0, 2, 0),
+        R.xml.kbd_12keys_flick_kana,
+        false,
+        CompositionMode.HIRAGANA,
+        SpecialRomanjiTable.FLICK_TO_HIRAGANA,
+        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
+        true,
+        CrossingEdgeBehavior.DO_NOTHING),
+
+    TWELVE_KEY_FLICK_ALPHABET(
+        new KeyboardSpecificationName("TWELVE_KEY_FLICK_ALPHABET", 0, 2, 0),
+        R.xml.kbd_12keys_flick_abc,
+        false,
+        CompositionMode.HALF_ASCII,
+        SpecialRomanjiTable.FLICK_TO_HALFWIDTHASCII,
+        SpaceOnAlphanumeric.COMMIT,
+        false,
+        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),
+
+    TWELVE_KEY_TOGGLE_FLICK_KANA(
+        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_KANA", 0, 2, 0),
+        R.xml.kbd_12keys_flick_kana,
+        false,
+        CompositionMode.HIRAGANA,
+        SpecialRomanjiTable.TOGGLE_FLICK_TO_HIRAGANA,
+        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
+        true,
+        CrossingEdgeBehavior.DO_NOTHING),
+
+    TWELVE_KEY_TOGGLE_FLICK_ALPHABET(
+        new KeyboardSpecificationName("TWELVE_KEY_TOGGLE_FLICK_ALPHABET", 0, 2, 0),
+        R.xml.kbd_12keys_flick_abc,
+        false,
+        CompositionMode.HALF_ASCII,
+        SpecialRomanjiTable.TOGGLE_FLICK_TO_HALFWIDTHASCII,
+        SpaceOnAlphanumeric.COMMIT,
+        false,
+        CrossingEdgeBehavior.DO_NOTHING),
+
+    // QWERTY keyboard.
+    QWERTY_KANA(
+        new KeyboardSpecificationName("QWERTY_KANA", 0, 4, 0),
+        R.xml.kbd_qwerty_kana,
+        false,
+        CompositionMode.HIRAGANA,
+        SpecialRomanjiTable.QWERTY_MOBILE_TO_HIRAGANA,
+        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
+        false,
+        CrossingEdgeBehavior.DO_NOTHING),
+
+    QWERTY_ALPHABET(
+        new KeyboardSpecificationName("QWERTY_ALPHABET", 0, 5, 0),
+        R.xml.kbd_qwerty_abc,
+        false,
+        CompositionMode.HALF_ASCII,
+        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
+        SpaceOnAlphanumeric.COMMIT,
+        false,
+        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),
+
+    QWERTY_ALPHABET_NUMBER(
+        new KeyboardSpecificationName("QWERTY_ALPHABET_NUMBER", 0, 3, 0),
+        R.xml.kbd_qwerty_abc_123,
+        false,
+        CompositionMode.HALF_ASCII,
+        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
+        SpaceOnAlphanumeric.COMMIT,
+        false,
+        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),
+
+    // Godan keyboard.
+    GODAN_KANA(
+        new KeyboardSpecificationName("GODAN_KANA", 0, 2, 0),
+        R.xml.kbd_godan_kana,
+        false,
+        CompositionMode.HIRAGANA,
+        SpecialRomanjiTable.GODAN_TO_HIRAGANA,
+        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
+        true,
+        CrossingEdgeBehavior.COMMIT_WITHOUT_CONSUMING),
+
+    NUMBER(
+        new KeyboardSpecificationName("NUMBER", 0, 1, 0),
+        R.xml.kbd_123,
+        false,
+        CompositionMode.HALF_ASCII,
+        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
+        SpaceOnAlphanumeric.COMMIT,
+        false,
+        CrossingEdgeBehavior.DO_NOTHING),
+
+    // Number keyboard on symbol input view.
+    SYMBOL_NUMBER(
+        new KeyboardSpecificationName("TWELVE_KEY_SYMBOL_NUMBER", 0, 1, 0),
+        R.xml.kbd_symbol_123,
+        false,
+        CompositionMode.HALF_ASCII,
+        SpecialRomanjiTable.QWERTY_MOBILE_TO_HALFWIDTHASCII,
+        SpaceOnAlphanumeric.COMMIT,
+        false,
+        CrossingEdgeBehavior.DO_NOTHING),
+
+    // HARDWARE QWERTY keyboard.
+    HARDWARE_QWERTY_KANA(
+        new KeyboardSpecificationName("HARDWARE_QWERTY_KANA", 0, 1, 0),
+        0,
+        true,
+        CompositionMode.HIRAGANA,
+        SpecialRomanjiTable.DEFAULT_TABLE,
+        SpaceOnAlphanumeric.SPACE_OR_CONVERT_KEEPING_COMPOSITION,
+        false,
+        CrossingEdgeBehavior.DO_NOTHING),
+
+    HARDWARE_QWERTY_ALPHABET(
+        new KeyboardSpecificationName("HARDWARE_QWERTY_ALPHABET", 0, 1, 0),
+        0,
+        true,
+        CompositionMode.HALF_ASCII,
+        SpecialRomanjiTable.DEFAULT_TABLE,
+        SpaceOnAlphanumeric.COMMIT,
+        false,
+        CrossingEdgeBehavior.DO_NOTHING),
+
+    ;
+
+    private final KeyboardSpecificationName specName;
+    private final int resourceId;
+    private final boolean isHardwareKeyboard;
+    private final CompositionMode compositionMode;
+    private final SpecialRomanjiTable specialRomanjiTable;
+    private final SpaceOnAlphanumeric spaceOnAlphanumeric;
+    private final boolean kanaModifierInsensitiveConversion;
+    private final CrossingEdgeBehavior crossingEdgeBehavior;
+
+    private KeyboardSpecification(
+        KeyboardSpecificationName specName,
+        int resourceId,
+        boolean isHardwareKeyboard,
+        CompositionMode compositionMode,
+        SpecialRomanjiTable specialRomanjiTable,
+        SpaceOnAlphanumeric spaceOnAlphanumeric,
+        boolean kanaModifierInsensitiveConversion,
+        CrossingEdgeBehavior crossingEdgeBehavior) {
+      this.specName = Preconditions.checkNotNull(specName);
+      this.resourceId = resourceId;
+      this.isHardwareKeyboard = isHardwareKeyboard;
+      this.compositionMode = Preconditions.checkNotNull(compositionMode);
+      this.specialRomanjiTable = Preconditions.checkNotNull(specialRomanjiTable);
+      this.spaceOnAlphanumeric = Preconditions.checkNotNull(spaceOnAlphanumeric);
+      this.kanaModifierInsensitiveConversion = kanaModifierInsensitiveConversion;
+      this.crossingEdgeBehavior = Preconditions.checkNotNull(crossingEdgeBehavior);
+    }
+
+    public int getXmlLayoutResourceId() {
+      return resourceId;
+    }
+
+    public CompositionMode getCompositionMode() {
+      return compositionMode;
+    }
+
+    public KeyboardSpecificationName getKeyboardSpecificationName() {
+      return specName;
+    }
+
+    public SpecialRomanjiTable getSpecialRomanjiTable() {
+      return specialRomanjiTable;
+    }
+
+    public SpaceOnAlphanumeric getSpaceOnAlphanumeric() {
+      return spaceOnAlphanumeric;
+    }
+
+    public boolean isKanaModifierInsensitiveConversion() {
+      return kanaModifierInsensitiveConversion;
+    }
+
+    public CrossingEdgeBehavior getCrossingEdgeBehavior() {
+      return crossingEdgeBehavior;
+    }
+
+    public boolean isHardwareKeyboard() {
+      return isHardwareKeyboard;
+    }
+  }
+
   private final Optional<String> contentDescription;
   private final float flickThreshold;
   private final List<Row> rowList;
@@ -50,9 +284,12 @@
   public final int contentRight;
   public final int contentTop;
   public final int contentBottom;
+  protected final KeyboardSpecification specification;
+  private Optional<SparseIntArray> sourceIdToKeyCode = Optional.absent();
 
   public Keyboard(Optional<String> contentDescription,
-                  List<? extends Row> rowList, float flickThreshold) {
+                  List<? extends Row> rowList, float flickThreshold,
+                  KeyboardSpecification specification) {
     this.contentDescription = Preconditions.checkNotNull(contentDescription);
     this.flickThreshold = flickThreshold;
     this.rowList = Collections.unmodifiableList(rowList);
@@ -71,6 +308,7 @@
     this.contentRight = right;
     this.contentTop = top;
     this.contentBottom = bottom;
+    this.specification = Preconditions.checkNotNull(specification);
   }
 
   public Optional<String> getContentDescription() {
@@ -84,4 +322,39 @@
   public List<Row> getRowList() {
     return rowList;
   }
+
+  public KeyboardSpecification getSpecification() {
+    return specification;
+  }
+
+  /**
+   * Returns keyCode from {@code souceId}.
+   *
+   * <p>If not found, {@code Integer.MIN_VALUE} is returned.
+   */
+  public int getKeyCode(int sourceId) {
+    ensureSourceIdToKeyCode();
+    return sourceIdToKeyCode.get().get(sourceId, Integer.MIN_VALUE);
+  }
+
+  private void ensureSourceIdToKeyCode() {
+    if (sourceIdToKeyCode.isPresent()) {
+      return;
+    }
+    SparseIntArray result = new SparseIntArray();
+    for (Row row : getRowList()) {
+      for (Key key : row.getKeyList()) {
+        for (KeyState keyState : key.getKeyStates()) {
+          for (Direction direction : Direction.values()) {
+            Optional<Flick> flick = keyState.getFlick(direction);
+            if (flick.isPresent()) {
+              KeyEntity keyEntity = flick.get().getKeyEntity();
+              result.put(keyEntity.getSourceId(), keyEntity.getKeyCode());
+            }
+          }
+        }
+      }
+    }
+    sourceIdToKeyCode = Optional.of(result);
+  }
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardActionListener.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardActionListener.java
index bdc8a5c..51ee9e3 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardActionListener.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardActionListener.java
@@ -35,11 +35,11 @@
 
 /**
  * A listener of keyboard actions.
- * 
+ *
  */
 public interface KeyboardActionListener {
   public void onCancel();
   public void onPress(int keycode);
   public void onRelease(int keycode);
-  public void onKey(int primaryCode, List<? extends TouchEvent> touchEventList);
+  public void onKey(int primaryCode, List<TouchEvent> touchEventList);
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardFactory.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardFactory.java
new file mode 100644
index 0000000..3f38a5f
--- /dev/null
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardFactory.java
@@ -0,0 +1,152 @@
+// Copyright 2010-2014, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package org.mozc.android.inputmethod.japanese.keyboard;
+
+import org.mozc.android.inputmethod.japanese.MozcLog;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
+import org.mozc.android.inputmethod.japanese.util.LeastRecentlyUsedCacheMap;
+import com.google.common.base.Optional;
+
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Factory of the keyboard data based on xml.
+ *
+ */
+public class KeyboardFactory {
+
+  /**
+   * Key for the cache map of keyboard.
+   *
+   * Currently the keyboard is depending on its specification and display's size.
+   */
+  private static class CacheKey {
+    private final KeyboardSpecification specification;
+    private final int width;
+    private final int height;
+
+    CacheKey(KeyboardSpecification specification, int width, int height) {
+      this.specification = specification;
+      this.width = width;
+      this.height = height;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof CacheKey) {
+        CacheKey other = CacheKey.class.cast(obj);
+        return specification == other.specification &&
+               width == other.width &&
+               height == other.height;
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return (specification.hashCode() * 31 ^ width) * 31 ^ height;
+    }
+  }
+
+  /**
+   * The max size of cached keyboards. This is based on the max number of keyboard variation
+   * for a configuration.
+   */
+  private static final int CACHE_SIZE = 6;
+
+  private final Map<CacheKey, Keyboard> cache =
+      new LeastRecentlyUsedCacheMap<CacheKey, Keyboard>(CACHE_SIZE);
+
+  /**
+   * @return JapaneseKeyboard instance based on given resources and specification.
+   *         If it is already parsed, just returns cached one. Otherwise, tries to parse
+   *         corresponding xml data, then caches and returns it.
+   *         Returns {@code null} if parsing is failed.
+   * @throws NullPointerException if given {@code resources} or {@code specification} is
+   *         {@code null}.
+   */
+  public Keyboard get(Resources resources, KeyboardSpecification specification,
+                      int keyboardWidth, int keyboardHeight) {
+    if (resources == null) {
+      throw new NullPointerException("resources is null.");
+    }
+    if (specification == null) {
+      throw new NullPointerException("specification is null.");
+    }
+
+    CacheKey cacheKey =
+        new CacheKey(specification, keyboardWidth, keyboardHeight);
+
+    // First, look up from the cache.
+    Keyboard keyboard = cache.get(cacheKey);
+    if (keyboard == null) {
+      // If not found, parse keyboard from a xml resource file. The result will be cached in
+      // the cache map.
+      keyboard = parseKeyboard(resources, specification, keyboardWidth, keyboardHeight);
+      if (keyboard != null) {
+        cache.put(cacheKey, keyboard);
+      }
+    }
+    return keyboard;
+  }
+
+  private static Keyboard parseKeyboard(
+      Resources resources, KeyboardSpecification specification,
+      int keyboardWidth, int keyboardHeight) {
+    KeyboardParser parser = new KeyboardParser(
+        resources, keyboardWidth, keyboardHeight, specification);
+    try {
+      return parser.parseKeyboard();
+    } catch (NotFoundException e) {
+      MozcLog.e(e.getMessage());
+    } catch (XmlPullParserException e) {
+      MozcLog.e(e.getMessage());
+    } catch (IOException e) {
+      MozcLog.e(e.getMessage());
+    }
+    // Returns dummy keyboard to avoid crash.
+    return new Keyboard(Optional.<String>absent(), Collections.<Row>emptyList(), 0,
+                        KeyboardSpecification.TWELVE_KEY_TOGGLE_FLICK_KANA);
+  }
+
+  /**
+   * Clears cached keyboards.
+   */
+  public void clear() {
+    cache.clear();
+  }
+}
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardParser.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardParser.java
index 80ac180..cd4c57e 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardParser.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardParser.java
@@ -31,7 +31,9 @@
 
 import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory.DrawableType;
 import org.mozc.android.inputmethod.japanese.keyboard.Key.Stick;
+import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
 import org.mozc.android.inputmethod.japanese.resources.R;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Objects;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
@@ -59,35 +61,172 @@
 public class KeyboardParser {
 
   /** Attributes for the key dimensions. */
-  private static class KeyAttributes {
+  @VisibleForTesting static class KeyAttributes {
+
+    @VisibleForTesting static class Builder {
+
+      private int width;
+      private int height;
+      private int horizontalLayoutWeight;
+      private int horizontalGap;
+      private int verticalGap;
+      private int defaultIconWidth;
+      private int defaultIconHeight;
+      private int defaultHorizontalPadding;
+      private int defaultVerticalPadding;
+      private DrawableType keyBackgroundDrawableType =
+          DrawableType.TWELVEKEYS_REGULAR_KEY_BACKGROUND;
+      private int edgeFlags;
+      private boolean isRepeatable;
+      private boolean isModifier;
+      private Stick stick = Stick.EVEN;
+      private List<KeyState> keyStateList = Collections.<KeyState>emptyList();
+
+      Builder setWidth(int width) {
+        this.width = width;
+        return this;
+      }
+      Builder setHeight(int height) {
+        this.height = height;
+        return this;
+      }
+      Builder setHorizontalLayoutWeight(int horizontalLayoutWeight) {
+        this.horizontalLayoutWeight = horizontalLayoutWeight;
+        return this;
+      }
+      Builder setHorizontalGap(int horizontalGap) {
+        this.horizontalGap = horizontalGap;
+        return this;
+      }
+      Builder setVerticalGap(int verticalGap) {
+        this.verticalGap = verticalGap;
+        return this;
+      }
+      Builder setDefaultIconWidth(int defaultIconWidth) {
+        this.defaultIconWidth = defaultIconWidth;
+        return this;
+      }
+      Builder setDefaultIconHeight(int defaultIconHeight) {
+        this.defaultIconHeight = defaultIconHeight;
+        return this;
+      }
+      Builder setDefaultHorizontalPadding(int defaultHorizontalPadding) {
+        this.defaultHorizontalPadding = defaultHorizontalPadding;
+        return this;
+      }
+      Builder setDefaultVerticalPadding(int defaultVerticalPadding) {
+        this.defaultVerticalPadding = defaultVerticalPadding;
+        return this;
+      }
+      Builder setKeybackgroundDrawableType(DrawableType type) {
+        this.keyBackgroundDrawableType = Preconditions.checkNotNull(type);
+        return this;
+      }
+      Builder setEdgeFlags(int edgeFlags) {
+        this.edgeFlags = edgeFlags;
+        return this;
+      }
+      Builder setRepeatable(boolean isRepeatable) {
+        this.isRepeatable = isRepeatable;
+        return this;
+      }
+      Builder setModifier(boolean isModifier) {
+        this.isModifier = isModifier;
+        return this;
+      }
+      Builder setStick(Stick stick) {
+        this.stick = Preconditions.checkNotNull(stick);
+        return this;
+      }
+      Builder setKeyStateList(List<KeyState> keyStateList) {
+        this.keyStateList = Preconditions.checkNotNull(keyStateList);
+        return this;
+      }
+      KeyAttributes build() {
+        return new KeyAttributes(this);
+      }
+    }
+
     final int width;
     final int height;
+    final int horizontalLayoutWeight;
     final int horizontalGap;
     final int verticalGap;
-    DrawableType keyBackgroundDrawableType;
+    final int defaultIconWidth;
+    final int defaultIconHeight;
+    final int defaultHorizontalPadding;
+    final int defaultVerticalPadding;
+    final DrawableType keyBackgroundDrawableType;
+    final int edgeFlags;
+    final boolean isRepeatable;
+    final boolean isModifier;
+    final Stick stick;
+    final List<KeyState> keyStateList;
 
-    KeyAttributes(int width, int height, int horizontalGap, int verticalGap,
-                 DrawableType keyBackgroundDrawableType) {
-      this.width = width;
-      this.height = height;
-      this.horizontalGap = horizontalGap;
-      this.verticalGap = verticalGap;
-      this.keyBackgroundDrawableType = keyBackgroundDrawableType;
+    private KeyAttributes(Builder builder) {
+      this.width = builder.width;
+      this.height = builder.height;
+      this.horizontalLayoutWeight = builder.horizontalLayoutWeight;
+      this.horizontalGap = builder.horizontalGap;
+      this.verticalGap = builder.verticalGap;
+      this.defaultIconWidth = builder.defaultIconWidth;
+      this.defaultIconHeight = builder.defaultIconHeight;
+      this.defaultHorizontalPadding = builder.defaultHorizontalPadding;
+      this.defaultVerticalPadding = builder.defaultVerticalPadding;
+      this.keyBackgroundDrawableType =
+          Preconditions.checkNotNull(builder.keyBackgroundDrawableType);
+      this.edgeFlags = builder.edgeFlags;
+      this.isRepeatable = builder.isRepeatable;
+      this.isModifier = builder.isModifier;
+      this.stick = builder.stick;
+      this.keyStateList = builder.keyStateList;
+    }
+
+    static Builder newBuilder() {
+      return new Builder();
+    }
+
+    Builder toBuilder() {
+      return newBuilder()
+          .setWidth(width)
+          .setHeight(height)
+          .setHorizontalLayoutWeight(horizontalLayoutWeight)
+          .setHorizontalGap(horizontalGap)
+          .setVerticalGap(verticalGap)
+          .setDefaultHorizontalPadding(defaultHorizontalPadding)
+          .setDefaultVerticalPadding(defaultVerticalPadding)
+          .setDefaultIconWidth(defaultIconWidth)
+          .setDefaultIconHeight(defaultIconHeight)
+          .setKeybackgroundDrawableType(keyBackgroundDrawableType)
+          .setEdgeFlags(edgeFlags)
+          .setRepeatable(isRepeatable)
+          .setModifier(isModifier)
+          .setStick(stick)
+          .setKeyStateList(keyStateList);
+    }
+
+    Key buildKey(int x, int y, int width) {
+      return new Key(x, y, width, height, horizontalGap, edgeFlags,
+          isRepeatable, isModifier, stick, keyBackgroundDrawableType, keyStateList);
     }
   }
 
   /** Attributes for the popup dimensions. */
   private static class PopUpAttributes {
-    final int popUpWidth;
+
     final int popUpHeight;
     final int popUpXOffset;
     final int popUpYOffset;
+    final int popUpIconWidth;
+    final int popUpIconHeight;
 
-    PopUpAttributes(int popUpWidth, int popUpHeight, int popUpXOffset, int popUpYOffset) {
-      this.popUpWidth = popUpWidth;
+    PopUpAttributes(int popUpHeight, int popUpXOffset, int popUpYOffset,
+                    int popUpIconWidth, int popUpIconHeight) {
       this.popUpHeight = popUpHeight;
       this.popUpXOffset = popUpXOffset;
       this.popUpYOffset = popUpYOffset;
+      this.popUpIconWidth = popUpIconWidth;
+      this.popUpIconHeight = popUpIconHeight;
     }
   }
 
@@ -115,16 +254,47 @@
   private static final int ROW_ROW_EDGE_FLAGS_INDEX =
       Arrays.binarySearch(ROW_ATTRIBUTES, R.attr.rowEdgeFlags);
 
+  private static final int[] POPUP_ATTRIBUTES = {
+    R.attr.popUpIcon,
+    R.attr.popUpLongPressIcon,
+    R.attr.popUpHeight,
+    R.attr.popUpXOffset,
+    R.attr.popUpYOffset,
+    R.attr.popUpIconWidth,
+    R.attr.popUpIconHeight,
+  };
+  static {
+    Arrays.sort(POPUP_ATTRIBUTES);
+  }
+  private static final int POPUP_KEY_ICON_INDEX =
+      Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpIcon);
+  private static final int POPUP_KEY_LONG_PRESS_ICON_INDEX =
+      Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpLongPressIcon);
+  private static final int POPUP_KEY_HEIGHT_INDEX =
+      Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpHeight);
+  private static final int POPUP_KEY_X_OFFSET_INDEX =
+      Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpXOffset);
+  private static final int POPUP_KEY_Y_OFFSET_INDEX =
+      Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpYOffset);
+  private static final int POPUP_KEY_ICON_WIDTH_INDEX =
+      Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpIconWidth);
+  private static final int POPUP_KEY_ICON_HEIGHT_INDEX =
+      Arrays.binarySearch(POPUP_ATTRIBUTES, R.attr.popUpIconHeight);
+
   /** Attributes for a {@code <Key>} element. */
   private static final int[] KEY_ATTRIBUTES = {
     R.attr.keyWidth,
     R.attr.keyHeight,
+    R.attr.keyHorizontalLayoutWeight,
     R.attr.horizontalGap,
+    R.attr.defaultIconWidth,
+    R.attr.defaultIconHeight,
+    R.attr.defaultHorizontalPadding,
+    R.attr.defaultVerticalPadding,
     R.attr.keyBackground,
     R.attr.keyEdgeFlags,
     R.attr.isRepeatable,
     R.attr.isModifier,
-    R.attr.isSticky,
   };
   static {
     Arrays.sort(KEY_ATTRIBUTES);
@@ -133,8 +303,18 @@
       Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.keyWidth);
   private static final int KEY_KEY_HEIGHT_INDEX =
       Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.keyHeight);
+  private static final int KEY_KEY_HORIZONTAL_LAYOUT_WEIGHT_INDEX =
+      Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.keyHorizontalLayoutWeight);
   private static final int KEY_HORIZONTAL_GAP_INDEX =
       Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.horizontalGap);
+  private static final int KEY_DEFAULT_ICON_WIDTH_INDEX =
+      Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.defaultIconWidth);
+  private static final int KEY_DEFAULT_ICON_HEIGHT_INDEX =
+      Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.defaultIconHeight);
+  private static final int KEY_DEFAULT_HORIZONTAL_PADDING_INDEX =
+      Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.defaultHorizontalPadding);
+  private static final int KEY_DEFAULT_VERTICAL_PADDING_INDEX =
+      Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.defaultVerticalPadding);
   private static final int KEY_KEY_BACKGROUND_INDEX =
       Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.keyBackground);
   private static final int KEY_KEY_EDGE_FLAGS_INDEX =
@@ -143,32 +323,35 @@
       Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.isRepeatable);
   private static final int KEY_IS_MODIFIER_INDEX =
       Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.isModifier);
-  private static final int KEY_IS_STICKY_INDEX =
-      Arrays.binarySearch(KEY_ATTRIBUTES, R.attr.isSticky);
 
   /** Attributes for a {@code <Spacer>} element. */
   private static final int[] SPACER_ATTRIBUTES = {
+    R.attr.keyWidth,
     R.attr.keyHeight,
-    R.attr.horizontalGap,
+    R.attr.keyHorizontalLayoutWeight,
     R.attr.keyEdgeFlags,
     R.attr.stick,
+    R.attr.keyBackground,
   };
   static {
     Arrays.sort(SPACER_ATTRIBUTES);
   }
+  private static final int SPACER_KEY_WIDTH_INDEX =
+      Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.keyWidth);
   private static final int SPACER_KEY_HEIGHT_INDEX =
       Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.keyHeight);
-  private static final int SPACER_HORIZONTAL_GAP_INDEX =
-      Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.horizontalGap);
+  private static final int SPACER_KEY_HORIZONTAL_LAYOUT_WEIGHT_INDEX =
+      Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.keyHorizontalLayoutWeight);
   private static final int SPACER_KEY_EDGE_FLAGS_INDEX =
       Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.keyEdgeFlags);
   private static final int SPACER_STICK_INDEX =
       Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.stick);
+  private static final int SPACER_KEY_BACKGROUND_INDEX =
+      Arrays.binarySearch(SPACER_ATTRIBUTES, R.attr.keyBackground);
 
   /** Attributes for a {@code <KeyState>} element. */
   private static final int[] KEY_STATE_ATTRIBUTES = {
     R.attr.contentDescription,
-    R.attr.keyBackground,
     R.attr.metaState,
     R.attr.nextMetaState,
     R.attr.nextRemovedMetaStates,
@@ -178,8 +361,6 @@
   }
   private static final int KEY_STATE_CONTENT_DESCRIPTION_INDEX =
       Arrays.binarySearch(KEY_STATE_ATTRIBUTES, R.attr.contentDescription);
-  private static final int KEY_STATE_KEY_BACKGROUND_INDEX =
-      Arrays.binarySearch(KEY_STATE_ATTRIBUTES, R.attr.keyBackground);
   private static final int KEY_STATE_META_STATE_INDEX =
       Arrays.binarySearch(KEY_STATE_ATTRIBUTES, R.attr.metaState);
   private static final int KEY_STATE_NEXT_META_STATE_INDEX =
@@ -192,9 +373,14 @@
     R.attr.sourceId,
     R.attr.keyCode,
     R.attr.longPressKeyCode,
+    R.attr.longPressTimeoutTrigger,
     R.attr.keyIcon,
     R.attr.keyCharacter,
     R.attr.flickHighlight,
+    R.attr.horizontalPadding,
+    R.attr.verticalPadding,
+    R.attr.iconWidth,
+    R.attr.iconHeight,
   };
   static {
     Arrays.sort(KEY_ENTITY_ATTRIBUTES);
@@ -205,12 +391,22 @@
       Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.keyCode);
   private static final int KEY_ENTITY_LONG_PRESS_KEY_CODE_INDEX =
       Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.longPressKeyCode);
+  private static final int KEY_ENTITY_LONG_PRESS_TIMEOUT_TRIGGER_INDEX =
+      Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.longPressTimeoutTrigger);
   private static final int KEY_ENTITY_KEY_ICON_INDEX =
       Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.keyIcon);
   private static final int KEY_ENTITY_KEY_CHAR_INDEX =
       Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.keyCharacter);
   private static final int KEY_ENTITY_FLICK_HIGHLIGHT_INDEX =
       Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.flickHighlight);
+  private static final int KEY_ENTITY_HORIZONTAL_PADDING_INDEX =
+      Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.horizontalPadding);
+  private static final int KEY_ENTITY_VERTICAL_PADDING_INDEX =
+      Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.verticalPadding);
+  private static final int KEY_ENTITY_ICON_WIDTH_INDEX =
+      Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.iconWidth);
+  private static final int KEY_ENTITY_ICON_HEIGHT_INDEX =
+      Arrays.binarySearch(KEY_ENTITY_ATTRIBUTES, R.attr.iconHeight);
 
   /**
    * Mapping table from enum value in xml to DrawableType by using the enum value as index.
@@ -222,30 +418,33 @@
     DrawableType.QWERTY_REGULAR_KEY_BACKGROUND,
     DrawableType.QWERTY_FUNCTION_KEY_BACKGROUND,
     DrawableType.QWERTY_FUNCTION_KEY_BACKGROUND_WITH_THREEDOTS,
-    DrawableType.QWERTY_FUNCTION_KEY_LIGHT_ON_BACKGROUND,
-    DrawableType.QWERTY_FUNCTION_KEY_LIGHT_OFF_BACKGROUND,
+    DrawableType.QWERTY_FUNCTION_KEY_SPACE_WITH_THREEDOTS,
+    DrawableType.KEYBOARD_SEPARATOR_TOP,
+    DrawableType.KEYBOARD_SEPARATOR_CENTER,
+    DrawableType.KEYBOARD_SEPARATOR_BOTTOM,
+    DrawableType.TRNASPARENT,
   };
 
   /**
    * @return "sourceId" assigned to {@code value}.
    */
-  static int getSourceId(TypedValue value, @SuppressWarnings("unused") int defaultValue) {
-    if (value == null ||
-        (value.type != TypedValue.TYPE_INT_DEC &&
-         value.type != TypedValue.TYPE_INT_HEX)) {
-      throw new IllegalArgumentException("sourceId is mandatory for KeyEntity.");
-    }
+  private static int getSourceId(TypedValue value, @SuppressWarnings("unused") int defaultValue) {
+    Preconditions.checkNotNull(value);
+    Preconditions.checkArgument(
+        value.type == TypedValue.TYPE_INT_DEC || value.type == TypedValue.TYPE_INT_HEX,
+        "sourceId is mandatory for KeyEntity.");
     return value.data;
   }
 
   /**
    * @return the pixel offsets based on metrics and base
    */
-  static int getDimensionOrFraction(
-      TypedValue value, int base, int defaultValue, DisplayMetrics metrics) {
-    if (value == null) {
+  @VisibleForTesting static int getDimensionOrFraction(
+      Optional<TypedValue> optionalValue, int base, int defaultValue, DisplayMetrics metrics) {
+    if (!optionalValue.isPresent()) {
       return defaultValue;
     }
+    TypedValue value = optionalValue.get();
 
     switch (value.type) {
       case TypedValue.TYPE_DIMENSION:
@@ -258,13 +457,29 @@
                                        "  value = " + value.toString());
   }
 
+  @VisibleForTesting static int getFraction(
+      Optional<TypedValue> optionalValue, int base, int defaultValue) {
+    if (!optionalValue.isPresent()) {
+      return defaultValue;
+    }
+    TypedValue value = optionalValue.get();
+
+    if (value.type == TypedValue.TYPE_FRACTION) {
+      return Math.round(TypedValue.complexToFraction(value.data, base, base));
+    }
+
+    throw new IllegalArgumentException(
+        "The type fraction is required.  value = " + value.toString());
+  }
+
   /**
    * @return "codes" assigned to {@code value}
    */
-  static int getCode(TypedValue value, int defaultValue) {
-    if (value == null) {
+  @VisibleForTesting static int getCode(Optional<TypedValue> optionalValue, int defaultValue) {
+    if (!optionalValue.isPresent()) {
       return defaultValue;
     }
+    TypedValue value = optionalValue.get();
 
     if (value.type == TypedValue.TYPE_INT_DEC ||
         value.type == TypedValue.TYPE_INT_HEX) {
@@ -277,20 +492,6 @@
     return defaultValue;
   }
 
-  /**
-   * A simple wrapper of {@link CharSequence#toString()}, in order to avoid
-   * {@code NullPointerException}.
-   * @param sequence input character sequence
-   * @return {@code sequence.toString()} if {@code sequence} is not {@code null}.
-   *         Otherwise, {@code null}.
-   */
-  static String toStringOrNull(CharSequence sequence) {
-    if (sequence == null) {
-      return null;
-    }
-    return sequence.toString();
-  }
-
   private static void ignoreWhiteSpaceAndComment(XmlPullParser parser)
       throws XmlPullParserException, IOException {
     int event = parser.getEventType();
@@ -302,8 +503,8 @@
   private static void assertStartDocument(XmlPullParser parser) throws XmlPullParserException {
     if (parser.getEventType() != XmlPullParser.START_DOCUMENT) {
       throw new IllegalArgumentException(
-          "The start of document is expected, but actually not: " +
-          parser.getPositionDescription());
+          "The start of document is expected, but actually not: "
+              + parser.getPositionDescription());
     }
   }
 
@@ -325,8 +526,8 @@
     String actualName = parser.getName();
     if (!actualName.equals(expectedName)) {
       throw new IllegalArgumentException(
-          "Tag <" + expectedName + "> is expected, but found <" + actualName + ">: " +
-          parser.getPositionDescription());
+          "Tag <" + expectedName + "> is expected, but found <" + actualName + ">: "
+              + parser.getPositionDescription());
     }
   }
 
@@ -353,20 +554,16 @@
   private final Set<Integer> sourceIdSet = new HashSet<Integer>();
   private final int keyboardWidth;
   private final int keyboardHeight;
+  private final KeyboardSpecification specification;
 
-  public KeyboardParser(Resources resources, XmlResourceParser xmlResourceParser,
-                        int keyboardWidth, int keyboardHeight) {
-    if (resources == null) {
-      throw new NullPointerException("resources shouldn't be null.");
-    }
-    if (xmlResourceParser == null) {
-      throw new NullPointerException("xmlResourceParser shouldn't be null.");
-    }
-
-    this.resources = resources;
-    this.xmlResourceParser = xmlResourceParser;
+  public KeyboardParser(Resources resources,
+                        int keyboardWidth, int keyboardHeight,
+                        KeyboardSpecification specification) {
+    this.resources = Preconditions.checkNotNull(resources);
     this.keyboardWidth = keyboardWidth;
     this.keyboardHeight = keyboardHeight;
+    this.specification = Preconditions.checkNotNull(specification);
+    this.xmlResourceParser = resources.getXml(specification.getXmlLayoutResourceId());
   }
 
   /**
@@ -427,23 +624,38 @@
         // The default keyWidth is 10% of the display for width, and 50px for height.
         keyAttributes = parseKeyAttributes(
             attributes,
-            new KeyAttributes(keyboardWidth / 10, 50, 0, 0, null),
+            KeyAttributes.newBuilder()
+                .setWidth(keyboardWidth / 10)
+                .setHeight(50)
+                .setKeybackgroundDrawableType(DrawableType.TWELVEKEYS_REGULAR_KEY_BACKGROUND)
+                .setDefaultIconWidth(keyboardWidth)
+                .setDefaultIconHeight(keyboardHeight)
+                .setDefaultHorizontalPadding(0)
+                .setDefaultVerticalPadding(0)
+                .build(),
             metrics,
             this.keyboardWidth,
             this.keyboardHeight,
             R.styleable.Keyboard_keyWidth,
             R.styleable.Keyboard_keyHeight,
+            R.styleable.Keyboard_keyHorizontalLayoutWeight,
             R.styleable.Keyboard_horizontalGap,
             R.styleable.Keyboard_verticalGap,
+            R.styleable.Keyboard_defaultIconWidth,
+            R.styleable.Keyboard_defaultIconHeight,
+            R.styleable.Keyboard_defaultHorizontalPadding,
+            R.styleable.Keyboard_defaultVerticalPadding,
             R.styleable.Keyboard_keyBackground);
         popUpAttributes = parsePopUpAttributes(
             attributes,
+            new PopUpAttributes(0, 0, 0, 0, 0),
             metrics,
             this.keyboardWidth,
-            R.styleable.Keyboard_popUpWidth,
             R.styleable.Keyboard_popUpHeight,
             R.styleable.Keyboard_popUpXOffset,
-            R.styleable.Keyboard_popUpYOffset);
+            R.styleable.Keyboard_popUpYOffset,
+            R.styleable.Keyboard_popUpIconWidth,
+            R.styleable.Keyboard_popUpIconHeight);
         flickThreshold = parseFlickThreshold(
             attributes, R.styleable.Keyboard_flickThreshold);
         contentDescription = Optional.fromNullable(
@@ -473,7 +685,36 @@
     ignoreWhiteSpaceAndComment(parser);
     assertEndDocument(parser);
 
-    return buildKeyboard(contentDescription, rowList, flickThreshold);
+    return new Keyboard(Preconditions.checkNotNull(contentDescription),
+        Preconditions.checkNotNull(rowList), flickThreshold, specification);
+  }
+
+  @VisibleForTesting
+  static List<Key> buildKeyList(List<KeyAttributes> keyAttributesList,
+                                        int y, int rowWidth) {
+    float remainingWidthByWeight = rowWidth;
+    int remainingWeight = 0;
+    for (KeyAttributes attributes : keyAttributesList) {
+      remainingWidthByWeight -= attributes.width;
+      remainingWeight += attributes.horizontalLayoutWeight;
+    }
+
+    List<Key> keyList = new ArrayList<Key>(keyAttributesList.size());
+    float exactX = 0;
+    for (KeyAttributes attributes : keyAttributesList) {
+      int weight = attributes.horizontalLayoutWeight;
+      Preconditions.checkState(remainingWeight >= weight);
+      float widthByWeight = weight > 0 ? remainingWidthByWeight * weight / remainingWeight : 0;
+      remainingWidthByWeight -= widthByWeight;
+      remainingWeight -= weight;
+      int x = Math.round(exactX);
+      Key key =
+          attributes.buildKey(x, y, Math.round(exactX + widthByWeight + attributes.width) - x);
+      keyList.add(key);
+      exactX += widthByWeight + attributes.width;
+    }
+    Preconditions.checkState(remainingWeight == 0);
+    return keyList;
   }
 
   /**
@@ -494,10 +735,10 @@
       TypedArray attributes = resources.obtainAttributes(parser, ROW_ATTRIBUTES);
       try {
         verticalGap = getDimensionOrFraction(
-            attributes.peekValue(ROW_VERTICAL_GAP_INDEX),
+            Optional.fromNullable(attributes.peekValue(ROW_VERTICAL_GAP_INDEX)),
             keyboardHeight, defaultKeyAttributes.verticalGap, metrics);
         rowHeight = getDimensionOrFraction(
-            attributes.peekValue(ROW_KEY_HEIGHT_INDEX), keyboardHeight,
+            Optional.fromNullable(attributes.peekValue(ROW_KEY_HEIGHT_INDEX)), keyboardHeight,
             defaultKeyAttributes.height, metrics);
         edgeFlags = attributes.getInt(ROW_ROW_EDGE_FLAGS_INDEX, 0);
       } finally {
@@ -505,8 +746,7 @@
       }
     }
 
-    List<Key> keyList = new ArrayList<Key>();
-    int x = 0;
+    List<KeyAttributes> keyAttributesList = new ArrayList<KeyAttributes>(16);  // 16 is heuristic
     while (true) {
       parser.next();
       ignoreWhiteSpaceAndComment(parser);
@@ -515,25 +755,22 @@
         break;
       }
       if ("Key".equals(parser.getName())) {
-        Key key = parseKey(x, y, edgeFlags, defaultKeyAttributes, popUpAttributes);
-        keyList.add(key);
-        x += key.getWidth();
+        keyAttributesList.add(parseKey(edgeFlags, defaultKeyAttributes, popUpAttributes));
       } else if ("Spacer".equals(parser.getName())) {
-        Key key = parseSpacer(x, y, edgeFlags, defaultKeyAttributes);
-        keyList.add(key);
-        x += key.getWidth();
+        keyAttributesList.add(parseSpacer(edgeFlags, defaultKeyAttributes));
       }
     }
 
     assertEndTag(parser, "Row");
+    List<Key> keyList = buildKeyList(keyAttributesList, y, keyboardWidth);
     return buildRow(keyList, rowHeight, verticalGap);
   }
 
   /**
    * Parses a {@code Key} element, and returns an instance.
    */
-  private Key parseKey(int x, int y, int edgeFlags,
-                       KeyAttributes defaultKeyAttributes, PopUpAttributes popUpAttributes)
+  private KeyAttributes parseKey(int edgeFlags, KeyAttributes defaultKeyAttributes,
+                                 PopUpAttributes popUpAttributes)
       throws XmlPullParserException, IOException {
     XmlResourceParser parser = this.xmlResourceParser;
     assertStartTag(parser, "Key");
@@ -541,19 +778,20 @@
     KeyAttributes keyAttributes;
     boolean isRepeatable;
     boolean isModifier;
-    boolean isSticky;
+    DisplayMetrics metrics = resources.getDisplayMetrics();
     {
       TypedArray attributes = resources.obtainAttributes(parser, KEY_ATTRIBUTES);
       try {
-        DisplayMetrics metrics = resources.getDisplayMetrics();
         keyAttributes = parseKeyAttributes(
             attributes, defaultKeyAttributes, metrics, keyboardWidth, keyboardHeight,
-            KEY_KEY_WIDTH_INDEX, KEY_KEY_HEIGHT_INDEX, KEY_HORIZONTAL_GAP_INDEX, -1,
+            KEY_KEY_WIDTH_INDEX, KEY_KEY_HEIGHT_INDEX, KEY_KEY_HORIZONTAL_LAYOUT_WEIGHT_INDEX,
+            KEY_HORIZONTAL_GAP_INDEX, -1,
+            KEY_DEFAULT_ICON_WIDTH_INDEX, KEY_DEFAULT_ICON_HEIGHT_INDEX,
+            KEY_DEFAULT_HORIZONTAL_PADDING_INDEX, KEY_DEFAULT_VERTICAL_PADDING_INDEX,
             KEY_KEY_BACKGROUND_INDEX);
         edgeFlags |= attributes.getInt(KEY_KEY_EDGE_FLAGS_INDEX, 0);
         isRepeatable = attributes.getBoolean(KEY_IS_REPEATABLE_INDEX, false);
         isModifier = attributes.getBoolean(KEY_IS_MODIFIER_INDEX, false);
-        isSticky = attributes.getBoolean(KEY_IS_STICKY_INDEX, false);
       } finally {
         attributes.recycle();
       }
@@ -568,18 +806,19 @@
         break;
       }
 
-      keyStateList.add(
-          parseKeyState(keyAttributes.keyBackgroundDrawableType, popUpAttributes));
+      keyStateList.add(parseKeyState(keyAttributes, popUpAttributes, metrics));
     }
 
     // At the moment, we just accept keys which has default state.
     boolean hasDefault = false;
     boolean hasLongPressKeyCode = false;
     for (KeyState keyState : keyStateList) {
-      if (keyState.getMetaStateSet().isEmpty()) {
+      if (keyState.getMetaStateSet().isEmpty()
+          || keyState.getMetaStateSet().contains(KeyState.MetaState.FALLBACK)) {
         hasDefault = true;
-        if (keyState.getFlick(Flick.Direction.CENTER).getKeyEntity().getLongPressKeyCode() !=
-            KeyEntity.INVALID_KEY_CODE) {
+        Optional<Flick> flick = keyState.getFlick(Flick.Direction.CENTER);
+        Preconditions.checkState(flick.isPresent());
+        if (flick.get().getKeyEntity().getLongPressKeyCode() != KeyEntity.INVALID_KEY_CODE) {
           hasLongPressKeyCode = true;
         }
         break;
@@ -589,23 +828,26 @@
       throw new IllegalArgumentException(
           "No default KeyState element is found: " + parser.getPositionDescription());
     }
-
     if (isRepeatable && hasLongPressKeyCode) {
       throw new IllegalArgumentException(
-          "The key has both isRepeatable attribute and longPressKeyCode: " +
-          parser.getPositionDescription());
+          "The key has both isRepeatable attribute and longPressKeyCode: "
+            + parser.getPositionDescription());
     }
 
     assertEndTag(parser, "Key");
-    return new Key(
-        x, y, keyAttributes.width, keyAttributes.height, keyAttributes.horizontalGap,
-        edgeFlags, isRepeatable, isModifier, isSticky, Stick.EVEN, keyStateList);
+    return keyAttributes.toBuilder()
+        .setEdgeFlags(edgeFlags)
+        .setRepeatable(isRepeatable)
+        .setModifier(isModifier)
+        .setStick(Stick.EVEN)
+        .setKeyStateList(keyStateList)
+        .build();
   }
 
   /**
    * Parses a {@code Spacer} element, and returns an instance.
    */
-  private Key parseSpacer(int x, int y, int edgeFlags, KeyAttributes defaultKeyAttributes)
+  private KeyAttributes parseSpacer(int edgeFlags, KeyAttributes defaultKeyAttributes)
       throws XmlPullParserException, IOException {
     XmlResourceParser parser = this.xmlResourceParser;
     DisplayMetrics metrics = resources.getDisplayMetrics();
@@ -618,7 +860,9 @@
       try {
         keyAttributes = parseKeyAttributes(
             attributes, defaultKeyAttributes, metrics, keyboardWidth, keyboardHeight,
-            -1, SPACER_KEY_HEIGHT_INDEX, SPACER_HORIZONTAL_GAP_INDEX, -1, -1);
+            SPACER_KEY_WIDTH_INDEX, SPACER_KEY_HEIGHT_INDEX,
+            SPACER_KEY_HORIZONTAL_LAYOUT_WEIGHT_INDEX, -1, -1, -1, -1, -1, -1,
+            SPACER_KEY_BACKGROUND_INDEX);
         edgeFlags |= attributes.getInt(SPACER_KEY_EDGE_FLAGS_INDEX, 0);
         stick = Stick.values()[attributes.getInt(SPACER_STICK_INDEX, 0)];
       } finally {
@@ -630,19 +874,21 @@
     assertEndTag(parser, "Spacer");
 
     // Returns a dummy key object.
-    return new Key(
-        x, y, keyAttributes.horizontalGap, keyAttributes.height,
-        0, edgeFlags, false, false, false, stick, Collections.<KeyState>emptyList());
+    return keyAttributes.toBuilder()
+        .setRepeatable(false)
+        .setModifier(false)
+        .setEdgeFlags(edgeFlags)
+        .setStick(stick)
+        .build();
   }
 
-  private KeyState parseKeyState(DrawableType defaultBackgroundDrawableType,
-                                 PopUpAttributes popUpAttributes)
+  private KeyState parseKeyState(KeyAttributes defaultKeyAttributes,
+                                 PopUpAttributes popUpAttributes, DisplayMetrics metrics)
       throws XmlPullParserException, IOException {
     XmlResourceParser parser = this.xmlResourceParser;
     assertStartTag(parser, "KeyState");
 
     String contentDescription;
-    DrawableType backgroundDrawableType;
     Set<KeyState.MetaState> metaStateSet;
     Set<KeyState.MetaState> nextAddMetaState;
     Set<KeyState.MetaState> nextRemoveMetaState;
@@ -651,8 +897,6 @@
       try {
         contentDescription = Objects.firstNonNull(
             attributes.getText(KEY_STATE_CONTENT_DESCRIPTION_INDEX), "").toString();
-        backgroundDrawableType = parseKeyBackgroundDrawableType(
-            attributes, KEY_STATE_KEY_BACKGROUND_INDEX, defaultBackgroundDrawableType);
         metaStateSet = parseMetaState(attributes, KEY_STATE_META_STATE_INDEX);
         nextAddMetaState = parseMetaState(attributes, KEY_STATE_NEXT_META_STATE_INDEX);
         nextRemoveMetaState = parseMetaState(attributes, KEY_STATE_NEXT_REMOVED_META_STATES_INDEX);
@@ -669,7 +913,7 @@
       if (parser.getEventType() == XmlResourceParser.END_TAG) {
         break;
       }
-      flickList.add(parseFlick(backgroundDrawableType, popUpAttributes));
+      flickList.add(parseFlick(defaultKeyAttributes, popUpAttributes, metrics));
     }
 
     // At the moment, we support only keys which has flick data to the CENTER direction.
@@ -690,8 +934,8 @@
                         flickList);
   }
 
-  private Flick parseFlick(DrawableType backgroundDrawableType,
-                           PopUpAttributes popUpAttributes)
+  private Flick parseFlick(KeyAttributes defaultKeyAttributes,
+                           PopUpAttributes popUpAttributes, DisplayMetrics metrics)
       throws XmlPullParserException, IOException {
     XmlResourceParser parser = this.xmlResourceParser;
     assertStartTag(parser, "Flick");
@@ -707,13 +951,13 @@
     }
 
     parser.next();
-    KeyEntity entity = parseKeyEntity(backgroundDrawableType, popUpAttributes);
+    KeyEntity entity = parseKeyEntity(defaultKeyAttributes, popUpAttributes, metrics);
 
-    if (entity.getLongPressKeyCode() != KeyEntity.INVALID_KEY_CODE &&
-        direction != Flick.Direction.CENTER) {
+    if (entity.getLongPressKeyCode() != KeyEntity.INVALID_KEY_CODE
+        && direction != Flick.Direction.CENTER) {
       throw new IllegalArgumentException(
-          "longPressKeyCode can be set to only a KenEntity for CENTER direction: " +
-          parser.getPositionDescription());
+          "longPressKeyCode can be set to only a KenEntity for CENTER direction: "
+              + parser.getPositionDescription());
     }
 
     parser.next();
@@ -722,8 +966,8 @@
     return new Flick(direction, entity);
   }
 
-  private KeyEntity parseKeyEntity(
-      DrawableType backgroundDrawableType, PopUpAttributes popUpAttributes)
+  private KeyEntity parseKeyEntity(KeyAttributes defaultKeyAttributes,
+                                   PopUpAttributes popUpAttributes, DisplayMetrics metrics)
       throws XmlPullParserException, IOException {
     XmlResourceParser parser = this.xmlResourceParser;
     assertStartTag(parser, "KeyEntity");
@@ -731,11 +975,14 @@
     int sourceId;
     int keyCode;
     int longPressKeyCode;
+    boolean longPressTimeoutTrigger;
     int keyIconResourceId;
-    String keyCharacter;
-    @SuppressWarnings("unused")
-    DrawableType keyBackgroundDrawableType;
+    Optional<String> keyCharacter = Optional.absent();
     boolean flickHighlight;
+    int horizontalPadding;
+    int verticalPadding;
+    int iconWidth;
+    int iconHeight;
     {
       TypedArray attributes = resources.obtainAttributes(parser, KEY_ENTITY_ATTRIBUTES);
       try {
@@ -743,15 +990,33 @@
         if (!sourceIdSet.add(sourceId)) {
           //  Same sourceId is found.
           throw new IllegalArgumentException(
-              "Duplicataed sourceId is found: " + xmlResourceParser.getPositionDescription());
+              "Duplicataed sourceId (" + sourceId + ") is found: "
+              + xmlResourceParser.getPositionDescription());
         }
         keyCode = getCode(
-            attributes.peekValue(KEY_ENTITY_KEY_CODE_INDEX), KeyEntity.INVALID_KEY_CODE);
-        longPressKeyCode = getCode(attributes.peekValue(KEY_ENTITY_LONG_PRESS_KEY_CODE_INDEX),
-                                   KeyEntity.INVALID_KEY_CODE);
+            Optional.fromNullable(attributes.peekValue(KEY_ENTITY_KEY_CODE_INDEX)),
+            KeyEntity.INVALID_KEY_CODE);
+        longPressKeyCode = getCode(
+            Optional.fromNullable(attributes.peekValue(KEY_ENTITY_LONG_PRESS_KEY_CODE_INDEX)),
+            KeyEntity.INVALID_KEY_CODE);
+        longPressTimeoutTrigger = attributes.getBoolean(KEY_ENTITY_LONG_PRESS_TIMEOUT_TRIGGER_INDEX,
+                                                        true);
         keyIconResourceId = attributes.getResourceId(KEY_ENTITY_KEY_ICON_INDEX, 0);
-        keyCharacter = attributes.getString(KEY_ENTITY_KEY_CHAR_INDEX);
+        keyCharacter = Optional.fromNullable(attributes.getString(KEY_ENTITY_KEY_CHAR_INDEX));
         flickHighlight = attributes.getBoolean(KEY_ENTITY_FLICK_HIGHLIGHT_INDEX, false);
+
+        horizontalPadding = getDimensionOrFraction(
+                Optional.fromNullable(attributes.peekValue(KEY_ENTITY_HORIZONTAL_PADDING_INDEX)),
+                keyboardWidth, defaultKeyAttributes.defaultHorizontalPadding, metrics);
+        verticalPadding = getDimensionOrFraction(
+                Optional.fromNullable(attributes.peekValue(KEY_ENTITY_VERTICAL_PADDING_INDEX)),
+                keyboardHeight, defaultKeyAttributes.defaultVerticalPadding, metrics);
+        iconWidth = getDimensionOrFraction(
+            Optional.fromNullable(attributes.peekValue(KEY_ENTITY_ICON_WIDTH_INDEX)),
+            keyboardWidth, defaultKeyAttributes.defaultIconWidth, metrics);
+        iconHeight = getDimensionOrFraction(
+            Optional.fromNullable(attributes.peekValue(KEY_ENTITY_ICON_HEIGHT_INDEX)),
+            keyboardHeight, defaultKeyAttributes.defaultIconHeight, metrics);
       } finally {
         attributes.recycle();
       }
@@ -760,84 +1025,128 @@
     parser.next();
     ignoreWhiteSpaceAndComment(parser);
 
-    PopUp popUp = null;
+    Optional<PopUp> popUp = Optional.absent();
     if (parser.getEventType() == XmlResourceParser.START_TAG) {
-      popUp = parsePopUp(popUpAttributes);
+      popUp = Optional.of(parsePopUp(popUpAttributes));
       parser.next();
       ignoreWhiteSpaceAndComment(parser);
     }
 
     assertEndTag(parser, "KeyEntity");
 
-    return new KeyEntity(sourceId, keyCode, longPressKeyCode, keyIconResourceId, keyCharacter,
-                         backgroundDrawableType, flickHighlight, popUp);
+    return new KeyEntity(sourceId, keyCode, longPressKeyCode, longPressTimeoutTrigger,
+        keyIconResourceId, keyCharacter, flickHighlight, popUp,
+        horizontalPadding, verticalPadding, iconWidth, iconHeight);
   }
 
-  private PopUp parsePopUp(PopUpAttributes popUpAttributes)
+  private PopUp parsePopUp(PopUpAttributes defaultValue)
       throws XmlPullParserException, IOException {
     XmlResourceParser parser = this.xmlResourceParser;
     assertStartTag(parser, "PopUp");
 
+    PopUpAttributes popUpAttributes;
+    TypedArray attributes = resources.obtainAttributes(parser, POPUP_ATTRIBUTES);
     int popUpIconResourceId;
-    {
-      TypedArray attributes = resources.obtainAttributes(parser, R.styleable.PopUp);
-      try {
-        popUpIconResourceId = attributes.getResourceId(R.styleable.PopUp_popUpIcon, 0);
-      } finally {
-        attributes.recycle();
-      }
+    int popUpLongPressIconResourceId;
+    try {
+      popUpAttributes = parsePopUpAttributes(attributes, defaultValue,
+          resources.getDisplayMetrics(),
+          keyboardWidth, POPUP_KEY_HEIGHT_INDEX,
+          POPUP_KEY_X_OFFSET_INDEX, POPUP_KEY_Y_OFFSET_INDEX,
+          POPUP_KEY_ICON_WIDTH_INDEX, POPUP_KEY_ICON_HEIGHT_INDEX);
+      popUpIconResourceId = attributes.getResourceId(POPUP_KEY_ICON_INDEX, 0);
+      popUpLongPressIconResourceId = attributes.getResourceId(POPUP_KEY_LONG_PRESS_ICON_INDEX, 0);
+    } finally {
+      attributes.recycle();
     }
     parser.next();
     assertEndTag(parser, "PopUp");
 
     return new PopUp(popUpIconResourceId,
-                     popUpAttributes.popUpWidth,
+                     popUpLongPressIconResourceId,
                      popUpAttributes.popUpHeight,
                      popUpAttributes.popUpXOffset,
-                     popUpAttributes.popUpYOffset);
+                     popUpAttributes.popUpYOffset,
+                     popUpAttributes.popUpIconWidth,
+                     popUpAttributes.popUpIconHeight);
   }
 
   private float parseFlickThreshold(TypedArray attributes, int index) {
     float flickThreshold = attributes.getDimension(
         index, resources.getDimension(R.dimen.default_flick_threshold));
-    if (flickThreshold <= 0) {
-      throw new IllegalArgumentException(
-          "flickThreshold must be greater than 0.  value = " + flickThreshold);
-    }
+    Preconditions.checkArgument(
+        flickThreshold > 0, "flickThreshold must be greater than 0.  value = " + flickThreshold);
     return flickThreshold;
   }
 
   private static KeyAttributes parseKeyAttributes(
       TypedArray attributes, KeyAttributes defaultValue, DisplayMetrics metrics,
       int keyboardWidth, int keyboardHeight,
-      int keyWidthIndex, int keyHeightIndex, int horizontalGapIndex, int verticalGapIndex,
+      int keyWidthIndex, int keyHeightIndex, int keyHorizontalLayoutWeightIndex,
+      int horizontalGapIndex, int verticalGapIndex,
+      int defaultIconWidthIndex, int defaultIconHeightIndex,
+      int defaultHorizontalPaddingIndex, int defaultVerticalPaddingIndex,
       int keyBackgroundIndex) {
+
     int keyWidth = (keyWidthIndex >= 0)
         ? getDimensionOrFraction(
-              attributes.peekValue(keyWidthIndex), keyboardWidth,
+              Optional.fromNullable(attributes.peekValue(keyWidthIndex)), keyboardWidth,
               defaultValue.width, metrics)
         : defaultValue.width;
     int keyHeight = (keyHeightIndex >= 0)
         ? getDimensionOrFraction(
-              attributes.peekValue(keyHeightIndex), keyboardHeight,
+              Optional.fromNullable(attributes.peekValue(keyHeightIndex)), keyboardHeight,
               defaultValue.height, metrics)
         : defaultValue.height;
+    int keyHorizontalLayoutWeight = (keyHorizontalLayoutWeightIndex >= 0)
+        ? attributes.getInt(keyHorizontalLayoutWeightIndex, defaultValue.horizontalLayoutWeight)
+        : defaultValue.horizontalLayoutWeight;
 
     int horizontalGap = (horizontalGapIndex >= 0)
         ? getDimensionOrFraction(
-              attributes.peekValue(horizontalGapIndex),
+              Optional.fromNullable(attributes.peekValue(horizontalGapIndex)),
               keyboardWidth, defaultValue.horizontalGap, metrics)
         : defaultValue.horizontalGap;
     int verticalGap = (verticalGapIndex >= 0)
         ? getDimensionOrFraction(
-              attributes.peekValue(verticalGapIndex),
+              Optional.fromNullable(attributes.peekValue(verticalGapIndex)),
               keyboardHeight, defaultValue.verticalGap, metrics)
         : defaultValue.verticalGap;
+    int defaultIconWidth = (defaultIconWidthIndex >= 0)
+        ? getDimensionOrFraction(
+            Optional.fromNullable(attributes.peekValue(defaultIconWidthIndex)),
+            keyboardWidth, defaultValue.defaultIconWidth, metrics)
+        : defaultValue.defaultIconWidth;
+    int defaultIconHeight = (defaultIconHeightIndex >= 0)
+        ? getDimensionOrFraction(
+            Optional.fromNullable(attributes.peekValue(defaultIconHeightIndex)),
+            keyboardHeight, defaultValue.defaultIconHeight, metrics)
+        : defaultValue.defaultIconHeight;
+    int defaultHorizontalPadding = (defaultHorizontalPaddingIndex >= 0)
+        ? getDimensionOrFraction(
+            Optional.fromNullable(attributes.peekValue(defaultHorizontalPaddingIndex)),
+            keyboardWidth, defaultValue.defaultHorizontalPadding, metrics)
+        : defaultValue.defaultHorizontalPadding;
+    int defaultVerticalPadding = (defaultVerticalPaddingIndex >= 0)
+        ? getDimensionOrFraction(
+            Optional.fromNullable(attributes.peekValue(defaultVerticalPaddingIndex)),
+            keyboardWidth, defaultValue.defaultVerticalPadding, metrics)
+        : defaultValue.defaultVerticalPadding;
 
     DrawableType keyBackgroundDrawableType = parseKeyBackgroundDrawableType(
         attributes, keyBackgroundIndex, defaultValue.keyBackgroundDrawableType);
-    return new KeyAttributes(
-        keyWidth, keyHeight, horizontalGap, verticalGap, keyBackgroundDrawableType);
+    return KeyAttributes.newBuilder()
+        .setWidth(keyWidth)
+        .setHeight(keyHeight)
+        .setHorizontalLayoutWeight(keyHorizontalLayoutWeight)
+        .setHorizontalGap(horizontalGap)
+        .setVerticalGap(verticalGap)
+        .setDefaultHorizontalPadding(defaultHorizontalPadding)
+        .setDefaultVerticalPadding(defaultVerticalPadding)
+        .setDefaultIconWidth(defaultIconWidth)
+        .setDefaultIconHeight(defaultIconHeight)
+        .setKeybackgroundDrawableType(keyBackgroundDrawableType)
+        .build();
   }
 
   private static DrawableType parseKeyBackgroundDrawableType(
@@ -850,17 +1159,37 @@
   }
 
   private static PopUpAttributes parsePopUpAttributes(
-      TypedArray attributes, DisplayMetrics metrics, int keyboardWidth,
-      int popUpWidthIndex, int popUpHeightIndex, int popUpXOffsetIndex, int popUpYOffsetIndex) {
-    int popUpWidth = getDimensionOrFraction(
-        attributes.peekValue(popUpWidthIndex), keyboardWidth, 0, metrics);
-    int popUpHeight = getDimensionOrFraction(
-        attributes.peekValue(popUpHeightIndex), keyboardWidth, 0, metrics);
-    int popUpXOffset = getDimensionOrFraction(
-        attributes.peekValue(popUpXOffsetIndex), keyboardWidth, 0, metrics);
-    int popUpYOffset = getDimensionOrFraction(
-        attributes.peekValue(popUpYOffsetIndex), keyboardWidth, 0, metrics);
-    return new PopUpAttributes(popUpWidth, popUpHeight, popUpXOffset, popUpYOffset);
+      TypedArray attributes, PopUpAttributes defaultValue, DisplayMetrics metrics,
+      int keyboardWidth, int popUpHeightIndex,
+      int popUpXOffsetIndex, int popUpYOffsetIndex,
+      int popUpIconWidthIndex, int popUpIconHeightIndex) {
+    int popUpHeight = (popUpHeightIndex >= 0)
+        ? getDimensionOrFraction(
+            Optional.fromNullable(attributes.peekValue(popUpHeightIndex)), keyboardWidth,
+            defaultValue.popUpHeight, metrics)
+        : defaultValue.popUpHeight;
+    int popUpXOffset = (popUpXOffsetIndex >= 0)
+        ? getDimensionOrFraction(
+            Optional.fromNullable(attributes.peekValue(popUpXOffsetIndex)), keyboardWidth,
+            defaultValue.popUpXOffset, metrics)
+        : defaultValue.popUpXOffset;
+    int popUpYOffset = (popUpYOffsetIndex >= 0)
+        ? getDimensionOrFraction(
+            Optional.fromNullable(attributes.peekValue(popUpYOffsetIndex)), keyboardWidth,
+            defaultValue.popUpYOffset, metrics)
+        : defaultValue.popUpYOffset;
+    int popUpIconWidth = (popUpIconWidthIndex >= 0)
+        ? getDimensionOrFraction(
+            Optional.fromNullable(attributes.peekValue(popUpIconWidthIndex)), keyboardWidth,
+            defaultValue.popUpIconWidth, metrics)
+        : defaultValue.popUpIconWidth;
+    int popUpIconHeight = (popUpIconHeightIndex >= 0)
+        ? getDimensionOrFraction(
+            Optional.fromNullable(attributes.peekValue(popUpIconHeightIndex)), keyboardWidth,
+            defaultValue.popUpIconHeight, metrics)
+        : defaultValue.popUpIconHeight;
+    return new PopUpAttributes(
+        popUpHeight, popUpXOffset, popUpYOffset, popUpIconWidth, popUpIconHeight);
   }
 
   /**
@@ -889,13 +1218,7 @@
     return Flick.Direction.valueOf(attributes.getInt(index, Flick.Direction.CENTER.index));
   }
 
-  protected Keyboard buildKeyboard(
-      Optional<String> contentDescription, List<Row> rowList, float flickThreshold) {
-    return new Keyboard(Preconditions.checkNotNull(contentDescription),
-                        Preconditions.checkNotNull(rowList), flickThreshold);
-  }
-
-  protected Row buildRow(List<Key> keyList, int height, int verticalGap) {
+  private Row buildRow(List<Key> keyList, int height, int verticalGap) {
     return new Row(keyList, height, verticalGap);
   }
 }
diff --git a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardView.java b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardView.java
index 6fa2d4c..ac47204 100644
--- a/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardView.java
+++ b/src/android/src/com/google/android/inputmethod/japanese/keyboard/KeyboardView.java
@@ -37,20 +37,17 @@
 import org.mozc.android.inputmethod.japanese.protobuf.ProtoCommands.Input.TouchEvent;
 import org.mozc.android.inputmethod.japanese.resources.R;
 import org.mozc.android.inputmethod.japanese.view.DrawableCache;
-import org.mozc.android.inputmethod.japanese.view.MozcDrawableFactory;
-import org.mozc.android.inputmethod.japanese.view.SkinType;
+import org.mozc.android.inputmethod.japanese.view.Skin;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ForwardingMap;
 import com.google.common.collect.Sets;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Shader.TileMode;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
 import android.os.Looper;
 import android.support.v4.view.ViewCompat;
 import android.text.InputType;
@@ -74,31 +71,85 @@
  *
  */
 public class KeyboardView extends View implements MemoryManageable {
+
   private final BackgroundDrawableFactory backgroundDrawableFactory =
-      new BackgroundDrawableFactory(getResources().getDisplayMetrics().density);
-  private final DrawableCache drawableCache =
-      new DrawableCache(new MozcDrawableFactory(getResources()));
+      new BackgroundDrawableFactory(getResources());
+  private final DrawableCache drawableCache = new DrawableCache(getResources());
   private final PopUpPreview.Pool popupPreviewPool =
       new PopUpPreview.Pool(
           this, Looper.getMainLooper(), backgroundDrawableFactory, drawableCache);
   private final long popupDismissDelay;
 
-  private Keyboard keyboard;
+  private Optional<Keyboard> keyboard = Optional.absent();
   // Do not update directly. Use setMetaState instead.
   @VisibleForTesting Set<MetaState> metaState;
   @VisibleForTesting final KeyboardViewBackgroundSurface backgroundSurface =
       new KeyboardViewBackgroundSurface(backgroundDrawableFactory, drawableCache);
   @VisibleForTesting boolean isKeyPressed;
 
-  private final int keycodeSymbol;
   private final float scaledDensity;
 
   private int flickSensitivity;
   private boolean popupEnabled = true;
-  private SkinType skinType = SkinType.ORANGE_LIGHTGRAY;
 
   private final KeyboardAccessibilityDelegate accessibilityDelegate;
 
+  /**
+   * Decorator class for {@code Map} for {@code KeyEventContextMap}.
+   * <p>
+   * When the number of the content is changed, meta state "HANDLING_TOUCH_EVENT"
+   * is updated.
+   */
+  private final class KeyEventContextMap extends ForwardingMap<Integer, KeyEventContext>{
+
+    private final Map<Integer, KeyEventContext> delegate;
+
+    private KeyEventContextMap(Map<Integer, KeyEventContext> delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    protected Map<Integer, KeyEventContext> delegate() {
+      return delegate;
+    }
+
+    @Override
+    public void clear() {
+      super.clear();
+      updateHandlingTouchEventMetaState();
+    }
+
+    @Override
+    public KeyEventContext put(Integer key, KeyEventContext value) {
+      KeyEventContext result = super.put(key, value);
+      updateHandlingTouchEventMetaState();
+      return result;
+    }
+
+    @Override
+    public void putAll(Map<? extends Integer, ? extends KeyEventContext> map) {
+      super.putAll(map);
+      updateHandlingTouchEventMetaState();
+    }
+
+    @Override
+    public KeyEventContext remove(Object object) {
+      KeyEventContext result = super.remove(object);
+      updateHandlingTouchEventMetaState();
+      return result;
+    }
+
+    private void updateHandlingTouchEventMetaState() {
+      if (keyEve