Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e363f427 authored by Abodunrinwa Toki's avatar Abodunrinwa Toki Committed by Android (Google) Code Review
Browse files

Merge "Implement TextClassifierImpl.getLinks"

parents 87794464 6b766752
Loading
Loading
Loading
Loading
+232 −33
Original line number Diff line number Diff line
@@ -17,20 +17,34 @@
package android.view.textclassifier;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.icu.text.BreakIterator;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.SmartSelection;
import android.text.Spannable;
import android.text.TextUtils;
import android.text.method.WordIterator;
import android.text.style.ClickableSpan;
import android.text.util.Linkify;
import android.util.Log;
import android.view.View;

import com.android.internal.util.Preconditions;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Default implementation of the {@link TextClassifier} interface.
@@ -99,29 +113,7 @@ final class TextClassifierImpl implements TextClassifier {
                    type = type.toLowerCase().trim();
                    // TODO: Added this log for debug only. Remove before release.
                    Log.d(LOG_TAG, String.format("Classification type: %s", type));
                    final Intent intent;
                    final String title;
                    switch (type) {
                        case TextClassifier.TYPE_EMAIL:
                            intent = new Intent(Intent.ACTION_SENDTO);
                            intent.setData(Uri.parse(String.format("mailto:%s", text)));
                            title = mContext.getString(com.android.internal.R.string.email);
                            return createClassificationResult(classified, type, intent, title);
                        case TextClassifier.TYPE_PHONE:
                            intent = new Intent(Intent.ACTION_DIAL);
                            intent.setData(Uri.parse(String.format("tel:%s", text)));
                            title = mContext.getString(com.android.internal.R.string.dial);
                            return createClassificationResult(classified, type, intent, title);
                        case TextClassifier.TYPE_ADDRESS:
                            intent = new Intent(Intent.ACTION_VIEW);
                            intent.setData(Uri.parse(String.format("geo:0,0?q=%s", text)));
                            title = mContext.getString(com.android.internal.R.string.map);
                            return createClassificationResult(classified, type, intent, title);
                        default:
                            // No classification type found. Return a no-op result.
                            break;
                        // TODO: Add other classification types.
                    }
                    return createClassificationResult(type, classified);
                }
            }
        } catch (Throwable t) {
@@ -132,9 +124,18 @@ final class TextClassifierImpl implements TextClassifier {
        return TextClassifier.NO_OP.getTextClassificationResult(text, startIndex, endIndex);
    }


    @Override
    public LinksInfo getLinks(@NonNull CharSequence text, int linkMask) {
        // TODO: Implement
    public LinksInfo getLinks(CharSequence text, int linkMask) {
        Preconditions.checkArgument(text != null);
        try {
            return LinksInfoFactory.create(
                    mContext, getSmartSelection(), text.toString(), linkMask);
        } catch (Throwable t) {
            // Avoid throwing from this method. Log the error.
            Log.e(LOG_TAG, "Error getting links info.", t);
        }
        // Getting here means something went wrong, return a NO_OP result.
        return TextClassifier.NO_OP.getLinks(text, linkMask);
    }

@@ -145,19 +146,23 @@ final class TextClassifierImpl implements TextClassifier {
        return mSmartSelection;
    }

    private TextClassificationResult createClassificationResult(
            CharSequence text, String type, Intent intent, String label) {
        TextClassificationResult.Builder builder = new TextClassificationResult.Builder()
    private TextClassificationResult createClassificationResult(String type, CharSequence text) {
        final Intent intent = IntentFactory.create(type, text.toString());
        if (intent == null) {
            return TextClassificationResult.EMPTY;
        }

        final TextClassificationResult.Builder builder = new TextClassificationResult.Builder()
                .setText(text.toString())
                .setEntityType(type, 1.0f /* confidence */)
                .setIntent(intent)
                .setOnClickListener(TextClassificationResult.createStartActivityOnClick(
                        mContext, intent))
                .setLabel(label);
        PackageManager pm = mContext.getPackageManager();
        ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
        // TODO: If the resolveInfo is the "chooser", do not set the package name and use a
        // default icon for this classification type.
                .setLabel(IntentFactory.getLabel(mContext, type));
        final PackageManager pm = mContext.getPackageManager();
        final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
        // TODO: If the resolveInfo is the "chooser", do not set the package name and use a default
        // icon for this classification type.
        intent.setPackage(resolveInfo.activityInfo.packageName);
        Drawable icon = resolveInfo.activityInfo.loadIcon(pm);
        if (icon == null) {
@@ -177,4 +182,198 @@ final class TextClassifierImpl implements TextClassifier {
        Preconditions.checkArgument(endIndex <= text.length());
        Preconditions.checkArgument(endIndex >= startIndex);
    }

    /**
     * Detects and creates links for specified text.
     */
    private static final class LinksInfoFactory {

        private LinksInfoFactory() {}

        public static LinksInfo create(
                Context context, SmartSelection smartSelection, String text, int linkMask) {
            final WordIterator wordIterator = new WordIterator();
            wordIterator.setCharSequence(text, 0, text.length());
            final List<SpanSpec> spans = new ArrayList<>();
            int start = 0;
            int end;
            while ((end = wordIterator.nextBoundary(start)) != BreakIterator.DONE) {
                final String token = text.substring(start, end);
                if (TextUtils.isEmpty(token)) {
                    continue;
                }

                final int[] selection = smartSelection.suggest(text, start, end);
                final int selectionStart = selection[0];
                final int selectionEnd = selection[1];
                if (selectionStart >= 0 && selectionEnd <= text.length()
                        && selectionStart <= selectionEnd) {
                    final String type =
                            smartSelection.classifyText(text, selectionStart, selectionEnd);
                    if (matches(type, linkMask)) {
                        final ClickableSpan span = createSpan(
                                context, type, text.substring(selectionStart, selectionEnd));
                        spans.add(new SpanSpec(selectionStart, selectionEnd, span));
                    }
                }
                start = end;
            }
            return new LinksInfoImpl(text, avoidOverlaps(spans, text));
        }

        /**
         * Returns true if the classification type matches the specified linkMask.
         */
        private static boolean matches(String type, int linkMask) {
            if ((linkMask & Linkify.PHONE_NUMBERS) != 0
                    && TextClassifier.TYPE_PHONE.equals(type)) {
                return true;
            }
            if ((linkMask & Linkify.EMAIL_ADDRESSES) != 0
                    && TextClassifier.TYPE_EMAIL.equals(type)) {
                return true;
            }
            if ((linkMask & Linkify.MAP_ADDRESSES) != 0
                    && TextClassifier.TYPE_ADDRESS.equals(type)) {
                return true;
            }
            return false;
        }

        /**
         * Trim the number of spans so that no two spans overlap.
         *
         * This algorithm first ensures that there is only one span per start index, then it
         * makes sure that no two spans overlap.
         */
        private static List<SpanSpec> avoidOverlaps(List<SpanSpec> spans, String text) {
            Collections.sort(spans, Comparator.comparingInt(span -> span.mStart));
            // Group spans by start index. Take the longest span.
            final Map<Integer, SpanSpec> reps = new LinkedHashMap<>();  // order matters.
            final int size = spans.size();
            for (int i = 0; i < size; i++) {
                final SpanSpec span = spans.get(i);
                final LinksInfoFactory.SpanSpec rep = reps.get(span.mStart);
                if (rep == null || rep.mEnd < span.mEnd) {
                    reps.put(span.mStart, span);
                }
            }
            // Avoid span intersections. Take the longer span.
            final LinkedList<SpanSpec> result = new LinkedList<>();
            for (SpanSpec rep : reps.values()) {
                if (result.isEmpty()) {
                    result.add(rep);
                    continue;
                }

                final SpanSpec last = result.getLast();
                if (rep.mStart < last.mEnd) {
                    // Spans intersect. Use the one with characters.
                    if ((rep.mEnd - rep.mStart) > (last.mEnd - last.mStart)) {
                        result.set(result.size() - 1, rep);
                    }
                } else {
                    result.add(rep);
                }
            }
            return result;
        }

        private static ClickableSpan createSpan(
                final Context context, final String type, final String text) {
            return new ClickableSpan() {
                // TODO: Style this span.
                @Override
                public void onClick(View widget) {
                    context.startActivity(IntentFactory.create(type, text));
                }
            };
        }

        /**
         * Implementation of LinksInfo that adds ClickableSpans to the specified text.
         */
        private static final class LinksInfoImpl implements LinksInfo {

            private final CharSequence mOriginalText;
            private final List<SpanSpec> mSpans;

            LinksInfoImpl(CharSequence originalText, List<SpanSpec> spans) {
                mOriginalText = originalText;
                mSpans = spans;
            }

            @Override
            public boolean apply(@NonNull CharSequence text) {
                Preconditions.checkArgument(text != null);
                if (text instanceof Spannable && mOriginalText.toString().equals(text.toString())) {
                    Spannable spannable = (Spannable) text;
                    final int size = mSpans.size();
                    for (int i = 0; i < size; i++) {
                        final SpanSpec span = mSpans.get(i);
                        spannable.setSpan(span.mSpan, span.mStart, span.mEnd, 0);
                    }
                    return true;
                }
                return false;
            }
        }

        /**
         * Span plus its start and end index.
         */
        private static final class SpanSpec {

            private final int mStart;
            private final int mEnd;
            private final ClickableSpan mSpan;

            SpanSpec(int start, int end, ClickableSpan span) {
                mStart = start;
                mEnd = end;
                mSpan = span;
            }
        }
    }

    /**
     * Creates intents based on the classification type.
     */
    private static final class IntentFactory {

        private IntentFactory() {}

        @Nullable
        public static Intent create(String type, String text) {
            switch (type) {
                case TextClassifier.TYPE_EMAIL:
                    return new Intent(Intent.ACTION_SENDTO)
                            .setData(Uri.parse(String.format("mailto:%s", text)));
                case TextClassifier.TYPE_PHONE:
                    return new Intent(Intent.ACTION_DIAL)
                            .setData(Uri.parse(String.format("tel:%s", text)));
                case TextClassifier.TYPE_ADDRESS:
                    return new Intent(Intent.ACTION_VIEW)
                            .setData(Uri.parse(String.format("geo:0,0?q=%s", text)));
                default:
                    return null;
                // TODO: Add other classification types.
            }
        }

        @Nullable
        public static String getLabel(Context context, String type) {
            switch (type) {
                case TextClassifier.TYPE_EMAIL:
                    return context.getString(com.android.internal.R.string.email);
                case TextClassifier.TYPE_PHONE:
                    return context.getString(com.android.internal.R.string.dial);
                case TextClassifier.TYPE_ADDRESS:
                    return context.getString(com.android.internal.R.string.map);
                default:
                    return null;
                // TODO: Add other classification types.
            }
        }
    }
}