Commit f201c139 authored by Viktor Semeniuk's avatar Viktor Semeniuk Committed by Commit Bot

[iOS][Password Check] Timestamp of the last check

This change adds timestamp of the last password check. Timestamp is
visible only during unsafe state.

Bug: 1075494
Change-Id: I8e1bf94b27a94bbc1cbb83736fd4cd3f7344889f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2332605
Commit-Queue: Viktor Semeniuk <vsemeniuk@google.com>
Reviewed-by: default avatarSergio Collazos <sczs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#800137}
parent 4e2f1999
...@@ -1718,6 +1718,15 @@ While in incognito, sites can't use cookies to see your browsing activity across ...@@ -1718,6 +1718,15 @@ While in incognito, sites can't use cookies to see your browsing activity across
<message name="IDS_IOS_CANCEL_PASSWORD_EDIT" desc="Cancel button inside confirmation alert when the user is trying to edit password [iOS only]" meaning="Cancels editing"> <message name="IDS_IOS_CANCEL_PASSWORD_EDIT" desc="Cancel button inside confirmation alert when the user is trying to edit password [iOS only]" meaning="Cancels editing">
Cancel Cancel
</message> </message>
<message name="IDS_IOS_LAST_COMPLETED_CHECK" desc="Footer for Password Check section which shows the timestamp of the last check." meaning="Time when passwords were checked last time.">
Last checked <ph name="TIME">$1<ex>10 minutes ago</ex></ph>.
</message>
<message name="IDS_IOS_CHECK_FINISHED_JUST_NOW" desc="Used instead of timestamp when Password Check finished less than 1 minute ago." meaning="Check finished just now.">
just now
</message>
<message name="IDS_IOS_CHECK_NEVER_RUN" desc="Used instead of timestamp when Password Check never ran" meaning="Check never run.">
Check never run.
</message>
<message name="IDS_IOS_SEARCH_COPIED" desc="The message displayed when the search is copied via long press (contextual search) [Length: 10em] [iOS only]"> <message name="IDS_IOS_SEARCH_COPIED" desc="The message displayed when the search is copied via long press (contextual search) [Length: 10em] [iOS only]">
Copied Copied
</message> </message>
......
e072f523fcc779ac3872814835d1b47c898cc72e
\ No newline at end of file
4b148d424ae868373f77746c4ccd2039a30e8686
\ No newline at end of file
426c8f6b9d12dea3e51d45e541f2a87cb96f05dd
\ No newline at end of file
...@@ -127,6 +127,7 @@ source_set("unit_tests") { ...@@ -127,6 +127,7 @@ source_set("unit_tests") {
"password_exporter_unittest.mm", "password_exporter_unittest.mm",
"password_issues_mediator_unittest.mm", "password_issues_mediator_unittest.mm",
"password_issues_table_view_controller_unittest.mm", "password_issues_table_view_controller_unittest.mm",
"passwords_mediator_unittest.mm",
"passwords_table_view_controller_unittest.mm", "passwords_table_view_controller_unittest.mm",
] ]
deps = [ deps = [
...@@ -139,12 +140,17 @@ source_set("unit_tests") { ...@@ -139,12 +140,17 @@ source_set("unit_tests") {
"//components/keyed_service/core", "//components/keyed_service/core",
"//components/password_manager/core/browser:test_support", "//components/password_manager/core/browser:test_support",
"//components/password_manager/core/common", "//components/password_manager/core/common",
"//components/prefs:test_support",
"//components/strings", "//components/strings",
"//ios/chrome/app/strings", "//ios/chrome/app/strings",
"//ios/chrome/app/strings", "//ios/chrome/app/strings",
"//ios/chrome/browser/browser_state:test_support", "//ios/chrome/browser/browser_state:test_support",
"//ios/chrome/browser/main:test_support", "//ios/chrome/browser/main:test_support",
"//ios/chrome/browser/passwords", "//ios/chrome/browser/passwords",
"//ios/chrome/browser/signin:signin",
"//ios/chrome/browser/signin:test_support",
"//ios/chrome/browser/sync:sync",
"//ios/chrome/browser/sync:test_support",
"//ios/chrome/browser/ui/settings/cells", "//ios/chrome/browser/ui/settings/cells",
"//ios/chrome/browser/ui/table_view:test_support", "//ios/chrome/browser/ui/table_view:test_support",
"//ios/chrome/browser/ui/table_view/cells", "//ios/chrome/browser/ui/table_view/cells",
......
...@@ -37,6 +37,12 @@ class PasswordStore; ...@@ -37,6 +37,12 @@ class PasswordStore;
// Returns detailed information about error if applicable. // Returns detailed information about error if applicable.
- (NSAttributedString*)passwordCheckErrorInfo; - (NSAttributedString*)passwordCheckErrorInfo;
// Returns string containing the timestamp of the last password check. If the
// check finished less than 1 minute ago string will look "Last check just
// now.", otherwise "Last check X minutes/hours... ago.". If check never run
// string will be "Check never run.".
- (NSString*)formatElapsedTimeSinceLastCheck;
@end @end
#endif // IOS_CHROME_BROWSER_UI_SETTINGS_PASSWORD_PASSWORDS_MEDIATOR_H_ #endif // IOS_CHROME_BROWSER_UI_SETTINGS_PASSWORD_PASSWORDS_MEDIATOR_H_
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
#import "ios/chrome/browser/ui/settings/password/passwords_mediator.h" #import "ios/chrome/browser/ui/settings/password/passwords_mediator.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "components/password_manager/core/browser/leak_detection_dialog_utils.h" #include "components/password_manager/core/browser/leak_detection_dialog_utils.h"
#include "components/password_manager/core/browser/password_store.h" #include "components/password_manager/core/browser/password_store.h"
#include "components/password_manager/core/common/password_manager_features.h" #include "components/password_manager/core/common/password_manager_features.h"
...@@ -19,14 +22,22 @@ ...@@ -19,14 +22,22 @@
#import "ios/chrome/common/string_util.h" #import "ios/chrome/common/string_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h" #import "ios/chrome/common/ui/colors/semantic_color_names.h"
#include "ios/chrome/grit/ios_chromium_strings.h" #include "ios/chrome/grit/ios_chromium_strings.h"
#include "ios/chrome/grit/ios_strings.h"
#import "net/base/mac/url_conversions.h" #import "net/base/mac/url_conversions.h"
#include "ui/base/l10n/l10n_util_mac.h" #include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/l10n/time_format.h"
#include "url/gurl.h" #include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc) #if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support." #error "This file requires ARC support."
#endif #endif
namespace {
// Amount of time after which timestamp is shown instead of "just now".
constexpr base::TimeDelta kJustCheckedTimeThresholdInMinutes =
base::TimeDelta::FromMinutes(1);
} // namespace
@interface PasswordsMediator () <PasswordCheckObserver, @interface PasswordsMediator () <PasswordCheckObserver,
PasswordStoreObserver, PasswordStoreObserver,
SavePasswordsConsumerDelegate> { SavePasswordsConsumerDelegate> {
...@@ -257,4 +268,29 @@ ...@@ -257,4 +268,29 @@
[self.consumer setPasswordsForms:std::move(results)]; [self.consumer setPasswordsForms:std::move(results)];
} }
- (NSString*)formatElapsedTimeSinceLastCheck {
base::Time lastCompletedCheck =
_passwordCheckManager->GetLastPasswordCheckTime();
// lastCompletedCheck is 0.0 in case the check never completely ran before.
if (lastCompletedCheck == base::Time())
return l10n_util::GetNSString(IDS_IOS_CHECK_NEVER_RUN);
base::TimeDelta elapsedTime = base::Time::Now() - lastCompletedCheck;
NSString* timestamp;
// If check finished in less than |kJustCheckedTimeThresholdInMinutes| show
// "just now" instead of timestamp.
if (elapsedTime < kJustCheckedTimeThresholdInMinutes)
timestamp = l10n_util::GetNSString(IDS_IOS_CHECK_FINISHED_JUST_NOW);
else
timestamp = base::SysUTF8ToNSString(
base::UTF16ToUTF8(ui::TimeFormat::SimpleWithMonthAndYear(
ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_LONG,
elapsedTime, true)));
return l10n_util::GetNSStringF(IDS_IOS_LAST_COMPLETED_CHECK,
base::SysNSStringToUTF16(timestamp));
}
@end @end
// 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.
#import "ios/chrome/browser/ui/settings/password/passwords_mediator.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "components/password_manager/core/browser/password_manager_test_utils.h"
#include "components/password_manager/core/browser/test_password_store.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/prefs/testing_pref_service.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/browser_state/test_chrome_browser_state.h"
#import "ios/chrome/browser/main/test_browser.h"
#include "ios/chrome/browser/passwords/ios_chrome_password_check_manager.h"
#include "ios/chrome/browser/passwords/ios_chrome_password_check_manager_factory.h"
#include "ios/chrome/browser/passwords/ios_chrome_password_store_factory.h"
#include "ios/chrome/browser/passwords/password_check_observer_bridge.h"
#include "ios/chrome/browser/signin/authentication_service_factory.h"
#import "ios/chrome/browser/signin/authentication_service_fake.h"
#include "ios/chrome/browser/sync/profile_sync_service_factory.h"
#include "ios/chrome/browser/sync/sync_setup_service_factory.h"
#include "ios/chrome/browser/sync/sync_setup_service_mock.h"
#import "ios/chrome/browser/ui/settings/password/passwords_consumer.h"
#import "ios/chrome/browser/ui/table_view/chrome_table_view_controller_test.h"
#include "ios/web/public/test/web_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
using autofill::PasswordForm;
using password_manager::CompromisedCredentials;
using password_manager::CompromiseType;
using password_manager::TestPasswordStore;
// Sets test password store and returns pointer to it.
scoped_refptr<TestPasswordStore> BuildTestPasswordStore(
ChromeBrowserState* _browserState) {
return base::WrapRefCounted(static_cast<password_manager::TestPasswordStore*>(
IOSChromePasswordStoreFactory::GetInstance()
->SetTestingFactoryAndUse(
_browserState,
base::BindRepeating(&password_manager::BuildPasswordStore<
web::BrowserState, TestPasswordStore>))
.get()));
}
// Sets test sync setup service and returns pointer to it.
std::unique_ptr<KeyedService> BuildMockSyncSetupService(
web::BrowserState* context) {
ChromeBrowserState* browser_state =
ChromeBrowserState::FromBrowserState(context);
return std::make_unique<SyncSetupServiceMock>(
ProfileSyncServiceFactory::GetForBrowserState(browser_state));
}
} // namespace
@interface FakePasswordsConsumer : NSObject <PasswordsConsumer>
@end
@implementation FakePasswordsConsumer
- (void)setPasswordCheckUIState:(PasswordCheckUIState)state {
}
- (void)setPasswordsForms:
(std::vector<std::unique_ptr<autofill::PasswordForm>>)form {
}
@end
// Tests for Password Issues mediator.
class PasswordsMediatorTest : public BlockCleanupTest {
protected:
void SetUp() override {
BlockCleanupTest::SetUp();
scoped_feature_list_.InitAndEnableFeature(
password_manager::features::kPasswordCheck);
TestChromeBrowserState::Builder builder;
builder.AddTestingFactory(
AuthenticationServiceFactory::GetInstance(),
base::BindRepeating(
&AuthenticationServiceFake::CreateAuthenticationService));
builder.AddTestingFactory(SyncSetupServiceFactory::GetInstance(),
base::BindRepeating(&BuildMockSyncSetupService));
browser_state_ = builder.Build();
auth_service_ = static_cast<AuthenticationServiceFake*>(
AuthenticationServiceFactory::GetInstance()->GetForBrowserState(
browser_state_.get()));
store_ = BuildTestPasswordStore(browser_state_.get());
password_check_ = IOSChromePasswordCheckManagerFactory::GetForBrowserState(
browser_state_.get());
consumer_ = [[FakePasswordsConsumer alloc] init];
mediator_ = [[PasswordsMediator alloc] initWithPasswordStore:store_
passwordCheckManager:password_check_
authService:auth_service_
syncService:syncService()];
mediator_.consumer = consumer_;
}
SyncSetupService* syncService() {
return SyncSetupServiceFactory::GetForBrowserState(browser_state_.get());
}
PasswordsMediator* mediator() { return mediator_; }
ChromeBrowserState* browserState() { return browser_state_.get(); }
private:
web::WebTaskEnvironment task_environment_;
std::unique_ptr<TestChromeBrowserState> browser_state_;
AuthenticationServiceFake* auth_service_;
scoped_refptr<TestPasswordStore> store_;
scoped_refptr<IOSChromePasswordCheckManager> password_check_;
FakePasswordsConsumer* consumer_;
PasswordsMediator* mediator_;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(PasswordsMediatorTest, ElapsedTimeSinceLastCheck) {
EXPECT_NSEQ(@"Check never run.",
[mediator() formatElapsedTimeSinceLastCheck]);
base::Time expected1 = base::Time::Now() - base::TimeDelta::FromSeconds(10);
browserState()->GetPrefs()->SetDouble(
password_manager::prefs::kLastTimePasswordCheckCompleted,
expected1.ToDoubleT());
EXPECT_NSEQ(@"Last checked just now.",
[mediator() formatElapsedTimeSinceLastCheck]);
base::Time expected2 = base::Time::Now() - base::TimeDelta::FromMinutes(5);
browserState()->GetPrefs()->SetDouble(
password_manager::prefs::kLastTimePasswordCheckCompleted,
expected2.ToDoubleT());
EXPECT_NSEQ(@"Last checked 5 minutes ago.",
[mediator() formatElapsedTimeSinceLastCheck]);
}
...@@ -104,6 +104,7 @@ typedef NS_ENUM(NSInteger, ItemType) { ...@@ -104,6 +104,7 @@ typedef NS_ENUM(NSInteger, ItemType) {
ItemTypeManagedSavePasswords, ItemTypeManagedSavePasswords,
ItemTypePasswordCheckStatus, ItemTypePasswordCheckStatus,
ItemTypeCheckForProblemsButton, ItemTypeCheckForProblemsButton,
ItemTypeLastCheckTimestampFooter,
ItemTypeSavedPassword, // This is a repeated item type. ItemTypeSavedPassword, // This is a repeated item type.
ItemTypeBlocked, // This is a repeated item type. ItemTypeBlocked, // This is a repeated item type.
ItemTypeExportPasswordsButton, ItemTypeExportPasswordsButton,
...@@ -470,6 +471,10 @@ std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf( ...@@ -470,6 +471,10 @@ std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf(
[self updatePasswordCheckButtonWithState:_passwordCheckState]; [self updatePasswordCheckButtonWithState:_passwordCheckState];
[model addItem:_checkForProblemsItem [model addItem:_checkForProblemsItem
toSectionWithIdentifier:SectionIdentifierPasswordCheck]; toSectionWithIdentifier:SectionIdentifierPasswordCheck];
[self updateLastCheckTimestampWithState:_passwordCheckState
fromState:_passwordCheckState
update:NO];
} }
// Saved passwords. // Saved passwords.
...@@ -605,6 +610,14 @@ std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf( ...@@ -605,6 +610,14 @@ std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf(
return checkForProblemsItem; return checkForProblemsItem;
} }
- (TableViewLinkHeaderFooterItem*)lastCompletedCheckTime {
TableViewLinkHeaderFooterItem* footerItem =
[[TableViewLinkHeaderFooterItem alloc]
initWithType:ItemTypeLastCheckTimestampFooter];
footerItem.text = [_mediator formatElapsedTimeSinceLastCheck];
return footerItem;
}
- (TableViewTextItem*)exportPasswordsItem { - (TableViewTextItem*)exportPasswordsItem {
TableViewTextItem* exportPasswordsItem = TableViewTextItem* exportPasswordsItem =
[[TableViewTextItem alloc] initWithType:ItemTypeExportPasswordsButton]; [[TableViewTextItem alloc] initWithType:ItemTypeExportPasswordsButton];
...@@ -726,13 +739,14 @@ std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf( ...@@ -726,13 +739,14 @@ std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf(
#pragma mark - PasswordsConsumer #pragma mark - PasswordsConsumer
- (void)setPasswordCheckUIState:(PasswordCheckUIState)state { - (void)setPasswordCheckUIState:(PasswordCheckUIState)state {
_passwordCheckState = state; // Update password check status and check button with new state.
[self updatePasswordCheckButtonWithState:state]; [self updatePasswordCheckButtonWithState:state];
[self updatePasswordCheckStatusLabelWithState:state]; [self updatePasswordCheckStatusLabelWithState:state];
// During searching Password Check section is hidden so cells should not be // During searching Password Check section is hidden so cells should not be
// reconfigured. // reconfigured.
if (self.navigationItem.searchController.active) { if (self.navigationItem.searchController.active) {
_passwordCheckState = state;
return; return;
} }
...@@ -741,6 +755,12 @@ std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf( ...@@ -741,6 +755,12 @@ std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf(
if (_passwordProblemsItem) { if (_passwordProblemsItem) {
[self reconfigureCellsForItems:@[ _passwordProblemsItem ]]; [self reconfigureCellsForItems:@[ _passwordProblemsItem ]];
} }
// Before updating cached state value update timestamp as for proper animation
// it requires both new and old values.
[self updateLastCheckTimestampWithState:state
fromState:_passwordCheckState
update:YES];
_passwordCheckState = state;
} }
- (void)setPasswordsForms: - (void)setPasswordsForms:
...@@ -1049,6 +1069,63 @@ std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf( ...@@ -1049,6 +1069,63 @@ std::vector<std::unique_ptr<autofill::PasswordForm>> CopyOf(
} }
} }
// Update timestamp of the last check. Both old and new password check state
// should be provided in order to animate footer in a proper way.
- (void)updateLastCheckTimestampWithState:(PasswordCheckUIState)state
fromState:(PasswordCheckUIState)oldState
update:(BOOL)update {
if (!_didReceiveSavedForms) {
return;
}
NSInteger checkSection = [self.tableViewModel
sectionForSectionIdentifier:SectionIdentifierPasswordCheck];
switch (state) {
case PasswordCheckStateUnSafe:
[self.tableViewModel setFooter:[self lastCompletedCheckTime]
forSectionWithIdentifier:SectionIdentifierPasswordCheck];
// Transition from disabled to unsafe state is possible only on page load.
// In this case we want to avoid animation.
if (oldState == PasswordCheckStateDisabled) {
[UIView performWithoutAnimation:^{
[self.tableView
reloadSections:[NSIndexSet indexSetWithIndex:checkSection]
withRowAnimation:UITableViewRowAnimationNone];
}];
return;
}
break;
case PasswordCheckStateSafe:
case PasswordCheckStateDefault:
case PasswordCheckStateError:
case PasswordCheckStateRunning:
case PasswordCheckStateDisabled:
if (oldState != PasswordCheckStateUnSafe)
return;
[self.tableViewModel setFooter:nil
forSectionWithIdentifier:SectionIdentifierPasswordCheck];
break;
}
if (update) {
[self.tableView
performBatchUpdates:^{
if (!self.tableView)
return;
// Deleting and inserting section results in pleasant animation of
// footer being added/removed.
[self.tableView
deleteSections:[NSIndexSet indexSetWithIndex:checkSection]
withRowAnimation:UITableViewRowAnimationNone];
[self.tableView
insertSections:[NSIndexSet indexSetWithIndex:checkSection]
withRowAnimation:UITableViewRowAnimationNone];
}
completion:nil];
}
}
// Updates password check button according to provided state. // Updates password check button according to provided state.
- (void)updatePasswordCheckButtonWithState:(PasswordCheckUIState)state { - (void)updatePasswordCheckButtonWithState:(PasswordCheckUIState)state {
if (!_checkForProblemsItem) if (!_checkForProblemsItem)
......
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