blob: 515bffdb676a9ea16697d671d9d91b3ea3120a80 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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.safety_check;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import android.text.format.DateUtils;
import android.view.View;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import org.chromium.base.BuildConfig;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.password_check.BulkLeakCheckServiceState;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.safe_browsing.settings.SecuritySettingsFragment;
import org.chromium.chrome.browser.safety_check.SafetyCheckBridge.SafetyCheckCommonObserver;
import org.chromium.chrome.browser.safety_check.SafetyCheckProperties.PasswordsState;
import org.chromium.chrome.browser.safety_check.SafetyCheckProperties.SafeBrowsingState;
import org.chromium.chrome.browser.safety_check.SafetyCheckProperties.UpdatesState;
import org.chromium.chrome.browser.settings.SettingsLauncher;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.ui.modelutil.PropertyModel;
import java.lang.ref.WeakReference;
class SafetyCheckMediator implements SafetyCheckCommonObserver {
/** The minimal amount of time to show the checking state. */
private static final int CHECKING_MIN_DURATION_MS = 1000;
/** Time after which the null-states will be shown: 10 minutes. */
private static final long RESET_TO_NULL_AFTER_MS = 10 * DateUtils.MINUTE_IN_MILLIS;
/** Bridge to the C++ side for the Safe Browsing and passwords checks. */
private SafetyCheckBridge mSafetyCheckBridge;
/** Model representing the current state of the checks. */
private PropertyModel mModel;
/** Client to interact with Omaha for the updates check. */
private SafetyCheckUpdatesDelegate mUpdatesClient;
/** Callbacks and related objects to show the checking state for at least 1 second. */
private Handler mHandler;
private Runnable mRunnablePasswords;
private Runnable mRunnableSafeBrowsing;
private Runnable mRunnableUpdates;
private long mCheckStartTime = -1;
private final SharedPreferencesManager mPreferenceManager;
/**
* Callback that gets invoked once the result of the updates check is available. Not inlined
* because a {@link WeakReference} needs to be passed (the check is asynchronous).
*/
private final Callback<Integer> mUpdatesCheckCallback = (status) -> {
mRunnableUpdates = () -> {
if (mModel != null) {
mModel.set(SafetyCheckProperties.UPDATES_STATE, status);
}
};
if (mHandler != null) {
// Show the checking state for at least 1 second for a smoother UX.
mHandler.postDelayed(mRunnableUpdates, getModelUpdateDelay());
}
};
/**
* Creates a new instance given a model, an updates client, and a settings launcher.
*
* @param model A model instance.
* @param client An updates client.
* @param settingsLauncher An instance of the {@link SettingsLauncher} implementation.
*/
public SafetyCheckMediator(PropertyModel model, SafetyCheckUpdatesDelegate client,
SettingsLauncher settingsLauncher) {
this(model, client, settingsLauncher, null, new Handler());
// Have to initialize this after the constructor call, since a "this" instance is needed.
mSafetyCheckBridge = new SafetyCheckBridge(SafetyCheckMediator.this);
// Determine and set the initial state.
setInitialState();
}
@VisibleForTesting
SafetyCheckMediator(PropertyModel model, SafetyCheckUpdatesDelegate client,
SettingsLauncher settingsLauncher, SafetyCheckBridge bridge, Handler handler) {
mModel = model;
mUpdatesClient = client;
mSafetyCheckBridge = bridge;
mHandler = handler;
mPreferenceManager = SharedPreferencesManager.getInstance();
// Set the listener for clicking the updates element.
mModel.set(SafetyCheckProperties.UPDATES_CLICK_LISTENER,
(Preference.OnPreferenceClickListener) (p) -> {
if (!BuildConfig.IS_CHROME_BRANDED) {
return true;
}
String chromeAppId = ContextUtils.getApplicationContext().getPackageName();
// Open the Play Store page for the installed Chrome channel.
p.getContext().startActivity(new Intent(Intent.ACTION_VIEW,
Uri.parse(ContentUrlConstants.PLAY_STORE_URL_PREFIX + chromeAppId)));
return true;
});
// Set the listener for clicking the Safe Browsing element.
mModel.set(SafetyCheckProperties.SAFE_BROWSING_CLICK_LISTENER,
(Preference.OnPreferenceClickListener) (p) -> {
String safeBrowsingSettingsClassName;
if (ChromeFeatureList.isEnabled(
ChromeFeatureList.SAFE_BROWSING_SECURITY_SECTION_UI)) {
// Open the Security settings since the flag for them is enabled.
safeBrowsingSettingsClassName = SecuritySettingsFragment.class.getName();
} else {
// Open the Sync and Services settings.
// TODO(crbug.com/1070620): replace the hardcoded class name with an import
// and ".class.getName()" once SyncAndServicesSettings is moved out of
// //chrome/android.
safeBrowsingSettingsClassName =
"org.chromium.chrome.browser.sync.settings.SyncAndServicesSettings";
}
p.getContext().startActivity(settingsLauncher.createSettingsActivityIntent(
p.getContext(), safeBrowsingSettingsClassName));
return true;
});
// Set the listener for clicking the passwords element.
mModel.set(SafetyCheckProperties.PASSWORDS_CLICK_LISTENER,
(Preference.OnPreferenceClickListener) (p) -> {
// Open the Passwords settings.
// TODO(crbug.com/1070620): replace the hardcoded class name with an import and
// ".class.getName()" once PasswordSettings is moved out of //chrome/android.
p.getContext().startActivity(settingsLauncher.createSettingsActivityIntent(
p.getContext(),
"org.chromium.chrome.browser.password_manager.settings.PasswordSettings"));
return true;
});
// Set the listener for clicking the Check button.
mModel.set(SafetyCheckProperties.SAFETY_CHECK_BUTTON_CLICK_LISTENER,
(View.OnClickListener) (v) -> performSafetyCheck());
// Get the timestamp of the last run.
mModel.set(SafetyCheckProperties.LAST_RUN_TIMESTAMP,
mPreferenceManager.readLong(
ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP, 0));
if (mSafetyCheckBridge != null) {
// Determine and set the initial state.
setInitialState();
}
}
/**
* Determines the initial state to show, triggering any fast checks if necessary based on the
* last run timestamp.
*/
public void setInitialState() {
long currentTime = System.currentTimeMillis();
long lastRun = mPreferenceManager.readLong(
ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP, 0);
// Always show the passwords unsafe state.
if (mSafetyCheckBridge.getNumberOfPasswordLeaksFromLastCheck() != 0) {
mModel.set(SafetyCheckProperties.PASSWORDS_STATE, PasswordsState.COMPROMISED_EXIST);
}
if (currentTime - lastRun < RESET_TO_NULL_AFTER_MS) {
// Show the passwords safe state
if (!mSafetyCheckBridge.savedPasswordsExist()) {
mModel.set(SafetyCheckProperties.PASSWORDS_STATE, PasswordsState.NO_PASSWORDS);
} else if (mSafetyCheckBridge.getNumberOfPasswordLeaksFromLastCheck() == 0) {
mModel.set(SafetyCheckProperties.PASSWORDS_STATE, PasswordsState.SAFE);
}
// Rerun the updates and Safe Browsing checks.
mModel.set(SafetyCheckProperties.SAFE_BROWSING_STATE, SafeBrowsingState.CHECKING);
mModel.set(SafetyCheckProperties.UPDATES_STATE, UpdatesState.CHECKING);
mSafetyCheckBridge.checkSafeBrowsing();
mUpdatesClient.checkForUpdates(new WeakReference(mUpdatesCheckCallback));
} else {
// The unsafe state was already set above, so only set to unchecked if it is safe.
if (mSafetyCheckBridge.getNumberOfPasswordLeaksFromLastCheck() == 0) {
mModel.set(SafetyCheckProperties.PASSWORDS_STATE, PasswordsState.UNCHECKED);
}
mModel.set(SafetyCheckProperties.SAFE_BROWSING_STATE, SafeBrowsingState.UNCHECKED);
mModel.set(SafetyCheckProperties.UPDATES_STATE, UpdatesState.UNCHECKED);
}
}
/** Triggers all safety check child checks. */
public void performSafetyCheck() {
// Cancel pending delayed show callbacks if a new check is starting while any existing
// elements are pending.
cancelCallbacks();
// Record the start time for tracking 1 second checking delay in the UI.
mCheckStartTime = SystemClock.elapsedRealtime();
// Record the absolute start time for showing when the last Safety check was performed.
long currentTime = System.currentTimeMillis();
mModel.set(SafetyCheckProperties.LAST_RUN_TIMESTAMP, currentTime);
mPreferenceManager.writeLong(
ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP, currentTime);
// Increment the stored number of Safety check starts.
mPreferenceManager.incrementInt(ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_RUN_COUNTER);
// Set the checking state for all elements.
mModel.set(SafetyCheckProperties.PASSWORDS_STATE, PasswordsState.CHECKING);
mModel.set(SafetyCheckProperties.SAFE_BROWSING_STATE, SafeBrowsingState.CHECKING);
mModel.set(SafetyCheckProperties.UPDATES_STATE, UpdatesState.CHECKING);
// Start all the checks.
mSafetyCheckBridge.checkSafeBrowsing();
mSafetyCheckBridge.checkPasswords();
mUpdatesClient.checkForUpdates(new WeakReference(mUpdatesCheckCallback));
}
/**
* Gets invoked once the Safe Browsing check is completed.
*
* @param status SafetyCheck::SafeBrowsingStatus enum value representing the Safe Browsing state
* (see //components/safety_check/safety_check.h).
*/
@Override
public void onSafeBrowsingCheckResult(@SafeBrowsingStatus int status) {
mRunnableSafeBrowsing = () -> {
mModel.set(SafetyCheckProperties.SAFE_BROWSING_STATE,
SafetyCheckProperties.safeBrowsingStateFromNative(status));
};
// Show the checking state for at least 1 second for a smoother UX.
mHandler.postDelayed(mRunnableSafeBrowsing, getModelUpdateDelay());
}
/**
* Gets invoked by the C++ code every time another credential is checked.
*
* @param checked Number of passwords already checked.
* @param total Total number of passwords to check.
*/
@Override
public void onPasswordCheckCredentialDone(int checked, int total) {}
/**
* Gets invoked by the C++ code when the status of the password check changes.
*
* @param state BulkLeakCheckService::State enum value representing the state (see
* //components/password_manager/core/browser/bulk_leak_check_service_interface.h).
*/
@Override
public void onPasswordCheckStateChange(@BulkLeakCheckServiceState int state) {
if (state == BulkLeakCheckServiceState.RUNNING) {
return;
}
mSafetyCheckBridge.stopObservingPasswordsCheck();
mRunnablePasswords = () -> {
// Handle the error states.
if (state != BulkLeakCheckServiceState.IDLE) {
mModel.set(SafetyCheckProperties.PASSWORDS_STATE,
SafetyCheckProperties.passwordsStatefromErrorState(state));
return;
}
// Non-error state depends on whether there are any passwords saved and/or leaked.
if (!mSafetyCheckBridge.savedPasswordsExist()) {
mModel.set(SafetyCheckProperties.PASSWORDS_STATE, PasswordsState.NO_PASSWORDS);
} else if (mSafetyCheckBridge.getNumberOfPasswordLeaksFromLastCheck() == 0) {
mModel.set(SafetyCheckProperties.PASSWORDS_STATE, PasswordsState.SAFE);
} else {
mModel.set(SafetyCheckProperties.PASSWORDS_STATE, PasswordsState.COMPROMISED_EXIST);
}
};
// Show the checking state for at least 1 second for a smoother UX.
mHandler.postDelayed(mRunnablePasswords, getModelUpdateDelay());
}
/** Cancels any pending callbacks and registered observers. */
public void destroy() {
cancelCallbacks();
mSafetyCheckBridge.destroy();
mSafetyCheckBridge = null;
mUpdatesClient = null;
mModel = null;
mHandler = null;
}
/** Cancels any delayed show callbacks. */
private void cancelCallbacks() {
if (mRunnablePasswords != null) {
mHandler.removeCallbacks(mRunnablePasswords);
mRunnablePasswords = null;
}
if (mRunnableSafeBrowsing != null) {
mHandler.removeCallbacks(mRunnableSafeBrowsing);
mRunnableSafeBrowsing = null;
}
if (mRunnableUpdates != null) {
mHandler.removeCallbacks(mRunnableUpdates);
mRunnableUpdates = null;
}
}
/**
* @return The delay in ms for updating the model in the running state.
*/
private long getModelUpdateDelay() {
return Math.max(
0, mCheckStartTime + CHECKING_MIN_DURATION_MS - SystemClock.elapsedRealtime());
}
}