aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/com/android/inputmethod/latin')
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java67
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java49
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java7
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsContentObserver.java28
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFacilitator.java21
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java89
-rw-r--r--java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java78
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java20
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java145
-rw-r--r--java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java39
-rw-r--r--java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java63
-rw-r--r--java/src/com/android/inputmethod/latin/permissions/PermissionsActivity.java97
-rw-r--r--java/src/com/android/inputmethod/latin/permissions/PermissionsManager.java91
-rw-r--r--java/src/com/android/inputmethod/latin/permissions/PermissionsUtil.java93
-rw-r--r--java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java42
-rw-r--r--java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java3
-rw-r--r--java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java91
-rw-r--r--java/src/com/android/inputmethod/latin/settings/Settings.java1
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SettingsActivity.java10
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java2
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java3
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java32
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java12
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java54
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java4
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java72
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java107
28 files changed, 1009 insertions, 313 deletions
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index bc62f3ae3..1fe0a4cce 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -29,6 +29,8 @@ import android.util.Log;
import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
import com.android.inputmethod.dictionarypack.MD5Calculator;
+import com.android.inputmethod.dictionarypack.UpdateHandler;
+import com.android.inputmethod.latin.common.FileUtils;
import com.android.inputmethod.latin.define.DecoderSpecificConstants;
import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
import com.android.inputmethod.latin.utils.DictionaryInfoUtils.DictionaryInfo;
@@ -220,11 +222,11 @@ public final class BinaryDictionaryFileDumper {
}
/**
- * Caches a word list the id of which is passed as an argument. This will write the file
+ * Stages a word list the id of which is passed as an argument. This will write the file
* to the cache file name designated by its id and locale, overwriting it if already present
* and creating it (and its containing directory) if necessary.
*/
- private static void cacheWordList(final String wordlistId, final String locale,
+ private static void installWordListToStaging(final String wordlistId, final String locale,
final String rawChecksum, final ContentProviderClient providerClient,
final Context context) {
final int COMPRESSED_CRYPTED_COMPRESSED = 0;
@@ -246,7 +248,7 @@ public final class BinaryDictionaryFileDumper {
return;
}
final String finalFileName =
- DictionaryInfoUtils.getCacheFileName(wordlistId, locale, context);
+ DictionaryInfoUtils.getStagingFileName(wordlistId, locale, context);
String tempFileName;
try {
tempFileName = BinaryDictionaryGetter.getTempFileName(wordlistId, context);
@@ -320,23 +322,24 @@ public final class BinaryDictionaryFileDumper {
}
}
+ // move the output file to the final staging file.
final File finalFile = new File(finalFileName);
- finalFile.delete();
- if (!outputFile.renameTo(finalFile)) {
- throw new IOException("Can't move the file to its final name");
+ if (!FileUtils.renameTo(outputFile, finalFile)) {
+ Log.e(TAG, String.format("Failed to rename from %s to %s.",
+ outputFile.getAbsoluteFile(), finalFile.getAbsoluteFile()));
}
+
wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT,
QUERY_PARAMETER_SUCCESS);
if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) {
Log.e(TAG, "Could not have the dictionary pack delete a word list");
}
- BinaryDictionaryGetter.removeFilesWithIdExcept(context, wordlistId, finalFile);
- Log.e(TAG, "Successfully copied file for wordlist ID " + wordlistId);
+ Log.d(TAG, "Successfully copied file for wordlist ID " + wordlistId);
// Success! Close files (through the finally{} clause) and return.
return;
} catch (Exception e) {
if (DEBUG) {
- Log.i(TAG, "Can't open word list in mode " + mode, e);
+ Log.e(TAG, "Can't open word list in mode " + mode, e);
}
if (null != outputFile) {
// This may or may not fail. The file may not have been created if the
@@ -403,7 +406,7 @@ public final class BinaryDictionaryFileDumper {
}
/**
- * Queries a content provider for word list data for some locale and cache the returned files
+ * Queries a content provider for word list data for some locale and stage the returned files
*
* This will query a content provider for word list data for a given locale, and copy the
* files locally so that they can be mmap'ed. This may overwrite previously cached word lists
@@ -411,7 +414,7 @@ public final class BinaryDictionaryFileDumper {
* @throw FileNotFoundException if the provider returns non-existent data.
* @throw IOException if the provider-returned data could not be read.
*/
- public static void cacheWordListsFromContentProvider(final Locale locale,
+ public static void installDictToStagingFromContentProvider(final Locale locale,
final Context context, final boolean hasDefaultWordList) {
final ContentProviderClient providerClient;
try {
@@ -429,7 +432,8 @@ public final class BinaryDictionaryFileDumper {
final List<WordListInfo> idList = getWordListWordListInfos(locale, context,
hasDefaultWordList);
for (WordListInfo id : idList) {
- cacheWordList(id.mId, id.mLocale, id.mRawChecksum, providerClient, context);
+ installWordListToStaging(id.mId, id.mLocale, id.mRawChecksum, providerClient,
+ context);
}
} finally {
providerClient.release();
@@ -437,6 +441,18 @@ public final class BinaryDictionaryFileDumper {
}
/**
+ * Downloads the dictionary if it was never requested/used.
+ *
+ * @param locale locale to download
+ * @param context the context for resources and providers.
+ * @param hasDefaultWordList whether the default wordlist exists in the resources.
+ */
+ public static void downloadDictIfNeverRequested(final Locale locale,
+ final Context context, final boolean hasDefaultWordList) {
+ getWordListWordListInfos(locale, context, hasDefaultWordList);
+ }
+
+ /**
* Copies the data in an input stream to a target file if the magic number matches.
*
* If the magic number does not match the expected value, this method throws an
@@ -475,6 +491,8 @@ public final class BinaryDictionaryFileDumper {
private static void reinitializeClientRecordInDictionaryContentProvider(final Context context,
final ContentProviderClient client, final String clientId) throws RemoteException {
final String metadataFileUri = MetadataFileUriGetter.getMetadataUri(context);
+ Log.i(TAG, "reinitializeClientRecordInDictionaryContentProvider() : MetadataFileUri = "
+ + metadataFileUri);
final String metadataAdditionalId = MetadataFileUriGetter.getMetadataAdditionalId(context);
// Tell the content provider to reset all information about this client id
final Uri metadataContentUri = getProviderUriBuilder(clientId)
@@ -499,9 +517,34 @@ public final class BinaryDictionaryFileDumper {
final int length = dictionaryList.size();
for (int i = 0; i < length; ++i) {
final DictionaryInfo info = dictionaryList.get(i);
+ Log.i(TAG, "reinitializeClientRecordInDictionaryContentProvider() : Insert " + info);
client.insert(Uri.withAppendedPath(dictionaryContentUriBase, info.mId),
info.toContentValues());
}
+
+ // Read from metadata file in resources to get the baseline dictionary info.
+ // This ensures we start with a sane list of available dictionaries.
+ final int metadataResourceId = context.getResources().getIdentifier("metadata",
+ "raw", DictionaryInfoUtils.RESOURCE_PACKAGE_NAME);
+ if (metadataResourceId == 0) {
+ Log.w(TAG, "Missing metadata.json resource");
+ return;
+ }
+ InputStream inputStream = null;
+ try {
+ inputStream = context.getResources().openRawResource(metadataResourceId);
+ UpdateHandler.handleMetadata(context, inputStream, clientId);
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to read metadata.json from resources", e);
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close metadata.json", e);
+ }
+ }
+ }
}
/**
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 5f2a112ba..60016371b 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -195,39 +195,6 @@ final public class BinaryDictionaryGetter {
return result;
}
- /**
- * Remove all files with the passed id, except the passed file.
- *
- * If a dictionary with a given ID has a metadata change that causes it to change
- * path, we need to remove the old version. The only way to do this is to check all
- * installed files for a matching ID in a different directory.
- */
- public static void removeFilesWithIdExcept(final Context context, final String id,
- final File fileToKeep) {
- try {
- final File canonicalFileToKeep = fileToKeep.getCanonicalFile();
- final File[] directoryList = DictionaryInfoUtils.getCachedDirectoryList(context);
- if (null == directoryList) return;
- for (File directory : directoryList) {
- // There is one directory per locale. See #getCachedDirectoryList
- if (!directory.isDirectory()) continue;
- final File[] wordLists = directory.listFiles();
- if (null == wordLists) continue;
- for (File wordList : wordLists) {
- final String fileId =
- DictionaryInfoUtils.getWordListIdFromFileName(wordList.getName());
- if (fileId.equals(id)) {
- if (!canonicalFileToKeep.equals(wordList.getCanonicalFile())) {
- wordList.delete();
- }
- }
- }
- }
- } catch (java.io.IOException e) {
- Log.e(TAG, "IOException trying to cleanup files", e);
- }
- }
-
// ## HACK ## we prevent usage of a dictionary before version 18. The reason for this is, since
// those do not include whitelist entries, the new code with an old version of the dictionary
// would lose whitelist functionality.
@@ -274,12 +241,18 @@ final public class BinaryDictionaryGetter {
*/
public static ArrayList<AssetFileAddress> getDictionaryFiles(final Locale locale,
final Context context, boolean notifyDictionaryPackForUpdates) {
-
- final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable(
- context, locale);
if (notifyDictionaryPackForUpdates) {
- BinaryDictionaryFileDumper.cacheWordListsFromContentProvider(locale, context,
- hasDefaultWordList);
+ final boolean hasDefaultWordList = DictionaryInfoUtils.isDictionaryAvailable(
+ context, locale);
+ // It makes sure that the first time keyboard comes up and the dictionaries are reset,
+ // the DB is populated with the appropriate values for each locale. Helps in downloading
+ // the dictionaries when the user enables and switches new languages before the
+ // DictionaryService runs.
+ BinaryDictionaryFileDumper.downloadDictIfNeverRequested(
+ locale, context, hasDefaultWordList);
+
+ // Move a staging files to the cache ddirectories if any.
+ DictionaryInfoUtils.moveStagingFilesIfExists(context);
}
final File[] cachedWordLists = getCachedWordLists(locale.toString(), context);
final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index 15a14e5af..dbd639fe8 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -16,6 +16,7 @@
package com.android.inputmethod.latin;
+import android.Manifest;
import android.content.Context;
import android.net.Uri;
import android.provider.ContactsContract;
@@ -25,6 +26,7 @@ import android.util.Log;
import com.android.inputmethod.annotations.ExternallyReferenced;
import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
import com.android.inputmethod.latin.common.StringUtils;
+import com.android.inputmethod.latin.permissions.PermissionsUtil;
import com.android.inputmethod.latin.personalization.AccountUtils;
import java.io.File;
@@ -108,6 +110,11 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary
* Loads data within content providers to the dictionary.
*/
private void loadDictionaryForUriLocked(final Uri uri) {
+ if (!PermissionsUtil.checkAllPermissionsGranted(
+ mContext, Manifest.permission.READ_CONTACTS)) {
+ Log.i(TAG, "No permission to read contacts. Not loading the Dictionary.");
+ }
+
final ArrayList<String> validNames = mContactsManager.getValidNames(uri);
for (final String name : validNames) {
addNameLocked(name);
diff --git a/java/src/com/android/inputmethod/latin/ContactsContentObserver.java b/java/src/com/android/inputmethod/latin/ContactsContentObserver.java
index 872e4c8fc..6103a8296 100644
--- a/java/src/com/android/inputmethod/latin/ContactsContentObserver.java
+++ b/java/src/com/android/inputmethod/latin/ContactsContentObserver.java
@@ -16,6 +16,7 @@
package com.android.inputmethod.latin;
+import android.Manifest;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
@@ -25,6 +26,7 @@ import android.util.Log;
import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
import com.android.inputmethod.latin.define.DebugFlags;
+import com.android.inputmethod.latin.permissions.PermissionsUtil;
import com.android.inputmethod.latin.utils.ExecutorUtils;
import java.util.ArrayList;
@@ -35,10 +37,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/
public class ContactsContentObserver implements Runnable {
private static final String TAG = "ContactsContentObserver";
- private static AtomicBoolean sRunning = new AtomicBoolean(false);
private final Context mContext;
private final ContactsManager mManager;
+ private final AtomicBoolean mRunning = new AtomicBoolean(false);
private ContentObserver mContentObserver;
private ContactsChangedListener mContactsChangedListener;
@@ -49,6 +51,13 @@ public class ContactsContentObserver implements Runnable {
}
public void registerObserver(final ContactsChangedListener listener) {
+ if (!PermissionsUtil.checkAllPermissionsGranted(
+ mContext, Manifest.permission.READ_CONTACTS)) {
+ Log.i(TAG, "No permission to read contacts. Not registering the observer.");
+ // do nothing if we do not have the permission to read contacts.
+ return;
+ }
+
if (DebugFlags.DEBUG_ENABLED) {
Log.d(TAG, "registerObserver()");
}
@@ -66,7 +75,14 @@ public class ContactsContentObserver implements Runnable {
@Override
public void run() {
- if (!sRunning.compareAndSet(false /* expect */, true /* update */)) {
+ if (!PermissionsUtil.checkAllPermissionsGranted(
+ mContext, Manifest.permission.READ_CONTACTS)) {
+ Log.i(TAG, "No permission to read contacts. Not updating the contacts.");
+ unregister();
+ return;
+ }
+
+ if (!mRunning.compareAndSet(false /* expect */, true /* update */)) {
if (DebugFlags.DEBUG_ENABLED) {
Log.d(TAG, "run() : Already running. Don't waste time checking again.");
}
@@ -78,10 +94,16 @@ public class ContactsContentObserver implements Runnable {
}
mContactsChangedListener.onContactsChange();
}
- sRunning.set(false);
+ mRunning.set(false);
}
boolean haveContentsChanged() {
+ if (!PermissionsUtil.checkAllPermissionsGranted(
+ mContext, Manifest.permission.READ_CONTACTS)) {
+ Log.i(TAG, "No permission to read contacts. Marking contacts as not changed.");
+ return false;
+ }
+
final long startTime = SystemClock.uptimeMillis();
final int contactCount = mManager.getContactCount();
if (contactCount > ContactsDictionaryConstants.MAX_CONTACTS_PROVIDER_QUERY_LIMIT) {
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
index ff798abd6..02015da09 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -17,6 +17,7 @@
package com.android.inputmethod.latin;
import android.content.Context;
+import android.util.LruCache;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.Keyboard;
@@ -55,6 +56,18 @@ public interface DictionaryFacilitator {
Dictionary.TYPE_USER};
/**
+ * The facilitator will put words into the cache whenever it decodes them.
+ * @param cache
+ */
+ void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache);
+
+ /**
+ * The facilitator will get words from the cache whenever it needs to check their spelling.
+ * @param cache
+ */
+ void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache);
+
+ /**
* Returns whether this facilitator is exactly for this locale.
*
* @param locale the locale to test against
@@ -88,12 +101,16 @@ public interface DictionaryFacilitator {
*
* WARNING: The service methods that call start/finish are very spammy.
*/
- void onFinishInput();
+ void onFinishInput(Context context);
boolean isActive();
Locale getLocale();
+ boolean usesContacts();
+
+ String getAccount();
+
void resetDictionaries(
final Context context,
final Locale newLocale,
@@ -149,7 +166,7 @@ public interface DictionaryFacilitator {
boolean isValidSuggestionWord(final String word);
- void clearUserHistoryDictionary(final Context context);
+ boolean clearUserHistoryDictionary(final Context context);
String dump(final Context context);
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java
index 7233d27ab..b435de867 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java
@@ -16,9 +16,11 @@
package com.android.inputmethod.latin;
+import android.Manifest;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
+import android.util.LruCache;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.Keyboard;
@@ -26,6 +28,8 @@ import com.android.inputmethod.latin.NgramContext.WordInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.common.ComposedData;
import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.StringUtils;
+import com.android.inputmethod.latin.permissions.PermissionsUtil;
import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
import com.android.inputmethod.latin.utils.ExecutorUtils;
@@ -82,6 +86,19 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES =
new Class[] { Context.class, Locale.class, File.class, String.class, String.class };
+ private LruCache<String, Boolean> mValidSpellingWordReadCache;
+ private LruCache<String, Boolean> mValidSpellingWordWriteCache;
+
+ @Override
+ public void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache) {
+ mValidSpellingWordReadCache = cache;
+ }
+
+ @Override
+ public void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache) {
+ mValidSpellingWordWriteCache = cache;
+ }
+
@Override
public boolean isForLocale(final Locale locale) {
return locale != null && locale.equals(mDictionaryGroup.mLocale);
@@ -207,7 +224,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
@Override
- public void onFinishInput() {
+ public void onFinishInput(Context context) {
}
@Override
@@ -220,6 +237,16 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
return mDictionaryGroup.mLocale;
}
+ @Override
+ public boolean usesContacts() {
+ return mDictionaryGroup.getSubDict(Dictionary.TYPE_CONTACTS) != null;
+ }
+
+ @Override
+ public String getAccount() {
+ return null;
+ }
+
@Nullable
private static ExpandableBinaryDictionary getSubDict(final String dictType,
final Context context, final Locale locale, final File dictFile,
@@ -262,7 +289,11 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
// TODO: Make subDictTypesToUse configurable by resource or a static final list.
final HashSet<String> subDictTypesToUse = new HashSet<>();
subDictTypesToUse.add(Dictionary.TYPE_USER);
- if (useContactsDict) {
+
+ // Do not use contacts dictionary if we do not have permissions to read contacts.
+ final boolean contactsPermissionGranted = PermissionsUtil.checkAllPermissionsGranted(
+ context, Manifest.permission.READ_CONTACTS);
+ if (useContactsDict && contactsPermissionGranted) {
subDictTypesToUse.add(Dictionary.TYPE_CONTACTS);
}
if (usePersonalizedDicts) {
@@ -341,6 +372,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
dictionarySetToCleanup.closeDict(dictType);
}
}
+
+ if (mValidSpellingWordWriteCache != null) {
+ mValidSpellingWordWriteCache.evictAll();
+ }
}
private void asyncReloadUninitializedMainDictionaries(final Context context,
@@ -464,6 +499,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized,
@Nonnull final NgramContext ngramContext, final long timeStampInSeconds,
final boolean blockPotentiallyOffensive) {
+ // Update the spelling cache before learning. Words that are not yet added to user history
+ // and appear in no other language model are not considered valid.
+ putWordIntoValidSpellingWordCache("addToUserHistory", suggestion);
+
final String[] words = suggestion.split(Constants.WORD_SEPARATOR);
NgramContext ngramContextForCurrentWord = ngramContext;
for (int i = 0; i < words.length; i++) {
@@ -477,6 +516,29 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
}
+ private void putWordIntoValidSpellingWordCache(
+ @Nonnull final String caller,
+ @Nonnull final String originalWord) {
+ if (mValidSpellingWordWriteCache == null) {
+ return;
+ }
+
+ final String lowerCaseWord = originalWord.toLowerCase(getLocale());
+ final boolean lowerCaseValid = isValidSpellingWord(lowerCaseWord);
+ mValidSpellingWordWriteCache.put(lowerCaseWord, lowerCaseValid);
+
+ final String capitalWord =
+ StringUtils.capitalizeFirstAndDowncaseRest(originalWord, getLocale());
+ final boolean capitalValid;
+ if (lowerCaseValid) {
+ // The lower case form of the word is valid, so the upper case must be valid.
+ capitalValid = true;
+ } else {
+ capitalValid = isValidSpellingWord(capitalWord);
+ }
+ mValidSpellingWordWriteCache.put(capitalWord, capitalValid);
+ }
+
private void addWordToUserHistory(final DictionaryGroup dictionaryGroup,
final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized,
final int timeStampInSeconds, final boolean blockPotentiallyOffensive) {
@@ -543,6 +605,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
if (eventType != Constants.EVENT_BACKSPACE) {
removeWord(Dictionary.TYPE_USER_HISTORY, word);
}
+
+ // Update the spelling cache after unlearning. Words that are removed from user history
+ // and appear in no other language model are not considered valid.
+ putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.toLowerCase());
}
// TODO: Revise the way to fusion suggestion results.
@@ -577,6 +643,13 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
}
public boolean isValidSpellingWord(final String word) {
+ if (mValidSpellingWordReadCache != null) {
+ final Boolean cachedValue = mValidSpellingWordReadCache.get(word);
+ if (cachedValue != null) {
+ return cachedValue;
+ }
+ }
+
return isValidWord(word, ALL_DICTIONARY_TYPES);
}
@@ -620,16 +693,18 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
return maxFreq;
}
- private void clearSubDictionary(final String dictName) {
+ private boolean clearSubDictionary(final String dictName) {
final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName);
- if (dictionary != null) {
- dictionary.clear();
+ if (dictionary == null) {
+ return false;
}
+ dictionary.clear();
+ return true;
}
@Override
- public void clearUserHistoryDictionary(final Context context) {
- clearSubDictionary(Dictionary.TYPE_USER_HISTORY);
+ public boolean clearUserHistoryDictionary(final Context context) {
+ return clearSubDictionary(Dictionary.TYPE_USER_HISTORY);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java b/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java
deleted file mode 100644
index 567087c81..000000000
--- a/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
-
-import com.android.inputmethod.latin.utils.DialogUtils;
-import com.android.inputmethod.latin.utils.ImportantNoticeUtils;
-
-/**
- * The dialog box that shows the important notice contents.
- */
-public final class ImportantNoticeDialog extends AlertDialog implements OnClickListener {
- public interface ImportantNoticeDialogListener {
- public void onUserAcknowledgmentOfImportantNoticeDialog(final int nextVersion);
- public void onClickSettingsOfImportantNoticeDialog(final int nextVersion);
- }
-
- private final ImportantNoticeDialogListener mListener;
- private final int mNextImportantNoticeVersion;
-
- public ImportantNoticeDialog(
- final Context context, final ImportantNoticeDialogListener listener) {
- super(DialogUtils.getPlatformDialogThemeContext(context));
- mListener = listener;
- mNextImportantNoticeVersion = ImportantNoticeUtils.getNextImportantNoticeVersion(context);
- setMessage(ImportantNoticeUtils.getNextImportantNoticeContents(context));
- // Create buttons and set listeners.
- setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok), this);
- if (shouldHaveSettingsButton()) {
- setButton(BUTTON_NEGATIVE, context.getString(R.string.go_to_settings), this);
- }
- // This dialog is cancelable by pressing back key. See {@link #onBackPress()}.
- setCancelable(true /* cancelable */);
- setCanceledOnTouchOutside(false /* cancelable */);
- }
-
- private boolean shouldHaveSettingsButton() {
- return mNextImportantNoticeVersion
- == ImportantNoticeUtils.VERSION_TO_ENABLE_PERSONALIZED_SUGGESTIONS;
- }
-
- private void userAcknowledged() {
- ImportantNoticeUtils.updateLastImportantNoticeVersion(getContext());
- mListener.onUserAcknowledgmentOfImportantNoticeDialog(mNextImportantNoticeVersion);
- }
-
- @Override
- public void onClick(final DialogInterface dialog, final int which) {
- if (shouldHaveSettingsButton() && which == BUTTON_NEGATIVE) {
- mListener.onClickSettingsOfImportantNoticeDialog(mNextImportantNoticeVersion);
- }
- userAcknowledged();
- }
-
- @Override
- public void onBackPressed() {
- super.onBackPressed();
- userAcknowledged();
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 330be377b..1f2b6f25d 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -20,6 +20,7 @@ import static com.android.inputmethod.latin.common.Constants.ImeOption.FORCE_ASC
import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_MICROPHONE;
import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_MICROPHONE_COMPAT;
+import android.Manifest.permission;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -73,6 +74,7 @@ import com.android.inputmethod.latin.common.InputPointers;
import com.android.inputmethod.latin.define.DebugFlags;
import com.android.inputmethod.latin.define.ProductionFlags;
import com.android.inputmethod.latin.inputlogic.InputLogic;
+import com.android.inputmethod.latin.permissions.PermissionsManager;
import com.android.inputmethod.latin.personalization.PersonalizationHelper;
import com.android.inputmethod.latin.settings.Settings;
import com.android.inputmethod.latin.settings.SettingsActivity;
@@ -106,7 +108,7 @@ import javax.annotation.Nonnull;
public class LatinIME extends InputMethodService implements KeyboardActionListener,
SuggestionStripView.Listener, SuggestionStripViewAccessor,
DictionaryFacilitator.DictionaryInitializationListener,
- ImportantNoticeDialog.ImportantNoticeDialogListener {
+ PermissionsManager.PermissionsResultCallback {
static final String TAG = LatinIME.class.getSimpleName();
private static final boolean TRACE = false;
@@ -972,7 +974,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
void onFinishInputInternal() {
super.onFinishInput();
- mDictionaryFacilitator.onFinishInput();
+ mDictionaryFacilitator.onFinishInput(this);
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
if (mainKeyboardView != null) {
mainKeyboardView.closing();
@@ -1251,18 +1253,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// pressed.
@Override
public void showImportantNoticeContents() {
- showOptionDialog(new ImportantNoticeDialog(this /* context */, this /* listener */));
+ PermissionsManager.get(this).requestPermissions(
+ this /* PermissionsResultCallback */,
+ null /* activity */, permission.READ_CONTACTS);
}
- // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener}
@Override
- public void onClickSettingsOfImportantNoticeDialog(final int nextVersion) {
- launchSettings(SettingsActivity.EXTRA_ENTRY_VALUE_NOTICE_DIALOG);
- }
-
- // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener}
- @Override
- public void onUserAcknowledgmentOfImportantNoticeDialog(final int nextVersion) {
+ public void onRequestPermissionsResult(boolean allGranted) {
+ ImportantNoticeUtils.updateContactsNoticeShown(this /* context */);
setNeutralSuggestionStrip();
}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index a123d282b..a10f2bdb0 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -16,11 +16,10 @@
package com.android.inputmethod.latin;
-import static com.android.inputmethod.latin.define.DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH;
-
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.os.Bundle;
+import android.os.SystemClock;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
@@ -37,7 +36,6 @@ import com.android.inputmethod.compat.InputConnectionCompatUtils;
import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.common.UnicodeSurrogate;
import com.android.inputmethod.latin.common.StringUtils;
-import com.android.inputmethod.latin.define.DecoderSpecificConstants;
import com.android.inputmethod.latin.inputlogic.PrivateCommandPerformer;
import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
import com.android.inputmethod.latin.utils.CapsModeUtils;
@@ -45,8 +43,11 @@ import com.android.inputmethod.latin.utils.DebugLogUtils;
import com.android.inputmethod.latin.utils.NgramContextUtils;
import com.android.inputmethod.latin.utils.ScriptUtils;
import com.android.inputmethod.latin.utils.SpannableStringUtils;
+import com.android.inputmethod.latin.utils.StatsUtils;
import com.android.inputmethod.latin.utils.TextRange;
+import java.util.concurrent.TimeUnit;
+
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -59,17 +60,42 @@ import javax.annotation.Nullable;
* for example.
*/
public final class RichInputConnection implements PrivateCommandPerformer {
- private static final String TAG = RichInputConnection.class.getSimpleName();
+ private static final String TAG = "RichInputConnection";
private static final boolean DBG = false;
private static final boolean DEBUG_PREVIOUS_TEXT = false;
private static final boolean DEBUG_BATCH_NESTING = false;
- // Provision for realistic N-grams like "Hello, how are you?" and "I'm running 5 late".
- // Technically, this will not handle 5-grams composed of long words, but in practice,
- // our language models don't include that much data.
- private static final int LOOKBACK_CHARACTER_NUM = 80;
+ private static final int NUM_CHARS_TO_GET_BEFORE_CURSOR = 40;
+ private static final int NUM_CHARS_TO_GET_AFTER_CURSOR = 40;
private static final int INVALID_CURSOR_POSITION = -1;
/**
+ * The amount of time a {@link #reloadTextCache} call needs to take for the keyboard to enter
+ * the {@link #hasSlowInputConnection} state.
+ */
+ private static final long SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS = 1000;
+ /**
+ * The amount of time a {@link #getTextBeforeCursor} or {@link #getTextAfterCursor} call needs
+ * to take for the keyboard to enter the {@link #hasSlowInputConnection} state.
+ */
+ private static final long SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS = 200;
+
+ private static final int OPERATION_GET_TEXT_BEFORE_CURSOR = 0;
+ private static final int OPERATION_GET_TEXT_AFTER_CURSOR = 1;
+ private static final int OPERATION_GET_WORD_RANGE_AT_CURSOR = 2;
+ private static final int OPERATION_RELOAD_TEXT_CACHE = 3;
+ private static final String[] OPERATION_NAMES = new String[] {
+ "GET_TEXT_BEFORE_CURSOR",
+ "GET_TEXT_AFTER_CURSOR",
+ "GET_WORD_RANGE_AT_CURSOR",
+ "RELOAD_TEXT_CACHE"};
+
+ /**
+ * The amount of time the keyboard will persist in the {@link #hasSlowInputConnection} state
+ * after observing a slow InputConnection event.
+ */
+ private static final long SLOW_INPUTCONNECTION_PERSIST_MS = TimeUnit.MINUTES.toMillis(10);
+
+ /**
* This variable contains an expected value for the selection start position. This is where the
* cursor or selection start may end up after all the keyboard-triggered updates have passed. We
* keep this to compare it to the actual selection start to guess whether the move was caused by
@@ -85,7 +111,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points
/**
* This contains the committed text immediately preceding the cursor and the composing
- * text if any. It is refreshed when the cursor moves by calling upon the TextView.
+ * text, if any. It is refreshed when the cursor moves by calling upon the TextView.
*/
private final StringBuilder mCommittedTextBeforeComposingText = new StringBuilder();
/**
@@ -100,8 +126,13 @@ public final class RichInputConnection implements PrivateCommandPerformer {
private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder();
private final InputMethodService mParent;
- InputConnection mIC;
- int mNestLevel;
+ private InputConnection mIC;
+ private int mNestLevel;
+
+ /**
+ * The timestamp of the last slow InputConnection operation
+ */
+ private long mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS;
public RichInputConnection(final InputMethodService parent) {
mParent = parent;
@@ -113,6 +144,19 @@ public final class RichInputConnection implements PrivateCommandPerformer {
return mIC != null;
}
+ /**
+ * Returns whether or not the underlying InputConnection is slow. When true, we want to avoid
+ * calling InputConnection methods that trigger an IPC round-trip (e.g., getTextAfterCursor).
+ */
+ public boolean hasSlowInputConnection() {
+ return (SystemClock.uptimeMillis() - mLastSlowInputConnectionTime)
+ <= SLOW_INPUTCONNECTION_PERSIST_MS;
+ }
+
+ public void onStartInput() {
+ mLastSlowInputConnectionTime = -SLOW_INPUTCONNECTION_PERSIST_MS;
+ }
+
private void checkConsistencyForDebug() {
final ExtractedTextRequest r = new ExtractedTextRequest();
r.hintMaxChars = 0;
@@ -211,9 +255,11 @@ public final class RichInputConnection implements PrivateCommandPerformer {
mIC = mParent.getCurrentInputConnection();
// Call upon the inputconnection directly since our own method is using the cache, and
// we want to refresh it.
- final CharSequence textBeforeCursor = isConnected()
- ? mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0)
- : null;
+ final CharSequence textBeforeCursor = getTextBeforeCursorAndDetectLaggyConnection(
+ OPERATION_RELOAD_TEXT_CACHE,
+ SLOW_INPUT_CONNECTION_ON_FULL_RELOAD_MS,
+ Constants.EDITOR_CONTENTS_CACHE_SIZE,
+ 0 /* flags */);
if (null == textBeforeCursor) {
// For some reason the app thinks we are not connected to it. This looks like a
// framework bug... Fall back to ground state and return false.
@@ -377,16 +423,54 @@ public final class RichInputConnection implements PrivateCommandPerformer {
}
return s;
}
+ return getTextBeforeCursorAndDetectLaggyConnection(
+ OPERATION_GET_TEXT_BEFORE_CURSOR,
+ SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS,
+ n, flags);
+ }
+
+ private CharSequence getTextBeforeCursorAndDetectLaggyConnection(
+ final int operation, final long timeout, final int n, final int flags) {
mIC = mParent.getCurrentInputConnection();
- return isConnected() ? mIC.getTextBeforeCursor(n, flags) : null;
+ if (!isConnected()) {
+ return null;
+ }
+ final long startTime = SystemClock.uptimeMillis();
+ final CharSequence result = mIC.getTextBeforeCursor(n, flags);
+ detectLaggyConnection(operation, timeout, startTime);
+ return result;
}
public CharSequence getTextAfterCursor(final int n, final int flags) {
+ return getTextAfterCursorAndDetectLaggyConnection(
+ OPERATION_GET_TEXT_AFTER_CURSOR,
+ SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS,
+ n, flags);
+ }
+
+ private CharSequence getTextAfterCursorAndDetectLaggyConnection(
+ final int operation, final long timeout, final int n, final int flags) {
mIC = mParent.getCurrentInputConnection();
- return isConnected() ? mIC.getTextAfterCursor(n, flags) : null;
+ if (!isConnected()) {
+ return null;
+ }
+ final long startTime = SystemClock.uptimeMillis();
+ final CharSequence result = mIC.getTextAfterCursor(n, flags);
+ detectLaggyConnection(operation, timeout, startTime);
+ return result;
+ }
+
+ private void detectLaggyConnection(final int operation, final long timeout, final long startTime) {
+ final long duration = SystemClock.uptimeMillis() - startTime;
+ if (duration >= timeout) {
+ final String operationName = OPERATION_NAMES[operation];
+ Log.w(TAG, "Slow InputConnection: " + operationName + " took " + duration + " ms.");
+ StatsUtils.onInputConnectionLaggy(operation, duration);
+ mLastSlowInputConnectionTime = SystemClock.uptimeMillis();
+ }
}
- public void deleteSurroundingText(final int beforeLength, final int afterLength) {
+ public void deleteTextBeforeCursor(final int beforeLength) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
// TODO: the following is incorrect if the cursor is not immediately after the composition.
// Right now we never come here in this case because we reset the composing state before we
@@ -411,7 +495,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
mExpectedSelStart = 0;
}
if (isConnected()) {
- mIC.deleteSurroundingText(beforeLength, afterLength);
+ mIC.deleteSurroundingText(beforeLength, 0);
}
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
}
@@ -576,9 +660,9 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (!isConnected()) {
return NgramContext.EMPTY_PREV_WORDS_INFO;
}
- final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
+ final CharSequence prev = getTextBeforeCursor(NUM_CHARS_TO_GET_BEFORE_CURSOR, 0);
if (DEBUG_PREVIOUS_TEXT && null != prev) {
- final int checkLength = LOOKBACK_CHARACTER_NUM - 1;
+ final int checkLength = NUM_CHARS_TO_GET_BEFORE_CURSOR - 1;
final String reference = prev.length() <= checkLength ? prev.toString()
: prev.subSequence(prev.length() - checkLength, prev.length()).toString();
// TODO: right now the following works because mComposingText holds the part of the
@@ -621,9 +705,15 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (!isConnected()) {
return null;
}
- final CharSequence before = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM,
+ final CharSequence before = getTextBeforeCursorAndDetectLaggyConnection(
+ OPERATION_GET_WORD_RANGE_AT_CURSOR,
+ SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS,
+ NUM_CHARS_TO_GET_BEFORE_CURSOR,
InputConnection.GET_TEXT_WITH_STYLES);
- final CharSequence after = mIC.getTextAfterCursor(LOOKBACK_CHARACTER_NUM,
+ final CharSequence after = getTextAfterCursorAndDetectLaggyConnection(
+ OPERATION_GET_WORD_RANGE_AT_CURSOR,
+ SLOW_INPUT_CONNECTION_ON_PARTIAL_RELOAD_MS,
+ NUM_CHARS_TO_GET_AFTER_CURSOR,
InputConnection.GET_TEXT_WITH_STYLES);
if (before == null || after == null) {
return null;
@@ -666,8 +756,9 @@ public final class RichInputConnection implements PrivateCommandPerformer {
hasUrlSpans);
}
- public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) {
- if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) {
+ public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations,
+ boolean checkTextAfter) {
+ if (checkTextAfter && isCursorFollowedByWordCharacter(spacingAndPunctuations)) {
// If what's after the cursor is a word character, then we're touching a word.
return true;
}
@@ -704,7 +795,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
final int codePointBeforeCursor = getCodePointBeforeCursor();
if (Constants.CODE_SPACE == codePointBeforeCursor) {
- deleteSurroundingText(1, 0);
+ deleteTextBeforeCursor(1);
}
}
@@ -730,7 +821,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
}
// Double-space results in ". ". A backspace to cancel this should result in a single
// space in the text field, so we replace ". " with a single space.
- deleteSurroundingText(2, 0);
+ deleteTextBeforeCursor(2);
final String singleSpace = " ";
commitText(singleSpace, 1);
return true;
@@ -752,7 +843,7 @@ public final class RichInputConnection implements PrivateCommandPerformer {
+ "find a space just before the cursor.");
return false;
}
- deleteSurroundingText(2, 0);
+ deleteTextBeforeCursor(2);
final String text = " " + textBeforeCursor.subSequence(0, 1);
commitText(text, 1);
return true;
diff --git a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java
index 0d081e0d2..90221512f 100644
--- a/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java
+++ b/java/src/com/android/inputmethod/latin/SystemBroadcastReceiver.java
@@ -16,6 +16,7 @@
package com.android.inputmethod.latin;
+import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -23,14 +24,15 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.database.Cursor;
import android.os.Process;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
-import com.android.inputmethod.dictionarypack.CommonPreferences;
import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
+import com.android.inputmethod.dictionarypack.DownloadManagerWrapper;
import com.android.inputmethod.keyboard.KeyboardLayoutSet;
import com.android.inputmethod.latin.settings.Settings;
import com.android.inputmethod.latin.setup.SetupActivity;
@@ -75,7 +77,12 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver {
final InputMethodSubtype[] additionalSubtypes = richImm.getAdditionalSubtypes();
richImm.setAdditionalInputMethodSubtypes(additionalSubtypes);
toggleAppIcon(context);
- downloadLatestDictionaries(context);
+
+ // Remove all the previously scheduled downloads. This will also makes sure
+ // that any erroneously stuck downloads will get cleared. (b/21797386)
+ removeOldDownloads(context);
+ // b/21797386
+ // downloadLatestDictionaries(context);
} else if (Intent.ACTION_BOOT_COMPLETED.equals(intentAction)) {
Log.i(TAG, "Boot has been completed");
toggleAppIcon(context);
@@ -103,13 +110,39 @@ public final class SystemBroadcastReceiver extends BroadcastReceiver {
}
}
+ private void removeOldDownloads(Context context) {
+ try {
+ Log.i(TAG, "Removing the old downloads in progress of the previous keyboard version.");
+ final DownloadManagerWrapper downloadManagerWrapper = new DownloadManagerWrapper(
+ context);
+ final DownloadManager.Query q = new DownloadManager.Query();
+ // Query all the download statuses except the succeeded ones.
+ q.setFilterByStatus(DownloadManager.STATUS_FAILED
+ | DownloadManager.STATUS_PAUSED
+ | DownloadManager.STATUS_PENDING
+ | DownloadManager.STATUS_RUNNING);
+ final Cursor c = downloadManagerWrapper.query(q);
+ if (c != null) {
+ for (c.moveToFirst(); !c.isAfterLast(); c.moveToNext()) {
+ final long downloadId = c
+ .getLong(c.getColumnIndex(DownloadManager.COLUMN_ID));
+ downloadManagerWrapper.remove(downloadId);
+ Log.i(TAG, "Removed the download with Id: " + downloadId);
+ }
+ c.close();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while removing old downloads.");
+ }
+ }
+
private void downloadLatestDictionaries(Context context) {
final Intent updateIntent = new Intent(
DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION);
context.sendBroadcast(updateIntent);
}
- private static void toggleAppIcon(final Context context) {
+ public static void toggleAppIcon(final Context context) {
final int appInfoFlags = context.getApplicationInfo().flags;
final boolean isSystemApp = (appInfoFlags & ApplicationInfo.FLAG_SYSTEM) > 0;
if (Log.isLoggable(TAG, Log.INFO)) {
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index f7dbc0a4d..1dd5850f8 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -139,6 +139,7 @@ public final class InputLogic {
public void startInput(final String combiningSpec, final SettingsValues settingsValues) {
mEnteredText = null;
mWordBeingCorrectedByCursor = null;
+ mConnection.onStartInput();
if (!mWordComposer.getTypedWord().isEmpty()) {
// For messaging apps that offer send button, the IME does not get the opportunity
// to capture the last word. This block should capture those uncommitted words.
@@ -398,9 +399,8 @@ public final class InputLogic {
if (!TextUtils.isEmpty(mWordBeingCorrectedByCursor)) {
final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds(
System.currentTimeMillis());
- mDictionaryFacilitator.addToUserHistory(mWordBeingCorrectedByCursor, false,
- NgramContext.EMPTY_PREV_WORDS_INFO, timeStampInSeconds,
- settingsValues.mBlockPotentiallyOffensive);
+ performAdditionToUserHistoryDictionary(settingsValues, mWordBeingCorrectedByCursor,
+ NgramContext.EMPTY_PREV_WORDS_INFO);
}
} else {
// resetEntireInputState calls resetCachesUponCursorMove, but forcing the
@@ -473,7 +473,7 @@ public final class InputLogic {
}
// Try to record the word being corrected when the user enters a word character or
// the backspace key.
- if (!mWordComposer.isComposingWord()
+ if (!mConnection.hasSlowInputConnection() && !mWordComposer.isComposingWord()
&& (settingsValues.isWordCodePoint(processedEvent.mCodePoint) ||
processedEvent.mKeyCode == Constants.CODE_DELETE)) {
mWordBeingCorrectedByCursor = getWordAtCursor(
@@ -833,8 +833,14 @@ public final class InputLogic {
&& settingsValues.needsToLookupSuggestions() &&
// In languages with spaces, we only start composing a word when we are not already
// touching a word. In languages without spaces, the above conditions are sufficient.
- (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)
- || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) {
+ // NOTE: If the InputConnection is slow, we skip the text-after-cursor check since it
+ // can incur a very expensive getTextAfterCursor() lookup, potentially making the
+ // keyboard UI slow and non-responsive.
+ // TODO: Cache the text after the cursor so we don't need to go to the InputConnection
+ // each time. We are already doing this for getTextBeforeCursor().
+ (!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
+ || !mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations,
+ !mConnection.hasSlowInputConnection() /* checkTextAfter */))) {
// Reset entirely the composing state anyway, then start composing a new word unless
// the character is a word connector. The idea here is, word connectors are not
// separators and they should be treated as normal characters, except in the first
@@ -1054,7 +1060,7 @@ public final class InputLogic {
// Cancel multi-character input: remove the text we just entered.
// This is triggered on backspace after a key that inputs multiple characters,
// like the smiley key or the .com key.
- mConnection.deleteSurroundingText(mEnteredText.length(), 0);
+ mConnection.deleteTextBeforeCursor(mEnteredText.length());
StatsUtils.onDeleteMultiCharInput(mEnteredText.length());
mEnteredText = null;
// If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
@@ -1099,7 +1105,7 @@ public final class InputLogic {
- mConnection.getExpectedSelectionStart();
mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
mConnection.getExpectedSelectionEnd());
- mConnection.deleteSurroundingText(numCharsDeleted, 0);
+ mConnection.deleteTextBeforeCursor(numCharsDeleted);
StatsUtils.onBackspaceSelectedText(numCharsDeleted);
} else {
// There is no selection, just delete one character.
@@ -1139,13 +1145,13 @@ public final class InputLogic {
// broken apps expect something to happen in this case so that they can
// catch it and have their broken interface react. If you need the keyboard
// to do this, you're doing it wrong -- please fix your app.
- mConnection.deleteSurroundingText(1, 0);
+ mConnection.deleteTextBeforeCursor(1);
// TODO: Add a new StatsUtils method onBackspaceWhenNoText()
return;
}
final int lengthToDelete =
Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
- mConnection.deleteSurroundingText(lengthToDelete, 0);
+ mConnection.deleteTextBeforeCursor(lengthToDelete);
int totalDeletedLength = lengthToDelete;
if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
// If this is an accelerated (i.e., double) deletion, then we need to
@@ -1158,7 +1164,7 @@ public final class InputLogic {
if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
codePointBeforeCursorToDeleteAgain) ? 2 : 1;
- mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
+ mConnection.deleteTextBeforeCursor(lengthToDeleteAgain);
totalDeletedLength += lengthToDeleteAgain;
}
}
@@ -1170,7 +1176,9 @@ public final class InputLogic {
unlearnWordBeingDeleted(
inputTransaction.mSettingsValues, currentKeyboardScriptId);
}
- if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings()
+ if (mConnection.hasSlowInputConnection()) {
+ mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
+ } else if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings()
&& inputTransaction.mSettingsValues.mSpacingAndPunctuations
.mCurrentLanguageHasSpaces
&& !mConnection.isCursorFollowedByWordCharacter(
@@ -1197,6 +1205,13 @@ public final class InputLogic {
boolean unlearnWordBeingDeleted(
final SettingsValues settingsValues, final int currentKeyboardScriptId) {
+ if (mConnection.hasSlowInputConnection()) {
+ // TODO: Refactor unlearning so that it does not incur any extra calls
+ // to the InputConnection. That way it can still be performed on a slow
+ // InputConnection.
+ Log.w(TAG, "Skipping unlearning due to slow InputConnection.");
+ return false;
+ }
// If we just started backspacing to delete a previous word (but have not
// entered the composing state yet), unlearn the word.
// TODO: Consider tracking whether or not this word was typed by the user.
@@ -1242,7 +1257,7 @@ public final class InputLogic {
if (Constants.CODE_SPACE != codePointBeforeCursor) {
return false;
}
- mConnection.deleteSurroundingText(1, 0);
+ mConnection.deleteTextBeforeCursor(1);
final String text = event.getTextToCommit() + " ";
mConnection.commitText(text, 1);
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
@@ -1332,7 +1347,7 @@ public final class InputLogic {
Character.codePointAt(lastTwo, length - 3) : lastTwo.charAt(length - 2);
if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) {
cancelDoubleSpacePeriodCountdown();
- mConnection.deleteSurroundingText(1, 0);
+ mConnection.deleteTextBeforeCursor(1);
final String textToInsert = inputTransaction.mSettingsValues.mSpacingAndPunctuations
.mSentenceSeparatorAndSpace;
mConnection.commitText(textToInsert, 1);
@@ -1400,7 +1415,7 @@ public final class InputLogic {
mConnection.finishComposingText();
mRecapitalizeStatus.rotate();
mConnection.setSelection(selectionEnd, selectionEnd);
- mConnection.deleteSurroundingText(numCharsSelected, 0);
+ mConnection.deleteTextBeforeCursor(numCharsSelected);
mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(),
mRecapitalizeStatus.getNewCursorEnd());
@@ -1412,6 +1427,12 @@ public final class InputLogic {
// That's to avoid unintended additions in some sensitive fields, or fields that
// expect to receive non-words.
if (!settingsValues.mAutoCorrectionEnabledPerUserSettings) return;
+ if (mConnection.hasSlowInputConnection()) {
+ // Since we don't unlearn when the user backspaces on a slow InputConnection,
+ // turn off learning to guard against adding typos that the user later deletes.
+ Log.w(TAG, "Skipping learning due to slow InputConnection.");
+ return;
+ }
if (TextUtils.isEmpty(suggestion)) return;
final boolean wasAutoCapitalized =
@@ -1515,7 +1536,8 @@ public final class InputLogic {
return;
}
final int expectedCursorPosition = mConnection.getExpectedSelectionStart();
- if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) {
+ if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations,
+ true /* checkTextAfter */)) {
// Show predictions.
mWordComposer.setCapitalizedModeAtStartComposingTime(WordComposer.CAPS_MODE_OFF);
mLatinIME.mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_RECORRECTION);
@@ -1638,7 +1660,7 @@ public final class InputLogic {
+ "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
}
}
- mConnection.deleteSurroundingText(deleteLength, 0);
+ mConnection.deleteTextBeforeCursor(deleteLength);
if (!TextUtils.isEmpty(committedWord)) {
unlearnWord(committedWordString, inputTransaction.mSettingsValues,
Constants.EVENT_REVERT);
@@ -2136,9 +2158,10 @@ public final class InputLogic {
final SuggestedWords suggestedWords = mSuggestedWords;
// TODO: Locale should be determined based on context and the text given.
final Locale locale = getDictionaryFacilitatorLocale();
- final CharSequence chosenWordWithSuggestions =
- SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord,
- suggestedWords, locale);
+ final CharSequence chosenWordWithSuggestions = chosenWord;
+ // b/21926256
+ // SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord,
+ // suggestedWords, locale);
if (DebugFlags.DEBUG_ENABLED) {
long runTimeMillis = System.currentTimeMillis() - startTimeMillis;
Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run "
diff --git a/java/src/com/android/inputmethod/latin/permissions/PermissionsActivity.java b/java/src/com/android/inputmethod/latin/permissions/PermissionsActivity.java
new file mode 100644
index 000000000..bdd63fa00
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/permissions/PermissionsActivity.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.inputmethod.latin.permissions;
+
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+
+/**
+ * An activity to help request permissions. It's used when no other activity is available, e.g. in
+ * InputMethodService. This activity assumes that all permissions are not granted yet.
+ */
+public final class PermissionsActivity
+ extends Activity implements ActivityCompat.OnRequestPermissionsResultCallback {
+
+ /**
+ * Key to retrieve requested permissions from the intent.
+ */
+ public static final String EXTRA_PERMISSION_REQUESTED_PERMISSIONS = "requested_permissions";
+
+ /**
+ * Key to retrieve request code from the intent.
+ */
+ public static final String EXTRA_PERMISSION_REQUEST_CODE = "request_code";
+
+ private static final int INVALID_REQUEST_CODE = -1;
+
+ private int mPendingRequestCode = INVALID_REQUEST_CODE;
+
+ /**
+ * Starts a PermissionsActivity and checks/requests supplied permissions.
+ */
+ public static void run(
+ @NonNull Context context, int requestCode, @NonNull String... permissionStrings) {
+ Intent intent = new Intent(context.getApplicationContext(), PermissionsActivity.class);
+ intent.putExtra(EXTRA_PERMISSION_REQUESTED_PERMISSIONS, permissionStrings);
+ intent.putExtra(EXTRA_PERMISSION_REQUEST_CODE, requestCode);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mPendingRequestCode = (savedInstanceState != null)
+ ? savedInstanceState.getInt(EXTRA_PERMISSION_REQUEST_CODE, INVALID_REQUEST_CODE)
+ : INVALID_REQUEST_CODE;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(EXTRA_PERMISSION_REQUEST_CODE, mPendingRequestCode);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // Only do request when there is no pending request to avoid duplicated requests.
+ if (mPendingRequestCode == INVALID_REQUEST_CODE) {
+ final Bundle extras = getIntent().getExtras();
+ final String[] permissionsToRequest =
+ extras.getStringArray(EXTRA_PERMISSION_REQUESTED_PERMISSIONS);
+ mPendingRequestCode = extras.getInt(EXTRA_PERMISSION_REQUEST_CODE);
+ // Assuming that all supplied permissions are not granted yet, so that we don't need to
+ // check them again.
+ PermissionsUtil.requestPermissions(this, mPendingRequestCode, permissionsToRequest);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ mPendingRequestCode = INVALID_REQUEST_CODE;
+ PermissionsManager.get(this).onRequestPermissionsResult(
+ requestCode, permissions, grantResults);
+ finish();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/permissions/PermissionsManager.java b/java/src/com/android/inputmethod/latin/permissions/PermissionsManager.java
new file mode 100644
index 000000000..08c623ab5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/permissions/PermissionsManager.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.inputmethod.latin.permissions;
+
+import android.app.Activity;
+import android.content.Context;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Manager to perform permission related tasks. Always call on the UI thread.
+ */
+public class PermissionsManager {
+
+ public interface PermissionsResultCallback {
+ void onRequestPermissionsResult(boolean allGranted);
+ }
+
+ private int mRequestCodeId;
+
+ private final Context mContext;
+ private final Map<Integer, PermissionsResultCallback> mRequestIdToCallback = new HashMap<>();
+
+ private static PermissionsManager sInstance;
+
+ public PermissionsManager(Context context) {
+ mContext = context;
+ }
+
+ @Nonnull
+ public static synchronized PermissionsManager get(@Nonnull Context context) {
+ if (sInstance == null) {
+ sInstance = new PermissionsManager(context);
+ }
+ return sInstance;
+ }
+
+ private synchronized int getNextRequestId() {
+ return ++mRequestCodeId;
+ }
+
+
+ public synchronized void requestPermissions(@Nonnull PermissionsResultCallback callback,
+ @Nullable Activity activity,
+ String... permissionsToRequest) {
+ List<String> deniedPermissions = PermissionsUtil.getDeniedPermissions(
+ mContext, permissionsToRequest);
+ if (deniedPermissions.isEmpty()) {
+ return;
+ }
+ // otherwise request the permissions.
+ int requestId = getNextRequestId();
+ String[] permissionsArray = deniedPermissions.toArray(
+ new String[deniedPermissions.size()]);
+
+ mRequestIdToCallback.put(requestId, callback);
+ if (activity != null) {
+ PermissionsUtil.requestPermissions(activity, requestId, permissionsArray);
+ } else {
+ PermissionsActivity.run(mContext, requestId, permissionsArray);
+ }
+ }
+
+ public synchronized void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ PermissionsResultCallback permissionsResultCallback = mRequestIdToCallback.get(requestCode);
+ mRequestIdToCallback.remove(requestCode);
+
+ boolean allGranted = PermissionsUtil.allGranted(grantResults);
+ permissionsResultCallback.onRequestPermissionsResult(allGranted);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/permissions/PermissionsUtil.java b/java/src/com/android/inputmethod/latin/permissions/PermissionsUtil.java
new file mode 100644
index 000000000..747f64f24
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/permissions/PermissionsUtil.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.inputmethod.latin.permissions;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class for permissions.
+ */
+public class PermissionsUtil {
+
+ /**
+ * Returns the list of permissions not granted from the given list of permissions.
+ * @param context Context
+ * @param permissions list of permissions to check.
+ * @return the list of permissions that do not have permission to use.
+ */
+ public static List<String> getDeniedPermissions(Context context,
+ String... permissions) {
+ final List<String> deniedPermissions = new ArrayList<>();
+ for (String permission : permissions) {
+ if (ContextCompat.checkSelfPermission(context, permission)
+ != PackageManager.PERMISSION_GRANTED) {
+ deniedPermissions.add(permission);
+ }
+ }
+ return deniedPermissions;
+ }
+
+ /**
+ * Uses the given activity and requests the user for permissions.
+ * @param activity activity to use.
+ * @param requestCode request code/id to use.
+ * @param permissions String array of permissions that needs to be requested.
+ */
+ public static void requestPermissions(Activity activity, int requestCode,
+ String[] permissions) {
+ ActivityCompat.requestPermissions(activity, permissions, requestCode);
+ }
+
+ /**
+ * Checks if all the permissions are granted.
+ */
+ public static boolean allGranted(@NonNull int[] grantResults) {
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Queries if al the permissions are granted for the given permission strings.
+ */
+ public static boolean checkAllPermissionsGranted(Context context, String... permissions) {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
+ // For all pre-M devices, we should have all the premissions granted on install.
+ return true;
+ }
+
+ for (String permission : permissions) {
+ if (ContextCompat.checkSelfPermission(context, permission)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java
index cb2097826..b39e6b477 100644
--- a/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java
@@ -19,6 +19,7 @@ package com.android.inputmethod.latin.settings;
import static com.android.inputmethod.latin.settings.LocalSettingsConstants.PREF_ACCOUNT_NAME;
import static com.android.inputmethod.latin.settings.LocalSettingsConstants.PREF_ENABLE_CLOUD_SYNC;
+import android.Manifest;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
@@ -40,6 +41,7 @@ import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.accounts.AccountStateChangedListener;
import com.android.inputmethod.latin.accounts.LoginAccountUtils;
import com.android.inputmethod.latin.define.ProductionFlags;
+import com.android.inputmethod.latin.permissions.PermissionsUtil;
import com.android.inputmethod.latin.utils.ManagedProfileUtils;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -254,11 +256,14 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
return;
}
- final String[] accountsForLogin =
- LoginAccountUtils.getAccountsForLogin(getActivity());
- final String currentAccount = getSignedInAccountName();
+ boolean hasAccountsPermission = PermissionsUtil.checkAllPermissionsGranted(
+ getActivity(), Manifest.permission.READ_CONTACTS);
- if (!mManagedProfileBeingDetected.get() &&
+ final String[] accountsForLogin = hasAccountsPermission ?
+ LoginAccountUtils.getAccountsForLogin(getActivity()) : new String[0];
+ final String currentAccount = hasAccountsPermission ? getSignedInAccountName() : null;
+
+ if (hasAccountsPermission && !mManagedProfileBeingDetected.get() &&
!mHasManagedProfile.get() && accountsForLogin.length > 0) {
// Sync can be used by user; enable all preferences.
enableSyncPreferences(accountsForLogin, currentAccount);
@@ -266,26 +271,35 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
// Sync cannot be used by user; disable all preferences.
disableSyncPreferences();
}
- refreshSyncSettingsMessaging(mManagedProfileBeingDetected.get(),
+ refreshSyncSettingsMessaging(hasAccountsPermission, mManagedProfileBeingDetected.get(),
mHasManagedProfile.get(), accountsForLogin.length > 0,
currentAccount);
}
/**
+ * @param hasAccountsPermission whether the app has the permission to read accounts.
* @param managedProfileBeingDetected whether we are in process of determining work profile.
* @param hasManagedProfile whether the device has work profile.
* @param hasAccountsForLogin whether the device has enough accounts for login.
* @param currentAccount the account currently selected in the application.
*/
- private void refreshSyncSettingsMessaging(boolean managedProfileBeingDetected,
- boolean hasManagedProfile, boolean hasAccountsForLogin, String currentAccount) {
+ private void refreshSyncSettingsMessaging(boolean hasAccountsPermission,
+ boolean managedProfileBeingDetected,
+ boolean hasManagedProfile,
+ boolean hasAccountsForLogin,
+ String currentAccount) {
if (!ProductionFlags.ENABLE_USER_HISTORY_DICTIONARY_SYNC) {
return;
}
- // If we are determining eligiblity, we show empty summaries.
- // Once we have some deterministic result, we set summaries based on different results.
- if (managedProfileBeingDetected) {
+ if (!hasAccountsPermission) {
+ mEnableSyncPreference.setChecked(false);
+ mEnableSyncPreference.setSummary(getString(R.string.cloud_sync_summary_disabled));
+ mAccountSwitcher.setSummary("");
+ return;
+ } else if (managedProfileBeingDetected) {
+ // If we are determining eligiblity, we show empty summaries.
+ // Once we have some deterministic result, we set summaries based on different results.
mEnableSyncPreference.setSummary("");
mAccountSwitcher.setSummary("");
} else if (hasManagedProfile) {
@@ -462,7 +476,7 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog,
- final int which) {
+ final int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
final Context context = getActivity();
final String[] accountsForLogin =
@@ -473,9 +487,9 @@ public final class AccountsSettingsFragment extends SubScreenFragment {
.show();
}
}
- })
- .setNegativeButton(R.string.cloud_sync_cancel, null)
- .create();
+ })
+ .setNegativeButton(R.string.cloud_sync_cancel, null)
+ .create();
optInDialog.setOnShowListener(this);
optInDialog.show();
}
diff --git a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
index f2e1aed4c..a6fb7f1f1 100644
--- a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
@@ -26,6 +26,7 @@ import android.preference.Preference;
import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SystemBroadcastReceiver;
import com.android.inputmethod.latin.define.ProductionFlags;
/**
@@ -106,6 +107,8 @@ public final class AdvancedSettingsFragment extends SubScreenFragment {
if (key.equals(Settings.PREF_POPUP_ON)) {
setPreferenceEnabled(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
Settings.readKeyPreviewPopupEnabled(prefs, res));
+ } else if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) {
+ SystemBroadcastReceiver.toggleAppIcon(getActivity());
}
updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
refreshEnablingsOfKeypressSoundAndVibrationSettings();
diff --git a/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java
index aa73a9a83..dfe899ece 100644
--- a/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java
@@ -16,20 +16,33 @@
package com.android.inputmethod.latin.settings;
+import android.Manifest;
+import android.app.Activity;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.Bundle;
import android.preference.Preference;
+import android.preference.SwitchPreference;
+import android.text.TextUtils;
import com.android.inputmethod.dictionarypack.DictionarySettingsActivity;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.permissions.PermissionsManager;
+import com.android.inputmethod.latin.permissions.PermissionsUtil;
+import com.android.inputmethod.latin.userdictionary.UserDictionaryList;
+import com.android.inputmethod.latin.userdictionary.UserDictionarySettings;
+
+import java.util.TreeSet;
/**
* "Text correction" settings sub screen.
*
* This settings sub screen handles the following text correction preferences.
+ * - Personal dictionary
* - Add-on dictionaries
* - Block offensive words
* - Auto-correction
@@ -38,12 +51,17 @@ import com.android.inputmethod.latin.R;
* - Suggest Contact names
* - Next-word suggestions
*/
-public final class CorrectionSettingsFragment extends SubScreenFragment {
+public final class CorrectionSettingsFragment extends SubScreenFragment
+ implements SharedPreferences.OnSharedPreferenceChangeListener,
+ PermissionsManager.PermissionsResultCallback {
+
private static final boolean DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS = false;
private static final boolean USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS =
DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS
|| Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2;
+ private SwitchPreference mUseContactsPreference;
+
@Override
public void onCreate(final Bundle icicle) {
super.onCreate(icicle);
@@ -59,5 +77,76 @@ public final class CorrectionSettingsFragment extends SubScreenFragment {
if (0 >= number) {
removePreference(Settings.PREF_CONFIGURE_DICTIONARIES_KEY);
}
+
+ final Preference editPersonalDictionary =
+ findPreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY);
+ final Intent editPersonalDictionaryIntent = editPersonalDictionary.getIntent();
+ final ResolveInfo ri = USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS ? null
+ : pm.resolveActivity(
+ editPersonalDictionaryIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (ri == null) {
+ overwriteUserDictionaryPreference(editPersonalDictionary);
+ }
+
+ mUseContactsPreference = (SwitchPreference) findPreference(Settings.PREF_KEY_USE_CONTACTS_DICT);
+ turnOffUseContactsIfNoPermission();
+ }
+
+ private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) {
+ final Activity activity = getActivity();
+ final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(activity);
+ if (null == localeList) {
+ // The locale list is null if and only if the user dictionary service is
+ // not present or disabled. In this case we need to remove the preference.
+ getPreferenceScreen().removePreference(userDictionaryPreference);
+ } else if (localeList.size() <= 1) {
+ userDictionaryPreference.setFragment(UserDictionarySettings.class.getName());
+ // If the size of localeList is 0, we don't set the locale parameter in the
+ // extras. This will be interpreted by the UserDictionarySettings class as
+ // meaning "the current locale".
+ // Note that with the current code for UserDictionaryList#getUserDictionaryLocalesSet()
+ // the locale list always has at least one element, since it always includes the current
+ // locale explicitly. @see UserDictionaryList.getUserDictionaryLocalesSet().
+ if (localeList.size() == 1) {
+ final String locale = (String)localeList.toArray()[0];
+ userDictionaryPreference.getExtras().putString("locale", locale);
+ }
+ } else {
+ userDictionaryPreference.setFragment(UserDictionaryList.class.getName());
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
+ if (!TextUtils.equals(key, Settings.PREF_KEY_USE_CONTACTS_DICT)) {
+ return;
+ }
+ if (!sharedPreferences.getBoolean(key, false)) {
+ // don't care if the preference is turned off.
+ return;
+ }
+
+ // Check for permissions.
+ if (PermissionsUtil.checkAllPermissionsGranted(
+ getActivity() /* context */, Manifest.permission.READ_CONTACTS)) {
+ return; // all permissions granted, no need to request permissions.
+ }
+
+ PermissionsManager.get(getActivity() /* context */).requestPermissions(
+ this /* PermissionsResultCallback */,
+ getActivity() /* activity */,
+ Manifest.permission.READ_CONTACTS);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(boolean allGranted) {
+ turnOffUseContactsIfNoPermission();
+ }
+
+ private void turnOffUseContactsIfNoPermission() {
+ if (!PermissionsUtil.checkAllPermissionsGranted(
+ getActivity(), Manifest.permission.READ_CONTACTS)) {
+ mUseContactsPreference.setChecked(false);
+ }
}
}
diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java
index 694f43d3f..940f1bdfc 100644
--- a/java/src/com/android/inputmethod/latin/settings/Settings.java
+++ b/java/src/com/android/inputmethod/latin/settings/Settings.java
@@ -56,6 +56,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
// PREF_VOICE_MODE_OBSOLETE is obsolete. Use PREF_VOICE_INPUT_KEY instead.
public static final String PREF_VOICE_MODE_OBSOLETE = "voice_mode";
public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key";
+ public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary";
public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key";
// PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE is obsolete. Use PREF_AUTO_CORRECTION instead.
public static final String PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE =
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java b/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java
index 9975277e4..a7d157a6b 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java
@@ -16,6 +16,7 @@
package com.android.inputmethod.latin.settings;
+import com.android.inputmethod.latin.permissions.PermissionsManager;
import com.android.inputmethod.latin.utils.FragmentUtils;
import com.android.inputmethod.latin.utils.StatsUtils;
import com.android.inputmethod.latin.utils.StatsUtilsManager;
@@ -24,9 +25,11 @@ import android.app.ActionBar;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceActivity;
+import android.support.v4.app.ActivityCompat;
import android.view.MenuItem;
-public final class SettingsActivity extends PreferenceActivity {
+public final class SettingsActivity extends PreferenceActivity
+ implements ActivityCompat.OnRequestPermissionsResultCallback {
private static final String DEFAULT_FRAGMENT = SettingsFragment.class.getName();
public static final String EXTRA_SHOW_HOME_AS_UP = "show_home_as_up";
@@ -77,4 +80,9 @@ public final class SettingsActivity extends PreferenceActivity {
public boolean isValidFragment(final String fragmentName) {
return FragmentUtils.isValidFragment(fragmentName);
}
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ PermissionsManager.get(this).onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java b/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java
index 240f8f89b..5994a76df 100644
--- a/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java
@@ -32,7 +32,7 @@ import android.util.Log;
* A base abstract class for a {@link PreferenceFragment} that implements a nested
* {@link PreferenceScreen} of the main preference screen.
*/
-abstract class SubScreenFragment extends PreferenceFragment
+public abstract class SubScreenFragment extends PreferenceFragment
implements OnSharedPreferenceChangeListener {
private OnSharedPreferenceChangeListener mSharedPreferenceChangeListener;
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
index 2c690aea7..c7622e7a1 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
@@ -84,8 +84,7 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck
if (TextUtils.isEmpty(splitText)) {
continue;
}
- if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString(), ngramContext)
- == null) {
+ if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString()) == null) {
continue;
}
final int newLength = splitText.length();
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index 1322ce240..9223923a7 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -71,30 +71,26 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
}
protected static final class SuggestionsCache {
- private static final char CHAR_DELIMITER = '\uFFFC';
private static final int MAX_CACHE_SIZE = 50;
private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
new LruCache<>(MAX_CACHE_SIZE);
- private static String generateKey(final String query, final NgramContext ngramContext) {
- if (TextUtils.isEmpty(query) || !ngramContext.isValid()) {
- return query;
- }
- return query + CHAR_DELIMITER + ngramContext;
+ private static String generateKey(final String query) {
+ return query + "";
}
- public SuggestionsParams getSuggestionsFromCache(String query,
- final NgramContext ngramContext) {
- return mUnigramSuggestionsInfoCache.get(generateKey(query, ngramContext));
+ public SuggestionsParams getSuggestionsFromCache(final String query) {
+ return mUnigramSuggestionsInfoCache.get(query);
}
- public void putSuggestionsToCache(final String query, final NgramContext ngramContext,
- final String[] suggestions, final int flags) {
+ public void putSuggestionsToCache(
+ final String query, final String[] suggestions, final int flags) {
if (suggestions == null || TextUtils.isEmpty(query)) {
return;
}
mUnigramSuggestionsInfoCache.put(
- generateKey(query, ngramContext), new SuggestionsParams(suggestions, flags));
+ generateKey(query),
+ new SuggestionsParams(suggestions, flags));
}
public void clearCache() {
@@ -232,16 +228,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
AndroidSpellCheckerService.SINGLE_QUOTE).
replaceAll("^" + quotesRegexp, "").
replaceAll(quotesRegexp + "$", "");
- final SuggestionsParams cachedSuggestionsParams =
- mSuggestionsCache.getSuggestionsFromCache(text, ngramContext);
-
- if (cachedSuggestionsParams != null) {
- Log.d(TAG, "onGetSuggestionsInternal() : Cache hit for [" + text + "]");
- return new SuggestionsInfo(
- cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
- }
- // If spell checking is impossible, return early.
if (!mService.hasMainDictionaryForLocale(mLocale)) {
return AndroidSpellCheckerService.getNotInDictEmptySuggestions(
false /* reportAsTypo */);
@@ -329,8 +316,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
.getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
: 0);
final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
- mSuggestionsCache.putSuggestionsToCache(text, ngramContext, result.mSuggestions,
- flags);
+ mSuggestionsCache.putSuggestionsToCache(text, result.mSuggestions, flags);
return retval;
} catch (RuntimeException e) {
// Don't kill the keyboard if there is a bug in the spell checker
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
index 294666b8b..356d9d021 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
@@ -16,6 +16,7 @@
package com.android.inputmethod.latin.spellcheck;
+import com.android.inputmethod.latin.permissions.PermissionsManager;
import com.android.inputmethod.latin.utils.FragmentUtils;
import android.annotation.TargetApi;
@@ -23,11 +24,13 @@ import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceActivity;
+import android.support.v4.app.ActivityCompat;
/**
* Spell checker preference screen.
*/
-public final class SpellCheckerSettingsActivity extends PreferenceActivity {
+public final class SpellCheckerSettingsActivity extends PreferenceActivity
+ implements ActivityCompat.OnRequestPermissionsResultCallback {
private static final String DEFAULT_FRAGMENT = SpellCheckerSettingsFragment.class.getName();
@Override
@@ -48,4 +51,11 @@ public final class SpellCheckerSettingsActivity extends PreferenceActivity {
public boolean isValidFragment(String fragmentName) {
return FragmentUtils.isValidFragment(fragmentName);
}
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ PermissionsManager.get(this).onRequestPermissionsResult(
+ requestCode, permissions, grantResults);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
index 6850e9b58..12005c25e 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
@@ -16,18 +16,31 @@
package com.android.inputmethod.latin.spellcheck;
+import android.Manifest;
+import android.content.SharedPreferences;
import android.os.Bundle;
-import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.text.TextUtils;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.permissions.PermissionsManager;
+import com.android.inputmethod.latin.permissions.PermissionsUtil;
+import com.android.inputmethod.latin.settings.SubScreenFragment;
import com.android.inputmethod.latin.settings.TwoStatePreferenceHelper;
import com.android.inputmethod.latin.utils.ApplicationUtils;
+import static com.android.inputmethod.latin.permissions.PermissionsManager.get;
+
/**
* Preference screen.
*/
-public final class SpellCheckerSettingsFragment extends PreferenceFragment {
+public final class SpellCheckerSettingsFragment extends SubScreenFragment
+ implements SharedPreferences.OnSharedPreferenceChangeListener,
+ PermissionsManager.PermissionsResultCallback {
+
+ private SwitchPreference mLookupContactsPreference;
+
@Override
public void onActivityCreated(final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
@@ -36,5 +49,42 @@ public final class SpellCheckerSettingsFragment extends PreferenceFragment {
preferenceScreen.setTitle(ApplicationUtils.getActivityTitleResId(
getActivity(), SpellCheckerSettingsActivity.class));
TwoStatePreferenceHelper.replaceCheckBoxPreferencesBySwitchPreferences(preferenceScreen);
+
+ mLookupContactsPreference = (SwitchPreference) findPreference(
+ AndroidSpellCheckerService.PREF_USE_CONTACTS_KEY);
+ turnOffLookupContactsIfNoPermission();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ if (!TextUtils.equals(key, AndroidSpellCheckerService.PREF_USE_CONTACTS_KEY)) {
+ return;
+ }
+
+ if (!sharedPreferences.getBoolean(key, false)) {
+ // don't care if the preference is turned off.
+ return;
+ }
+
+ // Check for permissions.
+ if (PermissionsUtil.checkAllPermissionsGranted(
+ getActivity() /* context */, Manifest.permission.READ_CONTACTS)) {
+ return; // all permissions granted, no need to request permissions.
+ }
+
+ get(getActivity() /* context */).requestPermissions(this /* PermissionsResultCallback */,
+ getActivity() /* activity */, Manifest.permission.READ_CONTACTS);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(boolean allGranted) {
+ turnOffLookupContactsIfNoPermission();
+ }
+
+ private void turnOffLookupContactsIfNoPermission() {
+ if (!PermissionsUtil.checkAllPermissionsGranted(
+ getActivity(), Manifest.permission.READ_CONTACTS)) {
+ mLookupContactsPreference.setChecked(false);
+ }
}
}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index d8926ffba..9577d0913 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -191,7 +191,9 @@ final class SuggestionStripLayoutHelper {
final Bitmap buffer = Bitmap.createBitmap(width, (height * 3 / 2), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(buffer);
canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint);
- return new BitmapDrawable(res, buffer);
+ BitmapDrawable bitmapDrawable = new BitmapDrawable(res, buffer);
+ bitmapDrawable.setTargetDensity(canvas);
+ return bitmapDrawable;
}
private CharSequence getStyledSuggestedWord(final SuggestedWords suggestedWords,
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index 7dd0f03df..c1d1fad68 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -220,7 +220,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
if (getWidth() <= 0) {
return false;
}
- final String importantNoticeTitle = ImportantNoticeUtils.getNextImportantNoticeTitle(
+ final String importantNoticeTitle = ImportantNoticeUtils.getSuggestContactsNoticeTitle(
getContext());
if (TextUtils.isEmpty(importantNoticeTitle)) {
return false;
diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
index cfa977a46..cea2e13b1 100644
--- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
@@ -30,6 +30,7 @@ import com.android.inputmethod.latin.AssetFileAddress;
import com.android.inputmethod.latin.BinaryDictionaryGetter;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.RichInputMethodManager;
+import com.android.inputmethod.latin.common.FileUtils;
import com.android.inputmethod.latin.common.LocaleUtils;
import com.android.inputmethod.latin.define.DecoderSpecificConstants;
import com.android.inputmethod.latin.makedict.DictionaryHeader;
@@ -53,7 +54,7 @@ import javax.annotation.Nullable;
*/
public class DictionaryInfoUtils {
private static final String TAG = DictionaryInfoUtils.class.getSimpleName();
- private static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName();
+ public static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName();
private static final String DEFAULT_MAIN_DICT = "main";
private static final String MAIN_DICT_PREFIX = "main_";
private static final String DECODER_DICT_SUFFIX = DecoderSpecificConstants.DECODER_DICT_SUFFIX;
@@ -102,6 +103,13 @@ public class DictionaryInfoUtils {
values.put(VERSION_COLUMN, mVersion);
return values;
}
+
+ @Override
+ public String toString() {
+ return "DictionaryInfo : Id = '" + mId
+ + "' : Locale=" + mLocale
+ + " : Version=" + mVersion;
+ }
}
private DictionaryInfoUtils() {
@@ -153,6 +161,13 @@ public class DictionaryInfoUtils {
}
/**
+ * Helper method to get the top level cache directory.
+ */
+ public static String getWordListStagingDirectory(final Context context) {
+ return context.getFilesDir() + File.separator + "staging";
+ }
+
+ /**
* Helper method to get the top level temp directory.
*/
public static String getWordListTempDirectory(final Context context) {
@@ -188,6 +203,10 @@ public class DictionaryInfoUtils {
return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles();
}
+ public static File[] getStagingDirectoryList(final Context context) {
+ return new File(DictionaryInfoUtils.getWordListStagingDirectory(context)).listFiles();
+ }
+
@Nullable
public static File[] getUnusedDictionaryList(final Context context) {
return context.getFilesDir().listFiles(new FilenameFilter() {
@@ -221,7 +240,7 @@ public class DictionaryInfoUtils {
/**
* Find out the cache directory associated with a specific locale.
*/
- private static String getCacheDirectoryForLocale(final String locale, final Context context) {
+ public static String getCacheDirectoryForLocale(final String locale, final Context context) {
final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale);
final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator
+ relativeDirectoryName;
@@ -254,6 +273,55 @@ public class DictionaryInfoUtils {
return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
}
+ public static String getStagingFileName(String id, String locale, Context context) {
+ final String stagingDirectory = getWordListStagingDirectory(context);
+ // create the directory if it does not exist.
+ final File directory = new File(stagingDirectory);
+ if (!directory.exists()) {
+ if (!directory.mkdirs()) {
+ Log.e(TAG, "Could not create the staging directory.");
+ }
+ }
+ // e.g. id="main:en_in", locale ="en_IN"
+ final String fileName = replaceFileNameDangerousCharacters(
+ locale + TEMP_DICT_FILE_SUB + id);
+ return stagingDirectory + File.separator + fileName;
+ }
+
+ public static void moveStagingFilesIfExists(Context context) {
+ final File[] stagingFiles = DictionaryInfoUtils.getStagingDirectoryList(context);
+ if (stagingFiles != null && stagingFiles.length > 0) {
+ for (final File stagingFile : stagingFiles) {
+ final String fileName = stagingFile.getName();
+ final int index = fileName.indexOf(TEMP_DICT_FILE_SUB);
+ if (index == -1) {
+ // This should never happen.
+ Log.e(TAG, "Staging file does not have ___ substring.");
+ continue;
+ }
+ final String[] localeAndFileId = fileName.split(TEMP_DICT_FILE_SUB);
+ if (localeAndFileId.length != 2) {
+ Log.e(TAG, String.format("malformed staging file %s. Deleting.",
+ stagingFile.getAbsoluteFile()));
+ stagingFile.delete();
+ continue;
+ }
+
+ final String locale = localeAndFileId[0];
+ // already escaped while moving to staging.
+ final String fileId = localeAndFileId[1];
+ final String cacheDirectoryForLocale = getCacheDirectoryForLocale(locale, context);
+ final String cacheFilename = cacheDirectoryForLocale + File.separator + fileId;
+ final File cacheFile = new File(cacheFilename);
+ // move the staging file to cache file.
+ if (!FileUtils.renameTo(stagingFile, cacheFile)) {
+ Log.e(TAG, String.format("Failed to rename from %s to %s.",
+ stagingFile.getAbsoluteFile(), cacheFile.getAbsoluteFile()));
+ }
+ }
+ }
+ }
+
public static boolean isMainWordListId(final String id) {
final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
// An id is supposed to be in format category:locale, so splitting on the separator
diff --git a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
index df0cd8437..cea263b3b 100644
--- a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java
@@ -16,6 +16,7 @@
package com.android.inputmethod.latin.utils;
+import android.Manifest;
import android.content.Context;
import android.content.SharedPreferences;
import android.provider.Settings;
@@ -25,6 +26,7 @@ import android.util.Log;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.permissions.PermissionsUtil;
import com.android.inputmethod.latin.settings.SettingsValues;
import java.util.concurrent.TimeUnit;
@@ -35,14 +37,14 @@ public final class ImportantNoticeUtils {
// {@link SharedPreferences} name to save the last important notice version that has been
// displayed to users.
private static final String PREFERENCE_NAME = "important_notice_pref";
+
+ private static final String KEY_SUGGEST_CONTACTS_NOTICE = "important_notice_suggest_contacts";
+
@UsedForTesting
- static final String KEY_IMPORTANT_NOTICE_VERSION = "important_notice_version";
- @UsedForTesting
- static final String KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE =
- "timestamp_of_first_important_notice";
+ static final String KEY_TIMESTAMP_OF_CONTACTS_NOTICE = "timestamp_of_suggest_contacts_notice";
+
@UsedForTesting
static final long TIMEOUT_OF_IMPORTANT_NOTICE = TimeUnit.HOURS.toMillis(23);
- public static final int VERSION_TO_ENABLE_PERSONALIZED_SUGGESTIONS = 1;
// Copy of the hidden {@link Settings.Secure#USER_SETUP_COMPLETE} settings key.
// The value is zero until each multiuser completes system setup wizard.
@@ -73,87 +75,66 @@ public final class ImportantNoticeUtils {
}
@UsedForTesting
- static int getCurrentImportantNoticeVersion(final Context context) {
- return context.getResources().getInteger(R.integer.config_important_notice_version);
- }
-
- @UsedForTesting
- static int getLastImportantNoticeVersion(final Context context) {
- return getImportantNoticePreferences(context).getInt(KEY_IMPORTANT_NOTICE_VERSION, 0);
- }
-
- public static int getNextImportantNoticeVersion(final Context context) {
- return getLastImportantNoticeVersion(context) + 1;
- }
-
- @UsedForTesting
- static boolean hasNewImportantNotice(final Context context) {
- final int lastVersion = getLastImportantNoticeVersion(context);
- return getCurrentImportantNoticeVersion(context) > lastVersion;
- }
-
- @UsedForTesting
- static boolean hasTimeoutPassed(final Context context, final long currentTimeInMillis) {
- final SharedPreferences prefs = getImportantNoticePreferences(context);
- if (!prefs.contains(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)) {
- prefs.edit()
- .putLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE, currentTimeInMillis)
- .apply();
- }
- final long firstDisplayTimeInMillis = prefs.getLong(
- KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE, currentTimeInMillis);
- final long elapsedTime = currentTimeInMillis - firstDisplayTimeInMillis;
- return elapsedTime >= TIMEOUT_OF_IMPORTANT_NOTICE;
+ static boolean hasContactsNoticeShown(final Context context) {
+ return getImportantNoticePreferences(context).getBoolean(
+ KEY_SUGGEST_CONTACTS_NOTICE, false);
}
public static boolean shouldShowImportantNotice(final Context context,
final SettingsValues settingsValues) {
- // Check to see whether personalization is enabled by the user.
- if (!settingsValues.isPersonalizationEnabled()) {
+ // Check to see whether "Use Contacts" is enabled by the user.
+ if (!settingsValues.mUseContactsDict) {
return false;
}
- if (!hasNewImportantNotice(context)) {
+
+ if (hasContactsNoticeShown(context)) {
+ return false;
+ }
+
+ // Don't show the dialog if we have all the permissions.
+ if (PermissionsUtil.checkAllPermissionsGranted(
+ context, Manifest.permission.READ_CONTACTS)) {
return false;
}
- final String importantNoticeTitle = getNextImportantNoticeTitle(context);
+
+ final String importantNoticeTitle = getSuggestContactsNoticeTitle(context);
if (TextUtils.isEmpty(importantNoticeTitle)) {
return false;
}
if (isInSystemSetupWizard(context)) {
return false;
}
- if (hasTimeoutPassed(context, System.currentTimeMillis())) {
- updateLastImportantNoticeVersion(context);
+ if (hasContactsNoticeTimeoutPassed(context, System.currentTimeMillis())) {
+ updateContactsNoticeShown(context);
return false;
}
return true;
}
- public static void updateLastImportantNoticeVersion(final Context context) {
- getImportantNoticePreferences(context)
- .edit()
- .putInt(KEY_IMPORTANT_NOTICE_VERSION, getNextImportantNoticeVersion(context))
- .remove(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)
- .apply();
+ public static String getSuggestContactsNoticeTitle(final Context context) {
+ return context.getResources().getString(R.string.important_notice_suggest_contact_names);
}
- public static String getNextImportantNoticeTitle(final Context context) {
- final int nextVersion = getNextImportantNoticeVersion(context);
- final String[] importantNoticeTitleArray = context.getResources().getStringArray(
- R.array.important_notice_title_array);
- if (nextVersion > 0 && nextVersion < importantNoticeTitleArray.length) {
- return importantNoticeTitleArray[nextVersion];
+ @UsedForTesting
+ static boolean hasContactsNoticeTimeoutPassed(
+ final Context context, final long currentTimeInMillis) {
+ final SharedPreferences prefs = getImportantNoticePreferences(context);
+ if (!prefs.contains(KEY_TIMESTAMP_OF_CONTACTS_NOTICE)) {
+ prefs.edit()
+ .putLong(KEY_TIMESTAMP_OF_CONTACTS_NOTICE, currentTimeInMillis)
+ .apply();
}
- return null;
+ final long firstDisplayTimeInMillis = prefs.getLong(
+ KEY_TIMESTAMP_OF_CONTACTS_NOTICE, currentTimeInMillis);
+ final long elapsedTime = currentTimeInMillis - firstDisplayTimeInMillis;
+ return elapsedTime >= TIMEOUT_OF_IMPORTANT_NOTICE;
}
- public static String getNextImportantNoticeContents(final Context context) {
- final int nextVersion = getNextImportantNoticeVersion(context);
- final String[] importantNoticeContentsArray = context.getResources().getStringArray(
- R.array.important_notice_contents_array);
- if (nextVersion > 0 && nextVersion < importantNoticeContentsArray.length) {
- return importantNoticeContentsArray[nextVersion];
- }
- return null;
+ public static void updateContactsNoticeShown(final Context context) {
+ getImportantNoticePreferences(context)
+ .edit()
+ .putBoolean(KEY_SUGGEST_CONTACTS_NOTICE, true)
+ .remove(KEY_TIMESTAMP_OF_CONTACTS_NOTICE)
+ .apply();
}
}