diff options
19 files changed, 399 insertions, 150 deletions
diff --git a/apex/appsearch/framework/api/current.txt b/apex/appsearch/framework/api/current.txt index 5ded446ff81a..386c6e23f865 100644 --- a/apex/appsearch/framework/api/current.txt +++ b/apex/appsearch/framework/api/current.txt @@ -18,9 +18,10 @@ package android.app.appsearch { } public static final class AppSearchManager.SearchContext.Builder { - ctor public AppSearchManager.SearchContext.Builder(); + ctor @Deprecated public AppSearchManager.SearchContext.Builder(); + ctor public AppSearchManager.SearchContext.Builder(@NonNull String); method @NonNull public android.app.appsearch.AppSearchManager.SearchContext build(); - method @NonNull public android.app.appsearch.AppSearchManager.SearchContext.Builder setDatabaseName(@NonNull String); + method @Deprecated @NonNull public android.app.appsearch.AppSearchManager.SearchContext.Builder setDatabaseName(@NonNull String); } public interface AppSearchMigrationHelper { diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java index a62bb504debc..0c6b86b68636 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java @@ -30,17 +30,19 @@ import java.util.function.Consumer; * Provides access to the centralized AppSearch index maintained by the system. * * <p>AppSearch is a search library for managing structured data featuring: + * * <ul> - * <li>A fully offline on-device solution - * <li>A set of APIs for applications to index documents and retrieve them via full-text search - * <li>APIs for applications to allow the System to display their content on system UI surfaces - * <li>Similarly, APIs for applications to allow the System to share their content with other - * specified applications. + * <li>A fully offline on-device solution + * <li>A set of APIs for applications to index documents and retrieve them via full-text search + * <li>APIs for applications to allow the System to display their content on system UI surfaces + * <li>Similarly, APIs for applications to allow the System to share their content with other + * specified applications. * </ul> * * <p>Applications create a database by opening an {@link AppSearchSession}. * * <p>Example: + * * <pre> * AppSearchManager appSearchManager = context.getSystemService(AppSearchManager.class); * @@ -51,11 +53,12 @@ import java.util.function.Consumer; * });</pre> * * <p>After opening the session, a schema must be set in order to define the organizational - * structure of data. The schema is set by calling {@link AppSearchSession#setSchema}. The schema - * is composed of a collection of {@link AppSearchSchema} objects, each of which defines a unique - * type of data. + * structure of data. The schema is set by calling {@link AppSearchSession#setSchema}. The schema is + * composed of a collection of {@link AppSearchSchema} objects, each of which defines a unique type + * of data. * * <p>Example: + * * <pre> * AppSearchSchema emailSchemaType = new AppSearchSchema.Builder("Email") * .addProperty(new StringPropertyConfig.Builder("subject") @@ -73,15 +76,16 @@ import java.util.function.Consumer; * });</pre> * * <p>The basic unit of data in AppSearch is represented as a {@link GenericDocument} object, - * containing a URI, namespace, time-to-live, score, and properties. A namespace organizes a - * logical group of documents. For example, a namespace can be created to group documents on a - * per-account basis. A URI identifies a single document within a namespace. The combination - * of URI and namespace uniquely identifies a {@link GenericDocument} in the database. + * containing a URI, namespace, time-to-live, score, and properties. A namespace organizes a logical + * group of documents. For example, a namespace can be created to group documents on a per-account + * basis. A URI identifies a single document within a namespace. The combination of URI and + * namespace uniquely identifies a {@link GenericDocument} in the database. * - * <p>Once the schema has been set, {@link GenericDocument} objects can be put into the database - * and indexed by calling {@link AppSearchSession#put}. + * <p>Once the schema has been set, {@link GenericDocument} objects can be put into the database and + * indexed by calling {@link AppSearchSession#put}. * * <p>Example: + * * <pre> * // Although for this example we use GenericDocument directly, we recommend extending * // GenericDocument to create specific types (i.e. Email) with specific setters/getters. @@ -106,18 +110,12 @@ import java.util.function.Consumer; * and namespace. * * <p>Document removal is done either by time-to-live expiration, or explicitly calling a remove - * operation. Remove operations can be done by URI and namespace via - * {@link AppSearchSession#remove(RemoveByUriRequest, Executor, BatchResultCallback)}, - * or by query via {@link AppSearchSession#remove(String, SearchSpec, Executor, Consumer)}. + * operation. Remove operations can be done by URI and namespace via {@link + * AppSearchSession#remove(RemoveByUriRequest, Executor, BatchResultCallback)}, or by query via + * {@link AppSearchSession#remove(String, SearchSpec, Executor, Consumer)}. */ @SystemService(Context.APP_SEARCH_SERVICE) public class AppSearchManager { - /** - * The default empty database name. - * - * @hide - */ - public static final String DEFAULT_DATABASE_NAME = ""; private final IAppSearchManager mService; private final Context mContext; @@ -149,10 +147,42 @@ public class AppSearchManager { /** Builder for {@link SearchContext} objects. */ public static final class Builder { - private String mDatabaseName = DEFAULT_DATABASE_NAME; + private String mDatabaseName; private boolean mBuilt = false; /** + * TODO(b/181887768): This method exists only for dogfooder transition and must be + * removed. + * + * @deprecated Please supply the databaseName in {@link #Builder(String)} instead. This + * method exists only for dogfooder transition and must be removed. + */ + @Deprecated + public Builder() { + mDatabaseName = ""; + } + + /** + * Creates a new {@link SearchContext.Builder}. + * + * <p>{@link AppSearchSession} will create or open a database under the given name. + * + * <p>Databases with different names are fully separate with distinct types, namespaces, + * and data. + * + * <p>Database name cannot contain {@code '/'}. + * + * @param databaseName The name of the database. + * @throws IllegalArgumentException if the databaseName contains {@code '/'}. + */ + public Builder(@NonNull String databaseName) { + Objects.requireNonNull(databaseName); + Preconditions.checkArgument( + !databaseName.contains("/"), "Database name cannot contain '/'"); + mDatabaseName = databaseName; + } + + /** * Sets the name of the database associated with {@link AppSearchSession}. * * <p>{@link AppSearchSession} will create or open a database under the given name. @@ -164,16 +194,21 @@ public class AppSearchManager { * * <p>If not specified, defaults to the empty string. * + * <p>TODO(b/181887768): This method exists only for dogfooder transition and must be + * removed. + * * @param databaseName The name of the database. * @throws IllegalArgumentException if the databaseName contains {@code '/'}. + * @deprecated Please supply the databaseName in {@link #Builder(String)} instead. This + * method exists only for dogfooder transition and must be removed. */ + @Deprecated @NonNull public Builder setDatabaseName(@NonNull String databaseName) { Preconditions.checkState(!mBuilt, "Builder has already been used"); Objects.requireNonNull(databaseName); - if (databaseName.contains("/")) { - throw new IllegalArgumentException("Database name cannot contain '/'"); - } + Preconditions.checkArgument( + !databaseName.contains("/"), "Database name cannot contain '/'"); mDatabaseName = databaseName; return this; } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index bfc205b9f9a6..251b3c46e681 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -11615,7 +11615,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isInEmergencySmsMode(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isLteCdmaEvdoGsmWcdmaEnabled(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isMobileDataPolicyEnabled(int); - method public boolean isNrDualConnectivityEnabled(); + method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isNrDualConnectivityEnabled(); method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, android.Manifest.permission.READ_PHONE_STATE}) public boolean isOffhook(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isOpportunisticNetworkEnabled(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean isPotentialEmergencyNumber(@NonNull String); @@ -11701,6 +11701,7 @@ package android.telephony { field public static final int CALL_WAITING_STATUS_NOT_SUPPORTED = 4; // 0x4 field public static final int CALL_WAITING_STATUS_UNKNOWN_ERROR = 3; // 0x3 field public static final String CAPABILITY_ALLOWED_NETWORK_TYPES_USED = "CAPABILITY_ALLOWED_NETWORK_TYPES_USED"; + field public static final String CAPABILITY_NR_DUAL_CONNECTIVITY_CONFIGURATION_AVAILABLE = "CAPABILITY_NR_DUAL_CONNECTIVITY_CONFIGURATION_AVAILABLE"; field public static final String CAPABILITY_SECONDARY_LINK_BANDWIDTH_VISIBLE = "CAPABILITY_SECONDARY_LINK_BANDWIDTH_VISIBLE"; field public static final int CARRIER_PRIVILEGE_STATUS_ERROR_LOADING_RULES = -2; // 0xfffffffe field public static final int CARRIER_PRIVILEGE_STATUS_HAS_ACCESS = 1; // 0x1 diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 3af07635c185..0358fe56203c 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2586,9 +2586,7 @@ class ContextImpl extends Context { @Override public Context createTokenContext(@NonNull IBinder token, @NonNull Display display) { if (display == null) { - throw new UnsupportedOperationException("Token context can only be created from " - + "other visual contexts, such as Activity or one created with " - + "Context#createDisplayContext(Display)"); + throw new IllegalArgumentException("Display must not be null"); } final ContextImpl tokenContext = createBaseWindowContext(token, display); tokenContext.setResources(createWindowContextResources()); diff --git a/core/java/android/view/DragAndDropPermissions.java b/core/java/android/view/DragAndDropPermissions.java index d47604d6ec41..16204d852a3a 100644 --- a/core/java/android/view/DragAndDropPermissions.java +++ b/core/java/android/view/DragAndDropPermissions.java @@ -16,12 +16,15 @@ package android.view; +import static java.lang.Integer.toHexString; + import android.app.Activity; import android.os.Binder; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; +import android.util.Log; import com.android.internal.view.IDragAndDropPermissions; @@ -56,9 +59,28 @@ import com.android.internal.view.IDragAndDropPermissions; */ public final class DragAndDropPermissions implements Parcelable { - private final IDragAndDropPermissions mDragAndDropPermissions; + private static final String TAG = "DragAndDrop"; + private static final boolean DEBUG = false; + + /** + * Permissions for a drop can be granted in one of two ways: + * <ol> + * <li>An app can explicitly request permissions using + * {@link Activity#requestDragAndDropPermissions(DragEvent)}. In this case permissions are + * revoked automatically when then activity is destroyed. See {@link #take(IBinder)}. + * <li>The platform can request permissions on behalf of the app (e.g. in + * {@link android.widget.Editor}). In this case permissions are revoked automatically when + * the app process terminates. See {@link #takeTransient()}. + * </ol> + * + * <p>In order to implement the second case above, we create a static token object here. This + * ensures that the token stays alive for the lifetime of the app process, allowing us to + * revoke permissions when the app process terminates using {@link IBinder#linkToDeath} in + * {@code DragAndDropPermissionsHandler}. + */ + private static IBinder sAppToken; - private IBinder mTransientToken; + private final IDragAndDropPermissions mDragAndDropPermissions; /** * Create a new {@link DragAndDropPermissions} object to control the access permissions for @@ -81,30 +103,51 @@ public final class DragAndDropPermissions implements Parcelable { } /** - * Take the permissions and bind their lifetime to the activity. + * Take permissions, binding their lifetime to the activity. + * + * <p>Note: This API is exposed to apps via + * {@link Activity#requestDragAndDropPermissions(DragEvent)}. + * * @param activityToken Binder pointing to an Activity instance to bind the lifetime to. * @return True if permissions are successfully taken. + * * @hide */ public boolean take(IBinder activityToken) { try { + if (DEBUG) { + Log.d(TAG, this + ": calling take() with activity-bound token: " + + toHexString(activityToken.hashCode())); + } mDragAndDropPermissions.take(activityToken); } catch (RemoteException e) { + Log.w(TAG, this + ": take() failed with a RemoteException", e); return false; } return true; } /** - * Take the permissions. Must call {@link #release} explicitly. + * Take permissions transiently. Permissions will be revoked when the app process terminates. + * + * <p>Note: This API is not exposed to apps. + * * @return True if permissions are successfully taken. + * * @hide */ public boolean takeTransient() { try { - mTransientToken = new Binder(); - mDragAndDropPermissions.takeTransient(mTransientToken); + if (sAppToken == null) { + sAppToken = new Binder(); + } + if (DEBUG) { + Log.d(TAG, this + ": calling takeTransient() with process-bound token: " + + toHexString(sAppToken.hashCode())); + } + mDragAndDropPermissions.takeTransient(sAppToken); } catch (RemoteException e) { + Log.w(TAG, this + ": takeTransient() failed with a RemoteException", e); return false; } return true; @@ -116,8 +159,8 @@ public final class DragAndDropPermissions implements Parcelable { public void release() { try { mDragAndDropPermissions.release(); - mTransientToken = null; } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); } } @@ -142,11 +185,9 @@ public final class DragAndDropPermissions implements Parcelable { @Override public void writeToParcel(Parcel destination, int flags) { destination.writeStrongInterface(mDragAndDropPermissions); - destination.writeStrongBinder(mTransientToken); } private DragAndDropPermissions(Parcel in) { mDragAndDropPermissions = IDragAndDropPermissions.Stub.asInterface(in.readStrongBinder()); - mTransientToken = in.readStrongBinder(); } } diff --git a/core/java/android/view/RoundedCorner.java b/core/java/android/view/RoundedCorner.java index cc7525b48aa2..56b438350d06 100644 --- a/core/java/android/view/RoundedCorner.java +++ b/core/java/android/view/RoundedCorner.java @@ -163,7 +163,7 @@ public final class RoundedCorner implements Parcelable { * @hide */ public boolean isEmpty() { - return mRadius == 0 || mCenter.x == 0 || mCenter.y == 0; + return mRadius == 0 || mCenter.x <= 0 || mCenter.y <= 0; } private String getPositionString(@Position int position) { diff --git a/core/java/android/view/RoundedCorners.java b/core/java/android/view/RoundedCorners.java index 569c287901c7..623d9692ac80 100644 --- a/core/java/android/view/RoundedCorners.java +++ b/core/java/android/view/RoundedCorners.java @@ -181,16 +181,16 @@ public class RoundedCorners implements Parcelable { boolean hasRoundedCorner; switch (position) { case POSITION_TOP_LEFT: - hasRoundedCorner = radius > insetTop || radius > insetLeft; + hasRoundedCorner = radius > insetTop && radius > insetLeft; break; case POSITION_TOP_RIGHT: - hasRoundedCorner = radius > insetTop || radius > insetRight; + hasRoundedCorner = radius > insetTop && radius > insetRight; break; case POSITION_BOTTOM_RIGHT: - hasRoundedCorner = radius > insetBottom || radius > insetRight; + hasRoundedCorner = radius > insetBottom && radius > insetRight; break; case POSITION_BOTTOM_LEFT: - hasRoundedCorner = radius > insetBottom || radius > insetLeft; + hasRoundedCorner = radius > insetBottom && radius > insetLeft; break; default: throw new IllegalArgumentException( diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index ca8967794c81..238ce85622b5 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -2904,9 +2904,6 @@ public class Editor { } finally { mTextView.endBatchEdit(); mUndoInputFilter.freezeLastEdit(); - if (permissions != null) { - permissions.release(); - } } } diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index d59a415469b6..2464b4af23eb 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -26,6 +26,7 @@ import android.text.style.SpellCheckSpan; import android.text.style.SuggestionSpan; import android.util.Log; import android.util.LruCache; +import android.util.Range; import android.view.textservice.SentenceSuggestionsInfo; import android.view.textservice.SpellCheckerSession; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; @@ -62,7 +63,8 @@ public class SpellChecker implements SpellCheckerSessionListener { // Pause between each spell check to keep the UI smooth private final static int SPELL_PAUSE_DURATION = 400; // milliseconds - private static final int MIN_SENTENCE_LENGTH = 50; + // The maximum length of sentence. + private static final int MAX_SENTENCE_LENGTH = WORD_ITERATOR_INTERVAL; private static final int USE_SPAN_RANGE = -1; @@ -89,7 +91,7 @@ public class SpellChecker implements SpellCheckerSessionListener { // Shared by all SpellParsers. Cannot be shared with TextView since it may be used // concurrently due to the asynchronous nature of onGetSuggestions. - private WordIterator mWordIterator; + private SentenceIteratorWrapper mSentenceIterator; @Nullable private TextServicesManager mTextServicesManager; @@ -151,8 +153,9 @@ public class SpellChecker implements SpellCheckerSessionListener { resetSession(); if (locale != null) { - // Change SpellParsers' wordIterator locale - mWordIterator = new WordIterator(locale); + // Change SpellParsers' sentenceIterator locale + mSentenceIterator = new SentenceIteratorWrapper( + BreakIterator.getSentenceInstance(locale)); } // This class is the listener for locale change: warn other locale-aware objects @@ -306,22 +309,30 @@ public class SpellChecker implements SpellCheckerSessionListener { final int start = editable.getSpanStart(spellCheckSpan); final int end = editable.getSpanEnd(spellCheckSpan); - // Do not check this word if the user is currently editing it - final boolean isEditing; + // Check the span if any of following conditions is met: + // - the user is not currently editing it + // - or `forceCheckWhenEditingWord` is true. + final boolean isNotEditing; // Defer spell check when typing a word ending with a punctuation like an apostrophe // which could end up being a mid-word punctuation. if (selectionStart == end + 1 && WordIterator.isMidWordPunctuation( mCurrentLocale, Character.codePointBefore(editable, end + 1))) { - isEditing = false; - } else { + isNotEditing = false; + } else if (selectionEnd <= start || selectionStart > end) { // Allow the overlap of the cursor and the first boundary of the spell check span // no to skip the spell check of the following word because the // following word will never be spell-checked even if the user finishes composing - isEditing = selectionEnd <= start || selectionStart > end; + isNotEditing = true; + } else { + // When cursor is at the end of spell check span, allow spell check if the + // character before cursor is a separator. + isNotEditing = selectionStart == end + && selectionStart > 0 + && isSeparator(Character.codePointBefore(editable, selectionStart)); } - if (start >= 0 && end > start && (forceCheckWhenEditingWord || isEditing)) { + if (start >= 0 && end > start && (forceCheckWhenEditingWord || isNotEditing)) { spellCheckSpan.setSpellCheckInProgress(true); final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]); textInfos[textInfosCount++] = textInfo; @@ -346,6 +357,19 @@ public class SpellChecker implements SpellCheckerSessionListener { } } + private static boolean isSeparator(int codepoint) { + final int type = Character.getType(codepoint); + return ((1 << type) & ((1 << Character.SPACE_SEPARATOR) + | (1 << Character.LINE_SEPARATOR) + | (1 << Character.PARAGRAPH_SEPARATOR) + | (1 << Character.DASH_PUNCTUATION) + | (1 << Character.END_PUNCTUATION) + | (1 << Character.FINAL_QUOTE_PUNCTUATION) + | (1 << Character.INITIAL_QUOTE_PUNCTUATION) + | (1 << Character.START_PUNCTUATION) + | (1 << Character.OTHER_PUNCTUATION))) != 0; + } + private SpellCheckSpan onGetSuggestionsInternal( SuggestionsInfo suggestionsInfo, int offset, int length) { if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) { @@ -534,6 +558,60 @@ public class SpellChecker implements SpellCheckerSessionListener { mTextView.invalidateRegion(start, end, false /* No cursor involved */); } + /** + * A wrapper of sentence iterator which only processes the specified window of the given text. + */ + private static class SentenceIteratorWrapper { + private BreakIterator mSentenceIterator; + private int mStartOffset; + private int mEndOffset; + + SentenceIteratorWrapper(BreakIterator sentenceIterator) { + mSentenceIterator = sentenceIterator; + } + + /** + * Set the char sequence and the text window to process. + */ + public void setCharSequence(CharSequence sequence, int start, int end) { + mStartOffset = Math.max(0, start); + mEndOffset = Math.min(end, sequence.length()); + mSentenceIterator.setText(sequence.subSequence(mStartOffset, mEndOffset).toString()); + } + + /** + * See {@link BreakIterator#preceding(int)} + */ + public int preceding(int offset) { + if (offset < mStartOffset) { + return BreakIterator.DONE; + } + int result = mSentenceIterator.preceding(offset - mStartOffset); + return result == BreakIterator.DONE ? BreakIterator.DONE : result + mStartOffset; + } + + /** + * See {@link BreakIterator#following(int)} + */ + public int following(int offset) { + if (offset > mEndOffset) { + return BreakIterator.DONE; + } + int result = mSentenceIterator.following(offset - mStartOffset); + return result == BreakIterator.DONE ? BreakIterator.DONE : result + mStartOffset; + } + + /** + * See {@link BreakIterator#isBoundary(int)} + */ + public boolean isBoundary(int offset) { + if (offset < mStartOffset || offset > mEndOffset) { + return false; + } + return mSentenceIterator.isBoundary(offset - mStartOffset); + } + } + private class SpellParser { private Object mRange = new Object(); @@ -582,27 +660,15 @@ public class SpellChecker implements SpellCheckerSessionListener { public void parse() { Editable editable = (Editable) mTextView.getText(); - // Iterate over the newly added text and schedule new SpellCheckSpans - final int start = Math.max( - 0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH); - - final int end = editable.getSpanEnd(mRange); - - int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL); - mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd); - - // Move back to the beginning of the current word, if any - int wordStart = mWordIterator.preceding(start); - int wordEnd; - if (wordStart == BreakIterator.DONE) { - wordEnd = mWordIterator.following(start); - if (wordEnd != BreakIterator.DONE) { - wordStart = mWordIterator.getBeginning(wordEnd); - } - } else { - wordEnd = mWordIterator.getEnd(wordStart); - } - if (wordEnd == BreakIterator.DONE) { + final int textChangeStart = editable.getSpanStart(mRange); + final int textChangeEnd = editable.getSpanEnd(mRange); + + Range<Integer> sentenceBoundary = detectSentenceBoundary(editable, textChangeStart, + textChangeEnd); + int sentenceStart = sentenceBoundary.getLower(); + int sentenceEnd = sentenceBoundary.getUpper(); + + if (sentenceStart == sentenceEnd) { if (DBG) { Log.i(TAG, "No more spell check."); } @@ -612,29 +678,16 @@ public class SpellChecker implements SpellCheckerSessionListener { boolean scheduleOtherSpellCheck = false; - if (wordIteratorWindowEnd < end) { + if (sentenceEnd < textChangeEnd) { if (DBG) { Log.i(TAG, "schedule other spell check."); } // Several batches needed on that region. Cut after last previous word scheduleOtherSpellCheck = true; } - int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd); - boolean correct = spellCheckEnd != BreakIterator.DONE; - if (correct) { - spellCheckEnd = mWordIterator.getEnd(spellCheckEnd); - correct = spellCheckEnd != BreakIterator.DONE; - } - if (!correct) { - if (DBG) { - Log.i(TAG, "Incorrect range span."); - } - stop(); - return; - } + int spellCheckEnd = sentenceEnd; do { - // TODO: Find the start position of the sentence. - int spellCheckStart = wordStart; + int spellCheckStart = sentenceStart; boolean createSpellCheckSpan = true; // Cancel or merge overlapped spell check spans for (int i = 0; i < mLength; ++i) { @@ -671,27 +724,23 @@ public class SpellChecker implements SpellCheckerSessionListener { } // Stop spell checking when there are no characters in the range. - if (spellCheckEnd < start) { - break; - } if (spellCheckEnd <= spellCheckStart) { Log.w(TAG, "Trying to spellcheck invalid region, from " - + start + " to " + end); + + sentenceStart + " to " + spellCheckEnd); break; } if (createSpellCheckSpan) { addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd); } } while (false); - wordStart = spellCheckEnd; - - if (scheduleOtherSpellCheck && wordStart != BreakIterator.DONE && wordStart <= end) { + sentenceStart = spellCheckEnd; + if (scheduleOtherSpellCheck && sentenceStart != BreakIterator.DONE + && sentenceStart <= textChangeEnd) { // Update range span: start new spell check from last wordStart - setRangeSpan(editable, wordStart, end); + setRangeSpan(editable, sentenceStart, textChangeEnd); } else { removeRangeSpan(editable); } - spellCheck(mForceCheckWhenEditingWord); } @@ -708,6 +757,94 @@ public class SpellChecker implements SpellCheckerSessionListener { } } + private Range<Integer> detectSentenceBoundary(CharSequence sequence, + int textChangeStart, int textChangeEnd) { + // Only process a substring of the full text due to performance concern. + final int iteratorWindowStart = findSeparator(sequence, + Math.max(0, textChangeStart - MAX_SENTENCE_LENGTH), + Math.max(0, textChangeStart - 2 * MAX_SENTENCE_LENGTH)); + final int iteratorWindowEnd = findSeparator(sequence, + Math.min(textChangeStart + 2 * MAX_SENTENCE_LENGTH, textChangeEnd), + Math.min(textChangeStart + 3 * MAX_SENTENCE_LENGTH, sequence.length())); + if (DBG) { + Log.d(TAG, "Set iterator window as [" + iteratorWindowStart + ", " + iteratorWindowEnd + + ")."); + } + mSentenceIterator.setCharSequence(sequence, iteratorWindowStart, iteratorWindowEnd); + + // Detect the offset of sentence begin/end on the substring. + int sentenceStart = mSentenceIterator.isBoundary(textChangeStart) ? textChangeStart + : mSentenceIterator.preceding(textChangeStart); + int sentenceEnd = mSentenceIterator.following(sentenceStart); + if (sentenceEnd == BreakIterator.DONE) { + sentenceEnd = iteratorWindowEnd; + } + if (DBG) { + if (sentenceStart != sentenceEnd) { + Log.d(TAG, "Sentence detected [" + sentenceStart + ", " + sentenceEnd + ")."); + } + } + + if (sentenceEnd - sentenceStart <= MAX_SENTENCE_LENGTH) { + // Add more sentences until the MAX_SENTENCE_LENGTH limitation is reached. + while (sentenceEnd < textChangeEnd) { + int nextEnd = mSentenceIterator.following(sentenceEnd); + if (nextEnd == BreakIterator.DONE + || nextEnd - sentenceStart > MAX_SENTENCE_LENGTH) { + break; + } + sentenceEnd = nextEnd; + } + } else { + // If the sentence containing `textChangeStart` is longer than MAX_SENTENCE_LENGTH, + // the sentence will be sliced into sub-sentences of about MAX_SENTENCE_LENGTH + // characters each. This is done by processing the unchecked part of that sentence : + // [textChangeStart, sentenceEnd) + // + // - If the `uncheckedLength` is bigger than MAX_SENTENCE_LENGTH, then check the + // [textChangeStart, textChangeStart + MAX_SENTENCE_LENGTH), and leave the rest + // part for the next check. + // + // - If the `uncheckedLength` is smaller than or equal to MAX_SENTENCE_LENGTH, + // then check [sentenceEnd - MAX_SENTENCE_LENGTH, sentenceEnd). + // + // The offset should be rounded up to word boundary. + int uncheckedLength = sentenceEnd - textChangeStart; + if (uncheckedLength > MAX_SENTENCE_LENGTH) { + sentenceEnd = findSeparator(sequence, sentenceStart + MAX_SENTENCE_LENGTH, + sentenceEnd); + sentenceStart = roundUpToWordStart(sequence, textChangeStart, sentenceStart); + } else { + sentenceStart = roundUpToWordStart(sequence, sentenceEnd - MAX_SENTENCE_LENGTH, + sentenceStart); + } + } + return new Range(sentenceStart, sentenceEnd); + } + + private int roundUpToWordStart(CharSequence sequence, int position, int frontBoundary) { + if (isSeparator(sequence.charAt(position))) { + return position; + } + int separator = findSeparator(sequence, position, frontBoundary); + return separator != frontBoundary ? separator + 1 : frontBoundary; + } + + /** + * Search the range [start, end) of sequence and returns the position of the first separator. + * If end is smaller than start, do a reverse search. + * Returns `end` if no separator is found. + */ + private static int findSeparator(CharSequence sequence, int start, int end) { + final int step = start < end ? 1 : -1; + for (int i = start; i != end; i += step) { + if (isSeparator(sequence.charAt(i))) { + return i; + } + } + return end; + } + public static boolean haveWordBoundariesChanged(final Editable editable, final int start, final int end, final int spanStart, final int spanEnd) { final boolean haveWordBoundariesChanged; diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java index d0c807d24b14..783d088b19b9 100644 --- a/core/java/com/android/internal/view/IInputConnectionWrapper.java +++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java @@ -221,10 +221,6 @@ public final class IInputConnectionWrapper extends IInputContext.Stub { dispatchMessage(mH.obtainMessage(DO_GET_SURROUNDING_TEXT, flags, 0 /* unused */, args)); } - public void setImeTemporarilyConsumesInput(boolean imeTemporarilyConsumesInput) { - // no-op - } - public void getCursorCapsMode(int reqModes, IIntResultCallback callback) { dispatchMessage( mH.obtainMessage(DO_GET_CURSOR_CAPS_MODE, reqModes, 0 /* unused */, callback)); diff --git a/core/tests/coretests/src/android/view/RoundedCornerTest.java b/core/tests/coretests/src/android/view/RoundedCornerTest.java index 8eb13bc03b4b..43490213c29a 100644 --- a/core/tests/coretests/src/android/view/RoundedCornerTest.java +++ b/core/tests/coretests/src/android/view/RoundedCornerTest.java @@ -62,6 +62,13 @@ public class RoundedCornerTest { } @Test + public void testIsEmpty_negativeCenter() { + RoundedCorner roundedCorner = + new RoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT, 1, -2, -3); + assertThat(roundedCorner.isEmpty(), is(true)); + } + + @Test public void testEquals() { RoundedCorner roundedCorner = new RoundedCorner( RoundedCorner.POSITION_BOTTOM_LEFT, 2, 3, 4); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java index cb54021d7a23..3708e151c372 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -119,7 +119,7 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { listeners.get(i).onDisplayAreaAppeared(displayAreaInfo); } } - applyConfigChangesToContext(displayId, displayAreaInfo.configuration); + applyConfigChangesToContext(displayAreaInfo); } @Override @@ -161,24 +161,27 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { listeners.get(i).onDisplayAreaInfoChanged(displayAreaInfo); } } - applyConfigChangesToContext(displayId, displayAreaInfo.configuration); + applyConfigChangesToContext(displayAreaInfo); } /** - * Applies the {@link Configuration} to the {@link DisplayAreaContext} specified by - * {@code displayId}. - * - * @param displayId The ID of the {@link Display} which the {@link DisplayAreaContext} is - * associated with - * @param newConfig The propagated configuration + * Applies the {@link DisplayAreaInfo} to the {@link DisplayAreaContext} specified by + * {@link DisplayAreaInfo#displayId}. */ - private void applyConfigChangesToContext(int displayId, @NonNull Configuration newConfig) { + private void applyConfigChangesToContext(@NonNull DisplayAreaInfo displayAreaInfo) { + final int displayId = displayAreaInfo.displayId; + final Display display = mContext.getSystemService(DisplayManager.class) + .getDisplay(displayId); + if (display == null) { + throw new UnsupportedOperationException("The display #" + displayId + " is invalid." + + "displayAreaInfo:" + displayAreaInfo); + } DisplayAreaContext daContext = mDisplayAreaContexts.get(displayId); if (daContext == null) { - daContext = new DisplayAreaContext(mContext, displayId); + daContext = new DisplayAreaContext(mContext, display); mDisplayAreaContexts.put(displayId, daContext); } - daContext.updateConfigurationChanges(newConfig); + daContext.updateConfigurationChanges(displayAreaInfo.configuration); } /** @@ -228,10 +231,8 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { private final IBinder mToken = new Binder(); private final ResourcesManager mResourcesManager = ResourcesManager.getInstance(); - public DisplayAreaContext(@NonNull Context context, int displayId) { + public DisplayAreaContext(@NonNull Context context, @NonNull Display display) { super(null); - final Display display = context.getSystemService(DisplayManager.class) - .getDisplay(displayId); attachBaseContext(context.createTokenContext(mToken, display)); } diff --git a/packages/Connectivity/framework/src/android/net/VpnTransportInfo.java b/packages/Connectivity/framework/src/android/net/VpnTransportInfo.java index 340141b78aa5..c5100794899a 100644 --- a/packages/Connectivity/framework/src/android/net/VpnTransportInfo.java +++ b/packages/Connectivity/framework/src/android/net/VpnTransportInfo.java @@ -42,9 +42,9 @@ public final class VpnTransportInfo implements TransportInfo, Parcelable { MessageUtils.findMessageNames(new Class[]{VpnManager.class}, new String[]{"TYPE_VPN_"}); /** Type of this VPN. */ - @VpnManager.VpnType public final int type; + public final int type; - public VpnTransportInfo(@VpnManager.VpnType int type) { + public VpnTransportInfo(int type) { this.type = type; } diff --git a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java index 781bfcdbc75e..1ccf4175bac5 100644 --- a/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java +++ b/packages/SettingsLib/AppPreference/src/com/android/settingslib/widget/AppSwitchPreference.java @@ -51,6 +51,7 @@ public class AppSwitchPreference extends SwitchPreference { @Override public void onBindViewHolder(PreferenceViewHolder holder) { + setSingleLineTitle(true); super.onBindViewHolder(holder); final View switchView = holder.findViewById(android.R.id.switch_widget); if (switchView != null) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java index 8fd1910041c8..129aca4a689f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java @@ -386,7 +386,6 @@ public class BluetoothEventManager { case BluetoothDevice.UNBOND_REASON_AUTH_TIMEOUT: case BluetoothDevice.UNBOND_REASON_REPEATED_ATTEMPTS: case BluetoothDevice.UNBOND_REASON_REMOTE_AUTH_CANCELED: - case BluetoothDevice.UNBOND_REASON_REMOVED: errorMsg = R.string.bluetooth_pairing_error_message; break; default: diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java index 6a4d650ebf31..4bff78f4af2c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java @@ -353,21 +353,6 @@ public class BluetoothEventManagerTest { } @Test - public void showUnbondMessage_reasonRemoved_showCorrectedErrorCode() { - mIntent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED); - mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice); - mIntent.putExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); - mIntent.putExtra(BluetoothDevice.EXTRA_REASON, BluetoothDevice.UNBOND_REASON_REMOVED); - when(mCachedDeviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedDevice1); - when(mCachedDevice1.getName()).thenReturn(DEVICE_NAME); - - mContext.sendBroadcast(mIntent); - - verify(mErrorListener).onShowError(any(Context.class), eq(DEVICE_NAME), - eq(R.string.bluetooth_pairing_error_message)); - } - - @Test public void showUnbondMessage_reasonAuthTimeout_showCorrectedErrorCode() { mIntent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED); mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 426e63181ff1..9f6087834a4e 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1862,6 +1862,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Update application display metrics. final WmDisplayCutout wmDisplayCutout = calculateDisplayCutoutForRotation(rotation); final DisplayCutout displayCutout = wmDisplayCutout.getDisplayCutout(); + final RoundedCorners roundedCorners = calculateRoundedCornersForRotation(rotation); final int appWidth = mDisplayPolicy.getNonDecorDisplayWidth(dw, dh, rotation, uiMode, displayCutout); @@ -1878,6 +1879,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null); } mDisplayInfo.displayCutout = displayCutout.isEmpty() ? null : displayCutout; + mDisplayInfo.roundedCorners = roundedCorners; mDisplayInfo.getAppMetrics(mDisplayMetrics); if (mDisplayScalingDisabled) { mDisplayInfo.flags |= Display.FLAG_SCALING_DISABLED; diff --git a/services/core/java/com/android/server/wm/DragAndDropPermissionsHandler.java b/services/core/java/com/android/server/wm/DragAndDropPermissionsHandler.java index 999c5859ba1d..62e4a8547600 100644 --- a/services/core/java/com/android/server/wm/DragAndDropPermissionsHandler.java +++ b/services/core/java/com/android/server/wm/DragAndDropPermissionsHandler.java @@ -16,12 +16,15 @@ package com.android.server.wm; +import static java.lang.Integer.toHexString; + import android.app.UriGrantsManager; import android.content.ClipData; import android.net.Uri; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import android.util.Log; import com.android.internal.view.IDragAndDropPermissions; import com.android.server.LocalServices; @@ -32,6 +35,9 @@ import java.util.ArrayList; class DragAndDropPermissionsHandler extends IDragAndDropPermissions.Stub implements IBinder.DeathRecipient { + private static final String TAG = "DragAndDrop"; + private static final boolean DEBUG = false; + private final WindowManagerGlobalLock mGlobalLock; private final int mSourceUid; private final String mTargetPackage; @@ -43,7 +49,7 @@ class DragAndDropPermissionsHandler extends IDragAndDropPermissions.Stub private IBinder mActivityToken = null; private IBinder mPermissionOwnerToken = null; - private IBinder mTransientToken = null; + private IBinder mAppToken = null; DragAndDropPermissionsHandler(WindowManagerGlobalLock lock, ClipData clipData, int sourceUid, String targetPackage, int mode, int sourceUserId, int targetUserId) { @@ -62,6 +68,10 @@ class DragAndDropPermissionsHandler extends IDragAndDropPermissions.Stub if (mActivityToken != null || mPermissionOwnerToken != null) { return; } + if (DEBUG) { + Log.d(TAG, this + ": taking permissions bound to activity: " + + toHexString(activityToken.hashCode())); + } mActivityToken = activityToken; // Will throw if Activity is not found. @@ -84,14 +94,18 @@ class DragAndDropPermissionsHandler extends IDragAndDropPermissions.Stub } @Override - public void takeTransient(IBinder transientToken) throws RemoteException { + public void takeTransient(IBinder appToken) throws RemoteException { if (mActivityToken != null || mPermissionOwnerToken != null) { return; } + if (DEBUG) { + Log.d(TAG, this + ": taking permissions bound to app process: " + + toHexString(appToken.hashCode())); + } mPermissionOwnerToken = LocalServices.getService(UriGrantsManagerInternal.class) .newUriPermissionOwner("drop"); - mTransientToken = transientToken; - mTransientToken.linkToDeath(this, 0); + mAppToken = appToken; + mAppToken.linkToDeath(this, 0); doTake(mPermissionOwnerToken); } @@ -112,11 +126,17 @@ class DragAndDropPermissionsHandler extends IDragAndDropPermissions.Stub } finally { mActivityToken = null; } + if (DEBUG) { + Log.d(TAG, this + ": releasing activity-bound permissions"); + } } else { permissionOwner = mPermissionOwnerToken; mPermissionOwnerToken = null; - mTransientToken.unlinkToDeath(this, 0); - mTransientToken = null; + mAppToken.unlinkToDeath(this, 0); + mAppToken = null; + if (DEBUG) { + Log.d(TAG, this + ": releasing process-bound permissions"); + } } UriGrantsManagerInternal ugm = LocalServices.getService(UriGrantsManagerInternal.class); @@ -139,6 +159,9 @@ class DragAndDropPermissionsHandler extends IDragAndDropPermissions.Stub @Override public void binderDied() { + if (DEBUG) { + Log.d(TAG, this + ": app process died: " + toHexString(mAppToken.hashCode())); + } try { release(); } catch (RemoteException e) { diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 4dc6c7ce35cf..3084b144c9b9 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -14634,6 +14634,10 @@ public class TelephonyManager { /** * Enable/Disable E-UTRA-NR Dual Connectivity. * + * This api is supported only if + * {@link android.telephony.TelephonyManager#isRadioInterfaceCapabilitySupported} + * ({@link TelephonyManager#CAPABILITY_NR_DUAL_CONNECTIVITY_CONFIGURATION_AVAILABLE}) + * returns true. * @param nrDualConnectivityState expected NR dual connectivity state * This can be passed following states * <ol> @@ -14648,6 +14652,9 @@ public class TelephonyManager { */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) + @RequiresFeature( + enforcement = "android.telephony.TelephonyManager#isRadioInterfaceCapabilitySupported", + value = TelephonyManager.CAPABILITY_NR_DUAL_CONNECTIVITY_CONFIGURATION_AVAILABLE) public @EnableNrDualConnectivityResult int setNrDualConnectivityState( @NrDualConnectivityState int nrDualConnectivityState) { try { @@ -14667,15 +14674,21 @@ public class TelephonyManager { /** * Is E-UTRA-NR Dual Connectivity enabled. + * This api is supported only if + * {@link android.telephony.TelephonyManager#isRadioInterfaceCapabilitySupported} + * ({@link TelephonyManager#CAPABILITY_NR_DUAL_CONNECTIVITY_CONFIGURATION_AVAILABLE}) + * returns true. * @return true if dual connectivity is enabled else false. Enabled state does not mean dual * connectivity is active. It means the device is allowed to connect to both primary and * secondary cell. - * <p>Requires Permission: - * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE READ_PRIVILEGED_PHONE_STATE} * @throws IllegalStateException if the Telephony process is not currently available. * @hide */ @SystemApi + @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature( + enforcement = "android.telephony.TelephonyManager#isRadioInterfaceCapabilitySupported", + value = TelephonyManager.CAPABILITY_NR_DUAL_CONNECTIVITY_CONFIGURATION_AVAILABLE) public boolean isNrDualConnectivityEnabled() { try { ITelephony telephony = getITelephony(); @@ -14927,11 +14940,23 @@ public class TelephonyManager { public static final String CAPABILITY_ALLOWED_NETWORK_TYPES_USED = "CAPABILITY_ALLOWED_NETWORK_TYPES_USED"; + /** + * Indicates whether {@link #setNrDualConnectivityState()} and + * {@link #isNrDualConnectivityEnabled()} ()} are available. See comments + * on respective methods for more information. + * + * @hide + */ + @SystemApi + public static final String CAPABILITY_NR_DUAL_CONNECTIVITY_CONFIGURATION_AVAILABLE = + "CAPABILITY_NR_DUAL_CONNECTIVITY_CONFIGURATION_AVAILABLE"; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @StringDef(prefix = "CAPABILITY_", value = { CAPABILITY_SECONDARY_LINK_BANDWIDTH_VISIBLE, CAPABILITY_ALLOWED_NETWORK_TYPES_USED, + CAPABILITY_NR_DUAL_CONNECTIVITY_CONFIGURATION_AVAILABLE }) public @interface RadioInterfaceCapability {} |