[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
main
Mohammadinamul Sheik 2015-07-15 13:32:50 -07:00
parent 7d5fb3a943
commit 604158669b
17 changed files with 466 additions and 361 deletions

View File

@ -18,7 +18,7 @@
coreApp="true"
package="com.android.inputmethod.latin">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21" />
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="23" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
@ -77,6 +77,13 @@
</intent-filter>
</activity>
<activity
android:name=".permissions.PermissionsActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="false"
android:taskAffinity="" >
</activity>
<activity android:name=".setup.SetupWizardActivity"
android:theme="@style/platformActivityTheme"
android:label="@string/english_ime_name"

View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
**
** Copyright 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.
*/
-->
<resources>
<!-- The array of the text of the important notices displayed on the suggestion strip. -->
<string-array name="important_notice_title_array" translatable="false">
<!-- empty -->
</string-array>
<!-- The array of the contents of the important notices. -->
<string-array name="important_notice_contents_array" translatable="false">
<!-- empty -->
</string-array>
</resources>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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
-->
<resources>
<!-- The text shown on the suggestion bar to request the contacts permission info. -->
<string name="important_notice_suggest_contact_names">Suggest contact names? Touch for info.</string>
</resources>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
**
** Copyright 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.
*/
-->
<resources>
<integer name="config_important_notice_version">0</integer>
<!-- Description for option enabling the use by the keyboards of sent/received messages, e-mail and typing history to improve suggestion accuracy [CHAR LIMIT=68] -->
<string name="use_personalized_dicts_summary">Learn from your communications and typed data to improve suggestions</string>
</resources>

View File

@ -16,6 +16,7 @@
package com.android.inputmethod.latin;
import android.Manifest;
import android.content.Context;
import android.net.Uri;
import android.provider.ContactsContract;
@ -25,6 +26,7 @@ import android.util.Log;
import com.android.inputmethod.annotations.ExternallyReferenced;
import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.permissions.PermissionsUtil;
import com.android.inputmethod.latin.personalization.AccountUtils;
import java.io.File;
@ -108,6 +110,11 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary
* Loads data within content providers to the dictionary.
*/
private void loadDictionaryForUriLocked(final Uri uri) {
if (!PermissionsUtil.checkAllPermissionsGranted(
mContext, Manifest.permission.READ_CONTACTS)) {
Log.i(TAG, "No permission to read contacts. Not loading the Dictionary.");
}
final ArrayList<String> validNames = mContactsManager.getValidNames(uri);
for (final String name : validNames) {
addNameLocked(name);

View File

@ -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) {

View File

@ -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<String> subDictTypesToUse = new HashSet<>();
subDictTypesToUse.add(Dictionary.TYPE_USER);
if (useContactsDict) {
// Do not use contacts dictionary if we do not have permissions to read contacts.
final boolean contactsPermissionGranted = PermissionsUtil.checkAllPermissionsGranted(
context, Manifest.permission.READ_CONTACTS);
if (useContactsDict && contactsPermissionGranted) {
subDictTypesToUse.add(Dictionary.TYPE_CONTACTS);
}
if (usePersonalizedDicts) {

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.inputmethod.latin.permissions;
import android.app.Activity;
import android.content.Context;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Manager to perform permission related tasks. Always call on the UI thread.
*/
public class PermissionsManager {
public interface PermissionsResultCallback {
void onRequestPermissionsResult(boolean allGranted);
}
private int mRequestCodeId;
private final Context mContext;
private final Map<Integer, PermissionsResultCallback> mRequestIdToCallback = new HashMap<>();
private static PermissionsManager sInstance;
public PermissionsManager(Context context) {
mContext = context;
}
@Nonnull
public static synchronized PermissionsManager get(@Nonnull Context context) {
if (sInstance == null) {
sInstance = new PermissionsManager(context);
}
return sInstance;
}
private synchronized int getNextRequestId() {
return ++mRequestCodeId;
}
public synchronized void requestPermissions(@Nonnull PermissionsResultCallback callback,
@Nullable Activity activity,
String... permissionsToRequest) {
List<String> deniedPermissions = PermissionsUtil.getDeniedPermissions(
mContext, permissionsToRequest);
if (deniedPermissions.isEmpty()) {
return;
}
// otherwise request the permissions.
int requestId = getNextRequestId();
String[] permissionsArray = deniedPermissions.toArray(
new String[deniedPermissions.size()]);
mRequestIdToCallback.put(requestId, callback);
if (activity != null) {
PermissionsUtil.requestPermissions(activity, requestId, permissionsArray);
} else {
PermissionsActivity.run(mContext, requestId, permissionsArray);
}
}
public synchronized void onRequestPermissionsResult(
int requestCode, String[] permissions, int[] grantResults) {
PermissionsResultCallback permissionsResultCallback = mRequestIdToCallback.get(requestCode);
mRequestIdToCallback.remove(requestCode);
boolean allGranted = PermissionsUtil.allGranted(grantResults);
permissionsResultCallback.onRequestPermissionsResult(allGranted);
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.inputmethod.latin.permissions;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;
/**
* Utility class for permissions.
*/
public class PermissionsUtil {
/**
* Returns the list of permissions not granted from the given list of permissions.
* @param context Context
* @param permissions list of permissions to check.
* @return the list of permissions that do not have permission to use.
*/
public static List<String> getDeniedPermissions(Context context,
String... permissions) {
final List<String> deniedPermissions = new ArrayList<>();
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(context, permission)
!= PackageManager.PERMISSION_GRANTED) {
deniedPermissions.add(permission);
}
}
return deniedPermissions;
}
/**
* Uses the given activity and requests the user for permissions.
* @param activity activity to use.
* @param requestCode request code/id to use.
* @param permissions String array of permissions that needs to be requested.
*/
public static void requestPermissions(Activity activity, int requestCode,
String[] permissions) {
ActivityCompat.requestPermissions(activity, permissions, requestCode);
}
/**
* Checks if all the permissions are granted.
*/
public static boolean allGranted(@NonNull int[] grantResults) {
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
/**
* Queries if al the permissions are granted for the given permission strings.
*/
public static boolean checkAllPermissionsGranted(Context context, String... permissions) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
// For all pre-M devices, we should have all the premissions granted on install.
return true;
}
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(context, permission)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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) {
getImportantNoticePreferences(context)
.edit()
.putInt(KEY_IMPORTANT_NOTICE_VERSION, getNextImportantNoticeVersion(context))
.remove(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)
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();
}
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;
final long firstDisplayTimeInMillis = prefs.getLong(
KEY_TIMESTAMP_OF_CONTACTS_NOTICE, currentTimeInMillis);
final long elapsedTime = currentTimeInMillis - firstDisplayTimeInMillis;
return elapsedTime >= TIMEOUT_OF_IMPORTANT_NOTICE;
}
public static String getNextImportantNoticeContents(final Context context) {
final int nextVersion = getNextImportantNoticeVersion(context);
final String[] importantNoticeContentsArray = context.getResources().getStringArray(
R.array.important_notice_contents_array);
if (nextVersion > 0 && nextVersion < importantNoticeContentsArray.length) {
return importantNoticeContentsArray[nextVersion];
}
return null;
public static void updateContactsNoticeShown(final Context context) {
getImportantNoticePreferences(context)
.edit()
.putBoolean(KEY_SUGGEST_CONTACTS_NOTICE, true)
.remove(KEY_TIMESTAMP_OF_CONTACTS_NOTICE)
.apply();
}
}

View File

@ -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();