chromium/android_webview/java/src/org/chromium/android_webview/selection/SamsungSelectionActionMenuHelper.java

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview.selection;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;

import androidx.annotation.IdRes;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;

import org.chromium.android_webview.R;
import org.chromium.base.ContextUtils;
import org.chromium.base.PackageManagerUtils;
import org.chromium.base.PackageUtils;
import org.chromium.content_public.browser.ContentFeatureMap;
import org.chromium.content_public.browser.SelectionMenuItem;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.common.ContentFeatures;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Helper class for customizing text selection menu items in {@link SelectionPopupController} for
 * Samsung devices.
 */
public class SamsungSelectionActionMenuHelper {
    private static final ComponentName MANAGE_APPS_COMPONENT =
            new ComponentName(
                    "com.android.settings",
                    "com.samsung.android.settings.display.SecProcessTextManageAppsFragment");
    private static final String STR_TEXT_MANAGER_APPS_RESOLVER = "process_text_manager_apps";
    private static final String TRANSLATOR_PACKAGE_NAME = "com.samsung.android.app.interpreter";

    /**
     * Android Intent size limitations prevent sending over a megabyte of data. Limit query lengths
     * to 100kB because other things may be added to the Intent.
     */
    private static final int MAX_SHARE_QUERY_LENGTH = 100000;

    /**
     * On Samsung devices, OS mandates a different ordering than stock Android, and we want to be
     * consistent. This ordering is only used on WebView.
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
        SamsungDefaultItemOrder.CUT,
        SamsungDefaultItemOrder.COPY,
        SamsungDefaultItemOrder.PASTE,
        SamsungDefaultItemOrder.TRANSLATE,
        SamsungDefaultItemOrder.PASTE_AS_PLAIN_TEXT,
        SamsungDefaultItemOrder.SELECT_ALL,
        SamsungDefaultItemOrder.SHARE,
        SamsungDefaultItemOrder.WEB_SEARCH
    })
    public @interface SamsungDefaultItemOrder {
        int CUT = 1;
        int COPY = 2;
        int PASTE = 3;
        int TRANSLATE = 4;
        int PASTE_AS_PLAIN_TEXT = 5;
        int SELECT_ALL = 6;
        int SHARE = 7;
        int WEB_SEARCH = 8;
    }

    public static void modifyDefaultMenuItems(
            List<SelectionMenuItem.Builder> menuItemBuilders,
            boolean isSelectionPassword,
            @NonNull String selectedText) {
        for (SelectionMenuItem.Builder builder : menuItemBuilders) {
            int menuItemOrder = getMenuItemOrder(builder.mId);
            if (menuItemOrder == -1) continue;
            builder.setOrderInCategory(menuItemOrder);
        }
        // TODO(crbug.com/41485684) Rewrite to have content APIs which support moving menu
        // items within groups instead of filtering our and re-adding.
        if (shouldAddTranslateMenu(selectedText, isSelectionPassword)) {
            // Get list of apps registered for text processing.
            List<ResolveInfo> textProcessActivities =
                    PackageManagerUtils.queryIntentActivities(createProcessTextIntent(), 0);
            // Identify and get ResolveInfo for Translate app.
            ResolveInfo translateResolveInfo =
                    textProcessActivities.stream()
                            .filter(
                                    resolveInfo ->
                                            resolveInfo.activityInfo.packageName.equals(
                                                    TRANSLATOR_PACKAGE_NAME))
                            .findAny()
                            .orElse(null);
            if (translateResolveInfo == null) {
                // Do not add Translate menu if resolve info is not available.
                return;
            }
            // Create menu item from Translate app resolve info and then add to default menu.
            menuItemBuilders.add(
                    new SelectionMenuItem.Builder(
                                    translateResolveInfo.loadLabel(
                                            ContextUtils.getApplicationContext()
                                                    .getPackageManager()))
                            .setId(Menu.NONE)
                            .setIcon(null)
                            .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM)
                            .setOrderInCategory(SamsungDefaultItemOrder.TRANSLATE)
                            .setClickListener(
                                    getTranslationActionClickListener(
                                            selectedText, translateResolveInfo)));
        }
    }

    public static List<SelectionMenuItem> getAdditionalTextProcessingItems() {
        List<SelectionMenuItem> additionalItemBuilderList = new ArrayList<>();
        additionalItemBuilderList.add(
                new SelectionMenuItem.Builder(R.string.actionbar_manage_apps)
                        .setId(Menu.NONE)
                        .setIcon(null)
                        .setOrderInCategory(Menu.CATEGORY_SECONDARY)
                        .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM)
                        .setClickListener(null)
                        .setIntent(createManageAppsIntent())
                        .build());
        return additionalItemBuilderList;
    }

    /**
     * Filters text processing activities. It should only be called if isManageAppsSupported returns
     * true.
     *
     * @param supportedActivities list of text processing activities to be filtered.
     * @return filtered list of text processing activities.
     */
    public static List<ResolveInfo> filterTextProcessingActivities(
            List<ResolveInfo> supportedActivities) {
        assert isManageAppsSupported();
        Context context = ContextUtils.getApplicationContext();
        String textManagerApps;
        try {
            // textManagerApps contains list of apps enabled in Manage apps fragment.
            // 1. A return value of null means user has not visited Manage apps fragment yet.
            // 2. A return value of empty means user has visited Manage apps fragment and has
            //    disabled all apps
            textManagerApps =
                    Settings.Global.getString(
                            context.getContentResolver(), STR_TEXT_MANAGER_APPS_RESOLVER);
        } catch (SecurityException e) {
            return supportedActivities;
        }
        if (textManagerApps == null || textManagerApps.isEmpty()) {
            // Return empty list if no text process menu item is enabled or user has never used
            // Manage apps, Settings#getString query on content resolver returns null in such case.
            return new ArrayList<>();
        }
        List<String> splitTextManagerApps = Arrays.asList(textManagerApps.split("#"));
        List<ResolveInfo> updatedSupportedItems = new ArrayList<>();
        for (int i = 0; i < supportedActivities.size(); i++) {
            ResolveInfo resolveInfo = supportedActivities.get(i);
            if (resolveInfo.activityInfo == null
                    || !splitTextManagerApps.contains(resolveInfo.activityInfo.packageName)) {
                continue;
            }
            updatedSupportedItems.add(resolveInfo);
        }
        return updatedSupportedItems;
    }

    public static boolean shouldUseSamsungMenuItemOrdering() {
        return Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
                && isSamsungDevice()
                && ContentFeatureMap.isEnabled(ContentFeatures.SELECTION_MENU_ITEM_MODIFICATION);
    }

    private static int getMenuItemOrder(@IdRes int id) {
        if (id == R.id.select_action_menu_cut) {
            return SamsungDefaultItemOrder.CUT;
        } else if (id == R.id.select_action_menu_copy) {
            return SamsungDefaultItemOrder.COPY;
        } else if (id == R.id.select_action_menu_paste) {
            return SamsungDefaultItemOrder.PASTE;
        } else if (id == R.id.select_action_menu_select_all) {
            return SamsungDefaultItemOrder.SELECT_ALL;
        } else if (id == R.id.select_action_menu_share) {
            return SamsungDefaultItemOrder.SHARE;
        } else if (id == R.id.select_action_menu_paste_as_plain_text) {
            return SamsungDefaultItemOrder.PASTE_AS_PLAIN_TEXT;
        } else if (id == R.id.select_action_menu_web_search) {
            return SamsungDefaultItemOrder.WEB_SEARCH;
        }
        return -1;
    }

    public static boolean isManageAppsSupported() {
        if (!isSamsungDevice()
                || Build.VERSION.SDK_INT != Build.VERSION_CODES.UPSIDE_DOWN_CAKE
                || !ContentFeatureMap.isEnabled(ContentFeatures.SELECTION_MENU_ITEM_MODIFICATION)) {
            return false;
        }
        Context context = ContextUtils.getApplicationContext();
        List<ResolveInfo> list =
                context.getPackageManager()
                        .queryIntentActivities(
                                createManageAppsIntent(), PackageManager.MATCH_DEFAULT_ONLY);
        return list.size() > 0;
    }

    private static Intent createManageAppsIntent() {
        return new Intent().setComponent(MANAGE_APPS_COMPONENT);
    }

    private static boolean isSamsungDevice() {
        return "SAMSUNG".equalsIgnoreCase(Build.MANUFACTURER);
    }

    private static boolean shouldAddTranslateMenu(
            @NonNull String selectedText, boolean isSelectionPassword) {
        return isManageAppsSupported()
                && PackageUtils.isPackageInstalled(TRANSLATOR_PACKAGE_NAME)
                && !selectedText.isEmpty()
                && !isSelectionPassword;
    }

    private static View.OnClickListener getTranslationActionClickListener(
            @NonNull String selectedText, ResolveInfo info) {
        return v -> {
            String textForProcessing =
                    (selectedText.length() >= MAX_SHARE_QUERY_LENGTH)
                            ? selectedText.substring(0, MAX_SHARE_QUERY_LENGTH) + "…"
                            : selectedText;
            Intent intent =
                    createProcessTextIntent()
                            .setClassName(info.activityInfo.packageName, info.activityInfo.name)
                            .putExtra(Intent.EXTRA_PROCESS_TEXT, textForProcessing);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            try {
                ContextUtils.getApplicationContext().startActivity(intent);
            } catch (android.content.ActivityNotFoundException ignored) {
            }
        };
    }

    private static Intent createProcessTextIntent() {
        return new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
    }
}