diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml index f58c401c7..8882cdea5 100644 --- a/java/AndroidManifest.xml +++ b/java/AndroidManifest.xml @@ -18,7 +18,7 @@ coreApp="true" package="com.android.inputmethod.latin"> - + @@ -77,6 +77,13 @@ + + + - - - - - - - - - - - - diff --git a/java/res/values/important_notice_strings.xml b/java/res/values/important_notice_strings.xml new file mode 100644 index 000000000..b1f3fc137 --- /dev/null +++ b/java/res/values/important_notice_strings.xml @@ -0,0 +1,21 @@ + + + + + + Suggest contact names? Touch for info. + \ No newline at end of file diff --git a/java/res/values/strings-config-important-notice.xml b/java/res/values/strings-config-important-notice.xml deleted file mode 100644 index aa3cd109c..000000000 --- a/java/res/values/strings-config-important-notice.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - 0 - - Learn from your communications and typed data to improve suggestions - 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 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/DictionaryFacilitatorImpl.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java index c7115c9d9..b435de867 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin; +import android.Manifest; import android.content.Context; import android.text.TextUtils; import android.util.Log; @@ -28,6 +29,7 @@ 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; @@ -287,7 +289,11 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { // TODO: Make subDictTypesToUse configurable by resource or a static final list. final HashSet 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) { 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 089670ebf..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; @@ -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/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 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 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 getDeniedPermissions(Context context, + String... permissions) { + final List 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/CorrectionSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java index d28e703fe..dfe899ece 100644 --- a/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/CorrectionSettingsFragment.java @@ -16,17 +16,23 @@ 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; @@ -45,12 +51,17 @@ import java.util.TreeSet; * - 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); @@ -76,6 +87,9 @@ public final class CorrectionSettingsFragment extends SubScreenFragment { if (ri == null) { overwriteUserDictionaryPreference(editPersonalDictionary); } + + mUseContactsPreference = (SwitchPreference) findPreference(Settings.PREF_KEY_USE_CONTACTS_DICT); + turnOffUseContactsIfNoPermission(); } private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) { @@ -101,4 +115,38 @@ public final class CorrectionSettingsFragment extends SubScreenFragment { 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/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/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/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; } - final String importantNoticeTitle = getNextImportantNoticeTitle(context); + + // Don't show the dialog if we have all the permissions. + if (PermissionsUtil.checkAllPermissionsGranted( + context, Manifest.permission.READ_CONTACTS)) { + return false; + } + + 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) { + public static String getSuggestContactsNoticeTitle(final Context context) { + return context.getResources().getString(R.string.important_notice_suggest_contact_names); + } + + @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(); + } + final long firstDisplayTimeInMillis = prefs.getLong( + KEY_TIMESTAMP_OF_CONTACTS_NOTICE, currentTimeInMillis); + final long elapsedTime = currentTimeInMillis - firstDisplayTimeInMillis; + return elapsedTime >= TIMEOUT_OF_IMPORTANT_NOTICE; + } + + public static void updateContactsNoticeShown(final Context context) { getImportantNoticePreferences(context) .edit() - .putInt(KEY_IMPORTANT_NOTICE_VERSION, getNextImportantNoticeVersion(context)) - .remove(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE) + .putBoolean(KEY_SUGGEST_CONTACTS_NOTICE, true) + .remove(KEY_TIMESTAMP_OF_CONTACTS_NOTICE) .apply(); } - - 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]; - } - return null; - } - - 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; - } } diff --git a/tests/src/com/android/inputmethod/latin/utils/ImportantNoticeUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/ImportantNoticeUtilsTests.java index e361c7704..df0180729 100644 --- a/tests/src/com/android/inputmethod/latin/utils/ImportantNoticeUtilsTests.java +++ b/tests/src/com/android/inputmethod/latin/utils/ImportantNoticeUtilsTests.java @@ -16,8 +16,7 @@ package com.android.inputmethod.latin.utils; -import static com.android.inputmethod.latin.utils.ImportantNoticeUtils.KEY_IMPORTANT_NOTICE_VERSION; -import static com.android.inputmethod.latin.utils.ImportantNoticeUtils.KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE; +import static com.android.inputmethod.latin.utils.ImportantNoticeUtils.KEY_TIMESTAMP_OF_CONTACTS_NOTICE; import static org.mockito.Mockito.when; import android.content.Context; @@ -35,8 +34,6 @@ import java.util.concurrent.TimeUnit; @MediumTest public class ImportantNoticeUtilsTests extends AndroidTestCase { - // This should be aligned with R.integer.config_important_notice_version. - private static final int CURRENT_IMPORTANT_NOTICE_VERSION = 1; private ImportantNoticePreferences mImportantNoticePreferences; @@ -87,18 +84,15 @@ public class ImportantNoticeUtilsTests extends AndroidTestCase { } public void save() { - mVersion = getInt(KEY_IMPORTANT_NOTICE_VERSION); - mLastTime = getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE); + mLastTime = getLong(KEY_TIMESTAMP_OF_CONTACTS_NOTICE); } public void restore() { - putInt(KEY_IMPORTANT_NOTICE_VERSION, mVersion); - putLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE, mLastTime); + putLong(KEY_TIMESTAMP_OF_CONTACTS_NOTICE, mLastTime); } public void clear() { - removePreference(KEY_IMPORTANT_NOTICE_VERSION); - removePreference(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE); + removePreference(KEY_TIMESTAMP_OF_CONTACTS_NOTICE); } } @@ -117,141 +111,6 @@ public class ImportantNoticeUtilsTests extends AndroidTestCase { mImportantNoticePreferences.restore(); } - public void testCurrentVersion() { - assertEquals("Current version", CURRENT_IMPORTANT_NOTICE_VERSION, - ImportantNoticeUtils.getCurrentImportantNoticeVersion(getContext())); - } - - public void testStateAfterFreshInstall() { - mImportantNoticePreferences.clear(); - - // Check internal state of {@link ImportantNoticeUtils.shouldShowImportantNotice(Context)} - // after fresh install. - assertEquals("Has new important notice after fresh install", true, - ImportantNoticeUtils.hasNewImportantNotice(getContext())); - assertEquals("Next important notice title after fresh install", false, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeTitle(getContext()))); - assertEquals("Is in system setup wizard after fresh install", false, - ImportantNoticeUtils.isInSystemSetupWizard(getContext())); - final long currentTimeMillis = System.currentTimeMillis(); - assertEquals("Has timeout passed after fresh install", false, - ImportantNoticeUtils.hasTimeoutPassed(getContext(), currentTimeMillis)); - assertEquals("Timestamp of first important notice after fresh install", - (Long)currentTimeMillis, - mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)); - - assertEquals("Current boolean before update", true, - ImportantNoticeUtils.shouldShowImportantNotice(getContext(), mMockSettingsValues)); - } - - public void testUpdateVersion() { - mImportantNoticePreferences.clear(); - - assertEquals("Current boolean before update", true, - ImportantNoticeUtils.shouldShowImportantNotice(getContext(), mMockSettingsValues)); - assertEquals("Last version before update", 0, - ImportantNoticeUtils.getLastImportantNoticeVersion(getContext())); - assertEquals("Next version before update ", 1, - ImportantNoticeUtils.getNextImportantNoticeVersion(getContext())); - assertEquals("Current title before update", false, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeTitle(getContext()))); - assertEquals("Current contents before update", false, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeContents(getContext()))); - - ImportantNoticeUtils.updateLastImportantNoticeVersion(getContext()); - - assertEquals("Current boolean after update", false, - ImportantNoticeUtils.shouldShowImportantNotice(getContext(), mMockSettingsValues)); - assertEquals("Last version after update", 1, - ImportantNoticeUtils.getLastImportantNoticeVersion(getContext())); - assertEquals("Next version after update", 2, - ImportantNoticeUtils.getNextImportantNoticeVersion(getContext())); - assertEquals("Current title after update", true, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeTitle(getContext()))); - assertEquals("Current contents after update", true, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeContents(getContext()))); - } - - private static void sleep(final long millseconds) { - try { Thread.sleep(millseconds); } catch (final Exception e) { /* ignore */ } - } - - public void testTimeout() { - final long lastTime = System.currentTimeMillis() - - ImportantNoticeUtils.TIMEOUT_OF_IMPORTANT_NOTICE - + TimeUnit.MILLISECONDS.toMillis(1000); - mImportantNoticePreferences.clear(); - assertEquals("Before set last time", null, - mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)); - assertEquals("Set last time", false, - ImportantNoticeUtils.hasTimeoutPassed(getContext(), lastTime)); - assertEquals("After set last time", (Long)lastTime, - mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)); - - // Call {@link ImportantNoticeUtils#shouldShowImportantNotice(Context)} before timeout. - assertEquals("Current boolean before timeout 1", true, - ImportantNoticeUtils.shouldShowImportantNotice(getContext(), mMockSettingsValues)); - assertEquals("Last version before timeout 1", 0, - ImportantNoticeUtils.getLastImportantNoticeVersion(getContext())); - assertEquals("Next version before timeout 1", 1, - ImportantNoticeUtils.getNextImportantNoticeVersion(getContext())); - assertEquals("Timestamp of first important notice before timeout 1", (Long)lastTime, - mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)); - assertEquals("Current title before timeout 1", false, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeTitle(getContext()))); - assertEquals("Current contents before timeout 1", false, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeContents(getContext()))); - - sleep(TimeUnit.MILLISECONDS.toMillis(600)); - - // Call {@link ImportantNoticeUtils#shouldShowImportantNotice(Context)} before timeout - // again. - assertEquals("Current boolean before timeout 2", true, - ImportantNoticeUtils.shouldShowImportantNotice(getContext(), mMockSettingsValues)); - assertEquals("Last version before timeout 2", 0, - ImportantNoticeUtils.getLastImportantNoticeVersion(getContext())); - assertEquals("Next version before timeout 2", 1, - ImportantNoticeUtils.getNextImportantNoticeVersion(getContext())); - assertEquals("Timestamp of first important notice before timeout 2", (Long)lastTime, - mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)); - assertEquals("Current title before timeout 2", false, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeTitle(getContext()))); - assertEquals("Current contents before timeout 2", false, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeContents(getContext()))); - - sleep(TimeUnit.MILLISECONDS.toMillis(600)); - - // Call {@link ImportantNoticeUtils#shouldShowImportantNotice(Context)} after timeout. - assertEquals("Current boolean after timeout 1", false, - ImportantNoticeUtils.shouldShowImportantNotice(getContext(), mMockSettingsValues)); - assertEquals("Last version after timeout 1", 1, - ImportantNoticeUtils.getLastImportantNoticeVersion(getContext())); - assertEquals("Next version after timeout 1", 2, - ImportantNoticeUtils.getNextImportantNoticeVersion(getContext())); - assertEquals("Timestamp of first important notice after timeout 1", null, - mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)); - assertEquals("Current title after timeout 1", true, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeTitle(getContext()))); - assertEquals("Current contents after timeout 1", true, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeContents(getContext()))); - - sleep(TimeUnit.MILLISECONDS.toMillis(600)); - - // Call {@link ImportantNoticeUtils#shouldShowImportantNotice(Context)} after timeout again. - assertEquals("Current boolean after timeout 2", false, - ImportantNoticeUtils.shouldShowImportantNotice(getContext(), mMockSettingsValues)); - assertEquals("Last version after timeout 2", 1, - ImportantNoticeUtils.getLastImportantNoticeVersion(getContext())); - assertEquals("Next version after timeout 2", 2, - ImportantNoticeUtils.getNextImportantNoticeVersion(getContext())); - assertEquals("Timestamp of first important notice after timeout 2", null, - mImportantNoticePreferences.getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)); - assertEquals("Current title after timeout 2", true, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeTitle(getContext()))); - assertEquals("Current contents after timeout 2", true, TextUtils.isEmpty( - ImportantNoticeUtils.getNextImportantNoticeContents(getContext()))); - } - public void testPersonalizationSetting() { mImportantNoticePreferences.clear();