chromium/android_webview/nonembedded/java/src/org/chromium/android_webview/devui/ComponentsListFragment.java

// Copyright 2021 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.devui;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Switch;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;

import org.chromium.android_webview.common.services.ServiceNames;
import org.chromium.android_webview.devui.util.ComponentInfo;
import org.chromium.android_webview.devui.util.ComponentsInfoLoader;
import org.chromium.android_webview.services.ComponentsProviderPathUtil;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.AsyncTask;
import org.chromium.ui.widget.Toast;

import java.io.File;
import java.util.ArrayList;
import java.util.Locale;

/**
 * A fragment to show a list of WebView components.
 * <p>This feature is currently under development and therefore not exposed in the UI.
 * <p>It can be launched via the adb shell by sending an intent with fragment-id = 3.
 */
@SuppressLint("SetTextI18n")
public class ComponentsListFragment extends DevUiBaseFragment {
    private Context mContext;
    private ComponentInfoListAdapter mComponentInfoListAdapter;
    private TextView mComponentsSummaryView;
    private Toast mUpdatingToast;
    private Toast mUpdatedToast;
    private boolean mOnDemandUpdate;

    private static String sComponentUpdateServiceName;
    private static @Nullable Runnable sComponentInfoLoadedListener;
    public static final String ON_DEMAND_UPDATE_REQUEST = "ON_DEMAND_UPDATE_REQUEST";
    public static final String SERVICE_FINISH_CALLBACK = "SERVICE_FINISH_CALLBACK";

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        mContext = context;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        sComponentUpdateServiceName = ServiceNames.AW_COMPONENT_UPDATE_SERVICE;
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
    }

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_components_list, null);
    }

    @Override
    @SuppressWarnings({"UseSwitchCompatOrMaterialCode"})
    public void onViewCreated(View view, Bundle savedInstanceState) {
        Activity activity = (Activity) mContext;
        activity.setTitle("WebView Components");
        mComponentsSummaryView = view.findViewById(R.id.components_summary_textview);
        ListView mComponentInfoListView = view.findViewById(R.id.components_list);
        mComponentInfoListAdapter = new ComponentInfoListAdapter(new ArrayList<ComponentInfo>());
        mComponentInfoListView.setAdapter(mComponentInfoListAdapter);
        updateComponentInfoList(/* showToast= */ false);
        mUpdatingToast = Toast.makeText(mContext, "Updating Components...", Toast.LENGTH_SHORT);
        mUpdatedToast = Toast.makeText(mContext, "Components Updated!", Toast.LENGTH_SHORT);

        // On-Demand Update enabled by default
        mOnDemandUpdate = true;
        Switch onDemandUpdateToggle = (Switch) view.findViewById(R.id.on_demand_update);
        onDemandUpdateToggle.setOnCheckedChangeListener(
                (buttonView, isChecked) -> {
                    mOnDemandUpdate = isChecked;
                });
    }

    /**
     * An ArrayAdapter to show a list of {@code ComponentInfo} objects.
     * It uses custom item layout {@code R.layout.components_list_item} which has two {@code
     * TextView}; {@code text1} acts as the component name and {@code text2} as the component
     * version.
     */
    private class ComponentInfoListAdapter extends ArrayAdapter<ComponentInfo> {
        public ComponentInfoListAdapter(ArrayList<ComponentInfo> items) {
            super(mContext, R.layout.components_list_item, items);
        }

        @Override
        public View getView(int position, View view, ViewGroup parent) {
            // If the the old view is already created then reuse it, else create a new one by layout
            // inflation.
            if (view == null) {
                view = getLayoutInflater().inflate(R.layout.components_list_item, null, true);
            }
            ComponentInfo item = getItem(position);
            TextView name = view.findViewById(android.R.id.text1);
            TextView version = view.findViewById(android.R.id.text2);

            name.setText(item.getComponentName());
            if (item.getComponentVersion().equals("")) {
                version.setText("No installed versions.");
            } else {
                version.setText("Version: " + item.getComponentVersion());
            }
            return view;
        }
    }

    /**
     * Asynchronously load components info on a background thread and then update the UI when the
     * data is loaded.
     */
    public void updateComponentInfoList(Boolean showToast) {
        AsyncTask<ArrayList<ComponentInfo>> asyncTask =
                new AsyncTask<ArrayList<ComponentInfo>>() {
                    @Override
                    @WorkerThread
                    protected ArrayList<ComponentInfo> doInBackground() {
                        ComponentsInfoLoader componentInfoLoader =
                                new ComponentsInfoLoader(
                                        new File(
                                                ComponentsProviderPathUtil
                                                        .getComponentUpdateServiceDirectoryPath()));
                        ArrayList<ComponentInfo> retrievedComponentInfoList =
                                componentInfoLoader.getComponentsInfo();

                        return retrievedComponentInfoList;
                    }

                    @Override
                    protected void onPostExecute(
                            ArrayList<ComponentInfo> retrievedComponentInfoList) {
                        mComponentInfoListAdapter.clear();
                        mComponentInfoListAdapter.addAll(retrievedComponentInfoList);
                        mComponentsSummaryView.setText(
                                String.format(
                                        Locale.US,
                                        "Components (%d)",
                                        retrievedComponentInfoList.size()));

                        // show toast only if the user is viewing current fragment
                        if (showToast && ComponentsListFragment.this.isVisible()) {
                            // show toast only if it is not already showing, prevent toast spam
                            if (mUpdatedToast.getView() != null
                                    && mUpdatedToast.getView().getWindowVisibility()
                                            != View.VISIBLE) {
                                mUpdatedToast.show();
                            }
                        }

                        if (sComponentInfoLoadedListener != null) {
                            sComponentInfoLoadedListener.run();
                        }
                    }
                };
        asyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /** Notifies the caller when all ComponentInfo is reloaded in the ListView. */
    public static void setComponentInfoLoadedListenerForTesting(@Nullable Runnable listener) {
        ThreadUtils.assertOnUiThread();
        sComponentInfoLoadedListener = listener;
    }

    public static void setComponentUpdateServiceNameForTesting(String name) {
        ThreadUtils.assertOnUiThread();
        sComponentUpdateServiceName = name;
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.components_options_menu, menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.options_menu_update) {
            MainActivity.logMenuSelection(MainActivity.MenuChoice.COMPONENTS_UPDATE);
            Intent intent = new Intent();
            intent.setClassName(mContext.getPackageName(), sComponentUpdateServiceName);
            intent.putExtra(
                    SERVICE_FINISH_CALLBACK,
                    new ResultReceiver(null) {
                        @Override
                        public void onReceiveResult(int resultCode, Bundle resultData) {
                            updateComponentInfoList(/* showToast= */ true);
                        }
                    });
            intent.putExtra(ON_DEMAND_UPDATE_REQUEST, mOnDemandUpdate);
            // show toast only if the user is viewing current fragment
            if (ComponentsListFragment.this.isVisible()) {
                // show toast only if it is not already showing, prevent toast spam
                if (mUpdatingToast.getView() != null
                        && mUpdatingToast.getView().getWindowVisibility() != View.VISIBLE) {
                    mUpdatingToast.show();
                }
            }
            mContext.startService(intent);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}