From 604158669b407a40cd0f23538fad4dce5d738f24 Mon Sep 17 00:00:00 2001 From: Mohammadinamul Sheik Date: Wed, 15 Jul 2015 13:32:50 -0700 Subject: [PATCH] [LatinIME] Support MNC permissions. This build has been compiled against API 23 This build is approved to go out with the M OTA, but may NOT be released to the public until the Play Store has enabled API level 23 apps Version: 4.1.2300x.build_id 1. Replaces the personalization is on information with the suggest contacts. 2. Enables "Use Contacts" only if the app has permission to read contacts. 3. Disables the contacts dictionary in the Facilitator. 4. Do not register/read the contacts in the contact observer. Bug: 22236416 Change-Id: I9674e13d0d0f4a2014c5024fde0178de684c07e7 --- java/AndroidManifest.xml | 9 +- ...donottranslate-config-important-notice.xml | 30 ---- java/res/values/important_notice_strings.xml | 21 +++ .../strings-config-important-notice.xml | 25 --- .../latin/ContactsBinaryDictionary.java | 7 + .../latin/ContactsContentObserver.java | 28 +++- .../latin/DictionaryFacilitatorImpl.java | 8 +- .../latin/ImportantNoticeDialog.java | 78 --------- .../android/inputmethod/latin/LatinIME.java | 18 +-- .../permissions/PermissionsActivity.java | 97 ++++++++++++ .../latin/permissions/PermissionsManager.java | 91 +++++++++++ .../latin/permissions/PermissionsUtil.java | 93 +++++++++++ .../settings/CorrectionSettingsFragment.java | 50 +++++- .../latin/settings/SettingsActivity.java | 10 +- .../suggestions/SuggestionStripView.java | 2 +- .../latin/utils/ImportantNoticeUtils.java | 111 ++++++------- .../utils/ImportantNoticeUtilsTests.java | 149 +----------------- 17 files changed, 466 insertions(+), 361 deletions(-) delete mode 100644 java/res/values/donottranslate-config-important-notice.xml create mode 100644 java/res/values/important_notice_strings.xml delete mode 100644 java/res/values/strings-config-important-notice.xml delete mode 100644 java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java create mode 100644 java/src/com/android/inputmethod/latin/permissions/PermissionsActivity.java create mode 100644 java/src/com/android/inputmethod/latin/permissions/PermissionsManager.java create mode 100644 java/src/com/android/inputmethod/latin/permissions/PermissionsUtil.java 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();