diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 81643e90428..5054c82e9f1 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -1135,6 +1135,36 @@ public class TextUtils {
return offset;
}
+ /** @hide */
+ public static int getOffsetStartOf(CharSequence text, int offset) {
+ if (offset == 0 || offset == text.length())
+ return offset;
+
+ char c = text.charAt(offset);
+
+ if (c >= '\uDC00' && c <= '\uDFFF') {
+ char c1 = text.charAt(offset - 1);
+
+ if (c1 >= '\uD800' && c1 <= '\uDBFF')
+ offset -= 1;
+ }
+
+ if (text instanceof Spanned) {
+ ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset,
+ ReplacementSpan.class);
+
+ for (int i = 0; i < spans.length; i++) {
+ int start = ((Spanned) text).getSpanStart(spans[i]);
+ int end = ((Spanned) text).getSpanEnd(spans[i]);
+
+ if (start < offset && end > offset)
+ offset = start;
+ }
+ }
+
+ return offset;
+ }
+
private static void readSpan(Parcel p, Spannable sp, Object o) {
sp.setSpan(o, p.readInt(), p.readInt(), p.readInt());
}
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index f34f9e6d5ce..ca0ba36faf7 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -123,4 +123,6 @@ oneway interface IWindow {
* Tell the window that it is either gaining or losing pointer capture.
*/
void dispatchPointerCaptureChanged(boolean hasCapture);
+
+ void getTextContent(float x, float y);
}
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index f6edb2bf315..9c9ce0cdc8a 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -648,4 +648,6 @@ interface IWindowManager
void hideShareScreenSurface();
void drawShareScreenBitMap(in Bitmap bg);
+
+ void getTextContent(int displayId, float x, float y);
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 5576de354c7..fe4e9444b11 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -29562,6 +29576,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
stream.addProperty("accessibility:importantForAccessibility", getImportantForAccessibility());
}
+ /** @hide */
+ public View dispatchFindView(float x, float y) {
+ return null;
+ }
+
+ /** @hide */
+ public void setFindText(String str) {
+ mFindText = str;
+ }
+
+ /** @hide */
+ public String getFindText() {
+ return mFindText;
+ }
+
+ /** @hide */
+ public int getFindTextIndex() {
+ return mFindTextIndex;
+ }
+
+ /** @hide */
+ public void setFindTextIndex(int index) {
+ mFindTextIndex = index;
+ }
+
+ private String mFindText;
+ private int mFindTextIndex = -1;
+
/**
* Determine if this view is rendered on a round wearable device and is the main view
* on the screen.
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 937bd1b34e6..164c0153f87 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -72,7 +72,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
-
+import android.webkit.WebView;
/**
* <p>
* A <code>ViewGroup</code> is a special view that can contain other views
@@ -8993,4 +8993,48 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
getChildAt(i).encode(encoder);
}
}
+
+ /**
+ * @hide
+ */
+ public View dispatchFindView(float x, float y) {
+ final int childrenCount = mChildrenCount;
+ if (childrenCount == 0 || getVisibility() != View.VISIBLE)
+ return null;
+
+ // Find a child that can receive the event.
+ // Scan children from front to back.
+ final ArrayList<View> preorderedList = buildOrderedChildList();
+ final boolean customOrder = preorderedList == null
+ && isChildrenDrawingOrderEnabled();
+ final View[] children = mChildren;
+ for (int i = childrenCount - 1; i >= 0; i--) {
+ final int childIndex = customOrder
+ ? getChildDrawingOrder(childrenCount, i) : i;
+ final View child = (preorderedList == null)
+ ? children[childIndex] : preorderedList.get(childIndex);
+ if (child.getVisibility() != View.VISIBLE) {
+ continue;
+ }
+ if (isTransformedTouchPointInView(x, y, child, null)) {
+ final float offsetX = mScrollX - child.mLeft;
+ final float offsetY = mScrollY - child.mTop;
+ float newX = x + offsetX;
+ float newY = y + offsetY;
+ View ret = child.dispatchFindView(newX, newY);
+ if (ret != null) {
+ if (preorderedList != null) preorderedList.clear();
+ return ret;
+ } else if (child instanceof ViewGroup) {
+ if (child instanceof WebView
+ || child.getClass().getName().startsWith("org.chromium.content.browser.ContentView")) {
+ if (preorderedList != null) preorderedList.clear();
+ return child;
+ }
+ }
+ }
+ }
+ if (preorderedList != null) preorderedList.clear();
+ return null;
+ }
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index c48e4e9b97e..a9a258f3f28 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -112,7 +112,7 @@ import android.view.contentcapture.ContentCaptureSession;
import android.view.contentcapture.MainContentCaptureSession;
import android.view.inputmethod.InputMethodManager;
import android.widget.Scroller;
-
+import android.widget.TextExtractor;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.IResultReceiver;
@@ -590,6 +590,8 @@ public final class ViewRootImpl implements ViewParent,
private final InputEventCompatProcessor mInputCompatProcessor;
+ private TextExtractor mTextExtractor;
+
/**
* Consistency verifier for debugging purposes.
*/
@@ -8035,6 +8037,18 @@ public final class ViewRootImpl implements ViewParent,
mHandler.sendMessage(msg);
}
+ public void getTextContent(float x, float y) {
+ synchronized (this) {
+ if (mTextExtractor != null) {
+ mTextExtractor.getTextContent(x, y);
+ }
+ }
+ }
+
+ public void setTextExtractor(TextExtractor textExtractor) {
+ mTextExtractor = textExtractor;
+ }
+
public void dispatchWindowShown() {
mHandler.sendEmptyMessage(MSG_DISPATCH_WINDOW_SHOWN);
}
@@ -8607,6 +8621,14 @@ public final class ViewRootImpl implements ViewParent,
}
}
+ @Override
+ public void getTextContent(float x, float y) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor != null) {
+ viewAncestor.getTextContent(x, y);
+ }
+ }
+
private static int checkCallingPermission(String permission) {
try {
return ActivityManager.getService().checkPermission(
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index a557bd1e191..17831d2f82f 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -418,6 +418,8 @@ public interface WindowManager extends ViewManager {
+ public void getTextContent(int displayId, float x, float y);
+
/**
* Special variation of {@link #removeView} that immediately invokes
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index 5c69c5f2185..a17c4a8550e 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -260,5 +260,14 @@ public final class WindowManagerImpl implements WindowManager {
} catch (RemoteException e) {
}
}
+
+ @Override
+ public void getTextContent(int displayId, float x, float y) {
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .getTextContent(displayId, x, y);
+ } catch (RemoteException e) {
+ }
+ }
// endregion
}
diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java
index 634cbe323d8..523bab827be 100644
--- a/core/java/android/widget/Button.java
+++ b/core/java/android/widget/Button.java
@@ -19,6 +19,7 @@ package android.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.widget.RemoteViews.RemoteView;
@@ -171,6 +172,12 @@ public class Button extends TextView {
return Button.class.getName();
}
+ /** @hide */
+ @Override
+ public View dispatchFindView(float x, float y) {
+ return null;
+ }
+
@Override
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
if (getPointerIcon() == null && isClickable() && isEnabled()) {
diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java
index be5d2211c67..92e3104bc0c 100644
--- a/core/java/android/widget/ImageView.java
+++ b/core/java/android/widget/ImageView.java
@@ -1717,6 +1717,12 @@ public class ImageView extends View {
stream.addProperty("layout:baseline", getBaseline());
}
+ /** @hide */
+ @Override
+ public View dispatchFindView(float x, float y) {
+ return null;
+ }
+
/** @hide */
@Override
@TestApi
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 36fdd1c7f7f..ead3e554dd9 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -212,6 +212,8 @@ import java.util.function.Supplier;
import android.app.ActivityThread;
import android.widget.FrameLayout;
import android.app.Application;
+import android.util.Patterns;
+import java.util.regex.Matcher;
/**
* A user interface element that displays text to the user.
@@ -839,6 +841,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// by removing it, but we would break apps targeting <= P that use it by reflection.
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
int mTextSelectHandleLeftRes;
+ private final int mMaxFindTextLength;
private Drawable mTextSelectHandleLeft;
// Note: this might be stale if setTextSelectHandleRight is used. We could simplify the code
// by removing it, but we would break apps targeting <= P that use it by reflection.
@@ -994,6 +997,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.density = res.getDisplayMetrics().density;
+ mMaxFindTextLength = 1000;
mTextPaint.setCompatibilityScaling(compat.applicationScale);
mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
@@ -13471,4 +13475,112 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
TextView.this.spanChange(buf, what, s, -1, e, -1);
}
}
+
+ private final static int EXTEND_WORD_OFFSET = 10;
+ private final static char NEW_LINE = '\n';
+
+ /** @hide */
+ @Override
+ public View dispatchFindView(float x, float y) {
+ String foundText = null;
+ int offset = getOffsetForPosition(x, y);
+ final int length = mText.length();
+ if (offset == length) offset = length - 1;
+ if (offset != -1) {
+ String stringText = mText.toString();
+ int start = stringText.lastIndexOf(NEW_LINE, offset - 1);
+ int end = stringText.indexOf(NEW_LINE, offset + 1);
+ if (start == -1 && end == -1) {
+ if (mMaxFindTextLength >= length) {
+ start = 0;
+ end = length;
+ } else {
+ boolean getBefore = offset >= mMaxFindTextLength / 2;
+ boolean getAfter = length - offset >= mMaxFindTextLength / 2;
+ if (getBefore && getAfter) {
+ start = TextUtils.getOffsetStartOf(mText, offset - mMaxFindTextLength / 2);
+ end = TextUtils.getOffsetStartOf(mText, offset + mMaxFindTextLength / 2);
+ } else if (getBefore) {
+ start = TextUtils.getOffsetStartOf(mText, length - mMaxFindTextLength);
+ end = length;
+ } else {
+ start = 0;
+ end = TextUtils.getOffsetStartOf(mText, mMaxFindTextLength);
+ }
+ }
+ } else if (start == -1) {
+ start = TextUtils.getOffsetStartOf(mText, Math.max(0, end - mMaxFindTextLength));
+ } else if (end == -1) {
+ end = TextUtils.getOffsetStartOf(mText, Math.min(length, start + mMaxFindTextLength));
+ } else {
+ if (end - start > mMaxFindTextLength) {
+ boolean getBefore = offset - start >= mMaxFindTextLength / 2;
+ boolean getAfter = end - offset >= mMaxFindTextLength / 2;
+ if (getBefore && getAfter) {
+ start = TextUtils.getOffsetStartOf(mText, offset - mMaxFindTextLength / 2);
+ end = TextUtils.getOffsetStartOf(mText, offset + mMaxFindTextLength / 2);
+ } else if (getBefore) {
+ start = TextUtils.getOffsetStartOf(mText, end - mMaxFindTextLength);
+ } else {
+ end = TextUtils.getOffsetStartOf(mText, start + mMaxFindTextLength);
+ }
+ }
+ }
+ offset -= start;
+ if (Character.isLetter(mText.charAt(start))) {
+ int i, count = 0;
+ for (i = start - 1; i >= 0 && count < EXTEND_WORD_OFFSET; --i) {
+ if (Character.isLetter(mText.charAt(i))) {
+ ++count;
+ } else {
+ break;
+ }
+ }
+ if (i < 0 || count < EXTEND_WORD_OFFSET) {
+ start -= count;
+ offset += count;
+ }
+ }
+ if (Character.isLetter(mText.charAt(end - 1))) {
+ int i, count = 0;
+ for (i = end; i < length && count < EXTEND_WORD_OFFSET; ++i) {
+ if (Character.isLetter(mText.charAt(i))) {
+ ++count;
+ } else {
+ break;
+ }
+ }
+ if (i == length || count < EXTEND_WORD_OFFSET) {
+ end += count;
+ }
+ }
+ Matcher m = Patterns.WEB_URL.matcher(mText);
+ while (m.find()) {
+ final int ps = m.start();
+ final int pe = m.end();
+ if (Linkify.sUrlMatchFilter.acceptMatch(mText, ps, pe)) {
+ if (pe > start && ps < end) {
+ final int newStart = Math.min(ps, start);
+ final int newEnd = Math.max(pe, end);
+ offset += start - newStart;
+ start = newStart;
+ end = newEnd;
+ }
+ }
+ }
+ foundText = mText.subSequence(start, end).toString();
+ } else if (length > 0) {
+ foundText = mText.subSequence(0, Math.min(length, mMaxFindTextLength)).toString();
+ }
+ mTrimmedFoundText = foundText;
+ setFindTextIndex(offset);
+ setFindText(mText.toString());
+ return this;
+ }
+
+ private String mTrimmedFoundText;
+ /** @hide */
+ public String getTrimmedFoundText() {
+ return mTrimmedFoundText;
+ }
}
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index fa83bd584fd..8a435ae323a 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -113,6 +113,7 @@ import com.android.internal.widget.DecorCaptionView;
import com.android.internal.widget.FloatingToolbar;
import java.util.List;
+import android.widget.TextExtractor;
/** @hide */
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
@@ -230,6 +231,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
private int mRootScrollY = 0;
+ private final TextExtractor mTextExtractor;
@UnsupportedAppUsage
private PhoneWindow mWindow;
@@ -286,6 +288,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
mSemiTransparentBarColor = context.getResources().getColor(
R.color.system_bar_background_semi_transparent, null /* theme */);
+ mTextExtractor = new TextExtractor(context, this);
updateAvailableWidth();
setWindow(window);
@@ -446,8 +449,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
public boolean superDispatchKeyEvent(KeyEvent event) {
// Give priority to closing action modes if applicable.
+ final int action = event.getAction();
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
- final int action = event.getAction();
// Back cancels action modes first.
if (mPrimaryActionMode != null) {
if (action == KeyEvent.ACTION_UP) {
@@ -461,6 +464,10 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
return true;
}
+ if (action == KeyEvent.ACTION_UP) {
+ mTextExtractor.handleBackKey();
+ }
+
return (getViewRootImpl() != null) && getViewRootImpl().dispatchUnhandledKeyEvent(event);
}
@@ -1739,6 +1746,13 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
// renderer about it.
mBackdropFrameRenderer.onConfigurationChange();
}
+ if (mTextExtractor != null) {
+ mTextExtractor.onAttached(mWindow.getAttributes().type);
+ }
+ ViewRootImpl vri = getViewRootImpl();
+ if (vri != null && mTextExtractor != null) {
+ vri.setTextExtractor(mTextExtractor);
+ }
mWindow.onViewRootImplSet(getViewRootImpl());
}
@@ -1766,7 +1780,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
mFloatingToolbar.dismiss();
mFloatingToolbar = null;
}
-
+ mTextExtractor.onDetached();
PhoneWindow.PanelFeatureState st = mWindow.getPanelState(Window.FEATURE_OPTIONS_PANEL, false);
if (st != null && st.menu != null && mFeatureId < 0) {
st.menu.close();
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index f9cdf3d0be6..e67687c58dd 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -145,4 +145,8 @@ public class BaseIWindow extends IWindow.Stub {
@Override
public void dispatchPointerCaptureChanged(boolean hasCapture) {
}
+
+ @Override
+ public void getTextContent(float x, float y) {
+ }
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 884c39c2427..942d094288a 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -3135,6 +3135,19 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
+ @Override
+ public void getTextContent(int displayId, float x, float y) {
+ synchronized (mGlobalLock) {
+ final DisplayContent displayContent = mRoot.getDisplayContent(displayId);
+ if (displayContent != null) {
+ final WindowState touchedWin = displayContent.getTouchableWinAtPointLocked(x, y);
+ if (touchedWin != null) {
+ touchedWin.getTextContent(x, y);
+ }
+ }
+ }
+ }
+
@Override
public void drawShareScreenBitMap(Bitmap bg) {
synchronized (mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 4fb8a6c8d28..e487f60e1f5 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -2420,6 +2420,17 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
if (614 == requestedWidth && "cn.guancha.app".equals(mAttrs.packageName)) {
mWmService.mPendingRemove.add(this);
}
}
+
+ public void getTextContent(float x, float y) {
+ try {
+ mClient.getTextContent(x, y);
+ } catch (RemoteException e) {
+ // Not a remote call, RemoteException won't be raised.
+ }
}
void prepareWindowToDisplayDuringRelayout(boolean wasVisible) {