1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.latin.personalization; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 import android.util.Log; 22 23 import com.android.inputmethod.annotations.UsedForTesting; 24 import com.android.inputmethod.latin.Constants; 25 import com.android.inputmethod.latin.Dictionary; 26 import com.android.inputmethod.latin.ExpandableBinaryDictionary; 27 import com.android.inputmethod.latin.LatinImeLogger; 28 import com.android.inputmethod.latin.makedict.DictDecoder; 29 import com.android.inputmethod.latin.makedict.FormatSpec; 30 import com.android.inputmethod.latin.settings.Settings; 31 import com.android.inputmethod.latin.utils.CollectionUtils; 32 import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils; 33 import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.OnAddWordListener; 34 35 import java.io.File; 36 import java.io.IOException; 37 import java.util.ArrayList; 38 import java.util.HashMap; 39 import java.util.Map; 40 41 /** 42 * This class is a base class of a dictionary that supports decaying for the personalized language 43 * model. 44 */ 45 public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableBinaryDictionary { 46 private static final String TAG = DecayingExpandableBinaryDictionaryBase.class.getSimpleName(); 47 public static final boolean DBG_SAVE_RESTORE = false; 48 private static final boolean DBG_STRESS_TEST = false; 49 private static final boolean PROFILE_SAVE_RESTORE = LatinImeLogger.sDBG; 50 51 /** Any pair being typed or picked */ 52 public static final int FREQUENCY_FOR_TYPED = 2; 53 54 public static final int FREQUENCY_FOR_WORDS_IN_DICTS = FREQUENCY_FOR_TYPED; 55 public static final int FREQUENCY_FOR_WORDS_NOT_IN_DICTS = Dictionary.NOT_A_PROBABILITY; 56 57 /** Locale for which this user history dictionary is storing words */ 58 private final String mLocale; 59 60 private final String mFileName; 61 62 private final SharedPreferences mPrefs; 63 64 private final ArrayList<PersonalizationDictionaryUpdateSession> mSessions = 65 CollectionUtils.newArrayList(); 66 67 // Should always be false except when we use this class for test 68 @UsedForTesting boolean mIsTest = false; 69 70 /* package */ DecayingExpandableBinaryDictionaryBase(final Context context, 71 final String locale, final SharedPreferences sp, final String dictionaryType, 72 final String fileName) { 73 super(context, fileName, dictionaryType, true); 74 mLocale = locale; 75 mFileName = fileName; 76 mPrefs = sp; 77 if (mLocale != null && mLocale.length() > 1) { 78 asyncLoadDictionaryToMemory(); 79 reloadDictionaryIfRequired(); 80 } 81 } 82 83 @Override 84 public void close() { 85 if (!ExpandableBinaryDictionary.ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { 86 closeBinaryDictionary(); 87 } 88 // Flush pending writes. 89 // TODO: Remove after this class become to use a dynamic binary dictionary. 90 asyncFlashAllBinaryDictionary(); 91 Settings.writeLastUserHistoryWriteTime(mPrefs, mLocale); 92 } 93 94 @Override 95 protected Map<String, String> getHeaderAttributeMap() { 96 HashMap<String, String> attributeMap = new HashMap<String, String>(); 97 attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE, 98 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); 99 attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_ATTRIBUTE, 100 FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); 101 attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFileName); 102 attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale); 103 return attributeMap; 104 } 105 106 @Override 107 protected boolean hasContentChanged() { 108 return false; 109 } 110 111 @Override 112 protected boolean needsToReloadBeforeWriting() { 113 return false; 114 } 115 116 /** 117 * Pair will be added to the decaying dictionary. 118 * 119 * The first word may be null. That means we don't know the context, in other words, 120 * it's only a unigram. The first word may also be an empty string : this means start 121 * context, as in beginning of a sentence for example. 122 * The second word may not be null (a NullPointerException would be thrown). 123 */ 124 public void addToDictionary(final String word0, final String word1, final boolean isValid) { 125 if (word1.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH || 126 (word0 != null && word0.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH)) { 127 return; 128 } 129 final int frequency = ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE ? 130 (isValid ? FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS) : 131 FREQUENCY_FOR_TYPED; 132 addWordDynamically(word1, null /* shortcutTarget */, frequency, 0 /* shortcutFreq */, 133 false /* isNotAWord */); 134 // Do not insert a word as a bigram of itself 135 if (word1.equals(word0)) { 136 return; 137 } 138 if (null != word0) { 139 addBigramDynamically(word0, word1, frequency, isValid); 140 } 141 } 142 143 public void cancelAddingUserHistory(final String word0, final String word1) { 144 removeBigramDynamically(word0, word1); 145 } 146 147 @Override 148 protected void loadDictionaryAsync() { 149 final int[] profTotalCount = { 0 }; 150 final String locale = getLocale(); 151 if (DBG_STRESS_TEST) { 152 try { 153 Log.w(TAG, "Start stress in loading: " + locale); 154 Thread.sleep(15000); 155 Log.w(TAG, "End stress in loading"); 156 } catch (InterruptedException e) { 157 } 158 } 159 final long last = Settings.readLastUserHistoryWriteTime(mPrefs, locale); 160 final long now = System.currentTimeMillis(); 161 final ExpandableBinaryDictionary dictionary = this; 162 final OnAddWordListener listener = new OnAddWordListener() { 163 @Override 164 public void setUnigram(final String word, final String shortcutTarget, 165 final int frequency, final int shortcutFreq) { 166 if (DBG_SAVE_RESTORE) { 167 Log.d(TAG, "load unigram: " + word + "," + frequency); 168 } 169 addWord(word, shortcutTarget, frequency, shortcutFreq, false /* isNotAWord */); 170 ++profTotalCount[0]; 171 } 172 173 @Override 174 public void setBigram(final String word0, final String word1, final int frequency) { 175 if (word0.length() < Constants.DICTIONARY_MAX_WORD_LENGTH 176 && word1.length() < Constants.DICTIONARY_MAX_WORD_LENGTH) { 177 if (DBG_SAVE_RESTORE) { 178 Log.d(TAG, "load bigram: " + word0 + "," + word1 + "," + frequency); 179 } 180 ++profTotalCount[0]; 181 addBigram(word0, word1, frequency, last); 182 } 183 } 184 }; 185 186 // Load the dictionary from binary file 187 final File dictFile = new File(mContext.getFilesDir(), mFileName); 188 final DictDecoder dictDecoder = FormatSpec.getDictDecoder(dictFile, 189 DictDecoder.USE_BYTEARRAY); 190 if (dictDecoder == null) { 191 // This is an expected condition: we don't have a user history dictionary for this 192 // language yet. It will be created sometime later. 193 return; 194 } 195 196 try { 197 dictDecoder.openDictBuffer(); 198 UserHistoryDictIOUtils.readDictionaryBinary(dictDecoder, listener); 199 } catch (IOException e) { 200 Log.d(TAG, "IOException on opening a bytebuffer", e); 201 } finally { 202 if (PROFILE_SAVE_RESTORE) { 203 final long diff = System.currentTimeMillis() - now; 204 Log.d(TAG, "PROF: Load UserHistoryDictionary: " 205 + locale + ", " + diff + "ms. load " + profTotalCount[0] + "entries."); 206 } 207 } 208 } 209 210 protected String getLocale() { 211 return mLocale; 212 } 213 214 public void registerUpdateSession(PersonalizationDictionaryUpdateSession session) { 215 session.setPredictionDictionary(this); 216 mSessions.add(session); 217 session.onDictionaryReady(); 218 } 219 220 public void unRegisterUpdateSession(PersonalizationDictionaryUpdateSession session) { 221 mSessions.remove(session); 222 } 223 224 @UsedForTesting 225 public void clearAndFlushDictionary() { 226 // Clear the node structure on memory 227 clear(); 228 // Then flush the cleared state of the dictionary on disk. 229 asyncFlashAllBinaryDictionary(); 230 } 231 232 /* package */ void decayIfNeeded() { 233 runGCIfRequired(false /* mindsBlockByGC */); 234 } 235 } 236