File: hats_notification_controller.cc

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (491 lines) | stat: -rw-r--r-- 18,481 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/hats/hats_notification_controller.h"

#include <optional>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/notification_utils.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/escape.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/ash/hats/hats_config.h"
#include "chrome/browser/ash/hats/hats_dialog.h"
#include "chrome/browser/ash/hats/hats_finch_helper.h"
#include "chrome/browser/ash/login/startup_utils.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/install_attributes/install_attributes.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/version/version_loader.h"
#include "components/language/core/browser/pref_names.h"
#include "components/language/core/common/locale_util.h"
#include "components/prefs/pref_service.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_thread.h"
#include "google_apis/gaia/gaia_auth_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "ui/strings/grit/ui_strings.h"

namespace ash {

namespace {

const char kNotificationOriginUrl[] = "chrome://hats";

const char kNotifierHats[] = "ash.hats";

// The state specific UMA enumerations
const int kSurveyTriggeredEnumeration = 1;

// TODO(jackshira): Migrate this to a manager class.
// Delimiters used to join the separate device info elements into a single
// string to be used as site context.
const char kDeviceInfoStopKeyword[] = "&";
const char kDeviceInfoKeyValueDelimiter[] = "=";
const char kDefaultProfileLocale[] = "en-US";

// TODO(jackshira): Migrate this to a manager class.
enum class DeviceInfoKey : unsigned int {
  BROWSER = 0,
  PLATFORM,
  FIRMWARE,
  LOCALE,
};

// TODO(jackshira): Migrate this to a manager class.
// Maps the given DeviceInfoKey |key| enum to the corresponding string value
// that can be used as a key when creating a URL parameter.
const std::string KeyEnumToString(DeviceInfoKey key) {
  switch (key) {
    case DeviceInfoKey::BROWSER:
      return "browser";
    case DeviceInfoKey::PLATFORM:
      return "platform";
    case DeviceInfoKey::FIRMWARE:
      return "firmware";
    case DeviceInfoKey::LOCALE:
      return "locale";
    default:
      NOTREACHED();
  }
}

// Returns true if the given `profile` interacted with non-prioritized HaTS
// by either dismissing the notification or taking the survey within a given
// `threshold_time`.
bool DidShowNonPrioritizedHatsToProfileRecently(
    const Profile* profile,
    const base::TimeDelta& threshold_time) {
  int64_t serialized_timestamp =
      profile->GetPrefs()->GetInt64(prefs::kHatsLastInteractionTimestamp);

  base::Time previous_interaction_timestamp =
      base::Time::FromInternalValue(serialized_timestamp);
  return previous_interaction_timestamp + threshold_time > base::Time::Now();
}

// Returns true if the given |profile| interacted with a prioritized HaTS
// by either dismissing the notification or taking another prioritized survey
// within |prioritized_threshold_time|.
// If |hats_config| is given, then also check if the given |profile| interacted
// with that specific prioritized HaTS |hats_config| based on the pref timestamp
// |HatsConfig::survey_last_interaction_timestamp_pref_name| within the
// |HatsConfig::threshold_time|.
bool DidShowPrioritizedHatsToProfileRecently(
    const Profile* profile,
    std::optional<raw_ref<const HatsConfig>> hats_config,
    const base::TimeDelta& prioritized_threshold_time) {
  base::Time prev_prioritized_interaction = profile->GetPrefs()->GetTime(
      prefs::kHatsPrioritizedLastInteractionTimestamp);
  if (prev_prioritized_interaction + prioritized_threshold_time >
      base::Time::Now()) {
    return true;
  }

  if (!hats_config.has_value()) {
    return false;
  }

  base::Time previous_interaction_timestamp = profile->GetPrefs()->GetTime(
      hats_config.value()->survey_last_interaction_timestamp_pref_name);

  return previous_interaction_timestamp + hats_config.value()->threshold_time >
         base::Time::Now();
}

bool DidShowAnyHatsToProfileRecently(const Profile* profile,
                                     const base::TimeDelta& threshold_time) {
  return DidShowNonPrioritizedHatsToProfileRecently(profile, threshold_time) ||
         DidShowPrioritizedHatsToProfileRecently(
             profile, /*hats_config=*/std::nullopt, threshold_time);
}

// Returns true if at least |new_device_threshold| time has passed since
// OOBE. This is an indirect measure of whether the owner has used the device
// for at least |new_device_threshold| time.
bool IsNewDevice(base::TimeDelta new_device_threshold) {
  return StartupUtils::GetTimeSinceOobeFlagFileCreation() <=
         new_device_threshold;
}

// Returns true if the |kForceHappinessTrackingSystem| flag is enabled for the
// current survey.
bool IsTestingEnabled(const HatsConfig& hats_config) {
  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();

  if (command_line->HasSwitch(switches::kForceHappinessTrackingSystem)) {
    auto switch_value = command_line->GetSwitchValueASCII(
        switches::kForceHappinessTrackingSystem);
    return switch_value.empty() || hats_config.feature.name == switch_value;
  }

  return false;
}

}  // namespace

// static
const char HatsNotificationController::kNotificationId[] = "hats_notification";

HatsNotificationController::HatsNotificationController(
    Profile* profile,
    const HatsConfig& hats_config,
    const base::flat_map<std::string, std::string>& product_specific_data,
    std::u16string title,
    std::u16string body)
    : profile_(profile),
      hats_config_(hats_config),
      product_specific_data_(product_specific_data),
      title_(std::move(title)),
      body_(std::move(body)) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  std::string histogram_name = HatsFinchHelper::GetHistogramName(*hats_config_);
  if (!histogram_name.empty()) {
    base::UmaHistogramSparse(histogram_name, kSurveyTriggeredEnumeration);
  }

  profile_observation_.Observe(profile_);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&IsNewDevice, hats_config.new_device_threshold),
      base::BindOnce(&HatsNotificationController::Initialize,
                     weak_pointer_factory_.GetWeakPtr()));
}

HatsNotificationController::HatsNotificationController(
    Profile* profile,
    const HatsConfig& hats_config,
    const base::flat_map<std::string, std::string>& product_specific_data)
    : HatsNotificationController(
          profile,
          hats_config,
          product_specific_data,
          l10n_util::GetStringUTF16(IDS_HATS_NOTIFICATION_TITLE),
          l10n_util::GetStringUTF16(IDS_HATS_NOTIFICATION_BODY)) {}

HatsNotificationController::HatsNotificationController(
    Profile* profile,
    const HatsConfig& hats_config)
    : HatsNotificationController(profile,
                                 hats_config,
                                 base::flat_map<std::string, std::string>()) {}

HatsNotificationController::~HatsNotificationController() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  base::UmaHistogramEnumeration("Browser.ChromeOS.HatsStatus", state_);

  if (NetworkHandler::IsInitialized())
    NetworkHandler::Get()->network_state_handler()->RemoveObserver(this);
}

void HatsNotificationController::Initialize(bool is_new_device) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (is_new_device && !IsTestingEnabled(*hats_config_)) {
    // This device has been chosen for a survey, but it is too new. Instead
    // of showing the user the survey, just mark it as completed.
    UpdateLastInteractionTime();

    state_ = HatsState::kNewDevice;
    return;
  }

  if (NetworkHandler::IsInitialized()) {
    // Observe NetworkStateHandler to be notified when an internet connection
    // is available.
    NetworkStateHandler* handler =
        NetworkHandler::Get()->network_state_handler();
    handler->AddObserver(this);
    // Create an immediate update for the current default network.
    const NetworkState* default_network = handler->DefaultNetwork();
    NetworkState::PortalState portal_state =
        default_network ? default_network->portal_state()
                        : NetworkState::PortalState::kUnknown;
    PortalStateChanged(default_network, portal_state);
  }
}

// static
bool HatsNotificationController::ShouldShowSurveyToProfile(
    Profile* profile,
    const HatsConfig& hats_config) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (IsTestingEnabled(hats_config))
    return true;

  // Do not show the survey if the HaTS feature is disabled for the device. This
  // flag is controlled by finch and is enabled only when the device has been
  // selected for the survey.
  if (!base::FeatureList::IsEnabled(hats_config.feature))
    return false;

  // Do not show survey if this is a guest session.
  if (profile->IsGuestSession())
    return false;

  // Do not show survey if the user is supervised.
  if (profile->IsChild())
    return false;

  const bool is_enterprise_enrolled =
      ash::InstallAttributes::Get()->IsEnterpriseManaged();

  HatsFinchHelper hats_finch_helper(profile, hats_config);

  // Do not show survey to enterprise users.
  // Exceptions for Googlers if the survey wants Googlers participation.
  if (is_enterprise_enrolled &&
      !(gaia::IsGoogleInternalAccountEmail(profile->GetProfileUserName()) &&
        hats_finch_helper.IsEnabledForGooglers(hats_config))) {
    return false;
  }

  // Do not show survey to non-owners. However, enterprise-enrolled Googlers
  // who passed the previous check will not be owners; don't exclude them.
  if (!is_enterprise_enrolled && !ProfileHelper::IsOwnerProfile(profile)) {
    return false;
  }

  if (!hats_finch_helper.IsDeviceSelectedForCurrentCycle())
    return false;

  // There are two types of HaTS: prioritized and the non prioritized,
  // both are kept track separately. The following checks both track records.
  if (DidShowAnyHatsToProfileRecently(profile, kMinimumHatsThreshold)) {
    return false;
  }

  if (hats_config.prioritized) {
    // Do not show survey to user if the survey is prioritized and:
    // - User already interacted with the survey within
    //   the threshold set in the config, or
    // - User already interacted with other prioritized survey within
    //   the past |kPrioritizedHatsThreshold|.
    if (DidShowPrioritizedHatsToProfileRecently(
            profile, raw_ref<const HatsConfig>(hats_config),
            kPrioritizedHatsThreshold)) {
      return false;
    }
  } else {
    const base::TimeDelta threshold_time =
        gaia::IsGoogleInternalAccountEmail(profile->GetProfileUserName())
            ? kHatsGooglerThreshold
            : kHatsThreshold;
    // Do not show survey to user if user has interacted with HaTS within the
    // past |threshold_time| time delta. This is a global cap applied across
    // surveys that have not opted out of the global cap of 1 per kHatsThreshold
    // days.
    if (DidShowNonPrioritizedHatsToProfileRecently(profile, threshold_time)) {
      base::UmaHistogramEnumeration("Browser.ChromeOS.HatsStatus",
                                    HatsState::kSurveyShownRecently);
      return false;
    }
  }
  return true;
}

void HatsNotificationController::Click(
    const std::optional<int>& button_index,
    const std::optional<std::u16string>& reply) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  CHECK(profile_) << "Profile must NOT be null.";

  UpdateLastInteractionTime();

  std::string user_locale =
      profile_->GetPrefs()->GetString(language::prefs::kApplicationLocale);
  language::ConvertToActualUILocale(&user_locale);
  if (!user_locale.length())
    user_locale = kDefaultProfileLocale;

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&GetFormattedSiteContext, user_locale,
                     product_specific_data_),
      base::BindOnce(&HatsNotificationController::ShowDialog,
                     weak_pointer_factory_.GetWeakPtr()));

  state_ = HatsState::kNotificationClicked;

  // Remove the notification.
  NetworkHandler::Get()->network_state_handler()->RemoveObserver(this);
  notification_.reset(nullptr);
  NotificationDisplayServiceFactory::GetForProfile(profile_)->Close(
      NotificationHandler::Type::TRANSIENT, kNotificationId);
}

void HatsNotificationController::ShowDialog(const std::string& site_context) {
  if (profile_ != ProfileManager::GetActiveUserProfile()) {
    DVLOG(1) << "Different user detected, not showing dialog";
    return;
  }

  HatsDialog::Show(HatsFinchHelper::GetTriggerID(*hats_config_),
                   HatsFinchHelper::GetHistogramName(*hats_config_),
                   site_context);
}

// message_center::NotificationDelegate override:
void HatsNotificationController::Close(bool by_user) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (by_user) {
    UpdateLastInteractionTime();
    NetworkHandler::Get()->network_state_handler()->RemoveObserver(this);
    notification_.reset(nullptr);
    state_ = HatsState::kNotificationDismissed;
  }
}

// NetworkStateHandlerObserver override:
void HatsNotificationController::PortalStateChanged(
    const NetworkState* default_network,
    NetworkState::PortalState portal_state) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  CHECK(profile_) << "Profile must NOT be null.";
  VLOG(1) << "PortalStateChanged: default_network="
          << (default_network ? default_network->path() : "")
          << ", portal_state=" << portal_state;
  if (portal_state == NetworkState::PortalState::kOnline) {
    // Create and display the notification for the user.
    if (!notification_) {
      notification_ = CreateSystemNotificationPtr(
          message_center::NOTIFICATION_TYPE_SIMPLE, kNotificationId, title_,
          body_,
          l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_NOTIFIER_HATS_NAME),
          GURL(kNotificationOriginUrl),
          message_center::NotifierId(
              message_center::NotifierType::SYSTEM_COMPONENT, kNotifierHats,
              NotificationCatalogName::kHats),
          message_center::RichNotificationData(), this, kNotificationGoogleIcon,
          message_center::SystemNotificationWarningLevel::NORMAL);
    }

    NotificationDisplayServiceFactory::GetForProfile(profile_)->Display(
        NotificationHandler::Type::TRANSIENT, *notification_,
        /*metadata=*/nullptr);

    state_ = HatsState::kNotificationDisplayed;
  } else if (notification_) {
    // Hide the notification if device loses its connection to the internet.
    NotificationDisplayServiceFactory::GetForProfile(profile_)->Close(
        NotificationHandler::Type::TRANSIENT, kNotificationId);
  }
}

void HatsNotificationController::OnShuttingDown() {
  NetworkHandler::Get()->network_state_handler()->RemoveObserver(this);
}

void HatsNotificationController::OnProfileWillBeDestroyed(Profile* profile) {
  CHECK_EQ(profile_, profile);
  profile_ = nullptr;
  profile_observation_.Reset();
}

// TODO(jackshira): Migrate this to a manager class.
// static
std::string HatsNotificationController::GetFormattedSiteContext(
    const std::string& user_locale,
    const base::flat_map<std::string, std::string>& product_specific_data) {
  base::flat_map<std::string, std::string> context;

  context[KeyEnumToString(DeviceInfoKey::BROWSER)] =
      version_info::GetVersionNumber();

  std::optional<std::string> version = chromeos::version_loader::GetVersion(
      chromeos::version_loader::VERSION_FULL);
  context[KeyEnumToString(DeviceInfoKey::PLATFORM)] =
      version.value_or("0.0.0.0");

  context[KeyEnumToString(DeviceInfoKey::FIRMWARE)] =
      chromeos::version_loader::GetFirmware();

  context[KeyEnumToString(DeviceInfoKey::LOCALE)] = user_locale;

  for (const auto& pair : context) {
    if (product_specific_data.contains(pair.first)) {
      LOG(WARNING) << "Product specific data contains reserved key "
                   << pair.first << ". Value will be overwritten.";
    }
  }
  context.insert(product_specific_data.begin(), product_specific_data.end());

  std::stringstream stream;
  bool first_iteration = true;
  for (const auto& pair : context) {
    if (!first_iteration)
      stream << kDeviceInfoStopKeyword;

    stream << base::EscapeQueryParamValue(pair.first, /*use_plus=*/false)
           << kDeviceInfoKeyValueDelimiter
           << base::EscapeQueryParamValue(pair.second, /*use_plus=*/false);

    first_iteration = false;
  }
  return stream.str();
}

void HatsNotificationController::UpdateLastInteractionTime() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  CHECK(profile_) << "Profile must NOT be null.";

  PrefService* pref_service = profile_->GetPrefs();
  if (!hats_config_->prioritized) {
    pref_service->SetInt64(prefs::kHatsLastInteractionTimestamp,
                           base::Time::Now().since_origin().InMicroseconds());
  } else {
    pref_service->SetTime(
        hats_config_->survey_last_interaction_timestamp_pref_name,
        base::Time::Now());
    pref_service->SetTime(prefs::kHatsPrioritizedLastInteractionTimestamp,
                           base::Time::Now());
  }
}

}  // namespace ash