Commit 9fb74e82 authored by Andrey Zaytsev's avatar Andrey Zaytsev Committed by Commit Bot

Safety check on Android: added the last run timestamp to the UI

UI demo: https://screenshot.googleplex.com/PdYajrS8FfE.gif

Bug: 1070620, 1107807
Change-Id: I2fac517d4aefd5999449988cfe7d289f76e67563
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2303714Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Reviewed-by: default avatarNatalie Chouinard <chouinard@chromium.org>
Reviewed-by: default avatarMartin Šrámek <msramek@chromium.org>
Commit-Queue: Andrey Zaytsev <andzaytsev@google.com>
Auto-Submit: Andrey Zaytsev <andzaytsev@google.com>
Cr-Commit-Position: refs/heads/master@{#791461}
parent f775e465
......@@ -593,6 +593,10 @@ public final class ChromePreferenceKeys {
"org.chromium.chrome.browser.settings.privacy."
+ "PREF_OTHER_FORMS_OF_HISTORY_DIALOG_SHOWN";
/** Stores the timestamp of the last performed Safety check. */
public static final String SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP =
"Chrome.SafetyCheck.LastRunTimestamp";
/** Stores the number of times the user has performed Safety check. */
public static final String SETTINGS_SAFETY_CHECK_RUN_COUNTER = "Chrome.SafetyCheck.RunCounter";
......@@ -800,6 +804,7 @@ public final class ChromePreferenceKeys {
HOMEPAGE_USE_CHROME_NTP,
PROMO_IS_DISMISSED.pattern(),
PROMO_TIMES_SEEN.pattern(),
SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
SETTINGS_SAFETY_CHECK_RUN_COUNTER,
TWA_DISCLOSURE_SEEN_PACKAGES
);
......
......@@ -100,7 +100,7 @@ android_library("junit") {
android_resources("java_resources") {
sources = [
"java/res/layout/safety_check_button.xml",
"java/res/layout/safety_check_bottom_elements.xml",
"java/res/layout/safety_check_status.xml",
"java/res/values/dimens.xml",
"java/res/xml/safety_check_preferences.xml",
......
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="end" >
<!-- Text view for displaying the timestamp of the last run. -->
<TextView
android:id="@+id/safety_check_timestamp"
android:layout_marginEnd="@dimen/safety_check_button_margin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end" />
<!-- Button for starting Safety check. -->
<org.chromium.ui.widget.ButtonCompat
android:id="@+id/safety_check_button"
style="@style/FilledButton.Flat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_margin="@dimen/safety_check_button_margin"
android:focusable="true"
android:text="@string/safety_check_button" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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. -->
<!-- Button for starting Safety check. -->
<org.chromium.ui.widget.ButtonCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/safety_check_button"
style="@style/FilledButton.Flat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_margin="@dimen/safety_check_button_margin"
android:focusable="true"
android:text="@string/safety_check_button" />
......@@ -5,7 +5,6 @@
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<!-- Spinning icon indicating a running check. -->
......
......@@ -80,6 +80,10 @@ class SafetyCheckMediator implements SafetyCheckCommonObserver {
// 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));
}
/** Triggers all safety check child checks. */
......@@ -89,6 +93,11 @@ class SafetyCheckMediator implements SafetyCheckCommonObserver {
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.
......
......@@ -10,6 +10,7 @@ import org.chromium.chrome.browser.password_check.BulkLeakCheckServiceState;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModel.WritableIntPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableLongPropertyKey;
import org.chromium.ui.modelutil.PropertyModel.WritableObjectPropertyKey;
import java.lang.annotation.Retention;
......@@ -25,6 +26,8 @@ class SafetyCheckProperties {
/** Listener for Safety check button click events. */
static final WritableObjectPropertyKey SAFETY_CHECK_BUTTON_CLICK_LISTENER =
new WritableObjectPropertyKey();
/** Timestamp of the last run, a Long object. */
static final WritableLongPropertyKey LAST_RUN_TIMESTAMP = new WritableLongPropertyKey();
@IntDef({PasswordsState.UNCHECKED, PasswordsState.CHECKING, PasswordsState.SAFE,
PasswordsState.COMPROMISED_EXIST, PasswordsState.OFFLINE, PasswordsState.NO_PASSWORDS,
......@@ -112,13 +115,14 @@ class SafetyCheckProperties {
}
static final PropertyKey[] ALL_KEYS = new PropertyKey[] {PASSWORDS_STATE, SAFE_BROWSING_STATE,
UPDATES_STATE, SAFETY_CHECK_BUTTON_CLICK_LISTENER};
UPDATES_STATE, SAFETY_CHECK_BUTTON_CLICK_LISTENER, LAST_RUN_TIMESTAMP};
static PropertyModel createSafetyCheckModel() {
return new PropertyModel.Builder(ALL_KEYS)
.with(PASSWORDS_STATE, PasswordsState.UNCHECKED)
.with(SAFE_BROWSING_STATE, SafeBrowsingState.UNCHECKED)
.with(UPDATES_STATE, UpdatesState.UNCHECKED)
.with(LAST_RUN_TIMESTAMP, 0)
.build();
}
}
......@@ -13,6 +13,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
......@@ -35,6 +36,8 @@ public class SafetyCheckSettingsFragment extends PreferenceFragmentCompat {
/** The "Check" button at the bottom that needs to be added after the View is inflated. */
private ButtonCompat mCheckButton;
private TextView mTimestampTextView;
public static CharSequence getSafetyCheckSettingsElementTitle(Context context) {
SharedPreferencesManager preferenceManager = SharedPreferencesManager.getInstance();
if (preferenceManager.readInt(ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_RUN_COUNTER)
......@@ -77,18 +80,28 @@ public class SafetyCheckSettingsFragment extends PreferenceFragmentCompat {
LinearLayout view =
(LinearLayout) super.onCreateView(inflater, container, savedInstanceState);
// Add a button to the bottom of the preferences view.
mCheckButton = (ButtonCompat) inflater.inflate(R.layout.safety_check_button, view, false);
view.addView(mCheckButton);
LinearLayout bottomView =
(LinearLayout) inflater.inflate(R.layout.safety_check_bottom_elements, view, false);
mCheckButton = (ButtonCompat) bottomView.findViewById(R.id.safety_check_button);
mTimestampTextView = (TextView) bottomView.findViewById(R.id.safety_check_timestamp);
view.addView(bottomView);
return view;
}
/**
* @return A {@link ButtonCompat} object for the Check button.
*/
public ButtonCompat getCheckButton() {
ButtonCompat getCheckButton() {
return mCheckButton;
}
/**
* @return A {@link TextView} object for the last run timestamp.
*/
TextView getTimestampTextView() {
return mTimestampTextView;
}
/**
* Update the status string of a given Safety check element, e.g. Passwords.
* @param key An android:key String corresponding to Safety check element.
......
......@@ -4,8 +4,11 @@
package org.chromium.chrome.browser.safety_check;
import android.content.Context;
import android.view.View;
import androidx.annotation.VisibleForTesting;
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;
......@@ -16,6 +19,9 @@ class SafetyCheckViewBinder {
private static final String PASSWORDS_KEY = "passwords";
private static final String SAFE_BROWSING_KEY = "safe_browsing";
private static final String UPDATES_KEY = "updates";
private static final long MIN_TO_MS = 60 * 1000;
private static final long H_TO_MS = 60 * MIN_TO_MS;
private static final long DAY_TO_MS = 24 * H_TO_MS;
private static int getStringForPasswords(@PasswordsState int state) {
switch (state) {
......@@ -142,6 +148,42 @@ class SafetyCheckViewBinder {
return 0;
}
/**
* Generates a String representing how long ago the Safety check was performed last time.
* @param context A {@link Context} instance to extract the strings.
* @param lastRunTime A long representing the last run timestamp in milliseconds.
* @param currentTime A long representing current time in milliseconds.
* @return A string to display in the UI for the last run timestamp.
*/
@VisibleForTesting
static String getLastRunTimestampText(Context context, long lastRunTime, long currentTime) {
if (lastRunTime == 0) {
return "";
}
long timeDiff = currentTime - lastRunTime;
if (timeDiff < MIN_TO_MS) {
return context.getString(R.string.safety_check_timestamp_after);
} else if (timeDiff < H_TO_MS) {
int minutes = (int) (timeDiff / MIN_TO_MS);
return context.getResources().getQuantityString(
R.plurals.safety_check_timestamp_after_mins, minutes, minutes);
} else if (timeDiff < DAY_TO_MS) {
int hours = (int) (timeDiff / H_TO_MS);
return context.getResources().getQuantityString(
R.plurals.safety_check_timestamp_after_hours, hours, hours);
} else if (timeDiff < 2 * DAY_TO_MS) {
return context.getString(R.string.safety_check_timestamp_after_yesterday);
} else {
int days = (int) (timeDiff / DAY_TO_MS);
return context.getResources().getQuantityString(
R.plurals.safety_check_timestamp_after_days, days, days);
}
}
private static void clearTimestampText(SafetyCheckSettingsFragment fragment) {
fragment.getTimestampTextView().setText("");
}
static void bind(
PropertyModel model, SafetyCheckSettingsFragment fragment, PropertyKey propertyKey) {
if (SafetyCheckProperties.PASSWORDS_STATE == propertyKey) {
......@@ -149,10 +191,12 @@ class SafetyCheckViewBinder {
int state = model.get(SafetyCheckProperties.PASSWORDS_STATE);
fragment.updateElementStatus(PASSWORDS_KEY, getStringForPasswords(state));
SafetyCheckElementPreference preference = fragment.findPreference(PASSWORDS_KEY);
preference.setEnabled(true);
if (state == PasswordsState.UNCHECKED) {
preference.clearStatusIndicator();
preference.setEnabled(true);
} else if (state == PasswordsState.CHECKING) {
clearTimestampText(fragment);
preference.showProgressBar();
preference.setEnabled(false);
} else {
......@@ -164,10 +208,12 @@ class SafetyCheckViewBinder {
int state = model.get(SafetyCheckProperties.SAFE_BROWSING_STATE);
fragment.updateElementStatus(SAFE_BROWSING_KEY, getStringForSafeBrowsing(state));
SafetyCheckElementPreference preference = fragment.findPreference(SAFE_BROWSING_KEY);
preference.setEnabled(true);
if (state == SafeBrowsingState.UNCHECKED) {
preference.clearStatusIndicator();
preference.setEnabled(true);
} else if (state == SafeBrowsingState.CHECKING) {
clearTimestampText(fragment);
preference.showProgressBar();
preference.setEnabled(false);
} else {
......@@ -179,10 +225,12 @@ class SafetyCheckViewBinder {
int state = model.get(SafetyCheckProperties.UPDATES_STATE);
fragment.updateElementStatus(UPDATES_KEY, getStringForUpdates(state));
SafetyCheckElementPreference preference = fragment.findPreference(UPDATES_KEY);
preference.setEnabled(true);
if (state == UpdatesState.UNCHECKED) {
preference.clearStatusIndicator();
preference.setEnabled(true);
} else if (state == UpdatesState.CHECKING) {
clearTimestampText(fragment);
preference.showProgressBar();
preference.setEnabled(false);
} else {
......@@ -192,6 +240,11 @@ class SafetyCheckViewBinder {
} else if (SafetyCheckProperties.SAFETY_CHECK_BUTTON_CLICK_LISTENER == propertyKey) {
fragment.getCheckButton().setOnClickListener((View.OnClickListener) model.get(
SafetyCheckProperties.SAFETY_CHECK_BUTTON_CLICK_LISTENER));
} else if (SafetyCheckProperties.LAST_RUN_TIMESTAMP == propertyKey) {
long lastRunTime = model.get(SafetyCheckProperties.LAST_RUN_TIMESTAMP);
long currentTime = System.currentTimeMillis();
fragment.getTimestampTextView().setText(
getLastRunTimestampText(fragment.getContext(), lastRunTime, currentTime));
} else {
assert false : "Unhandled property detected in SafetyCheckViewBinder!";
}
......
......@@ -8,6 +8,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import androidx.preference.Preference;
......@@ -36,6 +37,11 @@ public class SafetyCheckSettingsFragmentTest {
private static final String PASSWORDS = "passwords";
private static final String SAFE_BROWSING = "safe_browsing";
private static final String UPDATES = "updates";
private static final long S_TO_MS = 1000;
private static final long MIN_TO_MS = 60 * S_TO_MS;
private static final long H_TO_MS = 60 * MIN_TO_MS;
private static final long DAY_TO_MS = 24 * H_TO_MS;
@Rule
public SettingsActivityTestRule<SafetyCheckSettingsFragment> mSettingsActivityTestRule =
new SettingsActivityTestRule<>(SafetyCheckSettingsFragment.class);
......@@ -77,10 +83,38 @@ public class SafetyCheckSettingsFragmentTest {
assertFalse(title.contains("New"));
}
@Test
@SmallTest
public void testLastRunTimestampStrings() {
long t0 = 12345;
Context context = InstrumentationRegistry.getTargetContext();
// Start time not set - returns an empty string.
assertEquals("", SafetyCheckViewBinder.getLastRunTimestampText(context, 0, 123));
assertEquals("Checked just now",
SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 10 * S_TO_MS));
assertEquals("Checked 1 minute ago",
SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + MIN_TO_MS));
assertEquals("Checked 17 minutes ago",
SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 17 * MIN_TO_MS));
assertEquals("Checked 1 hour ago",
SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + H_TO_MS));
assertEquals("Checked 13 hours ago",
SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 13 * H_TO_MS));
assertEquals("Checked yesterday",
SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + DAY_TO_MS));
assertEquals("Checked yesterday",
SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 2 * DAY_TO_MS - 1));
assertEquals("Checked 2 days ago",
SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 2 * DAY_TO_MS));
assertEquals("Checked 315 days ago",
SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 315 * DAY_TO_MS));
}
private void createFragmentAndModel() {
mSettingsActivityTestRule.startSettingsActivity();
mFragment = (SafetyCheckSettingsFragment) mSettingsActivityTestRule.getFragment();
mModel = SafetyCheckCoordinator.createModelAndMcp(mFragment);
TestThreadUtils.runOnUiThreadBlocking(
() -> { mModel = SafetyCheckCoordinator.createModelAndMcp(mFragment); });
}
@Test
......
......@@ -925,6 +925,27 @@ Your Google account may have other forms of browsing history like searches and a
<message name="IDS_SAFETY_CHECK_UPDATES_ERROR" desc="Text to display when the updates check failed for some reason.">
Chrome can’t check for updates
</message>
<message name="IDS_SAFETY_CHECK_TIMESTAMP_AFTER" desc="A message shown on the safety check page, explaining to the user that it ran a moment ago.">
Checked just now
</message>
<message name="IDS_SAFETY_CHECK_TIMESTAMP_AFTER_MINS" desc="A message shown on the safety check page, explaining to the user that it ran minutes ago.">
{NUM_MINS, plural,
=1 {Checked 1 minute ago}
other {Checked # minutes ago}}
</message>
<message name="IDS_SAFETY_CHECK_TIMESTAMP_AFTER_HOURS" desc="A message shown on the safety check page, explaining to the user that it ran hours ago.">
{NUM_HOURS, plural,
=1 {Checked 1 hour ago}
other {Checked # hours ago}}
</message>
<message name="IDS_SAFETY_CHECK_TIMESTAMP_AFTER_YESTERDAY" desc="A message shown on the safety check page, showing to the user that it ran yesterday.">
Checked yesterday
</message>
<message name="IDS_SAFETY_CHECK_TIMESTAMP_AFTER_DAYS" desc="A message shown on the safety check page, showing to the user how many days ago it ran.">
{NUM_DAYS, plural,
=1 {Checked 1 day ago}
other {Checked # days ago}}
</message>
<!-- Security preferences -->
<message name="IDS_PREFS_SECURITY_TITLE" desc="Title for the Security preferences. [CHAR-LIMIT=32]">
......
......@@ -151,6 +151,42 @@ public class PropertyModel extends PropertyObservable<PropertyKey> {
}
}
/** The key type for read-only long model properties. */
public static class ReadableLongPropertyKey extends NamedPropertyKey {
/**
* Constructs a new unnamed read-only long property key.
*/
public ReadableLongPropertyKey() {
this(null);
}
/**
* Constructs a new named read-only long property key, e.g. for use in debugging.
* @param name The optional name of the property.
*/
public ReadableLongPropertyKey(@Nullable String name) {
super(name);
}
}
/** The key type for mutable int model properties. */
public static final class WritableLongPropertyKey extends ReadableLongPropertyKey {
/**
* Constructs a new unnamed writable long property key.
*/
public WritableLongPropertyKey() {
this(null);
}
/**
* Constructs a new named writable long property key, e.g. for use in debugging.
* @param name The optional name of the property.
*/
public WritableLongPropertyKey(@Nullable String name) {
super(name);
}
}
/**
* The key type for read-only Object model properties.
*
......@@ -296,6 +332,32 @@ public class PropertyModel extends PropertyObservable<PropertyKey> {
notifyPropertyChanged(key);
}
/**
* Get the current value from the long based key.
*/
public long get(ReadableLongPropertyKey key) {
validateKey(key);
LongContainer container = (LongContainer) mData.get(key);
return container == null ? 0 : container.value;
}
/**
* Set the value for the long based key.
*/
public void set(WritableLongPropertyKey key, long value) {
validateKey(key);
LongContainer container = (LongContainer) mData.get(key);
if (container == null) {
container = new LongContainer();
mData.put(key, container);
} else if (container.value == value) {
return;
}
container.value = value;
notifyPropertyChanged(key);
}
/**
* Get the current value from the boolean based key.
*/
......@@ -425,6 +487,14 @@ public class PropertyModel extends PropertyObservable<PropertyKey> {
return this;
}
public Builder with(ReadableLongPropertyKey key, long value) {
validateKey(key);
LongContainer container = new LongContainer();
container.value = value;
mData.put(key, container);
return this;
}
public Builder with(ReadableBooleanPropertyKey key, boolean value) {
validateKey(key);
BooleanContainer container = new BooleanContainer();
......@@ -525,6 +595,21 @@ public class PropertyModel extends PropertyObservable<PropertyKey> {
}
}
private static class LongContainer extends ValueContainer {
public long value;
@Override
public String toString() {
return value + " in " + super.toString();
}
@Override
public boolean equals(Object other) {
return other != null && other instanceof LongContainer
&& ((LongContainer) other).value == value;
}
}
private static class BooleanContainer extends ValueContainer {
public boolean value;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment