chromium/chrome/browser/ui/webui/ash/cloud_upload/hats_office_trigger.h

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROME_BROWSER_UI_WEBUI_ASH_CLOUD_UPLOAD_HATS_OFFICE_TRIGGER_H_
#define CHROME_BROWSER_UI_WEBUI_ASH_CLOUD_UPLOAD_HATS_OFFICE_TRIGGER_H_

#include <memory>
#include <optional>

#include "base/functional/callback_forward.h"
#include "base/no_destructor.h"
#include "base/scoped_observation.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/ash/hats/hats_notification_controller.h"
#include "components/services/app_service/public/cpp/instance_registry.h"

namespace ash::cloud_upload {

// Timeout used to wait before displaying survey.
inline constexpr const base::TimeDelta kDelayTriggerTimeout = base::Minutes(1);

// Timeout to abandon the app state observation if the expected initial
// event hasn't been received within this delay.
inline constexpr const base::TimeDelta kFirstAppStateEventTimeout =
    base::Seconds(5);

// Debounce delay used when observing the change of state of apps.
inline constexpr const base::TimeDelta kDebounceDelay = base::Milliseconds(500);

// Recorded as survey metadata, indicates what app was used when the survey
// was launched.
enum class HatsOfficeLaunchingApp {
  kDrive,
  kMS365,
  kQuickOffice,
  kQuickOfficeClippyOff,
};

// Callback used in tests, called when the survey is attempted to be shown.
using ShowSurveyCallbackForTesting =
    base::OnceCallback<void(const std::string, HatsOfficeLaunchingApp)>;

// Used to show a Happiness Tracking Survey after user opens an Office file
// through the Files app.
class HatsOfficeTrigger {
 public:
  // Gets the global instance.
  static HatsOfficeTrigger& Get();

  HatsOfficeTrigger(const HatsOfficeTrigger&) = delete;
  HatsOfficeTrigger& operator=(const HatsOfficeTrigger&) = delete;

  void SetShowSurveyCallbackForTesting(ShowSurveyCallbackForTesting callback);

  // Trigger survey after a given delay, if the user is selected.
  void ShowSurveyAfterDelay(HatsOfficeLaunchingApp app);

  // Trigger survey once the given app becomes inactive or closed, if the
  // user is selected.
  void ShowSurveyAfterAppInactive(const std::string& app_id,
                                  HatsOfficeLaunchingApp app);

 private:
  friend class base::NoDestructor<HatsOfficeTrigger>;
  friend class HatsOfficeTriggerTestBase;

  struct DelayTrigger {
    explicit DelayTrigger(base::OnceClosure callback);

    base::OneShotTimer notification_timer_;
  };

  // Triggers its callback for an app to be closed or inactive.
  struct AppStateTrigger : public apps::InstanceRegistry::Observer {
    AppStateTrigger(Profile* profile,
                    const std::string& app_id,
                    base::OnceClosure success_callback,
                    base::OnceClosure failure_callback);
    ~AppStateTrigger() override;

    // apps::InstanceRegistry::Observer overrides.
    void OnInstanceUpdate(const apps::InstanceUpdate& update) override;
    void OnInstanceRegistryWillBeDestroyed(
        apps::InstanceRegistry* cache) override;

    void HandleObservedAppStateUpdate(apps::InstanceState state);
    void StopTrackingAppState();

    const std::string app_id_;
    base::OnceClosure success_callback_;
    base::OnceClosure failure_callback_;
    base::UnguessableToken instance_id_;
    base::OneShotTimer first_app_state_event_timer_;
    base::OneShotTimer debounce_timer_;
    base::ScopedObservation<apps::InstanceRegistry,
                            apps::InstanceRegistry::Observer>
        observation_{this};
    base::WeakPtrFactory<AppStateTrigger> weak_ptr_factory_{this};
  };

  HatsOfficeTrigger();
  ~HatsOfficeTrigger();

  bool ShouldShowSurvey() const;

  const HatsNotificationController* GetHatsNotificationControllerForTesting()
      const;
  bool IsDelayTriggerActiveForTesting();
  bool IsAppStateTriggerActiveForTesting();

  Profile* GetProfile() const;

  void ShowSurveyIfSelected(HatsOfficeLaunchingApp app);
  void CleanupTriggers();

  std::unique_ptr<DelayTrigger> delay_trigger_;
  std::unique_ptr<AppStateTrigger> app_state_trigger_;
  ShowSurveyCallbackForTesting show_survey_callback_for_testing_;
  scoped_refptr<ash::HatsNotificationController> hats_notification_controller_;
  base::WeakPtrFactory<HatsOfficeTrigger> weak_ptr_factory_{this};
};

}  // namespace ash::cloud_upload

#endif  // CHROME_BROWSER_UI_WEBUI_ASH_CLOUD_UPLOAD_HATS_OFFICE_TRIGGER_H_