| // Copyright 2010-2015, 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.model; |
| |
| import org.mozc.android.inputmethod.japanese.EmoticonData; |
| import org.mozc.android.inputmethod.japanese.SymbolData; |
| import org.mozc.android.inputmethod.japanese.emoji.EmojiData; |
| import org.mozc.android.inputmethod.japanese.emoji.EmojiProviderType; |
| import org.mozc.android.inputmethod.japanese.emoji.EmojiUtil; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.Annotation; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateList; |
| import org.mozc.android.inputmethod.japanese.protobuf.ProtoCandidates.CandidateWord; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Optional; |
| import com.google.common.base.Preconditions; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.EnumMap; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Manages between MinorCategory and its candidates. |
| * |
| */ |
| public class SymbolCandidateStorage { |
| |
| /** Interface to handle symbol history data. */ |
| public interface SymbolHistoryStorage { |
| public List<String> getAllHistory(SymbolMajorCategory majorCategory); |
| public void addHistory(SymbolMajorCategory majorCategory, String value); |
| } |
| |
| /** Set of names of Emoji based on carriers. */ |
| private static class EmojiDescriptionSet { |
| static final EmojiDescriptionSet NULL_INSTANCE; |
| static { |
| String[] empty = new String[0]; |
| NULL_INSTANCE = new EmojiDescriptionSet(empty, empty, empty, empty, empty); |
| } |
| |
| final String[] faceDescription; |
| final String[] foodDescription; |
| final String[] activityDescription; |
| final String[] cityDescription; |
| final String[] natureDescription; |
| |
| EmojiDescriptionSet(String[] faceDescription, |
| String[] foodDescription, |
| String[] activityDescription, |
| String[] cityDescription, |
| String[] natureDescription) { |
| this.faceDescription = Preconditions.checkNotNull(faceDescription); |
| this.foodDescription = Preconditions.checkNotNull(foodDescription); |
| this.activityDescription = Preconditions.checkNotNull(activityDescription); |
| this.cityDescription = Preconditions.checkNotNull(cityDescription); |
| this.natureDescription = Preconditions.checkNotNull(natureDescription); |
| } |
| } |
| |
| /** Annotation for a Half-width one character candidate. */ |
| private static final String HALFWIDTH_DESCRIPTION = "[半]"; |
| private static final Annotation HALFWIDTH_ANNOTATION = Annotation.newBuilder() |
| .setDescription(HALFWIDTH_DESCRIPTION) |
| .build(); |
| |
| /** Specialized description map. */ |
| private static final Map<String, String> DESCRIPTION_MAP; |
| static { |
| // TODO(team): Move this rules compile time generated code, rather than hard coding here. |
| Map<String, String> descriptionMap = new HashMap<String, String>(); |
| descriptionMap.put("\u0020", "半角スペース"); // (space) |
| descriptionMap.put("\u002d", "[半]ハイフン,マイナス"); // - |
| descriptionMap.put("\u2010", "[全]ハイフン"); // ‐ |
| descriptionMap.put("\u2015", "[全]ダッシュ"); // ― |
| descriptionMap.put("\u2212", "[全]マイナス"); // − |
| descriptionMap.put("\u3000", "全角スペース"); // (space) |
| descriptionMap.put("\uDBBA\uDF4C", "全部ブランク"); // Full-width space emoji. |
| descriptionMap.put("\uDBBA\uDF4D", "半分ブランク"); // Half-width space emoji. |
| descriptionMap.put("\uDBBA\uDF4E", "1/4ブランク"); // Quater-width space emoji. |
| |
| DESCRIPTION_MAP = Collections.unmodifiableMap(descriptionMap); |
| } |
| |
| /** Map from the carrier to its Emoji description set. */ |
| private static final Map<EmojiProviderType, EmojiDescriptionSet> |
| CARRIER_EMOJI_DESCRIPTION_SET_MAP; |
| static { |
| EnumMap<EmojiProviderType, EmojiDescriptionSet> map = |
| new EnumMap<EmojiProviderType, EmojiDescriptionSet>(EmojiProviderType.class); |
| map.put(EmojiProviderType.DOCOMO, |
| new EmojiDescriptionSet(EmojiData.DOCOMO_FACE_NAME, |
| EmojiData.DOCOMO_FOOD_NAME, |
| EmojiData.DOCOMO_ACTIVITY_NAME, |
| EmojiData.DOCOMO_CITY_NAME, |
| EmojiData.DOCOMO_NATURE_NAME)); |
| map.put(EmojiProviderType.SOFTBANK, |
| new EmojiDescriptionSet(EmojiData.SOFTBANK_FACE_NAME, |
| EmojiData.SOFTBANK_FOOD_NAME, |
| EmojiData.SOFTBANK_ACTIVITY_NAME, |
| EmojiData.SOFTBANK_CITY_NAME, |
| EmojiData.SOFTBANK_NATURE_NAME)); |
| map.put(EmojiProviderType.KDDI, |
| new EmojiDescriptionSet(EmojiData.KDDI_FACE_NAME, |
| EmojiData.KDDI_FOOD_NAME, |
| EmojiData.KDDI_ACTIVITY_NAME, |
| EmojiData.KDDI_CITY_NAME, |
| EmojiData.KDDI_NATURE_NAME)); |
| CARRIER_EMOJI_DESCRIPTION_SET_MAP = Collections.unmodifiableMap(map); |
| } |
| |
| private final SymbolHistoryStorage symbolHistoryStorage; |
| private boolean isUnicodeEmojiEnabled = false; |
| private boolean isCarrierEmojiEnabled = false; |
| private EmojiProviderType emojiProviderType = EmojiProviderType.NONE; |
| private EmojiDescriptionSet carrierEmojiDescriptionSet = EmojiDescriptionSet.NULL_INSTANCE; |
| private Map<String, String> emojiDescriptionMap = Collections.emptyMap(); |
| |
| public SymbolCandidateStorage(SymbolHistoryStorage symbolHistoryStorage) { |
| this.symbolHistoryStorage = Preconditions.checkNotNull(symbolHistoryStorage); |
| setEmojiProviderTypeInternal(EmojiProviderType.NONE); |
| } |
| |
| public void setEmojiEnabled(boolean isUnicodeEmojiEnabled, boolean isCarrierEmojiEnabled) { |
| this.isUnicodeEmojiEnabled = isUnicodeEmojiEnabled; |
| this.isCarrierEmojiEnabled = isCarrierEmojiEnabled; |
| } |
| |
| public void setEmojiProviderType(EmojiProviderType emojiProviderType) { |
| if (this.emojiProviderType == Preconditions.checkNotNull(emojiProviderType)) { |
| // No change. |
| return; |
| } |
| setEmojiProviderTypeInternal(emojiProviderType); |
| } |
| |
| private void setEmojiProviderTypeInternal(EmojiProviderType emojiProviderType) { |
| EmojiDescriptionSet carrierEmojiDescriptionSet = |
| CARRIER_EMOJI_DESCRIPTION_SET_MAP.get(emojiProviderType); |
| this.emojiProviderType = emojiProviderType; |
| if (carrierEmojiDescriptionSet == null) { |
| this.carrierEmojiDescriptionSet = EmojiDescriptionSet.NULL_INSTANCE; |
| this.emojiDescriptionMap = createEmojiDescriptionMap(Optional.<EmojiDescriptionSet>absent()); |
| } else { |
| this.carrierEmojiDescriptionSet = carrierEmojiDescriptionSet; |
| this.emojiDescriptionMap = createEmojiDescriptionMap(Optional.of(carrierEmojiDescriptionSet)); |
| } |
| } |
| |
| private static Map<String, String> createEmojiDescriptionMap( |
| Optional<EmojiDescriptionSet> carrierEmojiDescriptionSet) { |
| Preconditions.checkNotNull(carrierEmojiDescriptionSet); |
| |
| Map<String, String> map = new HashMap<String, String>(); |
| |
| createEmojiDescriptionMapInternal( |
| EmojiData.FACE_VALUES, EmojiData.UNICODE_FACE_NAME, map); |
| createEmojiDescriptionMapInternal( |
| EmojiData.FOOD_VALUES, EmojiData.UNICODE_FOOD_NAME, map); |
| createEmojiDescriptionMapInternal( |
| EmojiData.ACTIVITY_VALUES, EmojiData.UNICODE_ACTIVITY_NAME, map); |
| createEmojiDescriptionMapInternal( |
| EmojiData.CITY_VALUES, EmojiData.UNICODE_CITY_NAME, map); |
| createEmojiDescriptionMapInternal( |
| EmojiData.NATURE_VALUES, EmojiData.UNICODE_NATURE_NAME, map); |
| |
| if (carrierEmojiDescriptionSet.isPresent()) { |
| EmojiDescriptionSet descriptionSet = carrierEmojiDescriptionSet.get(); |
| createEmojiDescriptionMapInternal( |
| EmojiData.FACE_PUA_VALUES, descriptionSet.faceDescription, map); |
| createEmojiDescriptionMapInternal( |
| EmojiData.FOOD_PUA_VALUES, descriptionSet.foodDescription, map); |
| createEmojiDescriptionMapInternal( |
| EmojiData.ACTIVITY_PUA_VALUES, descriptionSet.activityDescription, map); |
| createEmojiDescriptionMapInternal( |
| EmojiData.CITY_PUA_VALUES, descriptionSet.cityDescription, map); |
| createEmojiDescriptionMapInternal( |
| EmojiData.NATURE_PUA_VALUES, descriptionSet.natureDescription, map); |
| } |
| |
| return Collections.unmodifiableMap(map); |
| } |
| |
| private static void createEmojiDescriptionMapInternal( |
| String[] values, String[] descriptions, Map<String, String> map) { |
| Preconditions.checkArgument(values.length == descriptions.length); |
| |
| for (int i = 0; i < descriptions.length; ++i) { |
| String description = descriptions[i]; |
| if (description != null) { |
| map.put(values[i], description); |
| } |
| } |
| } |
| |
| /** @return the {@link CandidateList} instance for the given {@code minorCategory}. */ |
| public CandidateList getCandidateList(SymbolMinorCategory minorCategory) { |
| switch (minorCategory) { |
| // NUMBER major category candidates. |
| case NUMBER: |
| return CandidateList.getDefaultInstance(); |
| |
| // SYMBOL major category candidates. |
| case SYMBOL_HISTORY: |
| return toCandidateList(symbolHistoryStorage.getAllHistory(SymbolMajorCategory.SYMBOL)); |
| case SYMBOL_GENERAL: |
| return toCandidateList(Arrays.asList(SymbolData.GENERAL_VALUES)); |
| case SYMBOL_HALF: |
| return toCandidateList(Arrays.asList(SymbolData.HALF_VALUES)); |
| case SYMBOL_PARENTHESIS: |
| return toCandidateList(Arrays.asList(SymbolData.PARENTHESIS_VALUES)); |
| case SYMBOL_ARROW: |
| return toCandidateList(Arrays.asList(SymbolData.ARROW_VALUES)); |
| case SYMBOL_MATH: |
| return toCandidateList(Arrays.asList(SymbolData.MATH_VALUES)); |
| |
| // EMOTICON major category candidates. |
| case EMOTICON_HISTORY: |
| return toCandidateList(symbolHistoryStorage.getAllHistory(SymbolMajorCategory.EMOTICON)); |
| case EMOTICON_SMILE: |
| return toCandidateList(Arrays.asList(EmoticonData.SMILE_VALUES)); |
| case EMOTICON_SWEAT: |
| return toCandidateList(Arrays.asList(EmoticonData.SWEAT_VALUES)); |
| case EMOTICON_SURPRISE: |
| return toCandidateList(Arrays.asList(EmoticonData.SURPRISE_VALUES)); |
| case EMOTICON_SADNESS: |
| return toCandidateList(Arrays.asList(EmoticonData.SADNESS_VALUES)); |
| case EMOTICON_DISPLEASURE: |
| return toCandidateList(Arrays.asList(EmoticonData.DISPLEASURE_VALUES)); |
| |
| // EMOJI major category candidates. |
| case EMOJI_HISTORY: |
| return toEmojiCandidateListForHistory( |
| symbolHistoryStorage.getAllHistory(SymbolMajorCategory.EMOJI), |
| emojiDescriptionMap, isCarrierEmojiEnabled); |
| case EMOJI_FACE: |
| return toEmojiCandidateList( |
| EmojiData.FACE_VALUES, EmojiData.UNICODE_FACE_NAME, |
| EmojiData.FACE_PUA_VALUES, carrierEmojiDescriptionSet.faceDescription, |
| isUnicodeEmojiEnabled, isCarrierEmojiEnabled); |
| case EMOJI_FOOD: |
| return toEmojiCandidateList( |
| EmojiData.FOOD_VALUES, EmojiData.UNICODE_FOOD_NAME, |
| EmojiData.FOOD_PUA_VALUES, carrierEmojiDescriptionSet.foodDescription, |
| isUnicodeEmojiEnabled, isCarrierEmojiEnabled); |
| case EMOJI_ACTIVITY: |
| return toEmojiCandidateList( |
| EmojiData.ACTIVITY_VALUES, EmojiData.UNICODE_ACTIVITY_NAME, |
| EmojiData.ACTIVITY_PUA_VALUES, carrierEmojiDescriptionSet.activityDescription, |
| isUnicodeEmojiEnabled, isCarrierEmojiEnabled); |
| case EMOJI_CITY: |
| return toEmojiCandidateList( |
| EmojiData.CITY_VALUES, EmojiData.UNICODE_CITY_NAME, |
| EmojiData.CITY_PUA_VALUES, carrierEmojiDescriptionSet.cityDescription, |
| isUnicodeEmojiEnabled, isCarrierEmojiEnabled); |
| case EMOJI_NATURE: |
| return toEmojiCandidateList( |
| EmojiData.NATURE_VALUES, EmojiData.UNICODE_NATURE_NAME, |
| EmojiData.NATURE_PUA_VALUES, carrierEmojiDescriptionSet.natureDescription, |
| isUnicodeEmojiEnabled, isCarrierEmojiEnabled); |
| } |
| |
| throw new IllegalArgumentException("Unknown minor category: " + minorCategory.toString()); |
| } |
| |
| /** Just short cut of {@code toCandidateList(values, null)}. */ |
| private static CandidateList toCandidateList(List<String> values) { |
| return toCandidateList(values, Optional.<Map<String, String>>absent()); |
| } |
| |
| /** |
| * Builds the {@link CandidateList} based on the given values and emojiDescriptionMap. |
| * |
| * If {@code isCarrierEmojiEnabled} is {@code false}, this method ignores carrier emoji not to |
| * allow users to input it on the focused text edit. Otherwise some application |
| * (e.g. GoogleQuickSearchBox) crashes when they receives carrier emoji. |
| */ |
| private static CandidateList toEmojiCandidateListForHistory( |
| List<String> values, Map<String, String> emojiDescriptionMap, boolean isCarrierEmojiEnabled) { |
| if (isCarrierEmojiEnabled) { |
| return toCandidateList(values, Optional.of(emojiDescriptionMap)); |
| } |
| |
| List<String> valuesWithoutCarrierEmoji = new ArrayList<String>(values.size()); |
| for (String value : values) { |
| if (value.codePointCount(0, value.length()) != 1 || |
| !EmojiUtil.isCarrierEmoji(value.codePointAt(0))) { |
| valuesWithoutCarrierEmoji.add(value); |
| } |
| } |
| return toCandidateList(valuesWithoutCarrierEmoji, Optional.of(emojiDescriptionMap)); |
| } |
| |
| /** Builds the {@link CandidateList} based on the given values and emojiDescriptionMap. */ |
| @VisibleForTesting |
| static CandidateList toCandidateList( |
| List<String> values, Optional<Map<String, String>> emojiDescriptionMap) { |
| Preconditions.checkNotNull(emojiDescriptionMap); |
| if (Preconditions.checkNotNull(values).isEmpty()) { |
| return CandidateList.getDefaultInstance(); |
| } |
| |
| CandidateList.Builder builder = CandidateList.newBuilder(); |
| int index = 0; |
| for (String value : values) { |
| CandidateWord.Builder wordBuilder = CandidateWord.newBuilder(); |
| wordBuilder.setValue(value); |
| wordBuilder.setId(index); |
| wordBuilder.setIndex(index); |
| Optional<Annotation> annotation = getAnnotation(value, emojiDescriptionMap); |
| if (annotation.isPresent()) { |
| wordBuilder.setAnnotation(annotation.get()); |
| } |
| builder.addCandidates(wordBuilder); |
| ++index; |
| } |
| return builder.build(); |
| } |
| |
| private static Optional<Annotation> getAnnotation( |
| String value, Optional<Map<String, String>> emojiDescriptionMap) { |
| // We do not use resource to store the string below because |
| // there are no needs to translate the description. |
| // In addition we cannot access the resource from here |
| // because Context is not available. |
| |
| // Rule base annotation. |
| { |
| String description = DESCRIPTION_MAP.get(value); |
| if (description != null) { |
| return Optional.of(Annotation.newBuilder().setDescription(description).build()); |
| } |
| } |
| |
| // Emoji specialized annotation. Note that the given map depends on the current provider type. |
| // This is just only for history annotation. |
| if (emojiDescriptionMap.isPresent()){ |
| String description = emojiDescriptionMap.get().get(value); |
| if (description != null) { |
| return Optional.of(Annotation.newBuilder().setDescription(description).build()); |
| } |
| } |
| |
| // If the candidate has only one character and its codepoint is <= 0x7F, |
| // add HALFWIDTH_ANNOTATION. |
| // This criteria is different from the Mozc engine's |
| // (it is basically the same as here but length check is omitted) |
| // but the engine's criteria seems too aggressive for our purpose. |
| if (value.length() == 1 && value.charAt(0) <= 0x7F) { |
| return Optional.of(HALFWIDTH_ANNOTATION); |
| } |
| |
| // No description is available. |
| return Optional.absent(); |
| } |
| |
| private static CandidateList toEmojiCandidateList( |
| String[] unicodeEmojiValues, String[] unicodeEmojiDescriptions, |
| String[] carrierEmojiValues, String[] carrierEmojiDescriptions, |
| boolean isUnicodeEmojiEnabled, boolean isCarrierEmojiEnabled) { |
| CandidateList.Builder builder = CandidateList.newBuilder(); |
| int index = 0; |
| |
| if (isUnicodeEmojiEnabled) { |
| index += addEmojiCandidateListToBuilder( |
| builder, index, unicodeEmojiValues, unicodeEmojiDescriptions); |
| } |
| if (isCarrierEmojiEnabled) { |
| index += addEmojiCandidateListToBuilder( |
| builder, index, carrierEmojiValues, carrierEmojiDescriptions); |
| } |
| |
| if (index == 0) { |
| // No values are available. |
| return CandidateList.getDefaultInstance(); |
| } |
| return builder.build(); |
| } |
| |
| /** @return The number of added candidates. */ |
| private static int addEmojiCandidateListToBuilder( |
| CandidateList.Builder builder, int startIndex, String[] values, String[] descriptions) { |
| int index = startIndex; |
| for (int i = 0; i < descriptions.length; ++i) { |
| String description = descriptions[i]; |
| if (description == null) { |
| // If no description (name) is available, we skip the value, |
| // because the value is not supported under the current carrier. |
| continue; |
| } |
| |
| builder.addCandidates(CandidateWord.newBuilder() |
| .setValue(values[i]) |
| .setId(index) |
| .setIndex(index) |
| .setAnnotation(Annotation.newBuilder() |
| .setDescription(description))); |
| ++index; |
| } |
| return index - startIndex; |
| } |
| } |