Enable Personalized dictionaries based on the setting.
Bug: 11757851 Change-Id: I83e484195f036e35cbae21fe63148bc9c22bfad7main
parent
d4bb7df412
commit
8732f9c5f9
|
@ -77,9 +77,7 @@ import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
|
||||||
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
||||||
import com.android.inputmethod.latin.define.ProductionFlag;
|
import com.android.inputmethod.latin.define.ProductionFlag;
|
||||||
import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever;
|
import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever;
|
||||||
import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
|
|
||||||
import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister;
|
import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister;
|
||||||
import com.android.inputmethod.latin.personalization.PersonalizationHelper;
|
|
||||||
import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
|
import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
|
||||||
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;
|
||||||
|
@ -179,8 +177,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
|
|
||||||
private boolean mIsMainDictionaryAvailable;
|
private boolean mIsMainDictionaryAvailable;
|
||||||
private UserBinaryDictionary mUserDictionary;
|
private UserBinaryDictionary mUserDictionary;
|
||||||
private UserHistoryDictionary mUserHistoryDictionary;
|
|
||||||
private PersonalizationDictionary mPersonalizationDictionary;
|
|
||||||
private boolean mIsUserDictionaryAvailable;
|
private boolean mIsUserDictionaryAvailable;
|
||||||
|
|
||||||
private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
|
private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
|
||||||
|
@ -592,9 +588,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
// the layout; at this time, we need to skip resetting the contacts dictionary. It will
|
// the layout; at this time, we need to skip resetting the contacts dictionary. It will
|
||||||
// be done later inside {@see #initSuggest()} when the reopenDictionaries message is
|
// be done later inside {@see #initSuggest()} when the reopenDictionaries message is
|
||||||
// processed.
|
// processed.
|
||||||
if (!mHandler.hasPendingReopenDictionaries()) {
|
if (!mHandler.hasPendingReopenDictionaries() && mSuggest != null) {
|
||||||
// May need to reset the contacts dictionary depending on the user settings.
|
// May need to reset dictionaries depending on the user settings.
|
||||||
resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
|
mSuggest.setAdditionalDictionaries(mSuggest /* oldSuggest */, mSettings.getCurrent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -640,63 +636,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
mUserDictionary = new UserBinaryDictionary(this, subtypeLocale);
|
mUserDictionary = new UserBinaryDictionary(this, subtypeLocale);
|
||||||
mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
|
mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
|
||||||
newSuggest.setUserDictionary(mUserDictionary);
|
newSuggest.setUserDictionary(mUserDictionary);
|
||||||
|
newSuggest.setAdditionalDictionaries(mSuggest /* oldSuggest */, mSettings.getCurrent());
|
||||||
mUserHistoryDictionary = PersonalizationHelper.getUserHistoryDictionary(
|
|
||||||
this, subtypeLocale);
|
|
||||||
newSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
|
|
||||||
mPersonalizationDictionary =
|
|
||||||
PersonalizationHelper.getPersonalizationDictionary(this, subtypeLocale);
|
|
||||||
newSuggest.setPersonalizationDictionary(mPersonalizationDictionary);
|
|
||||||
|
|
||||||
final Suggest oldSuggest = mSuggest;
|
final Suggest oldSuggest = mSuggest;
|
||||||
resetContactsDictionary(null != oldSuggest ? oldSuggest.getContactsDictionary() : null);
|
|
||||||
mSuggest = newSuggest;
|
mSuggest = newSuggest;
|
||||||
if (oldSuggest != null) oldSuggest.close();
|
if (oldSuggest != null) oldSuggest.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the contacts dictionary in mSuggest according to the user settings.
|
|
||||||
*
|
|
||||||
* This method takes an optional contacts dictionary to use when the locale hasn't changed
|
|
||||||
* since the contacts dictionary can be opened or closed as necessary depending on the settings.
|
|
||||||
*
|
|
||||||
* @param oldContactsDictionary an optional dictionary to use, or null
|
|
||||||
*/
|
|
||||||
private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
|
|
||||||
final Suggest suggest = mSuggest;
|
|
||||||
final boolean shouldSetDictionary =
|
|
||||||
(null != suggest && mSettings.getCurrent().mUseContactsDict);
|
|
||||||
|
|
||||||
final ContactsBinaryDictionary dictionaryToUse;
|
|
||||||
if (!shouldSetDictionary) {
|
|
||||||
// Make sure the dictionary is closed. If it is already closed, this is a no-op,
|
|
||||||
// so it's safe to call it anyways.
|
|
||||||
if (null != oldContactsDictionary) oldContactsDictionary.close();
|
|
||||||
dictionaryToUse = null;
|
|
||||||
} else {
|
|
||||||
final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
|
|
||||||
if (null != oldContactsDictionary) {
|
|
||||||
if (!oldContactsDictionary.mLocale.equals(locale)) {
|
|
||||||
// If the locale has changed then recreate the contacts dictionary. This
|
|
||||||
// allows locale dependent rules for handling bigram name predictions.
|
|
||||||
oldContactsDictionary.close();
|
|
||||||
dictionaryToUse = new ContactsBinaryDictionary(this, locale);
|
|
||||||
} else {
|
|
||||||
// Make sure the old contacts dictionary is opened. If it is already open,
|
|
||||||
// this is a no-op, so it's safe to call it anyways.
|
|
||||||
oldContactsDictionary.reopen(this);
|
|
||||||
dictionaryToUse = oldContactsDictionary;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dictionaryToUse = new ContactsBinaryDictionary(this, locale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null != suggest) {
|
|
||||||
suggest.setContactsDictionary(dictionaryToUse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* package private */ void resetSuggestMainDict() {
|
/* package private */ void resetSuggestMainDict() {
|
||||||
final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
|
final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
|
||||||
mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */);
|
mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */);
|
||||||
|
@ -2854,7 +2799,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
final SettingsValues currentSettings = mSettings.getCurrent();
|
final SettingsValues currentSettings = mSettings.getCurrent();
|
||||||
if (!currentSettings.mCorrectionEnabled) return null;
|
if (!currentSettings.mCorrectionEnabled) return null;
|
||||||
|
|
||||||
final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
|
final UserHistoryDictionary userHistoryDictionary = suggest.getUserHistoryDictionary();
|
||||||
if (userHistoryDictionary == null) return null;
|
if (userHistoryDictionary == null) return null;
|
||||||
|
|
||||||
final String prevWord = mConnection.getNthPreviousWord(currentSettings, 2);
|
final String prevWord = mConnection.getNthPreviousWord(currentSettings, 2);
|
||||||
|
@ -3069,7 +3014,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|
||||||
}
|
}
|
||||||
mConnection.deleteSurroundingText(deleteLength, 0);
|
mConnection.deleteSurroundingText(deleteLength, 0);
|
||||||
if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
|
if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
|
||||||
mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
|
if (mSuggest != null) {
|
||||||
|
mSuggest.cancelAddingUserHistory(previousWord, committedWord);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
|
final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
|
||||||
if (mSettings.getCurrent().mCurrentLanguageHasSpaces) {
|
if (mSettings.getCurrent().mCurrentLanguageHasSpaces) {
|
||||||
|
|
|
@ -25,8 +25,10 @@ import com.android.inputmethod.annotations.UsedForTesting;
|
||||||
import com.android.inputmethod.keyboard.ProximityInfo;
|
import com.android.inputmethod.keyboard.ProximityInfo;
|
||||||
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
||||||
import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
|
import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
|
||||||
|
import com.android.inputmethod.latin.personalization.PersonalizationHelper;
|
||||||
import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
|
import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
|
||||||
import com.android.inputmethod.latin.settings.Settings;
|
import com.android.inputmethod.latin.settings.Settings;
|
||||||
|
import com.android.inputmethod.latin.settings.SettingsValues;
|
||||||
import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
|
import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
|
||||||
import com.android.inputmethod.latin.utils.BoundedTreeSet;
|
import com.android.inputmethod.latin.utils.BoundedTreeSet;
|
||||||
import com.android.inputmethod.latin.utils.CollectionUtils;
|
import com.android.inputmethod.latin.utils.CollectionUtils;
|
||||||
|
@ -71,7 +73,9 @@ public final class Suggest {
|
||||||
CollectionUtils.newConcurrentHashMap();
|
CollectionUtils.newConcurrentHashMap();
|
||||||
private HashSet<String> mOnlyDictionarySetForDebug = null;
|
private HashSet<String> mOnlyDictionarySetForDebug = null;
|
||||||
private Dictionary mMainDictionary;
|
private Dictionary mMainDictionary;
|
||||||
private ContactsBinaryDictionary mContactsDict;
|
private ContactsBinaryDictionary mContactsDictionary;
|
||||||
|
private UserHistoryDictionary mUserHistoryDictionary;
|
||||||
|
private PersonalizationDictionary mPersonalizationDictionary;
|
||||||
@UsedForTesting
|
@UsedForTesting
|
||||||
private boolean mIsCurrentlyWaitingForMainDictionary = false;
|
private boolean mIsCurrentlyWaitingForMainDictionary = false;
|
||||||
|
|
||||||
|
@ -80,10 +84,14 @@ public final class Suggest {
|
||||||
// Locale used for upper- and title-casing words
|
// Locale used for upper- and title-casing words
|
||||||
public final Locale mLocale;
|
public final Locale mLocale;
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
public Suggest(final Context context, final Locale locale,
|
public Suggest(final Context context, final Locale locale,
|
||||||
final SuggestInitializationListener listener) {
|
final SuggestInitializationListener listener) {
|
||||||
initAsynchronously(context, locale, listener);
|
initAsynchronously(context, locale, listener);
|
||||||
mLocale = locale;
|
mLocale = locale;
|
||||||
|
mContext = context;
|
||||||
|
// TODO: Use SettingsValues instead of Settings.
|
||||||
// initialize a debug flag for the personalization
|
// initialize a debug flag for the personalization
|
||||||
if (Settings.readUseOnlyPersonalizationDictionaryForDebug(
|
if (Settings.readUseOnlyPersonalizationDictionaryForDebug(
|
||||||
PreferenceManager.getDefaultSharedPreferences(context))) {
|
PreferenceManager.getDefaultSharedPreferences(context))) {
|
||||||
|
@ -93,10 +101,11 @@ public final class Suggest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@UsedForTesting
|
@UsedForTesting
|
||||||
Suggest(final AssetFileAddress[] dictionaryList, final Locale locale) {
|
Suggest(final Context context, final AssetFileAddress[] dictionaryList, final Locale locale) {
|
||||||
final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(dictionaryList,
|
final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(dictionaryList,
|
||||||
false /* useFullEditDistance */, locale);
|
false /* useFullEditDistance */, locale);
|
||||||
mLocale = locale;
|
mLocale = locale;
|
||||||
|
mContext = context;
|
||||||
mMainDictionary = mainDict;
|
mMainDictionary = mainDict;
|
||||||
addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, mainDict);
|
addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, mainDict);
|
||||||
}
|
}
|
||||||
|
@ -163,7 +172,15 @@ public final class Suggest {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContactsBinaryDictionary getContactsDictionary() {
|
public ContactsBinaryDictionary getContactsDictionary() {
|
||||||
return mContactsDict;
|
return mContactsDictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserHistoryDictionary getUserHistoryDictionary() {
|
||||||
|
return mUserHistoryDictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PersonalizationDictionary getPersonalizationDictionary() {
|
||||||
|
return mPersonalizationDictionary;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() {
|
public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() {
|
||||||
|
@ -184,18 +201,120 @@ public final class Suggest {
|
||||||
* won't be used.
|
* won't be used.
|
||||||
*/
|
*/
|
||||||
public void setContactsDictionary(final ContactsBinaryDictionary contactsDictionary) {
|
public void setContactsDictionary(final ContactsBinaryDictionary contactsDictionary) {
|
||||||
mContactsDict = contactsDictionary;
|
mContactsDictionary = contactsDictionary;
|
||||||
addOrReplaceDictionaryInternal(Dictionary.TYPE_CONTACTS, contactsDictionary);
|
addOrReplaceDictionaryInternal(Dictionary.TYPE_CONTACTS, contactsDictionary);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUserHistoryDictionary(final UserHistoryDictionary userHistoryDictionary) {
|
public void setUserHistoryDictionary(final UserHistoryDictionary userHistoryDictionary) {
|
||||||
|
mUserHistoryDictionary = userHistoryDictionary;
|
||||||
addOrReplaceDictionaryInternal(Dictionary.TYPE_USER_HISTORY, userHistoryDictionary);
|
addOrReplaceDictionaryInternal(Dictionary.TYPE_USER_HISTORY, userHistoryDictionary);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPersonalizationDictionary(
|
public void setPersonalizationDictionary(
|
||||||
final PersonalizationDictionary personalizationDictionary) {
|
final PersonalizationDictionary personalizationDictionary) {
|
||||||
addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION,
|
mPersonalizationDictionary = personalizationDictionary;
|
||||||
personalizationDictionary);
|
addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION, personalizationDictionary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set dictionaries that can be turned off according to the user settings.
|
||||||
|
*
|
||||||
|
* @param oldSuggest the instance having old dictionaries
|
||||||
|
* @param settingsValues current SettingsValues
|
||||||
|
*/
|
||||||
|
public void setAdditionalDictionaries(final Suggest oldSuggest,
|
||||||
|
final SettingsValues settingsValues) {
|
||||||
|
// Contacts dictionary
|
||||||
|
resetContactsDictionary(null != oldSuggest ? oldSuggest.getContactsDictionary() : null,
|
||||||
|
settingsValues);
|
||||||
|
// User history dictionary & Personalization dictionary
|
||||||
|
resetPersonalizedDictionaries(oldSuggest, settingsValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the user history dictionary and personalization dictionary according to the user
|
||||||
|
* settings.
|
||||||
|
*
|
||||||
|
* @param oldSuggest the instance that has been used
|
||||||
|
* @param settingsValues current settingsValues
|
||||||
|
*/
|
||||||
|
// TODO: Consolidate resetPersonalizedDictionaries() and resetContactsDictionary(). Call up the
|
||||||
|
// new method for each dictionary.
|
||||||
|
private void resetPersonalizedDictionaries(final Suggest oldSuggest,
|
||||||
|
final SettingsValues settingsValues) {
|
||||||
|
final boolean shouldSetDictionaries = settingsValues.mUsePersonalizedDicts;
|
||||||
|
|
||||||
|
final UserHistoryDictionary oldUserHistoryDictionary = (null == oldSuggest) ? null :
|
||||||
|
oldSuggest.getUserHistoryDictionary();
|
||||||
|
final PersonalizationDictionary oldPersonalizationDictionary = (null == oldSuggest) ? null :
|
||||||
|
oldSuggest.getPersonalizationDictionary();
|
||||||
|
final UserHistoryDictionary userHistoryDictionaryToUse;
|
||||||
|
final PersonalizationDictionary personalizationDictionaryToUse;
|
||||||
|
if (!shouldSetDictionaries) {
|
||||||
|
userHistoryDictionaryToUse = null;
|
||||||
|
personalizationDictionaryToUse = null;
|
||||||
|
} else {
|
||||||
|
if (null != oldUserHistoryDictionary
|
||||||
|
&& oldUserHistoryDictionary.mLocale.equals(mLocale)) {
|
||||||
|
userHistoryDictionaryToUse = oldUserHistoryDictionary;
|
||||||
|
} else {
|
||||||
|
userHistoryDictionaryToUse =
|
||||||
|
PersonalizationHelper.getUserHistoryDictionary(mContext, mLocale);
|
||||||
|
}
|
||||||
|
if (null != oldPersonalizationDictionary
|
||||||
|
&& oldPersonalizationDictionary.mLocale.equals(mLocale)) {
|
||||||
|
personalizationDictionaryToUse = oldPersonalizationDictionary;
|
||||||
|
} else {
|
||||||
|
personalizationDictionaryToUse =
|
||||||
|
PersonalizationHelper.getPersonalizationDictionary(mContext, mLocale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setUserHistoryDictionary(userHistoryDictionaryToUse);
|
||||||
|
setPersonalizationDictionary(personalizationDictionaryToUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the contacts dictionary according to the user settings.
|
||||||
|
*
|
||||||
|
* This method takes an optional contacts dictionary to use when the locale hasn't changed
|
||||||
|
* since the contacts dictionary can be opened or closed as necessary depending on the settings.
|
||||||
|
*
|
||||||
|
* @param oldContactsDictionary an optional dictionary to use, or null
|
||||||
|
* @param settingsValues current settingsValues
|
||||||
|
*/
|
||||||
|
private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary,
|
||||||
|
final SettingsValues settingsValues) {
|
||||||
|
final boolean shouldSetDictionary = settingsValues.mUseContactsDict;
|
||||||
|
final ContactsBinaryDictionary dictionaryToUse;
|
||||||
|
if (!shouldSetDictionary) {
|
||||||
|
// Make sure the dictionary is closed. If it is already closed, this is a no-op,
|
||||||
|
// so it's safe to call it anyways.
|
||||||
|
if (null != oldContactsDictionary) oldContactsDictionary.close();
|
||||||
|
dictionaryToUse = null;
|
||||||
|
} else {
|
||||||
|
if (null != oldContactsDictionary) {
|
||||||
|
if (!oldContactsDictionary.mLocale.equals(mLocale)) {
|
||||||
|
// If the locale has changed then recreate the contacts dictionary. This
|
||||||
|
// allows locale dependent rules for handling bigram name predictions.
|
||||||
|
oldContactsDictionary.close();
|
||||||
|
dictionaryToUse = new ContactsBinaryDictionary(mContext, mLocale);
|
||||||
|
} else {
|
||||||
|
// Make sure the old contacts dictionary is opened. If it is already open,
|
||||||
|
// this is a no-op, so it's safe to call it anyways.
|
||||||
|
oldContactsDictionary.reopen(mContext);
|
||||||
|
dictionaryToUse = oldContactsDictionary;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dictionaryToUse = new ContactsBinaryDictionary(mContext, mLocale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setContactsDictionary(dictionaryToUse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelAddingUserHistory(final String previousWord, final String committedWord) {
|
||||||
|
if (mUserHistoryDictionary != null) {
|
||||||
|
mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAutoCorrectionThreshold(float threshold) {
|
public void setAutoCorrectionThreshold(float threshold) {
|
||||||
|
|
|
@ -55,8 +55,8 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB
|
||||||
|
|
||||||
public static final int REQUIRED_BINARY_DICTIONARY_VERSION = FormatSpec.VERSION4;
|
public static final int REQUIRED_BINARY_DICTIONARY_VERSION = FormatSpec.VERSION4;
|
||||||
|
|
||||||
/** Locale for which this user history dictionary is storing words */
|
/** The locale for this dictionary. */
|
||||||
private final Locale mLocale;
|
public final Locale mLocale;
|
||||||
|
|
||||||
private final String mDictName;
|
private final String mDictName;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue