am 60415866
: [LatinIME] Support MNC permissions.
* commit '604158669b407a40cd0f23538fad4dce5d738f24': [LatinIME] Support MNC permissions.
This commit is contained in:
commit
db0081f63c
17 changed files with 466 additions and 361 deletions
|
@ -18,7 +18,7 @@
|
||||||
coreApp="true"
|
coreApp="true"
|
||||||
package="com.android.inputmethod.latin">
|
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.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||||
|
@ -77,6 +77,13 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".permissions.PermissionsActivity"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
|
android:exported="false"
|
||||||
|
android:taskAffinity="" >
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".setup.SetupWizardActivity"
|
<activity android:name=".setup.SetupWizardActivity"
|
||||||
android:theme="@style/platformActivityTheme"
|
android:theme="@style/platformActivityTheme"
|
||||||
android:label="@string/english_ime_name"
|
android:label="@string/english_ime_name"
|
||||||
|
|
|
@ -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>
|
|
21
java/res/values/important_notice_strings.xml
Normal file
21
java/res/values/important_notice_strings.xml
Normal 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>
|
|
@ -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>
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.android.inputmethod.latin;
|
package com.android.inputmethod.latin;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
|
@ -25,6 +26,7 @@ import android.util.Log;
|
||||||
import com.android.inputmethod.annotations.ExternallyReferenced;
|
import com.android.inputmethod.annotations.ExternallyReferenced;
|
||||||
import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
|
import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
|
||||||
import com.android.inputmethod.latin.common.StringUtils;
|
import com.android.inputmethod.latin.common.StringUtils;
|
||||||
|
import com.android.inputmethod.latin.permissions.PermissionsUtil;
|
||||||
import com.android.inputmethod.latin.personalization.AccountUtils;
|
import com.android.inputmethod.latin.personalization.AccountUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -108,6 +110,11 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary
|
||||||
* Loads data within content providers to the dictionary.
|
* Loads data within content providers to the dictionary.
|
||||||
*/
|
*/
|
||||||
private void loadDictionaryForUriLocked(final Uri uri) {
|
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);
|
final ArrayList<String> validNames = mContactsManager.getValidNames(uri);
|
||||||
for (final String name : validNames) {
|
for (final String name : validNames) {
|
||||||
addNameLocked(name);
|
addNameLocked(name);
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.android.inputmethod.latin;
|
package com.android.inputmethod.latin;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.ContentObserver;
|
import android.database.ContentObserver;
|
||||||
|
@ -25,6 +26,7 @@ import android.util.Log;
|
||||||
|
|
||||||
import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
|
import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
|
||||||
import com.android.inputmethod.latin.define.DebugFlags;
|
import com.android.inputmethod.latin.define.DebugFlags;
|
||||||
|
import com.android.inputmethod.latin.permissions.PermissionsUtil;
|
||||||
import com.android.inputmethod.latin.utils.ExecutorUtils;
|
import com.android.inputmethod.latin.utils.ExecutorUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -35,10 +37,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
*/
|
*/
|
||||||
public class ContactsContentObserver implements Runnable {
|
public class ContactsContentObserver implements Runnable {
|
||||||
private static final String TAG = "ContactsContentObserver";
|
private static final String TAG = "ContactsContentObserver";
|
||||||
private static AtomicBoolean sRunning = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final ContactsManager mManager;
|
private final ContactsManager mManager;
|
||||||
|
private final AtomicBoolean mRunning = new AtomicBoolean(false);
|
||||||
|
|
||||||
private ContentObserver mContentObserver;
|
private ContentObserver mContentObserver;
|
||||||
private ContactsChangedListener mContactsChangedListener;
|
private ContactsChangedListener mContactsChangedListener;
|
||||||
|
@ -49,6 +51,13 @@ public class ContactsContentObserver implements Runnable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerObserver(final ContactsChangedListener listener) {
|
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) {
|
if (DebugFlags.DEBUG_ENABLED) {
|
||||||
Log.d(TAG, "registerObserver()");
|
Log.d(TAG, "registerObserver()");
|
||||||
}
|
}
|
||||||
|
@ -66,7 +75,14 @@ public class ContactsContentObserver implements Runnable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
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) {
|
if (DebugFlags.DEBUG_ENABLED) {
|
||||||
Log.d(TAG, "run() : Already running. Don't waste time checking again.");
|
Log.d(TAG, "run() : Already running. Don't waste time checking again.");
|
||||||
}
|
}
|
||||||
|
@ -78,10 +94,16 @@ public class ContactsContentObserver implements Runnable {
|
||||||
}
|
}
|
||||||
mContactsChangedListener.onContactsChange();
|
mContactsChangedListener.onContactsChange();
|
||||||
}
|
}
|
||||||
sRunning.set(false);
|
mRunning.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean haveContentsChanged() {
|
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 long startTime = SystemClock.uptimeMillis();
|
||||||
final int contactCount = mManager.getContactCount();
|
final int contactCount = mManager.getContactCount();
|
||||||
if (contactCount > ContactsDictionaryConstants.MAX_CONTACTS_PROVIDER_QUERY_LIMIT) {
|
if (contactCount > ContactsDictionaryConstants.MAX_CONTACTS_PROVIDER_QUERY_LIMIT) {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.android.inputmethod.latin;
|
package com.android.inputmethod.latin;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
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.ComposedData;
|
||||||
import com.android.inputmethod.latin.common.Constants;
|
import com.android.inputmethod.latin.common.Constants;
|
||||||
import com.android.inputmethod.latin.common.StringUtils;
|
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.personalization.UserHistoryDictionary;
|
||||||
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
|
import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
|
||||||
import com.android.inputmethod.latin.utils.ExecutorUtils;
|
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.
|
// TODO: Make subDictTypesToUse configurable by resource or a static final list.
|
||||||
final HashSet<String> subDictTypesToUse = new HashSet<>();
|
final HashSet<String> subDictTypesToUse = new HashSet<>();
|
||||||
subDictTypesToUse.add(Dictionary.TYPE_USER);
|
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);
|
subDictTypesToUse.add(Dictionary.TYPE_CONTACTS);
|
||||||
}
|
}
|
||||||
if (usePersonalizedDicts) {
|
if (usePersonalizedDicts) {
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_MICROPHONE_COMPAT;
|
import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_MICROPHONE_COMPAT;
|
||||||
|
|
||||||
|
import android.Manifest.permission;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
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.DebugFlags;
|
||||||
import com.android.inputmethod.latin.define.ProductionFlags;
|
import com.android.inputmethod.latin.define.ProductionFlags;
|
||||||
import com.android.inputmethod.latin.inputlogic.InputLogic;
|
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.personalization.PersonalizationHelper;
|
||||||
import com.android.inputmethod.latin.settings.Settings;
|
import com.android.inputmethod.latin.settings.Settings;
|
||||||
import com.android.inputmethod.latin.settings.SettingsActivity;
|
import com.android.inputmethod.latin.settings.SettingsActivity;
|
||||||
|
@ -106,7 +108,7 @@ import javax.annotation.Nonnull;
|
||||||
public class LatinIME extends InputMethodService implements KeyboardActionListener,
|
public class LatinIME extends InputMethodService implements KeyboardActionListener,
|
||||||
SuggestionStripView.Listener, SuggestionStripViewAccessor,
|
SuggestionStripView.Listener, SuggestionStripViewAccessor,
|
||||||
DictionaryFacilitator.DictionaryInitializationListener,
|
DictionaryFacilitator.DictionaryInitializationListener,
|
||||||
ImportantNoticeDialog.ImportantNoticeDialogListener {
|
PermissionsManager.PermissionsResultCallback {
|
||||||
static final String TAG = LatinIME.class.getSimpleName();
|
static final String TAG = LatinIME.class.getSimpleName();
|
||||||
private static final boolean TRACE = false;
|
private static final boolean TRACE = false;
|
||||||
|
|
||||||
|
@ -1251,18 +1253,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
// pressed.
|
// pressed.
|
||||||
@Override
|
@Override
|
||||||
public void showImportantNoticeContents() {
|
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
|
@Override
|
||||||
public void onClickSettingsOfImportantNoticeDialog(final int nextVersion) {
|
public void onRequestPermissionsResult(boolean allGranted) {
|
||||||
launchSettings(SettingsActivity.EXTRA_ENTRY_VALUE_NOTICE_DIALOG);
|
ImportantNoticeUtils.updateContactsNoticeShown(this /* context */);
|
||||||
}
|
|
||||||
|
|
||||||
// Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener}
|
|
||||||
@Override
|
|
||||||
public void onUserAcknowledgmentOfImportantNoticeDialog(final int nextVersion) {
|
|
||||||
setNeutralSuggestionStrip();
|
setNeutralSuggestionStrip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,17 +16,23 @@
|
||||||
|
|
||||||
package com.android.inputmethod.latin.settings;
|
package com.android.inputmethod.latin.settings;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.ResolveInfo;
|
import android.content.pm.ResolveInfo;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
|
import android.preference.SwitchPreference;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import com.android.inputmethod.dictionarypack.DictionarySettingsActivity;
|
import com.android.inputmethod.dictionarypack.DictionarySettingsActivity;
|
||||||
import com.android.inputmethod.latin.R;
|
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.UserDictionaryList;
|
||||||
import com.android.inputmethod.latin.userdictionary.UserDictionarySettings;
|
import com.android.inputmethod.latin.userdictionary.UserDictionarySettings;
|
||||||
|
|
||||||
|
@ -45,12 +51,17 @@ import java.util.TreeSet;
|
||||||
* - Suggest Contact names
|
* - Suggest Contact names
|
||||||
* - Next-word suggestions
|
* - 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 DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS = false;
|
||||||
private static final boolean USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS =
|
private static final boolean USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS =
|
||||||
DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS
|
DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS
|
||||||
|| Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2;
|
|| Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2;
|
||||||
|
|
||||||
|
private SwitchPreference mUseContactsPreference;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(final Bundle icicle) {
|
public void onCreate(final Bundle icicle) {
|
||||||
super.onCreate(icicle);
|
super.onCreate(icicle);
|
||||||
|
@ -76,6 +87,9 @@ public final class CorrectionSettingsFragment extends SubScreenFragment {
|
||||||
if (ri == null) {
|
if (ri == null) {
|
||||||
overwriteUserDictionaryPreference(editPersonalDictionary);
|
overwriteUserDictionaryPreference(editPersonalDictionary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mUseContactsPreference = (SwitchPreference) findPreference(Settings.PREF_KEY_USE_CONTACTS_DICT);
|
||||||
|
turnOffUseContactsIfNoPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) {
|
private void overwriteUserDictionaryPreference(final Preference userDictionaryPreference) {
|
||||||
|
@ -101,4 +115,38 @@ public final class CorrectionSettingsFragment extends SubScreenFragment {
|
||||||
userDictionaryPreference.setFragment(UserDictionaryList.class.getName());
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.android.inputmethod.latin.settings;
|
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.FragmentUtils;
|
||||||
import com.android.inputmethod.latin.utils.StatsUtils;
|
import com.android.inputmethod.latin.utils.StatsUtils;
|
||||||
import com.android.inputmethod.latin.utils.StatsUtilsManager;
|
import com.android.inputmethod.latin.utils.StatsUtilsManager;
|
||||||
|
@ -24,9 +25,11 @@ import android.app.ActionBar;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceActivity;
|
import android.preference.PreferenceActivity;
|
||||||
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.view.MenuItem;
|
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();
|
private static final String DEFAULT_FRAGMENT = SettingsFragment.class.getName();
|
||||||
|
|
||||||
public static final String EXTRA_SHOW_HOME_AS_UP = "show_home_as_up";
|
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) {
|
public boolean isValidFragment(final String fragmentName) {
|
||||||
return FragmentUtils.isValidFragment(fragmentName);
|
return FragmentUtils.isValidFragment(fragmentName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||||
|
PermissionsManager.get(this).onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -220,7 +220,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
|
||||||
if (getWidth() <= 0) {
|
if (getWidth() <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final String importantNoticeTitle = ImportantNoticeUtils.getNextImportantNoticeTitle(
|
final String importantNoticeTitle = ImportantNoticeUtils.getSuggestContactsNoticeTitle(
|
||||||
getContext());
|
getContext());
|
||||||
if (TextUtils.isEmpty(importantNoticeTitle)) {
|
if (TextUtils.isEmpty(importantNoticeTitle)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package com.android.inputmethod.latin.utils;
|
package com.android.inputmethod.latin.utils;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
@ -25,6 +26,7 @@ import android.util.Log;
|
||||||
|
|
||||||
import com.android.inputmethod.annotations.UsedForTesting;
|
import com.android.inputmethod.annotations.UsedForTesting;
|
||||||
import com.android.inputmethod.latin.R;
|
import com.android.inputmethod.latin.R;
|
||||||
|
import com.android.inputmethod.latin.permissions.PermissionsUtil;
|
||||||
import com.android.inputmethod.latin.settings.SettingsValues;
|
import com.android.inputmethod.latin.settings.SettingsValues;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
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
|
// {@link SharedPreferences} name to save the last important notice version that has been
|
||||||
// displayed to users.
|
// displayed to users.
|
||||||
private static final String PREFERENCE_NAME = "important_notice_pref";
|
private static final String PREFERENCE_NAME = "important_notice_pref";
|
||||||
|
|
||||||
|
private static final String KEY_SUGGEST_CONTACTS_NOTICE = "important_notice_suggest_contacts";
|
||||||
|
|
||||||
@UsedForTesting
|
@UsedForTesting
|
||||||
static final String KEY_IMPORTANT_NOTICE_VERSION = "important_notice_version";
|
static final String KEY_TIMESTAMP_OF_CONTACTS_NOTICE = "timestamp_of_suggest_contacts_notice";
|
||||||
@UsedForTesting
|
|
||||||
static final String KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE =
|
|
||||||
"timestamp_of_first_important_notice";
|
|
||||||
@UsedForTesting
|
@UsedForTesting
|
||||||
static final long TIMEOUT_OF_IMPORTANT_NOTICE = TimeUnit.HOURS.toMillis(23);
|
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.
|
// Copy of the hidden {@link Settings.Secure#USER_SETUP_COMPLETE} settings key.
|
||||||
// The value is zero until each multiuser completes system setup wizard.
|
// The value is zero until each multiuser completes system setup wizard.
|
||||||
|
@ -73,87 +75,66 @@ public final class ImportantNoticeUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
@UsedForTesting
|
@UsedForTesting
|
||||||
static int getCurrentImportantNoticeVersion(final Context context) {
|
static boolean hasContactsNoticeShown(final Context context) {
|
||||||
return context.getResources().getInteger(R.integer.config_important_notice_version);
|
return getImportantNoticePreferences(context).getBoolean(
|
||||||
}
|
KEY_SUGGEST_CONTACTS_NOTICE, false);
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean shouldShowImportantNotice(final Context context,
|
public static boolean shouldShowImportantNotice(final Context context,
|
||||||
final SettingsValues settingsValues) {
|
final SettingsValues settingsValues) {
|
||||||
// Check to see whether personalization is enabled by the user.
|
// Check to see whether "Use Contacts" is enabled by the user.
|
||||||
if (!settingsValues.isPersonalizationEnabled()) {
|
if (!settingsValues.mUseContactsDict) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!hasNewImportantNotice(context)) {
|
|
||||||
|
if (hasContactsNoticeShown(context)) {
|
||||||
return false;
|
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)) {
|
if (TextUtils.isEmpty(importantNoticeTitle)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (isInSystemSetupWizard(context)) {
|
if (isInSystemSetupWizard(context)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (hasTimeoutPassed(context, System.currentTimeMillis())) {
|
if (hasContactsNoticeTimeoutPassed(context, System.currentTimeMillis())) {
|
||||||
updateLastImportantNoticeVersion(context);
|
updateContactsNoticeShown(context);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
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)
|
getImportantNoticePreferences(context)
|
||||||
.edit()
|
.edit()
|
||||||
.putInt(KEY_IMPORTANT_NOTICE_VERSION, getNextImportantNoticeVersion(context))
|
.putBoolean(KEY_SUGGEST_CONTACTS_NOTICE, true)
|
||||||
.remove(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE)
|
.remove(KEY_TIMESTAMP_OF_CONTACTS_NOTICE)
|
||||||
.apply();
|
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,7 @@
|
||||||
|
|
||||||
package com.android.inputmethod.latin.utils;
|
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_CONTACTS_NOTICE;
|
||||||
import static com.android.inputmethod.latin.utils.ImportantNoticeUtils.KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE;
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
@ -35,8 +34,6 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@MediumTest
|
@MediumTest
|
||||||
public class ImportantNoticeUtilsTests extends AndroidTestCase {
|
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;
|
private ImportantNoticePreferences mImportantNoticePreferences;
|
||||||
|
|
||||||
|
@ -87,18 +84,15 @@ public class ImportantNoticeUtilsTests extends AndroidTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save() {
|
public void save() {
|
||||||
mVersion = getInt(KEY_IMPORTANT_NOTICE_VERSION);
|
mLastTime = getLong(KEY_TIMESTAMP_OF_CONTACTS_NOTICE);
|
||||||
mLastTime = getLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void restore() {
|
public void restore() {
|
||||||
putInt(KEY_IMPORTANT_NOTICE_VERSION, mVersion);
|
putLong(KEY_TIMESTAMP_OF_CONTACTS_NOTICE, mLastTime);
|
||||||
putLong(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE, mLastTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
removePreference(KEY_IMPORTANT_NOTICE_VERSION);
|
removePreference(KEY_TIMESTAMP_OF_CONTACTS_NOTICE);
|
||||||
removePreference(KEY_TIMESTAMP_OF_FIRST_IMPORTANT_NOTICE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,141 +111,6 @@ public class ImportantNoticeUtilsTests extends AndroidTestCase {
|
||||||
mImportantNoticePreferences.restore();
|
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() {
|
public void testPersonalizationSetting() {
|
||||||
mImportantNoticePreferences.clear();
|
mImportantNoticePreferences.clear();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue