am 60415866: [LatinIME] Support MNC permissions.

* commit '604158669b407a40cd0f23538fad4dce5d738f24':
  [LatinIME] Support MNC permissions.
This commit is contained in:
Mohammadinamul Sheik 2015-07-16 00:03:56 +00:00 committed by Android Git Automerger
commit db0081f63c
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) {
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;
}
}

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