chromium/chrome/android/java/src/org/chromium/chrome/browser/DefaultBrowserInfo2.java

// Copyright 2017 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.chrome.browser;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.ResolveInfo;
import android.text.TextUtils;

import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.PackageManagerUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicReference;

/**
 * A utility class for querying information about the default browser setting.
 * TODO(crbug.com/40709747): Remove DefaultBrowserInfo and replace with this.
 */
public final class DefaultBrowserInfo2 {
    /** Contains all status related to the default browser state on the device. */
    public static class DefaultInfo {
        /** Whether or not Chrome is the system browser. */
        public final boolean isChromeSystem;

        /** Whether or not Chrome is the default browser. */
        public final boolean isChromeDefault;

        /** Whether or not the default browser is the system browser. */
        public final boolean isDefaultSystem;

        /** Whether or not the user has set a default browser. */
        public final boolean hasDefault;

        /** The number of browsers installed on this device. */
        public final int browserCount;

        /** The number of system browsers installed on this device. */
        public final int systemCount;

        /** Creates an instance of the {@link DefaultInfo} class. */
        public DefaultInfo(
                boolean isChromeSystem,
                boolean isChromeDefault,
                boolean isDefaultSystem,
                boolean hasDefault,
                int browserCount,
                int systemCount) {
            this.isChromeSystem = isChromeSystem;
            this.isChromeDefault = isChromeDefault;
            this.isDefaultSystem = isDefaultSystem;
            this.hasDefault = hasDefault;
            this.browserCount = browserCount;
            this.systemCount = systemCount;
        }
    }

    private static DefaultInfoTask sDefaultInfoTask;

    /** Don't instantiate me. */
    private DefaultBrowserInfo2() {}

    /**
     * Determines various information about browsers on the system.
     * @param callback To be called with a {@link DefaultInfo} instance if possible.  Can be {@code
     *         null}.
     * @see DefaultInfo
     */
    public static void getDefaultBrowserInfo(Callback<DefaultInfo> callback) {
        ThreadUtils.checkUiThread();
        if (sDefaultInfoTask == null) sDefaultInfoTask = new DefaultInfoTask();
        sDefaultInfoTask.get(callback);
    }

    public static void setDefaultInfoForTests(DefaultInfo info) {
        DefaultInfoTask.setDefaultInfoForTests(info);
    }

    public static void clearDefaultInfoForTests() {
        DefaultInfoTask.clearDefaultInfoForTests();
    }

    private static class DefaultInfoTask extends AsyncTask<DefaultInfo> {
        private static AtomicReference<DefaultInfo> sTestInfo;

        private final ObserverList<Callback<DefaultInfo>> mObservers = new ObserverList<>();

        public static void setDefaultInfoForTests(DefaultInfo info) {
            sTestInfo = new AtomicReference<DefaultInfo>(info);
        }

        public static void clearDefaultInfoForTests() {
            sTestInfo = null;
        }

        /**
         *  Queues up {@code callback} to be notified of the result of this {@link AsyncTask}.  If
         *  the task has not been started, this will start it.  If the task is finished, this will
         *  send the result.  If the task is running this will queue the callback up until the task
         *  is done.
         *
         * @param callback The {@link Callback} to notify with the right {@link DefaultInfo}.
         */
        public void get(Callback<DefaultInfo> callback) {
            ThreadUtils.checkUiThread();

            if (getStatus() == Status.FINISHED) {
                DefaultInfo info = null;
                try {
                    info = sTestInfo == null ? get() : sTestInfo.get();
                } catch (InterruptedException | ExecutionException e) {
                    // Fail silently here since this is not a critical task.
                }

                final DefaultInfo postInfo = info;
                PostTask.postTask(TaskTraits.UI_DEFAULT, () -> callback.onResult(postInfo));
            } else {
                if (getStatus() == Status.PENDING) {
                    try {
                        executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                    } catch (RejectedExecutionException e) {
                        // Fail silently here since this is not a critical task.
                        PostTask.postTask(TaskTraits.UI_DEFAULT, () -> callback.onResult(null));
                        return;
                    }
                }
                mObservers.addObserver(callback);
            }
        }

        @Override
        protected DefaultInfo doInBackground() {
            Context context = ContextUtils.getApplicationContext();

            boolean isChromeSystem = false;
            boolean isChromeDefault = false;
            boolean isDefaultSystem = false;
            boolean hasDefault = false;
            int browserCount = 0;
            int systemCount = 0;

            // Query the default handler first.
            ResolveInfo defaultRi = PackageManagerUtils.resolveDefaultWebBrowserActivity();
            if (defaultRi != null && defaultRi.match != 0) {
                hasDefault = true;
                isChromeDefault = isSamePackage(context, defaultRi);
                isDefaultSystem = isSystemPackage(defaultRi);
            }

            // Query all other intent handlers.
            Set<String> uniquePackages = new HashSet<>();
            List<ResolveInfo> ris = PackageManagerUtils.queryAllWebBrowsersInfo();
            if (ris != null) {
                for (ResolveInfo ri : ris) {
                    String packageName = ri.activityInfo.applicationInfo.packageName;
                    if (!uniquePackages.add(packageName)) continue;

                    if (isSystemPackage(ri)) {
                        if (isSamePackage(context, ri)) isChromeSystem = true;
                        systemCount++;
                    }
                }
            }

            browserCount = uniquePackages.size();

            return new DefaultInfo(
                    isChromeSystem,
                    isChromeDefault,
                    isDefaultSystem,
                    hasDefault,
                    browserCount,
                    systemCount);
        }

        @Override
        protected void onPostExecute(DefaultInfo defaultInfo) {
            flushCallbacks(sTestInfo == null ? defaultInfo : sTestInfo.get());
        }

        @Override
        protected void onCancelled() {
            flushCallbacks(null);
        }

        private void flushCallbacks(DefaultInfo info) {
            for (Callback<DefaultInfo> callback : mObservers) callback.onResult(info);
            mObservers.clear();
        }
    }

    private static boolean isSamePackage(Context context, ResolveInfo info) {
        return TextUtils.equals(
                context.getPackageName(), info.activityInfo.applicationInfo.packageName);
    }

    private static boolean isSystemPackage(ResolveInfo info) {
        return (info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
    }
}