chromium/chrome/browser/enterprise/util/android/java/src/org/chromium/chrome/browser/enterprise/util/EnterpriseInfoImpl.java

// Copyright 2022 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.enterprise.util;

import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.Callback;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.AsyncTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.flags.ChromeSwitches;

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.RejectedExecutionException;

/** The typical implementation of {@link EnterpriseInfo} at runtime. */
public class EnterpriseInfoImpl extends EnterpriseInfo {
    private static final String TAG = "EnterpriseInfoImpl";
    private final Handler mHandler;

    // Only ever read/written on the UI thread.
    private OwnedState mOwnedState;
    private Queue<Callback<OwnedState>> mCallbackList;

    private boolean mSkipAsyncCheckForTesting;

    EnterpriseInfoImpl() {
        mOwnedState = null;
        mCallbackList = new LinkedList<>();
        mHandler = new Handler(Looper.myLooper());
    }

    @Override
    @SuppressWarnings("QueryPermissionsNeeded")
    public void getDeviceEnterpriseInfo(Callback<OwnedState> callback) {
        // AsyncTask requires being called from UI thread.
        ThreadUtils.assertOnUiThread();
        assert callback != null;

        // If there is already a cached result post a task to return it.
        if (mOwnedState != null) {
            mHandler.post(() -> callback.onResult(mOwnedState));
            return;
        }

        // We need to make sure that nothing gets added to mCallbackList once there is a cached
        // result as nothing on this list will be ran again.
        mCallbackList.add(callback);

        if (mCallbackList.size() > 1) {
            // A pending callback is already being worked on, just add to the list and wait.
            return;
        }

        // Skip querying the device if we're testing.
        if (mSkipAsyncCheckForTesting) return;

        // There is no cached value and this is the first request, spin up a thread to query the
        // device.
        try {
            new AsyncTask<OwnedState>() {
                // TODO: Unit test this function. https://crbug.com/1099262
                private OwnedState calculateIsRunningOnManagedProfile(Context context) {
                    boolean hasProfileOwnerApp = false;
                    boolean hasDeviceOwnerApp = false;
                    PackageManager packageManager = context.getPackageManager();
                    DevicePolicyManager devicePolicyManager =
                            (DevicePolicyManager)
                                    context.getSystemService(Context.DEVICE_POLICY_SERVICE);

                    if (CommandLine.getInstance()
                            .hasSwitch(ChromeSwitches.FORCE_DEVICE_OWNERSHIP)) {
                        hasDeviceOwnerApp = true;
                    }

                    for (PackageInfo pkg : packageManager.getInstalledPackages(/* flags= */ 0)) {
                        assert devicePolicyManager != null;
                        if (devicePolicyManager.isProfileOwnerApp(pkg.packageName)) {
                            hasProfileOwnerApp = true;
                        }
                        if (devicePolicyManager.isDeviceOwnerApp(pkg.packageName)) {
                            hasDeviceOwnerApp = true;
                        }
                        if (hasProfileOwnerApp && hasDeviceOwnerApp) break;
                    }

                    return new OwnedState(hasDeviceOwnerApp, hasProfileOwnerApp);
                }

                @Override
                protected OwnedState doInBackground() {
                    Context context = ContextUtils.getApplicationContext();
                    return calculateIsRunningOnManagedProfile(context);
                }

                @Override
                protected void onPostExecute(OwnedState result) {
                    setCacheResult(result);
                    onEnterpriseInfoResultAvailable();
                }
            }.executeWithTaskTraits(TaskTraits.USER_VISIBLE);
        } catch (RejectedExecutionException e) {
            // This is an extreme edge case, but if it does happen then return null to indicate we
            // couldn't execute.
            Log.w(TAG, "Thread limit reached, unable to determine managed state.");

            // There will only ever be a single item in the queue as we only try()/catch() on the
            // first item.
            Callback<OwnedState> failedRunCallback = mCallbackList.remove();
            mHandler.post(() -> failedRunCallback.onResult(null));
        }
    }

    @VisibleForTesting
    void setCacheResult(OwnedState result) {
        ThreadUtils.assertOnUiThread();
        assert result != null;
        mOwnedState = result;
        Log.i(
                TAG,
                "#setCacheResult() deviceOwned:"
                        + result.mDeviceOwned
                        + " profileOwned:"
                        + result.mProfileOwned);
    }

    @VisibleForTesting
    void onEnterpriseInfoResultAvailable() {
        ThreadUtils.assertOnUiThread();
        assert mOwnedState != null;

        // Service every waiting callback.
        while (mCallbackList.size() > 0) mCallbackList.remove().onResult(mOwnedState);
    }

    @Override
    public void logDeviceEnterpriseInfo() {
        Callback<OwnedState> callback =
                (result) -> {
                    recordManagementHistograms(result);
                };
        getDeviceEnterpriseInfo(callback);
    }

    private static void recordManagementHistograms(OwnedState state) {
        if (state == null) return;

        RecordHistogram.recordBooleanHistogram("EnterpriseCheck.IsManaged2", state.mProfileOwned);
        RecordHistogram.recordBooleanHistogram(
                "EnterpriseCheck.IsFullyManaged2", state.mDeviceOwned);
    }

    /**
     * When true the check if a device/profile is managed is skipped, meaning that the callback
     * provided to getDeviceEnterpriseInfo is only added to mCallbackList. setCacheResult and
     * onEnterpriseInfoResultAvailable must be called manually.
     *
     * If mOwnedState != null then this function has no effect and a task to service the
     * callback will be posted immediately.
     */
    void setSkipAsyncCheckForTesting(boolean skip) {
        mSkipAsyncCheckForTesting = skip;
        ResettersForTesting.register(() -> mSkipAsyncCheckForTesting = false);
    }
}