chromium/chrome/android/java/src/org/chromium/chrome/browser/firstrun/TosDialogBehaviorSharedPrefInvalidator.java

// Copyright 2020 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.firstrun;

import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.chrome.browser.enterprise.util.EnterpriseInfo;
import org.chromium.chrome.browser.policy.PolicyServiceFactory;
import org.chromium.components.policy.PolicyService;

/**
 * Class that used to refresh "If the ToS was and should be skipped by policy TosDialogBehavior".
 *
 * Once FRE is skipped because of the enterprise policy TosDialogBehavior, this information is
 * stored in {@link ChromePreferenceKeys#FIRST_RUN_SKIPPED_BY_POLICY}. The FRE will be completely
 * avoided when this pref is true. This class checks if the enterprise policy is ever reset such
 * that the FRE should be run, and will clear the shared pref.
 */
public class TosDialogBehaviorSharedPrefInvalidator {
    private static final String TAG = "TosPolicyStatus";

    private final SkipTosDialogPolicyListener mPolicyListener;
    private final FirstRunAppRestrictionInfo mAppRestrictionInfo;
    private final long mTimeObjectCreated;

    /**
     * Refresh boolean value "If the ToS was and should be skipped by policy TosDialogBehavior"
     * that is stored in the shared preference. If the ToS was not skipped, do nothing.
     */
    public static void refreshSharedPreferenceIfTosSkipped() {
        ThreadUtils.assertOnUiThread();
        if (!FirstRunStatus.isFirstRunSkippedByPolicy()) return;

        FirstRunAppRestrictionInfo appRestrictionInfo =
                FirstRunAppRestrictionInfo.takeMaybeInitialized();
        OneshotSupplierImpl<PolicyService> policyServiceSupplier = new OneshotSupplierImpl<>();
        policyServiceSupplier.set(PolicyServiceFactory.getGlobalPolicyService());
        SkipTosDialogPolicyListener policyListener =
                new SkipTosDialogPolicyListener(
                        appRestrictionInfo,
                        policyServiceSupplier,
                        EnterpriseInfo.getInstance(),
                        null);

        new TosDialogBehaviorSharedPrefInvalidator(policyListener, appRestrictionInfo);
    }

    @VisibleForTesting
    TosDialogBehaviorSharedPrefInvalidator(
            SkipTosDialogPolicyListener listener, FirstRunAppRestrictionInfo appRestrictionInfo) {
        mTimeObjectCreated = SystemClock.elapsedRealtime();

        mAppRestrictionInfo = appRestrictionInfo;
        mPolicyListener = listener;
        mPolicyListener.onAvailable(this::onPolicyAvailable);
    }

    private void onPolicyAvailable(boolean shouldSkipTosDialog) {
        long duration = SystemClock.elapsedRealtime() - mTimeObjectCreated;
        Log.d(TAG, "Task finished. Duration: [%d], result: [%s]", duration, shouldSkipTosDialog);
        if (!shouldSkipTosDialog) FirstRunStatus.setFirstRunSkippedByPolicy(false);

        // Run cleanup in a separate task, otherwise the CallbackController inside
        // SkipTosDialogPolicyListener will deadlock. It currently holds a read lock because it is
        // what invoked us. Calling destroy requires a write lock, so the post allows the read lock
        // to be released first. See https://crbug.com/1201279 for more details.
        new Handler(Looper.getMainLooper()).post(this::destroy);
    }

    private void destroy() {
        mAppRestrictionInfo.destroy();
        mPolicyListener.destroy();
    }
}